Index: action.py
==================================================================
--- action.py
+++ action.py
@@ -65,19 +65,23 @@
# 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 .* ]*> .* """),
("html", r""" <(audio|video)\b[^>]+\bsrc\s*=\s*["']?https?:// """),
("wpl", r""" <\?wpl \s+ version="1\.0" \s* \?> """),
("b4s", r""" """), # http://gonze.com/playlists/playlist-format-survey.html
("jspf", r""" ^ \s* \{ \s* "playlist": \s* \{ """),
@@ -184,12 +188,12 @@
#
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):
+ # 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)
@@ -212,12 +216,12 @@
# 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):
+ 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:
@@ -269,26 +273,27 @@
# Content of playlist file
src = ""
def __init__(self, text):
self.src = text
+
+ # Extract only URLs from given source type
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("\#]+)", self.src)
+ 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) (\w+://[^<>\s]+) ",
+ "asx": r" (?x) ]+\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.
@@ -367,45 +372,57 @@
# 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 = '' + "\n"
- txt += '' + "\n"
- txt += '' + "\n"
- txt += " \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 += ' ' + "\n"
- txt += " \n\n"
+ return """\n""" \
+ + """\n""" \
+ + """\n\t\n""" \
+ + "".join("""\t\t\n""" % self.xspf_row(row, self.xspf_map) for row in rows) \
+ + """\t\n\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):
- pass
+ 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 = """\n"""
for row in rows:
- txt = "\n" \
- + " " + xmlentities(row["title"]) + "\n" \
- + " \n" \
- + " " + xmlentities(row["title"]) + "\n" \
- + " \n" \
- + " \n" \
- + " \n\n"
+ txt += """\t\n\t\t%s\n\t\t\n\t\n""" % (xmlentities(row["title"]), xmlentities(row["url"]))
+ txt += """\n"""
return txt
+
# SMIL
def smil(self, rows):
- return "\n\n \n\n" \
- + "\n \n \n \n\n\n"
+ txt = """\n\n\t\n\n\n\t\n""" % (rows[0]["title"])
+ for row in rows:
+ if row.get("url"):
+ txt += """\t\t\n""" % row["url"]
+ txt += """\t\n\n\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):
Index: help/config_apps.page
==================================================================
--- help/config_apps.page
+++ help/config_apps.page
@@ -14,19 +14,18 @@
configure 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.
-
Audacious
audacious %m3u
audio
+
Audacious
audacious
audio
XMMS2
xmms2 %m3u
audio
Amarok
amarok -l %pls
audio
-
Exaile
exaile %m3u
audio
-
Amarok
amarok -l %pls
audio
+
Exaile
exaile
audio
mplayer
mplayer %srv
console
VLC
vlc %u
video
Totem
totem %u
video
-
Media Player
mplayer2.exe %pls
Win32
+
Media Player
mplayer2.exe %asx
Win32
Some audio players open a second instance when you actually want to switch radios.
In this case it's a common workaround to write pkill vlc ; vlc %u instead,
which ends the previous player process and starts it anew.
@@ -45,16 +44,25 @@
specific playlist file types or URLs. You can control this by adding
a placeholder after the configured application name:
Placeholder
Alternatives
URL/Filename type
+
%pls
%url %u %r
Either a remote .pls resource (fastest), or a local .pls file (if converted)
%m3u
%f %g %m
Provides a local .m3u file for the streaming station
-
%pls
%url %u %r
Either a remote .pls resource, or a local .pls file (if converted)
%srv
%d %s
Direct link to first streaming address, e.g. http://72.5.9.33:7500
+
%xspf
%x
Xiph.org shareable playlist format (for modern players)
+
%jspf
%j
JSON playlist format (widely unsupported)
+
%asx
Some obscure Windows playlist format (don't use that)
You sould preferrably use the long forms. Most audio players like
- %m3u most, while streamripper needs %srv for recording.
+
Preferrably use the long %abbr names for configuration.
+
+
Most audio players like pls, yet sometimes the
+ older m3u format more. Streamripper requires %srv for recording.
+
+
Leave it to the default %pls to avoid Streamtuner2 doing unneeded
+ extra conversions (just delays playback).
Index: help/configuration.page
==================================================================
--- help/configuration.page
+++ help/configuration.page
@@ -58,14 +58,14 @@
likely to work. If it turns red / into a stop symbol, then the entered name is likely incorrect.
After the application name, you can use a placeholder like "%pls" (default),
- or "%m3u" and "%src". See placeholders.
+ or "%m3u" and "%srv". See placeholders.
Catch-all entries like */* or a generic audio/* entry allow to configure a default player.
- While video/youtube is specific to the Youtube channel. And url/http a psdeudo MIME type
+ While video/youtube is specific to the Youtube channel. And url/http a pseudo MIME type
to configure a web browser (for station homepages).
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.)
Index: st2.py
==================================================================
--- st2.py
+++ st2.py
@@ -353,14 +353,14 @@
# 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"),(".smil","*smil"),(".asx","*asx"),("all files","*")])
+ 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|wpl)8?$", fn)[0]
+ 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):