1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
|
"""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])
|