Index: action.py
==================================================================
--- action.py
+++ action.py
@@ -13,295 +13,313 @@
# audio playlist format. It's audio/x-scpls mostly, seldomly m3u,
# but sometimes url/direct if the entry[url] directly leads to the
# streaming server.
#
# As fallback there is a regex which just looks for URLs in the
-# given resource (works for m3u/pls/xspf/asx/...). There is no
-# actual url "filename" extension guessing.
+# given resource (works for m3u/pls/xspf/asx/...).
import re
import os
-import ahttp as http
-from config import conf, __print__, dbg
+from ahttp import fix_url as http_fix_url, get as http_get
+from config import conf, __print__ as debug, dbg
import platform
-# coupling to main window
-main = None
-
-
-#-- media actions
-#
-# implements "play" and "record" methods,
-# but also "browser" for web URLs
-#
-class action:
-
- # streamlink map
- lt = dict(
- asx = "video/x-ms-asf",
- pls = "audio/x-scpls",
- m3u = "audio/x-mpegurl",
- xspf = "application/xspf+xml",
- href = "url/http",
- src = "url/direct",
- ram = "audio/x-pn-realaudio",
- smil = "application/smil",
- )
- # media map
- mf = dict(
- mp3 = "audio/mpeg",
- ogg = "audio/ogg",
- aac = "audio/aac",
- )
-
-
- # web
- @staticmethod
- def browser(url):
- bin = conf.play.get("url/http", "sensible-browser")
- __print__( dbg.CONF, bin )
- action.run(bin + " " + action.quote(url))
-
-
-
- # os shell cmd escaping
- @staticmethod
- def quote(s):
- if conf.windows:
- return str(s) # should actually be "\\\"%s\\\"" % s
- else:
- return "%r" % str(s)
-
-
- # calls player for stream url and format
- @staticmethod
- def play(url, audioformat="audio/mpeg", listformat="text/x-href"):
- if (url):
- url = action.url(url, listformat)
- if audioformat == "audio/mp3":
- audioformat = "audio/mpeg"
- cmd = action.mime_match(audioformat, conf.play)
- try:
- __print__( dbg.PROC, "play", url, cmd )
- action.run( action.interpol(cmd, url) )
- except:
- pass
-
-
- # exec wrapper
- @staticmethod
- def run(cmd):
- if conf.windows:
- os.system("start \"%s\"")
- else:
- os.system(cmd + " &")
-
-
- # streamripper
- @staticmethod
- def record(url, audioformat="audio/mpeg", listformat="text/x-href", append="", row={}):
- __print__( dbg.PROC, "record", url )
- cmd = action.mime_match(audioformat, conf.record)
- try: action.run( action.interpol(cmd, url, row) + append )
- except: pass
-
-
- # Convert MIME type into list of ["audio/xyz", "audio/*", "*/*"] for comparison against record/play association
- @staticmethod
- def mime_match(fmt, cmd_list):
- for match in [ fmt, fmt[:fmt.find("/")] + "/*", "*/*" ]:
- if cmd_list.get(match, None):
- return cmd_list[match]
-
-
- # save as .m3u
- @staticmethod
- def save(row, fn, listformat="audio/x-scpls"):
-
- # output format
- format = re.findall("\.(m3u|pls|xspf|jspf|json|asx|smil)", fn)
-
- # modify stream url
- row["url"] = action.url(row["url"], listformat)
- stream_urls = action.extract_urls(row["url"], listformat)
-
- # M3U
- if "m3u" in format:
- txt = "#M3U\n"
- for url in stream_urls:
- txt += http.fix_url(url) + "\n"
-
- # PLS
- elif "pls" in format:
- txt = "[playlist]\n" + "numberofentries=1\n"
- for i,u in enumerate(stream_urls):
- i = str(i + 1)
- txt += "File"+i + "=" + u + "\n"
- txt += "Title"+i + "=" + row["title"] + "\n"
- txt += "Length"+i + "=-1\n"
- txt += "Version=2\n"
-
- # XSPF
- elif "xspf" in format:
- txt = '' + "\n"
- txt += '' + "\n"
- txt += '' + "\n"
- for attr,tag in [("title","title"), ("homepage","info"), ("playing","annotation"), ("description","annotation")]:
- if row.get(attr):
- txt += " <"+tag+">" + xmlentities(row[attr]) + ""+tag+">\n"
- txt += " \n"
- for u in stream_urls:
- txt += ' ' + "\n"
- txt += " \n\n"
-
- # JSPF
- elif "jspf" in format:
- pass
-
- # JSON
- elif "json" in format:
- row["stream_urls"] = stream_urls
- txt = str(row) # pseudo-json (python format)
-
- # ASX
- elif "asx" in format:
- txt = "\n" \
- + " " + xmlentities(row["title"]) + "\n" \
- + " \n" \
- + " " + xmlentities(row["title"]) + "\n" \
- + " \n" \
- + " \n" \
- + " \n\n"
-
- # SMIL
- elif "smil" in format:
- txt = "\n\n \n\n" \
- + "\n \n \n \n\n\n"
-
- # unknown
- else:
- return
-
- # write
- if txt:
- f = open(fn, "wb")
- f.write(txt)
- f.close()
- pass
-
-
- # replaces instances of %u, %l, %pls with urls / %g, %f, %s, %m, %m3u or local filenames
- @staticmethod
- def interpol(cmd, url, row={}):
- # inject other meta fields
- if row:
- for field in row:
- cmd = cmd.replace("%"+field, "%r" % row.get(field))
- # add default if cmd has no %url placeholder
- if cmd.find("%") < 0:
- cmd = cmd + " %m3u"
- # standard placeholders
- if (re.search("%(url|pls|[ulr])", cmd)):
- cmd = re.sub("%(url|pls|[ulr])", action.quote(url), cmd)
- if (re.search("%(m3u|[fgm])", cmd)):
- cmd = re.sub("%(m3u|[fgm])", action.quote(action.m3u(url)), cmd)
- if (re.search("%(srv|[ds])", cmd)):
- cmd = re.sub("%(srv|[ds])", action.quote(action.srv(url)), cmd)
- return cmd
-
-
- # eventually transforms internal URN/IRI to URL
- @staticmethod
- def url(url, listformat):
- if (listformat == "audio/x-scpls"):
- url = url
- elif (listformat == "text/x-urn-streamtuner2-script"):
- url = main.special.stream_url(url)
- else:
- url = url
- return url
-
-
- # download a .pls resource and extract urls
- @staticmethod
- def pls(url):
- text = http.get(url)
- __print__( dbg.DATA, "pls_text=", text )
- return re.findall("\s*File\d*\s*=\s*(\w+://[^\s]+)", text, re.I)
- # currently misses out on the titles
-
- # get a single direct ICY stream url (extract either from PLS or M3U)
- @staticmethod
- def srv(url):
- return action.extract_urls(url)[0]
-
-
- # retrieve real stream urls from .pls or .m3u links
- @staticmethod
- def extract_urls(pls, listformat="__not_used_yet__"):
- # extract stream address from .pls URL
- if (re.search("\.pls", pls)): #audio/x-scpls
- return action.pls(pls)
- elif (re.search("\.asx", pls)): #video/x-ms-asf
- return re.findall("[ 3 and stream_id != "XXXXXX")
-
- # check if there are any urls in a given file
- @staticmethod
- def has_urls(tmp_fn):
- if os.path.exists(tmp_fn):
- return open(tmp_fn, "r").read().find("http://") > 0
-
-
- # create a local .m3u file from it
- @staticmethod
- def m3u(pls):
-
- # temp filename
- (tmp_fn, unique) = action.tmp_fn(pls)
- # does it already exist?
- if tmp_fn and unique and conf.reuse_m3u and action.has_urls(tmp_fn):
- return tmp_fn
-
- # download PLS
- __print__( dbg.DATA, "pls=",pls )
- url_list = action.extract_urls(pls)
- __print__( dbg.DATA, "urls=", url_list )
-
- # output URL list to temporary .m3u file
- if (len(url_list)):
- #tmp_fn =
- f = open(tmp_fn, "w")
- f.write("#M3U\n")
- f.write("\n".join(url_list) + "\n")
- f.close()
- # return path/name of temporary file
- return tmp_fn
- else:
- __print__( dbg.ERR, "error, there were no URLs in ", pls )
- raise "Empty PLS"
-
-
- # open help browser
- @staticmethod
- def help(*args):
- action.run("yelp /usr/share/doc/streamtuner2/help/")
+# Coupling to main window
+#
+main = None
+
+
+
+# Streamlink/listformat mapping
+#
+lt = dict(
+ pls = "audio/x-scpls",
+ m3u = "audio/x-mpegurl",
+ asx = "video/x-ms-asf",
+ xspf = "application/xspf+xml",
+ href = "url/http",
+ srv = "url/direct",
+ ram = "audio/x-pn-realaudio",
+ smil = "application/smil",
+ script = "text/x-urn-streamtuner2-script", # unused
+)
+
+
+# Audio type MIME map
+#
+mf = dict(
+ mp3 = "audio/mpeg",
+ ogg = "audio/ogg",
+ aac = "audio/aac",
+ midi = "audio/midi",
+ mod = "audio/mod",
+)
+
+# Player command placeholders for playlist formats
+placeholder_map = dict(
+ pls = "%url | %pls | %u | %l | %r",
+ m3u = "%m3u | %f | %g | %m",
+ pls = "%srv | %d | %s",
+)
+
+
+
+# Exec wrapper
+#
+def run(cmd):
+ if cmd: debug(dbg.PROC, "Exec:", cmd)
+ try: os.system("start \"%s\"" % cmd if conf.windows else cmd + " &")
+ except: debug(dbg.ERR, "Command not found:", cmd)
+
+
+# Start web browser
+#
+def browser(url):
+ bin = conf.play.get("url/http", "sensible-browser")
+ run(bin + " " + quote(url))
+
+
+# Open help browser, streamtuner2 pages
+#
+def help(*args):
+ run("yelp /usr/share/doc/streamtuner2/help/")
+
+
+# Calls player for stream url and format
+#
+def play(url, audioformat="audio/mpeg", listformat="href"):
+ cmd = mime_app(audioformat, conf.play)
+ cmd = interpol(cmd, url, listformat)
+ run(cmd)
+
+
+# Call streamripper
+#
+def record(url, audioformat="audio/mpeg", listformat="href", append="", row={}):
+ cmd = mime_app(audioformat, conf.record)
+ cmd = interpol(cmd, url, listformat, row)
+ run(cmd)
+
+
+# OS shell command escaping
+#
+def quote(s):
+ return "%r" % str(s)
+
+
+# Convert e.g. "text/x-scpls" MIME types to just "pls" monikers
+#
+def listfmt(t = "pls"):
+ if t in lf.values():
+ for short,mime in lf.items():
+ if mime == t:
+ return short
+ return t # "pls"
+
+
+# Convert MIME type into list of ["audio/xyz", "audio/*", "*/*"]
+# for comparison against configured record/play association.
+def mime_app(fmt, cmd_list):
+ for match in [ fmt, fmt[:fmt.find("/")] + "/*", "*/*" ]:
+ if cmd_list.get(match):
+ return cmd_list[match]
+
+
+
+# Replaces instances of %m3u, %pls, %srv in a command string.
+# · Also understands short aliases %l, %f, %d.
+# · And can embed %title or %genre placeholders.
+# · Replace .pls URL with local .m3u file depending on map.
+#
+def interpol(cmd, url, source="pls", row={}):
+
+ # inject other meta fields
+ if row:
+ for field in row:
+ cmd = cmd.replace("%"+field, "%r" % row.get(field))
+
+ # add default if cmd has no %url placeholder
+ if cmd.find("%") < 0:
+ cmd = cmd + " %m3u"
+
+ # standard placeholders
+ for dest, rx in placeholder_map.items():
+ if re.search(rx, cmd, re.X):
+ # from .pls to .m3u
+ url = convert_playlist(url, listfmt(source), listfmt(dest))
+ # insert quoted URL/filepath
+ return re.sub(rx, cmd, quote(url), 2, re.X)
+
+ return "false"
+
+
+# Substitute .pls URL with local .m3u,
+# or direct srv address, or leave as-is.
+#
+def convert_playlist(url, source, dest):
+
+ # Leave alone
+ if source == dest or source in ("srv", "href"):
+ return url
+
+ # Else
+ return url
+
+
+
+# Save row(s) in one of the export formats,
+# depending on file extension:
+#
+# · m3u
+# · pls
+# · xspf
+# · asx
+# · json
+# · smil
+#
+def save(row, fn, listformat="audio/x-scpls"):
+
+ # output format
+ format = re.findall("\.(m3u|pls|xspf|jspf|json|asx|smil)", fn)
+
+ # modify stream url
+ stream_urls = extract_urls(row["url"], listformat)
+
+ # M3U
+ if "m3u" in format:
+ txt = "#M3U\n"
+ for url in stream_urls:
+ txt += http_fix_url(url) + "\n"
+
+ # PLS
+ elif "pls" in format:
+ txt = "[playlist]\n" + "numberofentries=1\n"
+ for i,u in enumerate(stream_urls):
+ i = str(i + 1)
+ txt += "File"+i + "=" + u + "\n"
+ txt += "Title"+i + "=" + row["title"] + "\n"
+ txt += "Length"+i + "=-1\n"
+ txt += "Version=2\n"
+
+ # XSPF
+ elif "xspf" in format:
+ txt = '' + "\n"
+ txt += '' + "\n"
+ txt += '' + "\n"
+ for attr,tag in [("title","title"), ("homepage","info"), ("playing","annotation"), ("description","annotation")]:
+ if row.get(attr):
+ txt += " <"+tag+">" + xmlentities(row[attr]) + ""+tag+">\n"
+ txt += " \n"
+ for u in stream_urls:
+ txt += ' ' + "\n"
+ txt += " \n\n"
+
+ # JSPF
+ elif "jspf" in format:
+ pass
+
+ # JSON
+ elif "json" in format:
+ row["stream_urls"] = stream_urls
+ txt = str(row) # pseudo-json (python format)
+
+ # ASX
+ elif "asx" in format:
+ txt = "\n" \
+ + " " + xmlentities(row["title"]) + "\n" \
+ + " \n" \
+ + " " + xmlentities(row["title"]) + "\n" \
+ + " \n" \
+ + " ]\n" \
+ + " \n\n"
+
+ # SMIL
+ elif "smil" in format:
+ txt = "\n\n \n\n" \
+ + "\n \n \n \n\n\n"
+
+ # unknown
+ else:
+ return
+
+ # write
+ if txt:
+ f = open(fn, "wb")
+ f.write(txt)
+ f.close()
+ pass
+
+
+
+
+# retrieve real stream urls from .pls or .m3u links
+def extract_urls(pls, listformat="__not_used_yet__"):
+ # extract stream address from .pls URL
+ if (re.search("\.pls", pls)): #audio/x-scpls
+ return pls(pls)
+ elif (re.search("\.asx", pls)): #video/x-ms-asf
+ return re.findall("[ 3 and stream_id != "XXXXXX")
+
+# check if there are any urls in a given file
+def has_urls(tmp_fn):
+ if os.path.exists(tmp_fn):
+ return open(tmp_fn, "r").read().find("http://") > 0
+
+
+# create a local .m3u file from it
+def m3u(pls):
+
+ # temp filename
+ (tmp_fn, unique) = tmp_fn(pls)
+ # does it already exist?
+ if tmp_fn and unique and conf.reuse_m3u and has_urls(tmp_fn):
+ return tmp_fn
+
+ # download PLS
+ debug( dbg.DATA, "pls=",pls )
+ url_list = extract_urls(pls)
+ debug( dbg.DATA, "urls=", url_list )
+
+ # output URL list to temporary .m3u file
+ if (len(url_list)):
+ #tmp_fn =
+ f = open(tmp_fn, "w")
+ f.write("#M3U\n")
+ f.write("\n".join(url_list) + "\n")
+ f.close()
+ # return path/name of temporary file
+ return tmp_fn
+ else:
+ debug( dbg.ERR, "error, there were no URLs in ", pls )
+ raise "Empty PLS"
+
+# Download a .pls resource and extract urls
+def extract_from_pls(url):
+ text = http_get(url)
+ debug(dbg.DATA, "pls_text=", text)
+ return re.findall("\s*File\d*\s*=\s*(\w+://[^\s]+)", text, re.I)
+ # currently misses out on the titles
+
+
+# get a single direct ICY stream url (extract either from PLS or M3U)
+def srv(url):
+ return extract_urls(url)[0]
Index: channels/__init__.py
==================================================================
--- channels/__init__.py
+++ channels/__init__.py
@@ -451,11 +451,11 @@
# parameters
audioformat = row.get("format", self.audioformat)
listformat = row.get("listformat", self.listformat)
# invoke audio player
- action.action.play(row["url"], audioformat, listformat)
+ action.play(row["url"], audioformat, listformat)
#--------------------------- utility functions -----------------------
@@ -478,11 +478,11 @@
"aac+":"aac", "aacp":"aac",
"realaudio":"x-pn-realaudio", "real":"x-pn-realaudio", "ra":"x-pn-realaudio", "ram":"x-pn-realaudio", "rm":"x-pn-realaudio",
# yes, we do video
"flv":"video/flv", "mp4":"video/mp4",
}
- map.update(action.action.lt) # list type formats (.m3u .pls and .xspf)
+ map.update(action.lt) # list type formats (.m3u .pls and .xspf)
if map.get(s):
s = map[s]
# add prefix:
if s.find("/") < 1:
s = "audio/" + s
Index: channels/global_key.py
==================================================================
--- channels/global_key.py
+++ channels/global_key.py
@@ -67,13 +67,13 @@
elif self.last < 0:
self.last = len(streams)-1
# play
i = self.last
- action.action.play(streams[i]["url"], streams[i]["format"])
+ action.play(streams[i]["url"], streams[i]["format"])
# set pointer in gtk.TreeView
if self.parent.channels[channel].current == cat:
self.parent.channels[channel].gtk_list.get_selection().select_path(i)
Index: channels/live365.py
==================================================================
--- channels/live365.py
+++ channels/live365.py
@@ -133,34 +133,6 @@
if kv[0] == "stationName":
self.gi += 1
return self.gi
- # inject session id etc. into direct audio url
- def UNUSED_play(self, row):
- if row.get("url"):
-
- # params
- id = row["id"]
- name = row["name"]
-
- # get mini.cgi station resource
- mini_url = "http://www.live365.com/cgi-bin/mini.cgi?version=3&templateid=xml&from=web&site=web" \
- + "&caller=&tag=web&station_name=%s&_=%i111" % (name, time())
- mini_r = http.get(mini_url, content=False)
- mini_xml = parseString(mini_r.text).getElementsByTagName("LIVE365_PLAYER_WINDOW")[0]
- mini = lambda name: mini_xml.getElementsByTagName(name)[0].childNodes[0].data
-
- # authorize with play.cgi
- play_url = ""
-
- # mk audio url
- play = "http://%s/play" % mini("STREAM_URL") \
- + "?now=0&" \
- + mini("NANOCASTER_PARAMS") \
- + "&token=" + mini("TOKEN") \
- + "&AuthType=NORMAL&lid=276006-deu&SaneID=178.24.130.71-1406763621701"
-
- # let's see what happens
- action.action.play(play, self.mediatype, self.listformat)
-
Index: channels/myoggradio.py
==================================================================
--- channels/myoggradio.py
+++ channels/myoggradio.py
@@ -27,11 +27,11 @@
#
from channels import *
from config import *
-from action import action
+import action
from uikit import uikit
import ahttp as http
import re
import json
Index: channels/punkcast.py
==================================================================
--- channels/punkcast.py
+++ channels/punkcast.py
@@ -88,11 +88,11 @@
# look up ANY audio url
for uu in rx_sound.findall(html):
__print__( dbg.DATA, uu )
(url, fmt) = uu
- action.action.play(url, self.mime_fmt(fmt), "url/direct")
+ action.play(url, self.mime_fmt(fmt), "url/direct")
return
# or just open webpage
- action.action.browser(row["homepage"])
+ action.browser(row["homepage"])
Index: channels/timer.py
==================================================================
--- channels/timer.py
+++ channels/timer.py
@@ -23,11 +23,11 @@
from config import *
from channels import *
import bundle.kronos as kronos # Doesn't work with Python3
from uikit import uikit
-from action import action
+import action
import copy
import re
Index: st2.py
==================================================================
--- st2.py
+++ st2.py
@@ -106,11 +106,10 @@
}
gui_startup(4/20.0)
# early module coupling
action.main = self # action (play/record) module needs a reference to main window for gtk interaction and some URL/URI callbacks
- self.action = action.action # shorter name (could also become a features. entry...)
ahttp.feedback = self.status # http module gives status feedbacks too
# append other channel modules and plugins
self.load_plugin_channels()
@@ -180,14 +179,14 @@
"update_favicons": self.update_favicons,
"app_state": self.app_state,
"bookmark": self.bookmark,
"save_as": self.save_as,
"menu_about": lambda w: AboutStreamtuner2(self),
- "menu_help": self.action.help,
- "menu_onlineforum": lambda w: self.action.browser("http://sourceforge.net/projects/streamtuner2/forums/forum/1173108"),
- "menu_fossilwiki": lambda w: self.action.browser("http://fossil.include-once.org/streamtuner2/"),
- "menu_projhomepage": lambda w: self.action.browser("http://milki.include-once.org/streamtuner2/"),
+ "menu_help": action.help,
+ "menu_onlineforum": lambda w: action.browser("http://sourceforge.net/projects/streamtuner2/forums/forum/1173108"),
+ "menu_fossilwiki": lambda w: action.browser("http://fossil.include-once.org/streamtuner2/"),
+ "menu_projhomepage": lambda w: action.browser("http://milki.include-once.org/streamtuner2/"),
# "menu_bugreport": lambda w: BugReport(),
"menu_copy": self.menu_copy,
"delete_entry": self.delete_entry,
# search dialog
"quicksearch_set": self.search.quicksearch_set,
@@ -291,24 +290,24 @@
[callback(row) for callback in self.hooks["play"]]
# Recording: invoke streamripper for current stream URL
def on_record_clicked(self, widget):
row = self.row()
- self.action.record(row.get("url"), row.get("format", "audio/mpeg"), "url/direct", row=row)
+ action.record(row.get("url"), row.get("format", "audio/mpeg"), "url/direct", row=row)
# Open stream homepage in web browser
def on_homepage_stream_clicked(self, widget):
url = self.selected("homepage")
- if url and len(url): self.action.browser(url)
+ if url and len(url): action.browser(url)
else: self.status("No homepage URL present.")
# Browse to channel homepage (double click on notebook tab)
def on_homepage_channel_clicked(self, widget, event=2):
if event == 2 or event.type == gtk.gdk._2BUTTON_PRESS:
__print__(dbg.UI, "dblclick")
url = self.channel().meta.get("url", "https://duckduckgo.com/?q=" + self.channel().module)
- self.action.browser(url)
+ action.browser(url)
# Reload stream list in current channel-category
def on_reload_clicked(self, widget=None, reload=1):
__print__(dbg.UI, "on_reload_clicked()", "reload=", reload, "current_channel=", self.current_channel, "c=", self.channels[self.current_channel], "cat=", self.channel().current)
category = self.channel().current
@@ -356,11 +355,11 @@
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","*")])
if fn:
- self.action.save(row, fn)
+ action.save(row, fn)
pass
# Save current stream URL into clipboard
def menu_copy(self, w):
gtk.clipboard_get().set_text(self.selected("url"))
]