Index: action.py ================================================================== --- action.py +++ action.py @@ -13,295 +13,353 @@ # 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, session +from config import conf, __print__ as debug, dbg import platform -# coupling to main window +# 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/") + +# Streamlink/listformat mapping +listfmt_t = { + "audio/x-scpls": "pls", + "audio/x-mpegurl": "m3u", + "video/x-ms-asf": "asx", + "application/xspf+xml": "xspf", + "*/*": "href", + "url/direct": "srv", + "url/youtube": "href", + "url/http": "href", + "audio/x-pn-realaudio": "ram", + "application/smil": "smil", + "application/vnd.ms-wpl":"smil", + "x-urn/st2-script": "script", # unused +} + +# Audio type MIME map +mediafmt_t = { + "audio/mpeg": "mp3", + "audio/ogg": "ogg", + "audio/aac" : "aac", + "audio/aacp" : "aac", + "audio/midi": "midi", + "audio/mod": "mod", + "audio/it+zip": "mod", + "audio/s3+zip": "mod", + "audio/xm+zip": "mod", +} + +# Player command placeholders for playlist formats +placeholder_map = dict( + pls = "%url | %pls | %u | %l | %r", + m3u = "%m3u | %f | %g | %m", + srv = "%srv | %d | %s", +) + +# Playlist format content probing (assert type) +playlist_content_map = { + "pls": r""" (?i)\[playlist\].*numberofentries""", + "xspf": r""" <\?xml .* ]*> .* """, + "wpl": r""" <\?wpl \s+ version="1\.0" \s* \?>""", + "jspf": r""" \{ \s* "playlist": \s* \{ """, + "json": r""" "url": \s* "\w+:// """, + "href": r""" .* """, +} + + + +# 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(ins): + if type(ins) is str: + return "%r" % str(ins) + else: + return " ".join(["%r" % str(s) for s in ins]) + + +# Convert e.g. "text/x-scpls" MIME types to just "pls" monikers +# +def listfmt(t = "pls"): + if t in listfmt_t.values(): + for short,mime in listfmt_t.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): + major = fmt[:fmt.find("/")] + for match in [ fmt, major + "/*", "*/*" ]: + 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 + urls = convert_playlist(url, listfmt(source), listfmt(dest)) + # insert quoted URL/filepath + return re.sub(rx, cmd, quote(urls), 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): + urls = [] + + print(dbg.PROC, "convert_playlist(", url, source, dest, ")") + + # Leave alone + is_url = re.search("\w+://", url) + if source == dest or source in ("srv", "href") or not is_url: + return [url] + + # Retrieve from URL + (mime, cnt) = http_probe_get(url) + + # Leave streaming server as is + if mime == "srv": + cnt = "" + return [url] + + # Test URL path "extension" for ".pls" / ".m3u" etc. + ext = re.findall("\.(\w)$|($)", url)[0] + + # Probe MIME type and content per regex + probe = None + for probe,rx in playlist_content_map.items(): + if re.search(rx, cnt, re.X|re.S): + break # with `probe` set + + # Check ambiguity (except pseudo extension) + if len(set([source, mime, probe])) > 1: + print(dbg.ERR, "Possible playlist format mismatch:", (source, mime, probe, ext)) + + # Extract URLs from content + for fmt,extractor in [ ("pls",extract_playlist.pls), ("asx",extract_playlist.asx), ("raw",extract_playlist.raw) ]: + if not urls and fmt in (source, mime, probe, ext): + urls = extractor(cnt) + + # Return asis for srv targets + if dest in ("srv", "href", "any"): + return urls + print urls + + # Otherwise convert to local file + fn = tmp_fn(cnt) + save(urls[0], fn, dest) + return [fn] + + + +# Tries to fetch a resource, aborts on ICY responses. +# +def http_probe_get(url): + + # possible streaming request + r = session.get(url, stream=True) + if not len(r.headers): + return ("srv", r) + + # extract payload + mime = r.headers.get("content-type", "any") + if mediafmt_t.get(mime): + mime = mediafmt_t.get(mime) + content = "".join(r.iter_lines()) + return (mime, content) + + + +# Extract URLs from playlist formats: +# +class extract_playlist(object): + + @staticmethod + def pls(text): + return re.findall("\s*File\d*\s*=\s*(\w+://[^\s]+)", text, re.I) + + @staticmethod + def asx(text): + return re.findall("" + 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: + with open(fn, "wb") as f: + f.write(txt) + pass + + + +# 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) + stream_id = stream_id and stream_id.group(1) or "XXXXXX" + try: + channelname = main.current_channel + except: + channelname = "unknown" + return (str(conf.tmp) + os.sep + "streamtuner2."+channelname+"."+stream_id+".m3u", len(stream_id) > 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 + Index: channels/__init__.py ================================================================== --- channels/__init__.py +++ channels/__init__.py @@ -58,11 +58,11 @@ # desc meta = { "config": [] } homepage = "http://fossil.include-once.org/streamtuner2/" base_url = "" - listformat = "audio/x-scpls" + listformat = "pls" audioformat = "audio/mpeg" # fallback value config = [] has_search = False # categories @@ -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/bookmarks.py ================================================================== --- channels/bookmarks.py +++ channels/bookmarks.py @@ -38,11 +38,11 @@ # desc module = "bookmarks" title = "bookmarks" base_url = "file:.config/streamtuner2/bookmarks.json" - listformat = "*/*" + listformat = "any" # content categories = ["favourite", ] # timer, links, search, and links show up as needed current = "favourite" default = "favourite" 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/icast.py ================================================================== --- channels/icast.py +++ channels/icast.py @@ -42,11 +42,11 @@ class icast (ChannelPlugin): # description homepage = "http://www.icast.io/" has_search = True - listformat = "audio/x-scpls" + listformat = "pls" titles = dict(listeners=False, bitrate=False, playing=False) categories = [] base = "http://api.icast.io/1/" Index: channels/internet_radio.py ================================================================== --- channels/internet_radio.py +++ channels/internet_radio.py @@ -41,11 +41,11 @@ # description title = "InternetRadio" module = "internet_radio" homepage = "http://www.internet-radio.org.uk/" - listformat = "audio/x-scpls" + listformat = "pls" # category map categories = [] current = "" default = "" Index: channels/itunes.py ================================================================== --- channels/itunes.py +++ channels/itunes.py @@ -44,11 +44,11 @@ title = "iTunes RS" module = "itunes" #module = "rs_playlist" homepage = "http://www.itunes.com?" has_search = False - listformat = "audio/x-scpls" + listformat = "pls" titles = dict(listeners=False, bitrate=False, playing=False) categories = [ "Adult Contemporary", "Alternative Rock", Index: channels/jamendo.py ================================================================== --- channels/jamendo.py +++ channels/jamendo.py @@ -60,11 +60,11 @@ homepage = "http://www.jamendo.com/" version = 0.3 has_search = True base = "http://www.jamendo.com/en/" - listformat = "url/http" + listformat = "srv" api_base = "http://api.jamendo.com/v3.0/" cid = "49daa4f5" categories = [] Index: channels/live365.py ================================================================== --- channels/live365.py +++ channels/live365.py @@ -57,11 +57,11 @@ module = "live365" title = "Live365" homepage = "http://www.live365.com/" base_url = "http://www.live365.com/" has_search = True - listformat = "url/http" + listformat = "pls" mediatype = "audio/mpeg" has_search = False # content categories = ['Alternative', 'Blues', 'Classical', 'Country', 'Easy Listening', 'Electronic/Dance', 'Folk', 'Freeform', 'Hip-Hop/Rap', 'Inspirational', 'International', 'Jazz', 'Latin', 'Metal', 'New Age', 'Oldies', 'Pop', 'R&B/Urban', 'Reggae', 'Rock', 'Seasonal/Holiday', 'Soundtracks', 'Talk'] @@ -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/modarchive.py ================================================================== --- channels/modarchive.py +++ channels/modarchive.py @@ -43,10 +43,11 @@ # description title = "modarchive" module = "modarchive" homepage = "http://www.modarchive.org/" base = "http://modarchive.org/" + listformat = "href" titles = dict(genre="Genre", title="Song", playing="File", listeners="Rating", bitrate=0) # keeps category titles->urls catmap = {"Chiptune": "54", "Electronic - Ambient": "2", "Electronic - Other": "100", "Rock (general)": "13", "Trance - Hard": "64", "Swing": "75", "Rock - Soft": "15", "R & B": "26", "Big Band": "74", "Ska": "24", "Electronic - Rave": "65", "Electronic - Progressive": "11", "Piano": "59", "Comedy": "45", "Christmas": "72", "Chillout": "106", "Reggae": "27", "Electronic - Industrial": "34", "Grunge": "103", "Medieval": "28", "Demo Style": "55", "Orchestral": "50", "Soundtrack": "43", "Electronic - Jungle": "60", "Fusion": "102", "Electronic - IDM": "99", "Ballad": "56", "Country": "18", "World": "42", "Jazz - Modern": "31", "Video Game": "8", "Funk": "32", "Electronic - Drum & Bass": "6", "Alternative": "48", "Electronic - Minimal": "101", "Electronic - Gabber": "40", "Vocal Montage": "76", "Metal (general)": "36", "Electronic - Breakbeat": "9", "Soul": "25", "Electronic (general)": "1", "Punk": "35", "Pop - Synth": "61", "Electronic - Dance": "3", "Pop (general)": "12", "Trance - Progressive": "85", "Trance (general)": "71", "Disco": "58", "Electronic - House": "10", "Experimental": "46", "Trance - Goa": "66", "Rock - Hard": "14", "Trance - Dream": "67", "Spiritual": "47", "Metal - Extreme": "37", "Jazz (general)": "29", "Trance - Tribal": "70", "Classical": "20", "Hip-Hop": "22", "Bluegrass": "105", "Halloween": "82", "Jazz - Acid": "30", "Easy Listening": "107", "New Age": "44", "Fantasy": "52", "Blues": "19", "Other": "41", "Trance - Acid": "63", "Gothic": "38", "Electronic - Hardcore": "39", "One Hour Compo": "53", "Pop - Soft": "62", "Electronic - Techno": "7", "Religious": "49", "Folk": "21"} categories = [] 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 @@ -45,11 +45,11 @@ # settings title ="MOR" #module = "myoggradio" api = "http://www.myoggradio.org/" - listformat = "url/direct" + listformat = "srv" # hide unused columns titles = dict(playing=False, listeners=False, bitrate=False) # category map 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), "srv") return # or just open webpage - action.action.browser(row["homepage"]) + action.browser(row["homepage"]) Index: channels/radiobrowser.py ================================================================== --- channels/radiobrowser.py +++ channels/radiobrowser.py @@ -59,11 +59,11 @@ class radiobrowser (ChannelPlugin): # description homepage = "http://www.radio-browser.info/" has_search = True - listformat = "audio/x-scpls" + listformat = "pls" titles = dict(listeners="Votes+", bitrate="Votes-", playing="Country") categories = [] pricat = ("topvote", "topclick") catmap = { "tags": "bytag", "countries": "bycountry", "languages": "bylanguage" } Index: channels/shoutcast.py ================================================================== --- channels/shoutcast.py +++ channels/shoutcast.py @@ -52,11 +52,11 @@ # desc module = "shoutcast" title = "SHOUTcast" base_url = "http://shoutcast.com/" - listformat = "audio/x-scpls" + listformat = "pls" # categories categories = [] catmap = {"Choral": 35, "Winter": 275, "JROCK": 306, "Motown": 237, "Political": 290, "Tango": 192, "Ska": 22, "Comedy": 283, "Decades": 212, "European": 143, "Reggaeton": 189, "Islamic": 307, "Freestyle": 114, "French": 145, "Western": 53, "Dancepunk": 6, "News": 287, "Xtreme": 23, "Bollywood": 138, "Celtic": 141, "Kids": 278, "Filipino": 144, "Hanukkah": 270, "Greek": 146, "Punk": 21, "Spiritual": 211, "Industrial": 14, "Baroque": 33, "Talk": 282, "JPOP": 227, "Scanner": 291, "Mediterranean": 154, "Swing": 174, "Themes": 89, "IDM": 75, "40s": 214, "Funk": 236, "Rap": 110, "House": 74, "Educational": 285, "Caribbean": 140, "Misc": 295, "30s": 213, "Anniversary": 266, "Sports": 293, "International": 134, "Tribute": 107, "Piano": 41, "Romantic": 42, "90s": 219, "Latin": 177, "Grunge": 10, "Dubstep": 312, "Government": 286, "Country": 44, "Salsa": 191, "Hardcore": 11, "Afrikaans": 309, "Downtempo": 69, "Merengue": 187, "Psychedelic": 260, "Female": 95, "Bop": 167, "Tribal": 80, "Metal": 195, "70s": 217, "Tejano": 193, "Exotica": 55, "Anime": 277, "BlogTalk": 296, "African": 135, "Patriotic": 101, "Blues": 24, "Turntablism": 119, "Chinese": 142, "Garage": 72, "Dance": 66, "Valentine": 273, "Barbershop": 222, "Alternative": 1, "Technology": 294, "Folk": 82, "Klezmer": 152, "Samba": 315, "Turkish": 305, "Trance": 79, "Dub": 245, "Rock": 250, "Polka": 59, "Modern": 39, "Lounge": 57, "Indian": 149, "Hindi": 148, "Brazilian": 139, "Eclectic": 93, "Korean": 153, "Creole": 316, "Dancehall": 244, "Surf": 264, "Reggae": 242, "Goth": 9, "Oldies": 226, "Zouk": 162, "Environmental": 207, "Techno": 78, "Adult": 90, "Rockabilly": 262, "Wedding": 274, "Russian": 157, "Sexy": 104, "Chill": 92, "Opera": 40, "Emo": 8, "Experimental": 94, "Showtunes": 280, "Breakbeat": 65, "Jungle": 76, "Soundtracks": 276, "LoFi": 15, "Metalcore": 202, "Bachata": 178, "Kwanzaa": 272, "Banda": 179, "Americana": 46, "Classical": 32, "German": 302, "Tamil": 160, "Bluegrass": 47, "Halloween": 269, "College": 300, "Ambient": 63, "Birthday": 267, "Meditation": 210, "Electronic": 61, "50s": 215, "Chamber": 34, "Heartache": 96, "Britpop": 3, "Soca": 158, "Grindcore": 199, "Reality": 103, "00s": 303, "Symphony": 43, "Pop": 220, "Ranchera": 188, "Electro": 71, "Christmas": 268, "Christian": 123, "Progressive": 77, "Jazz": 163, "Trippy": 108, "Instrumental": 97, "Tropicalia": 194, "Fusion": 170, "Healing": 209, "Glam": 255, "80s": 218, "KPOP": 308, "Worldbeat": 161, "Mixtapes": 117, "60s": 216, "Mariachi": 186, "Soul": 240, "Cumbia": 181, "Inspirational": 122, "Impressionist": 38, "Gospel": 129, "Disco": 68, "Arabic": 136, "Idols": 225, "Ragga": 247, "Demo": 67, "LGBT": 98, "Honeymoon": 271, "Japanese": 150, "Community": 284, "Weather": 317, "Asian": 137, "Hebrew": 151, "Flamenco": 314, "Shuffle": 105} current = "" Index: channels/surfmusik.py ================================================================== --- channels/surfmusik.py +++ channels/surfmusik.py @@ -44,11 +44,11 @@ # description title = "SurfMusik" module = "surfmusik" homepage = "http://www.surfmusik.de/" - listformat = "audio/x-scpls" + listformat = "pls" lang = "DE" # last configured categories base = { "DE": ("http://www.surfmusik.de/", "genre/", "land/"), "EN": ("http://www.surfmusic.de/", "format/", "country/"), 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 @@ -95,11 +95,11 @@ self.parent.timer_dialog.hide() row = self.parent.row() row = copy.copy(row) # add data - row["listformat"] = "url/direct" #self.parent.channel().listformat + row["listformat"] = "href" #self.parent.channel().listformat if row.get(self.timefield): row["title"] = row["title"] + " -- " + row[self.timefield] row[self.timefield] = self.parent.timer_value.get_text() # store @@ -170,11 +170,11 @@ # action wrapper def play(self, row, *args, **kwargs): action.play( url = row["url"], audioformat = row.get("format","audio/mpeg"), - listformat = row.get("listformat","url/direct"), + listformat = row.get("listformat","href"), ) # action wrapper def record(self, row, *args, **kwargs): #print("TIMED RECORD") @@ -188,13 +188,13 @@ # start recording action.record( url = row["url"], audioformat = row.get("format","audio/mpeg"), - listformat = row.get("listformat","url/direct"), + listformat = row.get("listformat","href"), append = append, ) def test(self, row, *args, **kwargs): print("TEST KRONOS", row) Index: channels/tunein.py ================================================================== --- channels/tunein.py +++ channels/tunein.py @@ -38,11 +38,11 @@ # description title = "TuneIn" module = "tunein" homepage = "http://tunein.com/" has_search = False - listformat = "audio/x-scpls" + listformat = "pls" titles = dict(listeners=False) base = "http://opml.radiotime.com/" categories = ["local", "60's", "70's", "80's", "90's", "Adult Contemporary", "Alternative Rock", "Ambient", "Bluegrass", "Blues", "Bollywood", "Children's Music", "Christmas", "Classic Hits", "Classic Rock", "Classical", "College Radio", "Country", "Decades", "Disco", "Easy Listening", "Eclectic", "Electronic", "Folk", "Hip Hop", "Indie", "Internet Only", "Jazz", "Live Music", "Oldies", "Polka", "Reggae", "Reggaeton", "Religious", "Rock", "Salsa", "Soul and R&B", "Spanish Music", "Specialty", "Tango", "Top 40/Pop", "World"] catmap = {"60's": "g407", "Live Music": "g2778", "Children's Music": "c530749", "Polka": "g84", "Tango": "g3149", "Top 40/Pop": "c57943", "90's": "g2677", "Eclectic": "g78", "Decades": "c481372", "Christmas": "g375", "Reggae": "g85", "Reggaeton": "g2771", "Oldies": "c57947", "Jazz": "c57944", "Specialty": "c418831", "Hip Hop": "c57942", "College Radio": "c100000047", "Salsa": "g124", "Bollywood": "g2762", "70's": "g92", "Country": "c57940", "Classic Hits": "g2755", "Internet Only": "c417833", "Disco": "g385", "Rock": "c57951", "Soul and R&B": "c1367173", "Blues": "g106", "Classic Rock": "g54", "Alternative Rock": "c57936", "Adult Contemporary": "c57935", "Classical": "c57939", "World": "c57954", "Indie": "g2748", "Religious": "c57950", "Bluegrass": "g63", "Spanish Music": "c57945", "Easy Listening": "c10635888", "Ambient": "g2804", "80's": "g42", "Electronic": "c57941", "Folk": "g79"} Index: channels/xiph.py ================================================================== --- channels/xiph.py +++ channels/xiph.py @@ -60,11 +60,11 @@ module = "xiph" title = "Xiph.org" homepage = "http://dir.xiph.org/" #xml_url = "http://dir.xiph.org/yp.xml" json_url = "http://api.include-once.org/xiph/cache.php" - listformat = "url/http" + listformat = "srv" has_search = True # content categories = [ "pop", "top40" ] current = "" 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"))