Commit 6dda5f75 by PLN (Algolia)

docs(board): resume points for the uploader, the ledger, and the gig list

Writing for a cold reader, because the valuable part of this session is the
DEAD hypotheses and those live only in context.

POST /tracks -> 400 {"errors":[]} has now survived three explanations and each
one cost a real experiment: it is not fingerprinting (identical from curl_cffi
and from the page's own fetch), not the envelope ({"track":{…}} is the only
shape that parses at all — flat, track[…] form and asset_data are rejected with
"Failed to parse track create request"), and not the missing web session
(retested after PLN logged in for real; session_ok() True, still 400). Anyone
resuming would try those three first, so the board says not to.

The one lead left is the consent dialog covering the upload form — session_ok()
is True while input[type=file] count is 0 — so probe_upload_ui.py moves out of
the scratchpad and into the repo. It attaches a 1s probe to the REAL form and
logs every non-GET api-v2 request, which is how we stop reconstructing a shape
from publish.py's older API and start copying one that demonstrably works.

Also recorded: the multi-platform architecture PLN asked for (one manifest, many
adapters, one state file), with the research behind it — Bandcamp has no upload
API or CLI at all, RouteNote has no open API but ships a desktop tool, YouTube
has a real one and gets chapters free from segments_v4. And the gig-list audit:
38 gigs published, 2026 has four, opal-festival-2026.md still has empty
audio/video/archive fields waiting on the upload.
parent 15ed1664
...@@ -78,7 +78,64 @@ uploading. ...@@ -78,7 +78,64 @@ uploading.
Two independent measurements agree. This is the finding that motivated D2's "shout until played". Two independent measurements agree. This is the finding that motivated D2's "shout until played".
- [ ] Measure the bass-"saturation" claim separately — it is a different question from loudness. - [ ] Measure the bass-"saturation" claim separately — it is a different question from loudness.
### A3 · #179 — SoundCloud uploader (Playwright) 🔴 **NEXT — A1 is done, this is unblocked** ### A3 · #179 — SoundCloud uploader 🟡 **BUILT; one call unresolved**
**Done** (tidal-ears `d8f275d` `e5f0693` `b72449e` `8c4df8e` `1a64c35`): `scbrowser.py`
= headful persistent Chromium; only `POST /uploads/track-upload-policy` goes through the
browser, bytes go to S3 from Python, metadata over the plain API. Proven durable:
`master sc browser-check --repeat 14 --every 100`**14/14 policies over 21.7 min**,
latency dropping to 0.1–0.2 s. The 790 MB mix reached S3 with a 200.
**BLOCKED ON ONE THING:** `POST /tracks` → 400 `{"errors":[]}`. Dead hypotheses, do NOT
retry: fingerprinting (same from both transports), the envelope (`{"track":{…}}` is the
only shape that parses), the web session (retested after a real login, still 400s).
**EXACT NEXT STEP:** the consent dialog is covering the upload form (`session_ok()` is
True but `input[type=file]` count is 0). Dismiss it in the persistent profile, then run
the capture script at `scratchpad/probe_ui.py` — it attaches a 1 s probe to the real form
and logs every non-GET api-v2 request. Copy the finalize call's shape verbatim into
`upsert_track`. Then re-run:
```
python3 armada/tide-table/build_release_plan.py judge_specs/opal26.json \
--variant streaming --include-mix --out /tmp/opal26_plan.json
cd ~/Work/Sound/tidal-ears && .venv/bin/python -m tidal_ears.cli master sc album \
--plan /tmp/opal26_plan.json --sharing private --transport browser
```
Idempotent by permalink — a half-finished run is safe to simply re-run. **Nothing is on
the account today.**
### A4 · Rights ledger 🟡 **29 of 60 banks answered**
`armada/tide-table/rights_audit.py` + `rights_ledger.json` (`a00066b`, `15ed166`).
Discriminator: `git ls-files` in the Dirt-Samples checkout — 217 upstream vs 509 local;
"lives in that folder" is NOT "is a stock bank". PLN cleared the five keystones
(jungle_breaks / h2o* / kick / risers / vec1*), which answered 10 banks.
**Next: `snare` (4 tracks), then 30 mostly-single-track banks.** Per-track release on
every platform waits on this; the continuous mix does not.
### A5 · Multi-platform adapters — the agreed architecture
One manifest (`build_release_plan.py`, platform-neutral, refuses without an ear signoff),
many adapters, one state file `releases/<gig>.json` recording what landed where.
Researched 2026-08-16: **Bandcamp has NO upload API or CLI** (its OAuth API is for labels
/ merch-fulfilment, by application; every community tool is a scraper) → headful browser
reusing `scbrowser.Browser`. **RouteNote** likewise has no open API but ships a desktop
upload tool worth probing first. **YouTube** has a real API — and chapters come free from
`segments_v4.json`. PLN's priority: *"imo we need them all. youtube important, routenote
is top tier, bandcamp fine but prob clis exist, a4 pass indeed sounds important"*.
Rights rule, his words: *"cant push the single tracks when samples hit, but full mix
might well pass!"* — RouteNote/DSP is the one place a claim costs the account, not a track.
### A6 · www gig list 🔴 NOT STARTED
38 gigs published 2022–2026; 2026 has four (le-vortex 01-04, ete-surprise 03-07,
montreuil-algorave 05-22, opal-festival-2026 08-08). PLN believes some are missing —
reconcile against `backlog.md`'s month sections (`Juin Faites de la musique`, `Juillet
Douillet`, `AOUT Tout Doux`…). Also **opal-festival-2026.md has empty `audio`/`video`/
`archive`** — fill once the upload lands.
### A3-old · #179 original scope
Still **NOT BUILT**. PLN 2026-08-16: *"yes go build the uploader. im open to giving you SC Still **NOT BUILT**. PLN 2026-08-16: *"yes go build the uploader. im open to giving you SC
pass/user as in the .env if the cookie approach doesnt suffice. imo we need headful browser pass/user as in the .env if the cookie approach doesnt suffice. imo we need headful browser
......
"""Observe what the SoundCloud upload UI actually calls. Uploads nothing.
Attaches a 1-second probe file to the real upload form and records every
api-v2 request the page makes, then stops WITHOUT saving. The goal is the
shape of the finalize call, since POST /tracks now answers 400 with an empty
error list from both transports — which means the endpoint or body is wrong,
not that we are blocked.
"""
import sys
from pathlib import Path
sys.path.insert(0, "/home/pln/Work/Sound/tidal-ears/src")
from tidal_ears.scbrowser import Browser # noqa: E402
PROBE = Path(sys.argv[1])
seen = []
def note(req):
if "api-v2.soundcloud.com" not in req.url and "api.soundcloud" not in req.url:
return
if req.method == "GET":
return
body = None
try:
body = req.post_data
except Exception:
pass
seen.append((req.method, req.url.split("?")[0], (body or "")[:400]))
with Browser() as b:
b.seed("firefox")
b.open()
b.require_login()
b.page.on("request", note)
b.page.goto("https://soundcloud.com/upload", wait_until="domcontentloaded")
b.page.wait_for_timeout(5000)
inputs = b.page.locator('input[type="file"]')
print(f"file inputs on page: {inputs.count()}")
if inputs.count():
inputs.first.set_input_files(str(PROBE))
b.page.wait_for_timeout(12000)
print("\n--- form controls visible after attaching:")
for sel in ('input[name]', 'button', '[role="button"]'):
loc = b.page.locator(sel)
for i in range(min(loc.count(), 25)):
el = loc.nth(i)
try:
if not el.is_visible():
continue
txt = (el.get_attribute("name") or el.inner_text() or "").strip()
if txt:
print(f" {sel:<18} {txt[:60]!r}")
except Exception:
pass
print("\n--- api-v2 non-GET requests observed:")
for m, u, body in seen:
print(f" {m} {u}")
if body:
print(f" {body[:300]}")
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