Index: action.py ================================================================== --- action.py +++ action.py @@ -319,11 +319,11 @@ self.src = open(fn, "rt").read() # Test URL/path "extension" for ".pls" / ".m3u" etc. def probe_ext(self, url): - e = re.findall("\.(pls|m3u|xspf|jspf|asx|wpl|wsf|smil|html|url|json)$", url) + e = re.findall("\.(pls|m3u|xspf|jspf|asx|wpl|wsf|smil|html|url|json|desktop)$", url) if e: return e[0] else: pass # Probe MIME type and content per regex @@ -428,11 +428,11 @@ url = r" (?x) ]+\b href \s*=\s* [\'\"] (\w+://[^\s\"\']+) [\'\"] ", title = r"(?x) ([^<>]+) ", unesc = "xml", ), "smil": dict( - url = r" (?x) <(?:audio|video|media)\b [^>]+ \b src \s*=\s* [^\"\']? \s* (\w+://[^\"\'\s]+) ", + url = r" (?x) <(?:audio|video|media)\b [^>]+ \b src \s*=\s* [^\"\']? \s* (\w+://[^\"\'\s\>]+) ", unesc = "xml", ), "jspf": dict( split = r"(?s) \"track\":\s*\{ >", url = r"(?s) \"location\" \s*:\s* \"(\w+://[^\"\s]+)\" ", @@ -442,11 +442,11 @@ url = r" (?x) \"audio\" \s*:\s* \"(\w+:\\?/\\?/[^\"\s]+)\" ", title = r" (?x) \"name\" \s*:\s* \"([^\"]+)\" ", unesc = "json", ), "json": dict( - url = r" (?x) \"url\" \s*:\s* \"(\w+://[^\"\s]+)\" ", + url = r" (?x) \"url\" \s*:\s* \"(\w+:\\?/\\?/[^\"\s]+)\" ", title = r" (?x) \"title\" \s*:\s* \"([^\"]+)\" ", unesc = "json", ), "asf": dict( url = r" (?m) ^ \s*Ref\d+ = (\w+://[^\s]+) ", @@ -471,20 +471,31 @@ # Add placeholder fields to extracted row def mkrow(self, row, title=None): url = row.get("url", "") comb = { - "title": title or re.sub("\.\w+$", "", os.path.basename(self.fn)), + "title": row.get("title") or re.sub("\.\w+$", "", os.path.basename(self.fn)), "playing": "", "url": None, "homepage": "", - "listformat": self.probe_ext(url) or "href", - "format": ",".join(re.findall("ogg|mpeg|mp\d+", url)), + "listformat": self.probe_ext(url) or "href", # or srv? + "format": self.mime_guess(url), "genre": "copy", } comb.update(row) return comb + + # Probe url "extensions" for common media types + # (only care about the common audio formats, don't need an exact match or pre-probing in practice) + def mime_guess(self, url): + audio = re.findall("(ogg|opus|spx|aacp|aac|mpeg|mp3|m4a|mp2|flac|midi|mod|kar|aiff|wma|ram|wav)", url) + if audio: + return "audio/{}".format(*audio) + video = re.findall("(mp4|flv|avi|mp2|theora|3gp|nsv|fli|ogv|webm|mng|mxu|wmv|mpv|mkv)", url) + if audio: + return "video/{}".format(*audio) + return "x-audio-video/unknown" # Save rows in one of the export formats. # @@ -612,10 +623,14 @@ if row.get("url"): txt += """\t\t<{} src="{}"/>\n""".format(row.get("format", "audio").split("/")[0], row["url"]) txt += """\t</seq>\n</body>\n</smil>\n""" return txt + # .DESKTOP links + def desktop(self, rows): + row = rows[0] + return "[Desktop Entry]\nVersion=1.0\nIcon=media-playback-start\nType=Link\nName={title}\nComment={playing}\nURL={url}\n".format(**row) # Generate filename for temporary .m3u, if possible with unique id def tmp_fn(pls, ext="m3u"): Index: channels/dnd.py ================================================================== --- channels/dnd.py +++ channels/dnd.py @@ -4,11 +4,11 @@ # description: Copy streams/stations from and to other applications. # depends: uikit # version: 0.5 # type: interface # config: -# { name: dnd_format, type: select, value: xspf, select: "pls|m3u|xspf|jspf|asx|smil", description: "Default temporary file format for copying a station." } +# { name: dnd_format, type: select, value: xspf, select: "pls|m3u|xspf|jspf|asx|smil|desktop", description: "Default temporary file format for copying a station." } # category: ui # priority: default # support: experimental # # Implements Gtk/X11 drag and drop support for station lists. @@ -72,10 +72,11 @@ ("application/xspf+xml", 0, 22), ("application/smil", 0, 23), ("text/html", 0, 23), ("text/richtext", 0, 23), ("application/jspf+json", 0, 25), + ("application/x-desktop", 0, 26), # direct srv urls ("text/url", 0, 15), #@TODO: support in action.save_/convert_ ("message/external-body", 0, 15), ("url/direct", 0, 15), # filename, file:// IRL @@ -93,10 +94,11 @@ 20: "m3u", 21: "pls", 22: "xspf", 23: "smil", 25: "jspf", + 26: "desktop", 15: "srv", 4: "temp", 5: "srv", 51: "json", } @@ -194,11 +196,11 @@ self.buf[info] = buf return buf - # -- DESTINATION, when playlist/file gets dragged in from other app -- + # -- DESTINATION, when playlist/file gets dragged into ST2 from other app -- # Just a notification for incoming drop def drop(self, widget, context, x, y, time): log.DND("destā†in: drop-probing, possible targets:", context.targets) # find a matching target @@ -229,25 +231,26 @@ # Received files or payload has to be converted, copied into streams def import_row(self, info, urls, data, y=5000): # Internal target dicts cn = self.parent.channel() rows = [] - print info # Direct/internal row import if data and info >= 51: - log.DND("Received row, append, reload") + log.DND("Received row in internal format, append+reload") rows += [ json.loads(data) ] # Convertible formats as direct payload elif data and info >= 5: + log.DND("Converting direct payload playlist") cnv = action.extract_playlist(data) add = cnv.rows(self.cnv_types[info] if info>=20 else cnv.probe_fmt() or "raw") rows += [ cnv.mkrow(row) for row in add ] # Extract from playlist files, either passed as text/uri-list or single FILE_NAME elif urls: + log.DND("Importing from playlist file") for fn in urls or [data]: if not re.match("^(scp|file)://(localhost)?/|/", fn): continue fn = compat2and3.urldecode(re.sub("^\w+://[^/]*", "", fn)) cnv = action.extract_playlist(fn=fn) @@ -254,21 +257,17 @@ if cnv.src: rows += [ cnv.mkrow(row) for row in cnv.rows() ] # Insert and update view if rows: - # Inserting at correct row requires deducing index from dnd `y` position - streams = cn.streams[cn.current] - i_pos = (cn.gtk_list.get_path_at_pos(10, y) or [[len(streams) + 1]])[0][0] - for row in rows: - streams.insert(i_pos - 1, row) - i_pos = i_pos + 1 - # Now appending to the liststore directly would be even nicer - uikit.do(lambda *x: cn.load(cn.current))#, cn.gtk_list.scroll_to_point(0, y)) - if cn.module == "bookmarks": - cn.save() - #self.parent.streamedit() + cn.insert_rows(rows, y) + # if cn.module == "bookmarks": + cn.save() + # Show streamedit window if title is empty + if not len(rows[0].get("title", "")): + self.parent.configwin.load_config(rows[0], "streamedit_") + self.parent.win_streamedit.show() else: self.parent.status("Unsupported station format. Not imported.")