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]) + "\n" - txt += " \n" - for u in stream_urls: - txt += ' ' + xmlentities(u) + '' + "\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" - - # 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]) + "\n" + txt += " \n" + for u in stream_urls: + txt += ' ' + xmlentities(u) + '' + "\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" + + # 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"))