"""Second pass over the feeds whose ID3 tag did not fit the first window. Reads the declared tag size from the 10-byte header, then fetches exactly that much, so the answer does not depend on a guessed window. Also handles ID3v2.2, whose frame headers are 6 bytes with 3-byte ids rather than 10 with 4 -- misparsing those is what made some of them look truncated in the first pass. """ import sys import urllib.request import xml.etree.ElementTree as ET UA = ("Mozilla/5.0 (Linux; Android 14) AppleWebKit/537.36 (KHTML, like Gecko) " "Chrome/120.0.0.0 Mobile Safari/537.36 Listen/1.0") WANTED = { "Cruising the Waves Podcast", "Conversations with Tyler", "Darknet Diaries", "Dan Carlin's Hardcore History", "1619", "Common Sense with Dan Carlin", "Reply All", "Dan Carlin's Hardcore History: Addendum", "Flesh and Code", "Talk Easy with Sam Fragoso", "This American Life", "Hooked On Wooden Boats", "Humans", "Dream Chasers and Eccentrics", "Paranormal Investigations Hawaii", } def get(url, rangebytes=None, timeout=40): req = urllib.request.Request(url, headers={"User-Agent": UA}) if rangebytes: req.add_header("Range", f"bytes={rangebytes[0]}-{rangebytes[1]}") with urllib.request.urlopen(req, timeout=timeout) as r: return r.read() def enclosure_of(feed_xml): root = ET.fromstring(feed_xml) for item in root.iter(): if item.tag.split('}')[-1] not in ("item", "entry"): continue for child in item: if child.tag.split('}')[-1] == "enclosure": return child.get("url") return None def synchsafe(b): return (b[0] << 21) | (b[1] << 14) | (b[2] << 7) | b[3] def scan(blob, major): """Walk the frame list. v2.2 uses 3-byte ids and 3-byte sizes; v2.3/2.4 use 4 and 4.""" pos, ids = 10, [] while True: if major == 2: if pos + 6 > len(blob): break fid = blob[pos:pos + 3] if not fid.strip(b"\x00"): break size = int.from_bytes(blob[pos + 3:pos + 6], "big") step = 6 + size else: if pos + 10 > len(blob): break fid = blob[pos:pos + 4] if not fid.strip(b"\x00"): break raw = blob[pos + 4:pos + 8] size = synchsafe(raw) if major == 4 else int.from_bytes(raw, "big") step = 10 + size if size <= 0: break ids.append(fid.decode("latin-1")) pos += step return ids def check(name, url): feed = get(url) enclosure = enclosure_of(feed) if not enclosure: return name, "no enclosure", [] head = get(enclosure, rangebytes=(0, 9)) if not head.startswith(b"ID3"): return name, "no ID3 tag at all", [] major = head[3] size = synchsafe(head[6:10]) blob = get(enclosure, rangebytes=(0, 10 + size - 1)) got = len(blob) >= 10 + size ids = scan(blob, major) verdict = "CHAP PRESENT" if "CHAP" in ids else "no CHAP" detail = f"v2.{major}, tag {size / 1024:.0f}KB, {'complete' if got else 'SHORT'}, frames: {sorted(set(ids))}" return name, verdict, detail def main(path): tree = ET.parse(path) feeds = [(o.get("text"), o.get("xmlUrl")) for o in tree.iter("outline") if o.get("xmlUrl") and o.get("text") in WANTED] for name, url in feeds: try: n, verdict, detail = check(name, url) print(f"{verdict:<14} {n[:42]:<42} {detail}", flush=True) except Exception as e: print(f"{'error':<14} {name[:42]:<42} {type(e).__name__}", flush=True) if __name__ == "__main__": main(sys.argv[1])