︙ | | | ︙ | |
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
|
"audio/xm+zip": "mod",
}
# Player command placeholders for playlist formats
placeholder_map = dict(
pls = "(%url | %pls | %u | %l | %r) \\b",
m3u = "(%m3u | %f | %g | %m) \\b",
srv = "(%srv | %d | %s) \\b",
)
# Playlist format content probing (assert type)
playlist_content_map = [
("pls", r""" (?i)\[playlist\].*numberofentries """),
("xspf", r""" <\?xml .* <playlist .* http://xspf\.org/ns/0/ """),
("m3u", r""" ^ \s* #(EXT)?M3U """),
("asx" , r""" <ASX\b """),
("smil", r""" <smil[^>]*> .* <seq> """),
("html", r""" <(audio|video)\b[^>]+\bsrc\s*=\s*["']?https?:// """),
("wpl", r""" <\?wpl \s+ version="1\.0" \s* \?> """),
("b4s", r""" <WinampXML> """), # http://gonze.com/playlists/playlist-format-survey.html
("jspf", r""" ^ \s* \{ \s* "playlist": \s* \{ """),
("json", r""" "url": \s* "\w+:// """),
("href", r""" .* """),
|
>
>
>
>
|
|
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
|
"audio/xm+zip": "mod",
}
# Player command placeholders for playlist formats
placeholder_map = dict(
pls = "(%url | %pls | %u | %l | %r) \\b",
m3u = "(%m3u | %f | %g | %m) \\b",
xspf= "(%xspf | %xpsf | %x) \\b",
jspf= "(%jspf | %j) \\b",
asx = "(%asx) \\b",
smil= "(%smil) \\b",
srv = "(%srv | %d | %s) \\b",
)
# Playlist format content probing (assert type)
playlist_content_map = [
("pls", r""" (?i)\[playlist\].*numberofentries """),
("xspf", r""" <\?xml .* <playlist .* http://xspf\.org/ns/0/ """),
("m3u", r""" ^ \s* #(EXT)?M3U """),
("asx" , r""" <asx\b """),
("smil", r""" <smil[^>]*> .* <seq> """),
("html", r""" <(audio|video)\b[^>]+\bsrc\s*=\s*["']?https?:// """),
("wpl", r""" <\?wpl \s+ version="1\.0" \s* \?> """),
("b4s", r""" <WinampXML> """), # http://gonze.com/playlists/playlist-format-survey.html
("jspf", r""" ^ \s* \{ \s* "playlist": \s* \{ """),
("json", r""" "url": \s* "\w+:// """),
("href", r""" .* """),
|
︙ | | | ︙ | |
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
|
# ยท But returns a list of [urls] after playlist extraction.
# ยท If repackaging as .m3u/.pls/.xspf, returns the local [fn].
#
def convert_playlist(url, source, dest, local_file=True, title=""):
urls = []
debug(dbg.PROC, "convert_playlist(", url, source, dest, ")")
# Leave alone if format matches, or if "srv" URL class, or if it's a local path already
if source == dest or source in ("srv", "href") or not re.search("\w+://", url):
return [url]
# Retrieve from URL
(mime, cnt) = http_probe_get(url)
# Leave streaming server as is
if mime == "srv":
|
|
|
|
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
|
# ยท But returns a list of [urls] after playlist extraction.
# ยท If repackaging as .m3u/.pls/.xspf, returns the local [fn].
#
def convert_playlist(url, source, dest, local_file=True, title=""):
urls = []
debug(dbg.PROC, "convert_playlist(", url, source, dest, ")")
# Leave alone if format matches, or if "srv" URL class, or if not http (local path, mms:/rtsp:)
if source == dest or source in ("srv", "href") or not re.match("(https?|spdy)://", url):
return [url]
# Retrieve from URL
(mime, cnt) = http_probe_get(url)
# Leave streaming server as is
if mime == "srv":
|
︙ | | | ︙ | |
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
|
break # with `probe` set
# Check ambiguity (except pseudo extension)
if len(set([source, mime, probe])) > 1:
debug(dbg.ERR, "Possible playlist format mismatch:", (source, mime, probe, ext))
# Extract URLs from content
for fmt in [ "pls", "asx", "raw" ]:
if not urls and fmt in (source, mime, probe, ext):
urls = extract_playlist(source).format(fmt)
debug(dbg.DATA, "conversion from:", source, " to dest:", fmt, "got URLs=", urls)
# Return original, or asis for srv targets
if not urls:
return [url]
elif dest in ("srv", "href"):
|
|
|
|
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
|
break # with `probe` set
# Check ambiguity (except pseudo extension)
if len(set([source, mime, probe])) > 1:
debug(dbg.ERR, "Possible playlist format mismatch:", (source, mime, probe, ext))
# Extract URLs from content
for fmt in [ "pls", "xspf", "asx", "smil", "jspf", "m3u", "json", "raw" ]:
if not urls and fmt in (source, mime, probe, ext, "raw"):
urls = extract_playlist(source).format(fmt)
debug(dbg.DATA, "conversion from:", source, " to dest:", fmt, "got URLs=", urls)
# Return original, or asis for srv targets
if not urls:
return [url]
elif dest in ("srv", "href"):
|
︙ | | | ︙ | |
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
|
#
class extract_playlist(object):
# Content of playlist file
src = ""
def __init__(self, text):
self.src = text
def format(self, fmt):
cnv = getattr(self, fmt)
return cnv()
# PLS
def pls(self):
return re.findall("\s*File\d*\s*=\s*(\w+://[^\s]+)", self.src, re.I)
# ASX
def asx(self):
return re.findall("<Ref\s+href=\"(http://.+?)\"", self.src)
# Regexp out any URL
def raw(self):
debug(dbg.WARN, "Raw playlist extraction")
return re.findall("([\w+]+://[^\s\"\'\>\#]+)", self.src)
# Save rows in one of the export formats.
# Takes a few combinations of parameters (either rows[], or urls[]+title),
# because it's used by playlist_convert() as well as the station saving.
#
class save_playlist(object):
|
>
>
|
|
<
>
|
|
|
<
<
|
>
>
>
>
>
|
<
<
<
<
|
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
|
#
class extract_playlist(object):
# Content of playlist file
src = ""
def __init__(self, text):
self.src = text
# Extract only URLs from given source type
def format(self, fmt):
debug(dbg.DATA, fmt)
return re.findall(self.extr_urls[fmt], self.src, re.X);
# Only look out for URLs, not local file paths
extr_urls = {
"pls": r" (?i) ^ \s*File\d* \s*=\s* (\w+://[^\s]+) ",
"m3u": r" (?m) ^( \w+:// [^#\n]+ )",
"xspf": r" (?x) <location> (\w+://[^<>\s]+) </location> ",
"asx": r" (?x) <ref \b[^>]+\b href \s*=\s* [\'\"] (\w+://[^\s\"\']+) [\'\"] ",
"smil": r" (?x) <(?:audio|video)\b [^>]+ \b src \s*=\s* [^\"\']? \s* (\w+://[^\"\'\s]+) ",
"jspf": r" (?x) \"location\" \s*:\s* \"(\w+://[^\"\s]+)\" ",
"json": r" (?x) \"url\" \s*:\s* \"(\w+://[^\"\s]+)\" ",
"raw": r" (?i) ( [\w+]+:// [^\s\"\'\>\#]+ ) ",
}
# Save rows in one of the export formats.
# Takes a few combinations of parameters (either rows[], or urls[]+title),
# because it's used by playlist_convert() as well as the station saving.
#
class save_playlist(object):
|
︙ | | | ︙ | |
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
|
return txt
# JSON (native lists of streamtuner2)
def json(self, rows):
return json.dumps(rows, indent=4)
#-- all others need rework --
# XSPF
def xspf(self, rows):
txt = '<?xml version="1.0" encoding="UTF-8"?>' + "\n"
txt += '<?http header="Content-Type: application/xspf+xml" ?>' + "\n"
txt += '<playlist version="1" xmlns="http://xspf.org/ns/0/">' + "\n"
txt += " <trackList>\n"
for row in rows:
for attr,tag in [("title","title"), ("homepage","info"), ("playing","annotation"), ("description","annotation")]:
if rows.get(attr):
txt += " <"+tag+">" + xmlentities(row[attr]) + "</"+tag+">\n"
u = row.get("url")
txt += ' <track><location>' + xmlentities(u) + '</location></track>' + "\n"
txt += " </trackList>\n</playlist>\n"
# JSPF
def jspf(self, rows):
pass
# ASX
def asx(self, rows):
for row in rows:
txt = "<ASX version=\"3.0\">\n" \
+ " <Title>" + xmlentities(row["title"]) + "</Title>\n" \
+ " <Entry>\n" \
+ " <Title>" + xmlentities(row["title"]) + "</Title>\n" \
+ " <MoreInfo href=\"" + row["homepage"] + "\"/>\n" \
+ " <Ref href=\"" + stream_urls[0] + "\"/>\n" \
+ " </Entry>\n</ASX>\n"
return txt
# SMIL
def smil(self, rows):
return "<smil>\n<head>\n <meta name=\"title\" content=\"" + xmlentities(row["title"]) + "\"/>\n</head>\n" \
+ "<body>\n <seq>\n <audio src=\"" + stream_urls[0] + "\"/>\n </seq>\n</body>\n</smil>\n"
# generate filename for temporary .m3u, if possible with unique id
def tmp_fn(pls):
# use shoutcast unique stream id if available
stream_id = re.search("http://.+?/.*?(\d+)", pls, re.M)
|
<
<
|
|
|
>
|
|
<
|
|
>
>
|
<
<
>
>
>
>
|
>
>
<
|
|
<
<
<
<
>
>
>
>
>
>
>
|
|
>
>
>
>
>
>
|
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
|
return txt
# JSON (native lists of streamtuner2)
def json(self, rows):
return json.dumps(rows, indent=4)
# XSPF
def xspf(self, rows):
return """<?xml version="1.0" encoding="UTF-8"?>\n""" \
+ """<?http header="Content-Type: application/xspf+xml" ?>\n""" \
+ """<playlist version="1" xmlns="http://xspf.org/ns/0/">\n\t<trackList>\n""" \
+ "".join("""\t\t<track>\n%s\t\t</track>\n""" % self.xspf_row(row, self.xspf_map) for row in rows) \
+ """\t</trackList>\n</playlist>\n"""
# individual tracks
def xspf_row(self, row, map):
return "".join("""\t\t\t<%s>%s</%s>\n""" % (tag, xmlentities(row[attr]), tag) for attr,tag in map.items() if row.get(attr))
# dict to xml tags
xspf_map = dict(title="title", url="location", homepage="info", playing="annotation", description="info")
# JSPF
def jspf(self, rows):
tracks = []
for row in rows:
tracks.append( { tag: row[attr] for attr,tag in self.xspf_map.items() if row.get(attr) } )
return json.dumps({ "playlist": { "track": tracks } }, indent=4)
# ASX
def asx(self, rows):
txt = """<asx version="3.0">\n"""
for row in rows:
txt += """\t<entry>\n\t\t<title>%s</title>\n\t\t<ref href="%s"/>\n\t</entry>\n""" % (xmlentities(row["title"]), xmlentities(row["url"]))
txt += """</asx>\n"""
return txt
# SMIL
def smil(self, rows):
txt = """<smil>\n<head>\n\t<meta name="title" content="%s""/>\n</head>\n<body>\n\t<seq>\n""" % (rows[0]["title"])
for row in rows:
if row.get("url"):
txt += """\t\t<audio src="%s"/>\n""" % row["url"]
txt += """\t</seq>\n</body>\n</smil>\n"""
return txt
# Stub import, only if needed
def xmlentities(s):
global xmlentities
from xml.sax.saxutils import escape as xmlentities
return xmlentities(s)
# generate filename for temporary .m3u, if possible with unique id
def tmp_fn(pls):
# use shoutcast unique stream id if available
stream_id = re.search("http://.+?/.*?(\d+)", pls, re.M)
|
︙ | | | ︙ | |