21113bd6ba
Verified end to end against a live match: 1080p H.264/AAC MPEG-TS at ~5.2 Mbit/s, recorded while the browser played the same stream. Components: - tampermonkey sniffer hooks fetch/XHR at document-start, captures the manifest URL verbatim and POSTs it to the local catcher. v0.4 also traces the API chain to find which call mints the token. - catcher receives the URL and launches yt-dlp. Dedupes on the Kaltura asset id and checks downloader liveness, so a mid-match page reload is both safe and the recovery path for a dead downloader. - creds stores secrets in the OS vault rather than a gitignored file, and refuses to run on a backend that does not encrypt. Protocol notes are in the README: the stream uses a two-stage token chain (a ~25s hdnts token for the master playlist, a ~24h hdntl token for media), concurrency is unrestricted, and the live window is ~30s with no DVR. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Kyt7QHQvLJtQRuiY4wAqKB
96 lines
3.6 KiB
Python
96 lines
3.6 KiB
Python
#!/usr/bin/env python3
|
|
"""Byte-exact manifest fetcher + concurrency test.
|
|
|
|
curl normalises percent-escapes (%2f -> %2F), which breaks an Akamai path
|
|
token because the HMAC covers the literal path. This sends the request target
|
|
verbatim via http.client, and can fire N of them at once to see whether the
|
|
edge objects to concurrent readers of one token.
|
|
|
|
python probe.py --url "<manifest url>" # single fetch
|
|
python probe.py --url "<...>" --parallel 3 # concurrency test
|
|
python probe.py --url "<...>" --cookie "name=val" # with a CDN cookie
|
|
"""
|
|
import argparse
|
|
import http.client
|
|
import sys
|
|
import threading
|
|
from urllib.parse import urlsplit
|
|
|
|
UA = ("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
|
|
"(KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36")
|
|
|
|
|
|
def fetch(url, cookie=None, referer="https://www.bristolrovers.co.uk/", tag=""):
|
|
parts = urlsplit(url)
|
|
# Rebuild the target from the raw split so nothing is re-encoded.
|
|
target = parts.path + (("?" + parts.query) if parts.query else "")
|
|
conn = http.client.HTTPSConnection(parts.netloc, timeout=15)
|
|
try:
|
|
conn.putrequest("GET", target, skip_accept_encoding=True)
|
|
conn.putheader("User-Agent", UA)
|
|
conn.putheader("Accept", "*/*")
|
|
conn.putheader("Referer", referer)
|
|
conn.putheader("Origin", referer.rstrip("/"))
|
|
if cookie:
|
|
conn.putheader("Cookie", cookie)
|
|
conn.endheaders()
|
|
resp = conn.getresponse()
|
|
body = resp.read()
|
|
print(f"{tag}HTTP {resp.status} {resp.reason} ({len(body)} bytes)")
|
|
if resp.status == 200:
|
|
text = body.decode("utf-8", "replace")
|
|
variants = [l for l in text.splitlines()
|
|
if l.startswith("#EXT-X-STREAM-INF") or l.endswith(".m3u8")]
|
|
segs = [l for l in text.splitlines() if l and not l.startswith("#")]
|
|
if variants:
|
|
print(f"{tag} master playlist, {len(variants)//2 or len(variants)} rendition(s):")
|
|
for v in variants[:12]:
|
|
print(f"{tag} {v}")
|
|
else:
|
|
print(f"{tag} media playlist, {len(segs)} segment(s)")
|
|
for s in segs[:2]:
|
|
print(f"{tag} {s[:110]}")
|
|
else:
|
|
print(f"{tag} {body.decode('utf-8', 'replace')[:200].strip()}")
|
|
return resp.status
|
|
except Exception as exc:
|
|
print(f"{tag}ERROR {type(exc).__name__}: {exc}")
|
|
return -1
|
|
finally:
|
|
conn.close()
|
|
|
|
|
|
def main():
|
|
ap = argparse.ArgumentParser()
|
|
ap.add_argument("--url", required=True)
|
|
ap.add_argument("--cookie", default=None)
|
|
ap.add_argument("--parallel", type=int, default=1,
|
|
help="fire N simultaneous fetches to test concurrency")
|
|
args = ap.parse_args()
|
|
|
|
if args.parallel <= 1:
|
|
sys.exit(0 if fetch(args.url, args.cookie) == 200 else 1)
|
|
|
|
print(f"firing {args.parallel} simultaneous requests "
|
|
f"(keep the browser playing while this runs)\n")
|
|
results = {}
|
|
start = threading.Barrier(args.parallel)
|
|
|
|
def worker(i):
|
|
start.wait() # release them together
|
|
results[i] = fetch(args.url, args.cookie, tag=f"[{i}] ")
|
|
|
|
threads = [threading.Thread(target=worker, args=(i,)) for i in range(args.parallel)]
|
|
for t in threads:
|
|
t.start()
|
|
for t in threads:
|
|
t.join()
|
|
|
|
ok = sum(1 for v in results.values() if v == 200)
|
|
print(f"\n{ok}/{args.parallel} succeeded concurrently"
|
|
f"{' -- plus the browser itself' if ok else ''}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|