Check-in [1937c5766b]
Overview
Comment: | Fixed ASX and SMIL playlist exporting, allowed new placeholders %xspf, %jspf, %asx, %smil for application configuration. Documented in help/ pages. |
---|---|
Downloads: | Tarball | ZIP archive | SQL archive |
Timelines: | family | ancestors | descendants | both | action-mapfmts |
Files: | files | file ages | folders |
SHA1: |
1937c5766b8ddd88c304baac7cc206ee |
User & Date: | mario on 2015-04-10 02:36:21 |
Other Links: | branch diff | manifest | tags |
Context
2015-04-10
| ||
10:45 | Update comment on rewritten action module. Add alternative MIME types for m3u and asx, spport asf detection and extraction. Fix listformat→source arg. Move save() and filename handling out of save_playlist. Fix mediafmt_t lookup and print warning when there's an audio-response on playlist fetching (and it does happen). Change myoggradio plugin "format" population, and set listformat to "mixed(..)" for automatic probing. check-in: 223368ebbf user: mario tags: action-mapfmts | |
02:36 | Fixed ASX and SMIL playlist exporting, allowed new placeholders %xspf, %jspf, %asx, %smil for application configuration. Documented in help/ pages. check-in: 1937c5766b user: mario tags: action-mapfmts | |
2015-04-09
| ||
21:58 | Python3 doesn't like `if [x = ...]` inline assignment trickery (kwargs out of scope). check-in: 82cf514e49 user: mario tags: action-mapfmts | |
Changes
Modified action.py from [941a6a0502] to [f7b4eccd84].
︙ | ︙ | |||
63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 | "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 """), | > > > > | | 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 | # · 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, ")") | | | | 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 | 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 | | | | 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 | # class extract_playlist(object): # Content of playlist file src = "" def __init__(self, text): self.src = text def format(self, fmt): | > > | | < > | | | < < | > > > > > | < < < < | 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 | return txt # JSON (native lists of streamtuner2) def json(self, rows): return json.dumps(rows, indent=4) | < < | | | > | | < | | > > | < < > > > > | > > < | | < < < < > > > > > > > | | > > > > > > | 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) |
︙ | ︙ |
Modified help/config_apps.page from [eda5c4f097] to [d8a5ff9529].
︙ | ︙ | |||
12 13 14 15 16 17 18 | <p>On BSD/Linux systems there are a plethora of audio players. In streamtuner2 you can <link xref="configuration">configure</link> most of them as target application. Mostly it makes sense to use a single application for all audio formats. But at least the */* media type should be handled by a generic player, like vlc.</p> <table shade="rows" rules="rows cols"> | | | < | | 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | <p>On BSD/Linux systems there are a plethora of audio players. In streamtuner2 you can <link xref="configuration">configure</link> most of them as target application. Mostly it makes sense to use a single application for all audio formats. But at least the */* media type should be handled by a generic player, like vlc.</p> <table shade="rows" rules="rows cols"> <tr><td><p><app>Audacious</app></p></td> <td><p><cmd>audacious</cmd></p></td> <td><p>audio</p></td></tr> <tr><td><p><app>XMMS2</app></p></td> <td><p><cmd>xmms2 %m3u</cmd></p></td> <td><p>audio</p></td></tr> <tr><td><p><app>Amarok</app></p></td> <td><p><cmd>amarok -l %pls</cmd></p></td> <td><p>audio</p></td></tr> <tr><td><p><app>Exaile</app></p></td> <td><p><cmd>exaile</cmd></p></td> <td><p>audio</p></td></tr> <tr><td><p><app>mplayer</app></p></td> <td><p><cmd>mplayer %srv</cmd></p></td> <td><p>console</p></td></tr> <tr><td><p><app>VLC</app></p></td> <td><p><cmd>vlc %u</cmd></p></td> <td><p>video</p></td></tr> <tr><td><p><app>Totem</app></p></td> <td><p><cmd>totem %u</cmd></p></td> <td><p>video</p></td></tr> <tr><td><p><app>Media Player</app></p></td> <td><p><cmd>mplayer2.exe %asx</cmd></p></td> <td><p>Win32</p></td></tr> </table> <p>Some audio players open a second instance when you actually want to switch radios. In this case it's a common workaround to write <code>pkill vlc ; vlc %u</code> instead, which ends the previous player process and starts it anew. For VLC there's however also the <code>--one-instance</code> option, which sometimes works better. (And sometimes not.)</p> |
︙ | ︙ | |||
43 44 45 46 47 48 49 50 | <p>Any listed application can be invoked with a different kind of URL or filename. Most are rather flexible, but some depend on specific playlist file types or URLs. You can control this by adding a placeholder after the configured application name:</p> <table shade="rows" rules="rows cols"> <thead> <tr><td><p>Placeholder</p></td><td><p>Alternatives</p></td><td><p>URL/Filename type</p></td></tr> </thead> <tr><td><p>%m3u</p></td><td><p>%f %g %m</p></td><td><p>Provides a local .m3u file for the streaming station</p></td></tr> | > < > > > > | > > | > > > | 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 | <p>Any listed application can be invoked with a different kind of URL or filename. Most are rather flexible, but some depend on specific playlist file types or URLs. You can control this by adding a placeholder after the configured application name:</p> <table shade="rows" rules="rows cols"> <thead> <tr><td><p>Placeholder</p></td><td><p>Alternatives</p></td><td><p>URL/Filename type</p></td></tr> </thead> <tr><td><p>%pls</p></td><td><p>%url %u %r</p></td><td><p>Either a remote .pls resource (fastest), or a local .pls file (if converted)</p></td></tr> <tr><td><p>%m3u</p></td><td><p>%f %g %m</p></td><td><p>Provides a local .m3u file for the streaming station</p></td></tr> <tr><td><p>%srv</p></td><td><p>%d %s</p></td><td><p>Direct link to first streaming address, e.g. http://72.5.9.33:7500</p></td></tr> <tr><td><p>%xspf</p></td><td><p>%x</p></td><td><p>Xiph.org shareable playlist format (for modern players)</p></td></tr> <tr><td><p>%jspf</p></td><td><p>%j</p></td><td><p>JSON playlist format (widely unsupported)</p></td></tr> <tr><td><p>%asx</p></td><td><p></p></td><td><p>Some obscure Windows playlist format (don't use that)</p></td></tr> <tr><td><p>%smil</p></td><td><p></p></td><td><p>Standardized multimedia sequencing lists (which nobody uses either)</p></td></tr> </table> <p>Preferrably use the long %abbr names for configuration.</p> <note style="info"><p>Most audio players like pls, yet sometimes the older m3u format more. Streamripper requires %srv for recording.</p> <p>Leave it to the default %pls to avoid Streamtuner2 doing unneeded extra conversions (just delays playback).</p> </note> </section> </page> |
Modified help/configuration.page from [6c83ddf5a5] to [cec2e8132d].
︙ | ︙ | |||
56 57 58 59 60 61 62 | <p>Application names are most always lowercase binary names. Double click an entry to edit it. After editing the icon next to the application name will be updated. If it stays green, it's likely to work. If it turns red / into a stop symbol, then the entered name is likely incorrect.</p> <p><media type="image" src="img/configapps.png" mime="image/png" /></p> <p>After the application name, you can use a placeholder like "<var>%pls</var>" (default), | | | | 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 | <p>Application names are most always lowercase binary names. Double click an entry to edit it. After editing the icon next to the application name will be updated. If it stays green, it's likely to work. If it turns red / into a stop symbol, then the entered name is likely incorrect.</p> <p><media type="image" src="img/configapps.png" mime="image/png" /></p> <p>After the application name, you can use a placeholder like "<var>%pls</var>" (default), or "<var>%m3u</var>" and "<var>%srv</var>". See <link xref="config_apps#placeholders">placeholders</link>.</p> <p>Catch-all entries like <var>*/*</var> or a generic <var>audio/*</var> entry allow to configure a default player. While <var>video/youtube</var> is specific to the Youtube channel. And <var>url/http</var> a pseudo MIME type to configure a web browser (for station homepages).</p> <p>You can remove default entries by clearing both the Format field and its associated Application. Add completely new associations through the emtpy line. (Reopen the dialog to add another one.)</p> </section> <section id="recording"> |
︙ | ︙ |
Modified st2.py from [ad8ec4ecd9] to [707c0d6df4].
︙ | ︙ | |||
351 352 353 354 355 356 357 | entries = self.channel().stations() favicon.download_all(entries) # Save stream to file (.m3u) def save_as(self, widget): row = self.row() default_fn = row["title"] + ".m3u" | | | | 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 | entries = self.channel().stations() favicon.download_all(entries) # Save stream to file (.m3u) def save_as(self, widget): row = self.row() default_fn = row["title"] + ".m3u" fn = uikit.save_file("Save Stream", None, default_fn, [(".m3u","*m3u"),(".pls","*pls"),(".xspf","*xspf"),(".jspf","*jspf"),(".smil","*smil"),(".asx","*asx"),("all files","*")]) if fn: source = row.get("listformat", self.channel().listformat) dest = (re.findall("\.(m3u|pls|xspf|jspf|json|smil|asx|wpl)8?$", fn) or ["pls"])[0] action.save_playlist(source=source, multiply=True).store(rows=[row], fn=fn, dest=dest) pass # Save current stream URL into clipboard def menu_copy(self, w): gtk.clipboard_get().set_text(self.selected("url")) |
︙ | ︙ |