Commit 34b02925 by PLN (Algolia)

fix(release): the shelf is one namespace and a live catalogue reuses titles

Caught before --go, on the last check I nearly skipped. The CosmicSet plan
resolved four of its fifteen permalinks onto tracks Opal had already uploaded:
/you-my-sunshine, /take-five-drops, /piment-bresilien, /eh-ouais-je-funk. Of
course it did — those are the same scores, played at both gigs a fortnight
apart, and a permalink is derived from the title.

What makes this worth a gate rather than a fix is the failure mode. SoundCloud
is idempotent by permalink and the uploader treats a taken permalink as "already
done" — correct for resuming an interrupted 1.4 GB run, catastrophic for a
different recording of the same track. So the upload would NOT have errored. It
would have reported success with four tracks missing from the release, or, with
--overwrite, replaced Opal's audio with CosmicFest's. Neither is visible from
the terminal that ran it. `feedback_absence_needs_proof`: a release that is
quietly short looks exactly like a release that is complete.

check_permalinks.py resolves a plan against the live account and sorts every
permalink into FREE / already-up-as-this-same-audio / COLLISION, using duration
as the discriminator because it is the one property that survives SoundCloud's
transcode and is already in the plan. A collision exits 1. Verified both ways:
it fails the un-suffixed plan naming all four real collisions, and passes the
suffixed one 15/15 free. Its shelf query carries access=playable,preview,blocked
because scform.py's own notes record that private tracks are omitted without it
— and an audition release is private by definition, so the gate would have
called every permalink free.

build_release_plan.py gains --permalink-suffix to fix the cause, so a gig's
permalinks are scoped to the gig and no future pair of sets can collide.

Two things about --force, which this release is the first to actually need (the
signoff cannot precede the upload when SoundCloud is where PLN listens):

  * it never worked. The no-signoff branch did `return problems` before reaching
    the override, so the flag documented as "emit even without a valid ear
    signoff" could only override a STALE signoff, never a missing one. The early
    return is gone and both problems now reach the same override.
  * it now demands --why, and records the reason, the timestamp and a
    do-not-flip-public warning in the plan's own `_UNSIGNED` block. A terminal
    warning scrolls away; the JSON is what the uploader reads. An override with
    no stated reason is indistinguishable from an approved release two weeks
    later, and the gate exists precisely so nobody has to remember.

Left alone deliberately: the plan cannot tell private from public — the uploader's
--sharing flag decides that — so _UNSIGNED documents rather than enforces.
parent c5aaf892
......@@ -33,6 +33,7 @@ from __future__ import annotations
import argparse
import json
import re
import sys
from datetime import datetime, timezone
from pathlib import Path
......@@ -94,15 +95,22 @@ def check_signoff(ear: dict, tracks: list[Path], force: bool) -> list[str]:
problems.append(
"no `release_signoff` in the ear file — nobody has confirmed this "
"record by ear. Run render_release + build_release_joins and listen.")
return problems
signed = datetime.fromisoformat(so["date"]).date()
newest = max((datetime.fromtimestamp(p.stat().st_mtime).date() for p in tracks),
default=None)
if newest and newest > signed:
problems.append(
f"the rendered tracks ({newest}) are NEWER than the signoff "
f"({signed}). Audio has changed since PLN approved it; re-audition "
f"before uploading.")
# NO early return. This branch used to `return problems` right here,
# which meant --force — documented as "emit even without a valid ear
# signoff" — could only ever override the STALE-signoff problem, never
# the missing-signoff one it was named for. The first gig to need it
# (CosmicFest 2026: private audition upload, because SoundCloud is where
# PLN listens, so the signoff cannot precede the upload) is how that came
# out. Fall through to the shared override below.
else:
signed = datetime.fromisoformat(so["date"]).date()
newest = max((datetime.fromtimestamp(p.stat().st_mtime).date() for p in tracks),
default=None)
if newest and newest > signed:
problems.append(
f"the rendered tracks ({newest}) are NEWER than the signoff "
f"({signed}). Audio has changed since PLN approved it; re-audition "
f"before uploading.")
if force and problems:
for p in problems:
print(f" ! OVERRIDDEN: {p}", file=sys.stderr)
......@@ -116,6 +124,13 @@ def main() -> int:
ap.add_argument("spec")
ap.add_argument("--variant", default="streaming")
ap.add_argument("--artwork", help="cover image applied to every track")
ap.add_argument("--permalink-suffix", default="",
help="append to every permalink, e.g. `cosmicfest-2026`. A "
"live set's tracks are NOT unique on the shelf: the same "
"score gets played at several gigs, and SoundCloud is "
"idempotent by permalink, so the second gig's upload "
"either silently skips or overwrites the first's. Scope "
"the permalinks per gig and neither can happen.")
ap.add_argument("--include-mix", action="store_true",
help="also publish the continuous *_nodrop mix, listed first")
ap.add_argument("--out", required=True)
......@@ -123,7 +138,11 @@ def main() -> int:
help="track names from the canonical tracklist (default) or "
"from the rendered filenames")
ap.add_argument("--force", action="store_true",
help="emit even without a valid ear signoff (say why in the log)")
help="emit even without a valid ear signoff (say why in --why)")
ap.add_argument("--why", default="",
help="required with --force: why this plan may exist unsigned. "
"Recorded IN the plan, because a terminal warning scrolls "
"away and the JSON is what the uploader reads.")
a = ap.parse_args()
spec = json.loads(Path(a.spec).read_text())
......@@ -146,6 +165,10 @@ def main() -> int:
if len(files) != len(segs):
sys.exit(f"{len(files)} rendered files but {len(segs)} segments in {tdir}")
if a.force and not a.why.strip():
sys.exit("--force needs --why. An override with no stated reason is "
"indistinguishable from an approved release once the terminal "
"is closed, and this plan is what the uploader trusts.")
problems = check_signoff(ear, files, a.force)
if problems:
print("REFUSING TO WRITE A PLAN — the record is not ear-approved:")
......@@ -170,6 +193,10 @@ def main() -> int:
print(f" {t['name']!r}")
return 1
def permalink(title: str) -> str:
base = re.sub(r"-+", "-", re.sub(r"[^a-z0-9]+", "-", title.lower())).strip("-")
return f"{base}-{a.permalink_suffix}" if a.permalink_suffix else base
tracks, renamed = [], []
for seg, f in zip(segs, files):
src = by_name[match_key(seg["title"])]
......@@ -187,6 +214,7 @@ def main() -> int:
tags = base_tags + [t for t in (src.get("style"), src.get("section")) if t]
tracks.append({
"title": title,
"permalink": permalink(title),
"audio": str(f),
"artwork": a.artwork,
"genre": src.get("style", ""),
......@@ -235,6 +263,9 @@ def main() -> int:
# Composed from canonical FIELDS (title · venue · date), never from
# a remembered string. The format is here; the facts are theirs.
"title": f"{album} — {fm.get('address','')} {fm.get('date','')[:4]} (full set)",
# No year in the base: the suffix already carries the gig, and
# "cosmicset-2026-2026-full-set-cosmicfest-2026" is nobody's URL.
"permalink": permalink(f"{album} full set"),
"audio": str(mix),
"artwork": a.artwork,
"genre": "livecoding",
......@@ -259,6 +290,16 @@ def main() -> int:
],
"album": album,
"artist": spec.get("artist", "ParVagues"),
# An unsigned plan must announce itself. The uploader's --sharing flag
# decides private vs public, so nothing here can enforce it; what this
# CAN do is make sure a plan built for a private audition is never
# mistaken for an approved record when someone re-runs it later.
"_UNSIGNED": ({"reason": a.why,
"forced": datetime.now(timezone.utc).isoformat(timespec="seconds"),
"means": "NOT ear-approved. Private audition only — do not "
"flip these permalinks public without a "
"release_signoff in the ear file."}
if a.force else None),
"date": fm.get("date"),
"_provenance": {
"album/date/description/tags": str(md_path.relative_to(WWW)),
......
#!/usr/bin/env python3
"""check_permalinks — refuse a release plan whose permalinks are already taken.
## Why this is a gate and not a warning
SoundCloud is idempotent by permalink, and the uploader treats an existing
permalink as "already done". That is the right behaviour for resuming a
half-finished upload and the wrong behaviour for a *different* recording of the
same track — and a live catalogue is full of those. ParVagues played Piment
Brésilien at Opal in August and at CosmicFest a fortnight later; both render to
`/piment-bresilien`.
So the failure mode is not an error. It is a release that reports success with
four tracks quietly missing, or — with `--overwrite` — the previous gig's
release silently replaced by this one's. Both are invisible from the terminal
that ran the upload. Caught on CosmicFest-2026 before `--go`: four of fourteen
permalinks were already Opal's private uploads.
## What it does
Resolves the plan's permalinks (explicit `permalink`, else the same slugify the
uploader applies to `title`) against the live account, and separates:
* FREE — nothing there, a normal upload
* OURS-RESUME — taken, and the duration matches this plan's audio, so it is
almost certainly a resumed upload of THIS release
* COLLISION — taken by audio of a different length: a different recording
A COLLISION is fatal unless `--allow-collisions`. Duration is the discriminator
because it is the one property that survives SoundCloud's transcode and is
already in the plan (`_duration_s`).
python3 check_permalinks.py <plan.json> [--tolerance 3]
"""
from __future__ import annotations
import argparse
import json
import re
import sys
from pathlib import Path
sys.path.insert(0, "/home/pln/Work/Sound/tidal-ears/src")
def slugify(title: str) -> str:
return re.sub(r"-+", "-", re.sub(r"[^a-z0-9]+", "-", title.lower())).strip("-")
def shelf() -> dict[str, dict]:
from tidal_ears.sc import SC, resolve_creds
api = SC(resolve_creds())
me = api.get("/me")
out, path = {}, f"/users/{me['id']}/tracks"
# `access=playable,preview,blocked` is load-bearing: scform.py's own notes
# record that without it the owner's PRIVATE tracks are omitted, which would
# make every private permalink look free — the exact opposite of this gate's
# job, since an audition release is private by definition.
r = api.get(path, limit=200, access="playable,preview,blocked")
for t in (r.get("collection", []) if isinstance(r, dict) else r):
out[t["permalink"]] = {"title": t.get("title"),
"dur_s": round((t.get("duration") or 0) / 1000, 1),
"sharing": t.get("sharing")}
return out
def main() -> int:
ap = argparse.ArgumentParser(description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("plan")
ap.add_argument("--tolerance", type=float, default=3.0,
help="seconds of duration difference still counted as the "
"same audio (transcode + the uploader's own trim)")
ap.add_argument("--allow-collisions", action="store_true",
help="proceed anyway — say why in the release log")
a = ap.parse_args()
plan = json.loads(Path(a.plan).read_text())
live = shelf()
free, resume, collide = [], [], []
for t in plan["tracks"]:
pl = t.get("permalink") or slugify(t["title"])
got = live.get(pl)
if not got:
free.append((pl, t["title"]))
elif abs(got["dur_s"] - (t.get("_duration_s") or -1)) <= a.tolerance:
resume.append((pl, t["title"], got))
else:
collide.append((pl, t["title"], got, t.get("_duration_s")))
def hms(s):
s = int(s or 0)
return f"{s // 60}:{s % 60:02d}"
print(f"{plan.get('album')} · {len(plan['tracks'])} planned upload(s)")
print(f" {len(free):>3} free · {len(resume):>3} already up as this same audio "
f"· {len(collide):>3} COLLISION(S)")
if resume:
print("\nalready up, same length — an upload run would resume, not duplicate:")
for pl, title, got in resume:
print(f" /{pl:<44} {hms(got['dur_s'])} {got['sharing']}")
if collide:
print("\nCOLLISIONS — the permalink is taken by DIFFERENT audio:")
for pl, title, got, want in collide:
print(f" /{pl:<44} shelf {hms(got['dur_s'])} ({got['title']!r}) "
f"vs plan {hms(want)} ({title!r})")
print("\n An upload would SKIP these tracks (release silently short) or,")
print(" with --overwrite, replace the other release's audio. Re-generate")
print(" the plan with `build_release_plan.py --permalink-suffix <gig>`.")
if not a.allow_collisions:
return 1
print("\n ! OVERRIDDEN by --allow-collisions")
print("\n✓ no collisions" if not collide else "")
return 0
if __name__ == "__main__":
sys.exit(main())
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment