Index: action.py ================================================================== --- action.py +++ action.py @@ -17,12 +17,12 @@ # Some channels list raw "srv" addresses, while Youtube "href" # entries to Flash videos. # # As fallback the playlist URL is retrieved and its MIME type # checked, and its content regexped to guess the link format. -# Lastly a playlist type suitable for audio players recreated. -# Which is somewhat of a security feature, playlists get cleaned +# Lastly a playlist format suitable for audio players recreated. +# Which is somewhat of a security feature; playlists get cleaned # up this way. The conversion is not strictly necessary for all # players, as basic PLS is supported by most. # # And finally this module is also used by exporting and (perhaps # in the future) playlist importing features. @@ -33,10 +33,11 @@ from ahttp import fix_url as http_fix_url, session from config import conf, __print__ as debug, dbg import platform import copy import json +from datetime import datetime # Coupling to main window # main = None @@ -128,21 +129,21 @@ run("yelp /usr/share/doc/streamtuner2/help/") # Calls player for stream url and format # -def play(url, audioformat="audio/mpeg", source="pls", row={}): +def play(row={}, audioformat="audio/mpeg", source="pls", url=None): cmd = mime_app(audioformat, conf.play) - cmd = interpol(cmd, url, source, row) + cmd = interpol(cmd, url or row["url"], source, row) run(cmd) # Call streamripper # -def record(url, audioformat="audio/mpeg", source="href", row={}): +def record(row={}, audioformat="audio/mpeg", source="href", url=None): cmd = mime_app(audioformat, conf.record) - cmd = interpol(cmd, url, source, row) + cmd = interpol(cmd, url or row["url"], source, row) run(cmd) # OS shell command escaping # @@ -175,10 +176,11 @@ # · Replace .pls URL with local .m3u file depending on map. # def interpol(cmd, url, source="pls", row={}): # inject other meta fields + row = copy.copy(row) if row: for field in row: cmd = cmd.replace("%"+field, "%r" % row.get(field)) # add default if cmd has no %url placeholder @@ -188,30 +190,27 @@ # standard placeholders for dest, rx in placeholder_map.items(): if re.search(rx, cmd, re.X): # from .pls to .m3u - fn_or_urls = convert_playlist(url, listfmt(source), listfmt(dest), local_file=True, title=row.get("title", "")) + fn_or_urls = convert_playlist(url, listfmt(source), listfmt(dest), local_file=True, row=row) # insert quoted URL/filepath return re.sub(rx, quote(fn_or_urls), cmd, 2, re.X) return "false" # Substitute .pls URL with local .m3u, or direct srv addresses, or leaves URL asis. -# · Takes a single input `url`. +# · Takes a single input `url` (and original row{} as template). # · But returns a list of [urls] after playlist extraction. # · If repackaging as .m3u/.pls/.xspf, returns the local [fn]. # -# TODO: This still needs some rewrite to reuse the incoming row={}, -# and keep station titles for converted playlists. -# -def convert_playlist(url, source, dest, local_file=True, title=""): +def convert_playlist(url, source, dest, local_file=True, row={}): 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:) + # Leave alone if format matches, or if already "srv" URL, 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) @@ -245,18 +244,17 @@ # Return original, or asis for srv targets if not urls: return [url] elif dest in ("srv", "href"): return urls - debug( urls ) # Otherwise convert to local file if local_file: fn, is_unique = tmp_fn(cnt, dest) with open(fn, "wb") as f: debug(dbg.DATA, "exporting with format:", dest, " into filename:", fn) - f.write( save_playlist(source="srv", multiply=True).export(urls=urls, dest=dest, title=title) ) + f.write( save_playlist(source="srv", multiply=True).export(urls, row, dest) ) return [fn] else: return urls @@ -283,11 +281,11 @@ elif mediafmt_t.get(mime): debug(dbg.ERR, "Got media MIME type for expected playlist", mime, " on url=", url) mime = mediafmt_t.get(mime) return (mime, url) # Rejoin body - content = "\n".join(r.iter_lines()) + content = "\n".join(str.decode(errors='replace') for str in r.iter_lines()) return (mime, content) # Extract URLs from playlist formats: @@ -318,12 +316,12 @@ } # Save rows in one of the export formats. # -# The export() version uses urls[]+title= as input, converts it into a -# list of rows{} beforehand. +# The export() version uses urls[]+row/title= as input, converts it into +# a list of rows{} beforehand. # # While store() requires rows{} to begin with, to perform a full # conversion. Can save directly to a file name. # class save_playlist(object): @@ -338,13 +336,17 @@ self.multiply = multiply # Used by playlist_convert(), to transform a list of extracted URLs # into a local .pls/.m3u collection again. Therefore injects the - # `title` back into each of the URL rows. - def export(self, urls=None, title=None, dest="pls"): - rows = [ { "url": url, "title": title } for url in urls ] + # `title` back into each of the URL rows / or uses row{} template. + def export(self, urls=[], row={}, dest="pls", title=None): + row["title"] = row.get("title", title or "unnamed stream") + rows = [] + for url in urls: + row.update(url=url) + rows.append(row) return self.store(rows, dest) # Export a playlist from rows{} def store(self, rows=None, dest="pls"): @@ -400,13 +402,14 @@ return json.dumps(rows, indent=4) # XSPF def xspf(self, rows): - return """\n""" \ - + """\n""" \ - + """\n\t\n""" \ + return """\n""" \ + + """\n""" \ + + """\n""" \ + + """\t%s\n\t\n""" % datetime.now().isoformat() \ + "".join("""\t\t\n%s\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\n""" % (tag, xmlentities(row[attr]), tag) for attr,tag in map.items() if row.get(attr)) @@ -431,11 +434,11 @@ return txt # SMIL def smil(self, rows): - txt = """\n\n\t\n\n\n\t\n""" % (rows[0]["title"]) + txt = """\n\n\t\n\n\n\t\n""" % (rows[0]["title"]) for row in rows: if row.get("url"): txt += """\t\t\n\n\n""" return txt Index: channels/__init__.py ================================================================== --- channels/__init__.py +++ channels/__init__.py @@ -485,27 +485,27 @@ # Invoke action.play() for current station. # Can be overridden to provide channel-specific "play" alternative def play(self): row = self.row() - if row: + if row and "url" in row: # playlist and audio type audioformat = row.get("format", self.audioformat) listformat = row.get("listformat", self.listformat) # invoke audio player - action.play(row["url"], audioformat, listformat, row) + action.play(row, audioformat, listformat) else: self.status("No station selected for playing.") return row # Start streamripper/youtube-dl/etc def record(self): row = self.row() - if row: + if row and "url" in row: audioformat = row.get("format", self.audioformat) listformat = row.get("listformat", self.listformat) - action.record(row.get("url"), audioformat, listformat, row=row) + action.record(row, audioformat, listformat) return row #--------------------------- utility functions ----------------------- Index: channels/exportcat.py ================================================================== --- channels/exportcat.py +++ channels/exportcat.py @@ -47,8 +47,15 @@ source = cn.listformat streams = cn.streams[cn.current] fn = uikit.save_file("Export category", None, "%s.%s.%s" % (cn.module, cn.current, conf.export_format)) __print__(dbg.PROC, "Exporting category to", fn) if fn: - dest = re.findall("\.(m3u|pls|xspf|jspf|json|smil|wpl)8?$", fn)[0] + dest = re.findall("\.(m3u8?|pls|xspf|jspf|json|smil|asx)$", fn.lower()) + if dest: + dest = dest[0] + else: + self.parent.status("Unsupported export playlist type (file extension).") + return + if dest == "m3u8": + dest = "m3u" action.save_playlist(source="asis", multiply=False).file(rows=streams, fn=fn, dest=dest) pass Index: config.py ================================================================== --- config.py +++ config.py @@ -340,11 +340,11 @@ try: bin = pkgutil.get_data(file_base, fn) if gz: bin = gzip_decode(bin) if decode: - return bin.decode("utf-8") + return bin.decode("utf-8", errors='ignore') else: return str(bin) except: pass @@ -396,11 +396,11 @@ src = zipfile.ZipFile(fn, "r").read(intfn.strip("/")) if not src: src = "" if type(src) is not str: - src = src.decode("utf-8") + src = src.decode("utf-8", errors='replace') return plugin_meta_extract(src, fn) # Actual comment extraction logic Index: gtk3.xml.gz ================================================================== --- gtk3.xml.gz +++ gtk3.xml.gz cannot compute difference between binary files Index: uikit.py ================================================================== --- uikit.py +++ uikit.py @@ -397,11 +397,11 @@ @staticmethod def save_file_filterchange(c): fn, ext = c.get_filename(), c.get_filter().get_name() if fn and ext: fn = os.path.basename(fn) - c.set_current_name(re.sub(r"\.(m3u|pls|xspf|jspf|asx|json|smil|wpl)$", ext, fn)) + c.set_current_name(re.sub(r"\.(m3u|pls|xspf|jspf|asx|json|smil|wpl)$", ext.strip("*"), fn)) # pass updates from another thread, ensures that it is called just once