Commit 85123f6a by PLN (Algolia)

feat(probe): look at the page instead of guessing — the form was in an iframe

Two probes for the SoundCloud upload flow. `probe_upload_dom.py` is the one that
settled it: rather than hypothesise about why /upload showed no file input, it
dumps overlays and z-indexes, enumerates every frame, walks shadow roots, prints
the main region's text, and tries clicking whatever looks like a drop zone. The
answer arrived in one run — 0 file inputs in the top document, 1 in a nested
same-origin iframe at /n/upload.

`probe_upload_ui.py` now attaches in that frame and listens on the CONTEXT
rather than the page, because a frame's fetches never surface on the parent's
request event — the same blind spot in a second guise. It reached the real form:
inputs named title / trackPermalink / artist, plus an Upload button.

It captured zero finalize requests, and that is correct: it deliberately stops
short of clicking Upload, and the finalize call only fires on that click. Which
raises the better question — with a real form and a real Upload button in front
of us, reconstructing `POST /tracks` may be work we never have to do.
parent 6dda5f75
"""Why does /upload render no file input? Look at the page instead of guessing.
`session_ok()` says the session is fine and the probe says there are zero
`input[type=file]` elements, so one of them is measuring the wrong thing. The
candidate explanations are all cheap to separate by LOOKING:
* a consent gate -> a dialog element, or a body with scroll locked
* a lazy React island -> the drop zone text exists but the input appears
only after a click
* a shadow root -> the input exists but `locator` cannot see it
* a cross-origin frame-> the form is in an iframe
So: dump the main region's text, every frame, every element with a high
z-index, and count file inputs again after a click on whatever looks like the
drop zone. Uploads nothing.
"""
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
with Browser() as b:
b.open()
b.page.goto("https://soundcloud.com/upload", wait_until="load")
b.page.wait_for_timeout(8000)
print("url:", b.page.url)
print("title:", b.page.title())
# -- 1. is anything overlaying the page, and is scroll locked? ----------
over = b.page.evaluate("""() => {
const out = [];
for (const el of document.querySelectorAll('body *')) {
const s = getComputedStyle(el);
const z = parseInt(s.zIndex);
if ((s.position === 'fixed' || s.position === 'absolute') && z >= 100) {
const r = el.getBoundingClientRect();
if (r.width > 200 && r.height > 100 && s.display !== 'none')
out.push({tag: el.tagName, cls: (el.className || '').toString().slice(0, 60),
z, w: Math.round(r.width), h: Math.round(r.height)});
}
}
return {overlays: out.slice(0, 12),
bodyOverflow: getComputedStyle(document.body).overflow,
dialogs: document.querySelectorAll('dialog,[role=dialog]').length};
}""")
print("\n-- overlays / scroll lock:")
print(" body overflow:", over["bodyOverflow"], " dialogs:", over["dialogs"])
for o in over["overlays"]:
print(f" z={o['z']:<6} {o['tag']:<8} {o['w']}x{o['h']} {o['cls']!r}")
# -- 2. frames: is the form simply somewhere else? ----------------------
print("\n-- frames:")
for f in b.page.frames:
try:
n = f.locator('input[type="file"]').count()
except Exception:
n = "?"
print(f" {n} file input(s) {f.url[:90]}")
# -- 3. shadow roots hide elements from ordinary selectors --------------
deep = b.page.evaluate("""() => {
let inputs = 0, roots = 0;
const walk = (root) => {
for (const el of root.querySelectorAll('*')) {
if (el.tagName === 'INPUT' && el.type === 'file') inputs++;
if (el.shadowRoot) { roots++; walk(el.shadowRoot); }
}
};
walk(document);
return {inputs, roots};
}""")
print(f"\n-- deep scan (shadow roots included): {deep['inputs']} file input(s), "
f"{deep['roots']} shadow root(s)")
# -- 4. what does the main region actually SAY? -------------------------
txt = b.page.evaluate("""() => {
const m = document.querySelector('main, #content, .l-container, body');
return (m ? m.innerText : '').replace(/\\n{2,}/g, '\\n').slice(0, 1200);
}""")
print("\n-- main region text:")
for line in txt.splitlines():
if line.strip():
print(" |", line.strip()[:100])
# -- 5. does a click on the drop zone summon the input? ----------------
for label in ("Choose files", "Choose file", "Select files", "Upload",
"drag and drop", "Drag and drop"):
loc = b.page.get_by_text(label, exact=False)
if loc.count():
print(f"\n-- clicking {label!r} ({loc.count()} match(es))")
try:
loc.first.click(timeout=4000)
b.page.wait_for_timeout(3000)
except Exception as e:
print(" click failed:", str(e).splitlines()[0][:100])
print(" file inputs now:",
b.page.locator('input[type="file"]').count())
break
else:
print("\n-- no drop-zone-looking text found at all")
"""Observe what the SoundCloud upload UI actually calls. Uploads nothing.
"""Observe what the SoundCloud upload UI actually calls. Creates no track.
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.
`POST /tracks` answers 400 with an empty error list from both transports, which
means our request shape is wrong rather than blocked. The only reliable way to
learn the right shape is to watch SoundCloud's own client make it.
## The correction that made this work
The first version attached the file to `page` and reported "file inputs on
page: 0", which got read as "the upload form does not render" — and sent a
whole investigation after a consent dialog. The form renders fine; it lives in
a NESTED same-origin iframe (`/n/upload?…&embedded=crossfade&v2_layout=true`)
inside a `webiIframe`. Requests must be watched on the CONTEXT, not the page,
for the same reason: a frame's fetches never surface on the parent's `request`
event.
Nothing is saved, so no track is created — but the page will legitimately push
the probe's bytes to S3 on its own, which is why the probe is one second of
sine and not a record.
"""
import json
import sys
from pathlib import Path
sys.path.insert(0, "/home/pln/Work/Sound/tidal-ears/src")
from tidal_ears import consent # noqa: E402
from tidal_ears.scbrowser import Browser # noqa: E402
PROBE = Path(sys.argv[1])
seen = []
OUT = Path(sys.argv[2]) if len(sys.argv) > 2 else None
seen: list[dict] = []
INTERESTING = ("api-v2.soundcloud.com", "api.soundcloud.com",
"api-mobile.soundcloud.com")
# Telemetry drowns the signal: /me?client_id… analytics batches fire constantly
# and say nothing about track creation.
NOISE = ("/me?", "/me/", "smartnudges", "/events", "/gate", "/pageview")
def note(req):
if "api-v2.soundcloud.com" not in req.url and "api.soundcloud" not in req.url:
url = req.url
if not any(h in url for h in INTERESTING):
return
if req.method == "GET":
if req.method == "GET" or any(n in url for n in NOISE):
return
body = None
try:
body = req.post_data
except Exception:
pass
seen.append((req.method, req.url.split("?")[0], (body or "")[:400]))
seen.append({"method": req.method, "url": url.split("?")[0],
"body": (body or "")[:2000],
"frame": (req.frame.url or "")[:60] if req.frame else ""})
with Browser() as b:
b.seed("firefox")
b.open()
consent.ensure(b.page, policy="reject")
b.require_login()
b.page.on("request", note)
# Listen on the CONTEXT so nested-frame traffic is included.
b.ctx.on("request", note)
b.page.goto("https://soundcloud.com/upload", wait_until="domcontentloaded")
b.page.wait_for_timeout(5000)
frame = b.upload_frame()
if frame is None:
print("no frame carries a file input — the form really is absent this time")
for f in b.page.frames:
print(" frame:", (f.url or "")[:100])
sys.exit(1)
print(f"upload form found in frame: {frame.url[:80]}")
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)
frame.locator('input[type="file"]').first.set_input_files(str(PROBE))
print("attached probe; watching for the finalize call…")
b.page.wait_for_timeout(20000)
print("\n--- form controls visible after attaching:")
print("\n--- controls in the upload frame:")
for sel in ('input[name]', 'button', '[role="button"]'):
loc = b.page.locator(sel)
for i in range(min(loc.count(), 25)):
loc = frame.locator(sel)
for i in range(min(loc.count(), 30)):
el = loc.nth(i)
try:
if not el.is_visible():
......@@ -57,8 +88,12 @@ with Browser() as b:
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]}")
print(f"\n--- {len(seen)} non-GET api request(s), telemetry excluded:")
for r in seen:
print(f" {r['method']} {r['url']}")
if r["body"]:
print(f" {r['body'][:500]}")
if OUT:
OUT.write_text(json.dumps(seen, indent=1) + "\n")
print(f"\nwrote {OUT}")
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