Log what the master offers and let the caller pin audio or video

The recorder passed no -f, so yt-dlp used its default, which prefers video.
That was never a decision: on 2 Sep the master held exactly one rendition, so
there was nothing to choose between. If a master ever carries a video variant
the account is not entitled to, the default would take it and 403 on segments.

--prefer audio|video|best makes it explicit. best keeps the old behaviour.

Every run now logs the renditions the master advertises, parsed from
EXT-X-STREAM-INF, so we build evidence about whether video and audio ever
appear together instead of assuming entitlement shapes the master.

Observed so far, both audio nights: one rendition, mp4a.40.2 at 140 kbps.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Kyt7QHQvLJtQRuiY4wAqKB
This commit is contained in:
Mikkeli Matlock
2026-09-05 19:01:18 +09:00
parent 84c29cc0b7
commit fcf509fda5
+41 -4
View File
@@ -106,7 +106,35 @@ def mint_master(entry_id):
"Referer": "https://www.bristolrovers.co.uk/"})
data = json.loads(urllib.request.urlopen(req, timeout=20)
.read().decode("utf-8", "replace"))
return (data.get("media") or {}).get("hls") or None
hls = (data.get("media") or {}).get("hls") or None
if hls:
log_renditions(hls)
return hls
def log_renditions(master_url):
"""Record what the master offers. We have never seen one carrying both
video and audio, so log it every time until we know how that looks."""
import urllib.request
try:
req = urllib.request.Request(master_url, headers={"User-Agent": UA,
"Referer": "https://www.bristolrovers.co.uk/"})
body = urllib.request.urlopen(req, timeout=15).read().decode("utf-8", "replace")
except Exception as exc:
log(f" (could not read master for rendition list: {exc})")
return
PREFIX = "#EXT-X-STREAM-INF:"
streams = [ln[len(PREFIX):] for ln in body.splitlines()
if ln.startswith(PREFIX)]
log(f" master offers {len(streams)} rendition(s):")
for st in streams:
bw = re.search(r"BANDWIDTH=(\d+)", st)
res = re.search(r"RESOLUTION=([\dx]+)", st)
cod = re.search(r'CODECS="([^"]+)"', st)
codecs = cod.group(1) if cod else "?"
kind = "audio-only" if ("avc" not in codecs and "hvc" not in codecs) else "video"
log(f" {kind:11s} {int(bw.group(1))//1000 if bw else '?':>6} kbps "
f"{res.group(1) if res else '-':>10} {codecs}")
def find_manifest(timeout_s=90):
@@ -175,7 +203,7 @@ def find_manifest(timeout_s=90):
return m.group(1) if m else None
def record(url, duration_min):
def record(url, duration_min, prefer='best'):
stamp = datetime.now().strftime("%Y%m%d-%H%M%S")
out = os.path.join(RECORDINGS, f"brfc-{stamp}.ts")
os.makedirs(RECORDINGS, exist_ok=True)
@@ -197,8 +225,14 @@ def record(url, duration_min):
"-reconnect_delay_max 30",
"--downloader-args", f"ffmpeg_o:-t {duration_min * 60}",
"-o", out,
url,
]
if prefer == "audio":
# Pin audio when the entitlement is audio-only: if a video rendition
# ever appears in the master, taking it would 403 on the segments.
cmd += ["-f", "bestaudio/best"]
elif prefer == "video":
cmd += ["-f", "bestvideo*+bestaudio/best"]
cmd.append(url)
log(f"recording -> {os.path.basename(out)} (cap {duration_min} min)")
log(f" yt-dlp log -> {os.path.basename(logfile)}")
with open(logfile, "w", encoding="utf-8") as fh:
@@ -213,6 +247,9 @@ def main():
ap.add_argument("--duration", type=int, default=150,
help="recording cap in minutes")
ap.add_argument("--poll", type=int, default=60, help="seconds between attempts")
ap.add_argument("--prefer", choices=("best", "audio", "video"), default="best",
help="which rendition to take when the master offers both; "
"'best' is yt-dlp's default and prefers video")
args = ap.parse_args()
if args.at:
@@ -244,7 +281,7 @@ def main():
time.sleep(args.poll)
continue
log(" minted fresh master playlist")
proc = record(url, args.duration)
proc = record(url, args.duration, args.prefer)
log(f" downloader pid {proc.pid}; waiting for it to finish")
t0 = time.time()
proc.wait()