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:
+41
-4
@@ -106,7 +106,35 @@ def mint_master(entry_id):
|
|||||||
"Referer": "https://www.bristolrovers.co.uk/"})
|
"Referer": "https://www.bristolrovers.co.uk/"})
|
||||||
data = json.loads(urllib.request.urlopen(req, timeout=20)
|
data = json.loads(urllib.request.urlopen(req, timeout=20)
|
||||||
.read().decode("utf-8", "replace"))
|
.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):
|
def find_manifest(timeout_s=90):
|
||||||
@@ -175,7 +203,7 @@ def find_manifest(timeout_s=90):
|
|||||||
return m.group(1) if m else None
|
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")
|
stamp = datetime.now().strftime("%Y%m%d-%H%M%S")
|
||||||
out = os.path.join(RECORDINGS, f"brfc-{stamp}.ts")
|
out = os.path.join(RECORDINGS, f"brfc-{stamp}.ts")
|
||||||
os.makedirs(RECORDINGS, exist_ok=True)
|
os.makedirs(RECORDINGS, exist_ok=True)
|
||||||
@@ -197,8 +225,14 @@ def record(url, duration_min):
|
|||||||
"-reconnect_delay_max 30",
|
"-reconnect_delay_max 30",
|
||||||
"--downloader-args", f"ffmpeg_o:-t {duration_min * 60}",
|
"--downloader-args", f"ffmpeg_o:-t {duration_min * 60}",
|
||||||
"-o", out,
|
"-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"recording -> {os.path.basename(out)} (cap {duration_min} min)")
|
||||||
log(f" yt-dlp log -> {os.path.basename(logfile)}")
|
log(f" yt-dlp log -> {os.path.basename(logfile)}")
|
||||||
with open(logfile, "w", encoding="utf-8") as fh:
|
with open(logfile, "w", encoding="utf-8") as fh:
|
||||||
@@ -213,6 +247,9 @@ def main():
|
|||||||
ap.add_argument("--duration", type=int, default=150,
|
ap.add_argument("--duration", type=int, default=150,
|
||||||
help="recording cap in minutes")
|
help="recording cap in minutes")
|
||||||
ap.add_argument("--poll", type=int, default=60, help="seconds between attempts")
|
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()
|
args = ap.parse_args()
|
||||||
|
|
||||||
if args.at:
|
if args.at:
|
||||||
@@ -244,7 +281,7 @@ def main():
|
|||||||
time.sleep(args.poll)
|
time.sleep(args.poll)
|
||||||
continue
|
continue
|
||||||
log(" minted fresh master playlist")
|
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")
|
log(f" downloader pid {proc.pid}; waiting for it to finish")
|
||||||
t0 = time.time()
|
t0 = time.time()
|
||||||
proc.wait()
|
proc.wait()
|
||||||
|
|||||||
Reference in New Issue
Block a user