Check-in [8e3b1e4d5b]
Overview
Comment: | Move playlist extension and context probing into separate functions. Introduce some rather crude import functionality for a few playlist file formats. (Still requires proper importer with title= reading, and entirely rows[] based function signatures in action module.) |
---|---|
Downloads: | Tarball | ZIP archive | SQL archive |
Timelines: | family | ancestors | descendants | both | trunk |
Files: | files | file ages | folders |
SHA1: |
8e3b1e4d5b8309c839a0bb119f87b47c |
User & Date: | mario on 2015-04-20 16:24:24 |
Other Links: | manifest | tags |
Context
2015-04-20
| ||
23:18 | Add overly crude playlist_convert.title() extraction (for M3U/PLS/XSPF/ASX and the custom STRING/TEXT format used by DND module). Insert imported rows at the right position. Scrolling won't work, because it's done in a separate thread. check-in: 6dfe1fdeb5 user: mario tags: trunk | |
16:24 | Move playlist extension and context probing into separate functions. Introduce some rather crude import functionality for a few playlist file formats. (Still requires proper importer with title= reading, and entirely rows[] based function signatures in action module.) check-in: 8e3b1e4d5b user: mario tags: trunk | |
16:22 | Introduce log.ERR() etc. instead of __print__(dbg.XY...) workaround (was meant for Py3 only). check-in: 256b1e5833 user: mario tags: trunk | |
Changes
Modified action.py from [ceb9b4a4e2] to [ee9e1bcb9d].
︙ | ︙ | |||
219 220 221 222 223 224 225 | (mime, cnt) = http_probe_get(url) # Leave streaming server as is if mime == "srv": cnt = "" return [url] | < < < | < | | < < < | 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 | (mime, cnt) = http_probe_get(url) # Leave streaming server as is if mime == "srv": cnt = "" return [url] # Deduce likely content format ext = probe_playlist_fn_ext(url) probe = probe_playlist_content(cnt) # Check ambiguity (except pseudo extension) if len(set([source, mime, probe])) > 1: debug(dbg.ERR, "Possible playlist format mismatch:", "listformat={}, http_mime={}, rx_probe={}, ext={}".format(source, mime, probe, ext)) # Extract URLs from content for fmt in [id[0] for id in extract_playlist.extr_urls]: |
︙ | ︙ | |||
256 257 258 259 260 261 262 263 264 265 266 267 268 269 | with open(fn, "w") as f: debug(dbg.DATA, "exporting with format:", dest, " into filename:", fn) f.write( save_playlist(source="srv", multiply=True).export(urls, row, dest) ) return [fn] else: return urls # Tries to fetch a resource, aborts on ICY responses. # def http_probe_get(url): # HTTP request, abort if streaming server hit (no HTTP/ header, but ICY/ response) | > > > > > > > > > > > > > > | 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 | with open(fn, "w") as f: debug(dbg.DATA, "exporting with format:", dest, " into filename:", fn) f.write( save_playlist(source="srv", multiply=True).export(urls, row, dest) ) return [fn] else: return urls # Test URL/path "extension" for ".pls" / ".m3u" etc. def probe_playlist_fn_ext(url): e = re.findall("\.(pls|m3u|xspf|jspf|asx|wpl|wsf|smil|html|url|json)$", url) if e: return e[0] else: pass # Probe MIME type and content per regex def probe_playlist_content(cnt): for probe,rx in playlist_content_map: if re.search(rx, cnt, re.X|re.S): return listfmt(probe) return None # Tries to fetch a resource, aborts on ICY responses. # def http_probe_get(url): # HTTP request, abort if streaming server hit (no HTTP/ header, but ICY/ response) |
︙ | ︙ |
Modified channels/dnd.py from [25fd725420] to [9f80845729].
1 2 3 4 5 6 7 8 | # encoding: UTF-8 # api: streamtuner2 # title: Drag and Drop # description: Move streams/stations from and to other applications. # depends: uikit # version: 0.1 # type: interface # config: | | | | < < < < < < | > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 | # encoding: UTF-8 # api: streamtuner2 # title: Drag and Drop # description: Move streams/stations from and to other applications. # depends: uikit # version: 0.1 # 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 entry." } # category: ui # priority: experimental # # Implements Gtk/X11 drag and drop support for station lists. # Should allow to export either just stream URLs, or complete # PLS, XSPF collections. # # Also used by the bookmarks channel to copy favourites around. # Which perhaps should even be constrained to just the bookmarks tab. import copy from config import conf, json, log from uikit import * import action # Welcome to my new blog. # # Now it's perhaps not even Gtks fault, but all the gory implementation # details of XDND are pretty gory. Neither match up to reality anymore. # # Pretty much only the ridiculous `TEXT/URI-LIST` is used in practice. # Without host names, of course, despite the spec saying otherwise. (It # perhaps leaked into the Gnome UI, and they decreed it banished). And # needless to say, there's no actual IRI/URI support in any file manager # or pairing apps beyond local paths. # # Supporting PLS, XSPF, M3U as direct payload was a pointless exercise. # It's not gonna get requested by anyone. Instead there's another config # option now, which predefines the exchange format for temporary file:/// # dumps. Because, you know, there was never any point in type negotiation # due to all the API overhead. # # What works, and what's widely used in practice instead, is declaring # yet another custom type per application. Our row format is transferred # unfiltered over the selection buffer as JSON. However, it's decidedly # never exposed to other apps as x-special/x-custom whatever. (It's also # not using the MIME 1.0 application/* trash bin for that very reason.) # Drag and Drop support class dnd(object): module = "dnd" meta = plugin_meta() |
︙ | ︙ | |||
53 54 55 56 57 58 59 60 61 62 63 64 | ("text/html", 0, 23), ("text/richtext", 0, 23), ("application/jspf+json", 0, 25), # direct srv urls ("text/url", 0, 15), #@TODO: support in action.save_/convert_ ("message/external-body", 0, 15), ("url/direct", 0, 15), # url+comments ("TEXT", 0, 5), ("STRING", 0, 5), ("UTF8_STRING", 0, 5), ("text/plain", 0, 5), | > > > < < < > | 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 | ("text/html", 0, 23), ("text/richtext", 0, 23), ("application/jspf+json", 0, 25), # 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 ("FILE_NAME", 0, 3), ("text/uri-list", 0, 4), # url+comments ("TEXT", 0, 5), ("STRING", 0, 5), ("UTF8_STRING", 0, 5), ("text/plain", 0, 5), ] cnv_types = { 20: "m3u", 21: "pls", 22: "xspf", 23: "smil", 25: "jspf", 15: "srv", 4: "temp", 5: "srv", 51: "json", } # Hook to main, and extend channel tabs def __init__(self, parent): self.parent = parent parent.hooks["init"].append(self.add_dnd) conf.add_plugin_defaults(self.meta, self.module) log.colors["DND"] = "1;33;41m" # Attach drag and drop handlers to each channels´ station TreeView def add_dnd(self, parent): # visit each module for cn,module in parent.channels.items(): |
︙ | ︙ | |||
103 104 105 106 107 108 109 | # -- SOURCE, drag'n'drop from ST2 to elsewhere -- # Starting to drag a row def begin(self, widget, context): | | < | < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < | | > > > | > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | | > > | > | > < | < < < < | < < < < < | < < | < < < < | | < | < < < > | | | | > > > > > | > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 | # -- SOURCE, drag'n'drop from ST2 to elsewhere -- # Starting to drag a row def begin(self, widget, context): log.DND("source→out: begin-drag, store current row") self.row = self.treelist_row() self.buf = {} uikit.do(context.set_icon_stock, gtk.STOCK_ADD, 16, 16) return "url" in self.row # Keep currently selected row when source dragging starts def treelist_row(self): cn = self.parent.channel() row = copy.copy(cn.row()) row.setdefault("format", cn.audioformat) row.setdefault("listformat", cn.listformat) row.setdefault("url", row.get("homepage")) return row # Target window/app requests data for offered drop def data_get(self, widget, context, selection, info, time): log.DND("source→out: data-get, send and convert to requested target type:", info, selection.get_target()) # Return prepared data func, data = self.export_row(info, self.row) if func.find("text") >= 0: # Yay for trial and error. Nay for docs. PyGtks selection.set_text() doesn't # actually work unless the requested target type is an Atom. Therefore "STRING". selection.set("STRING", 8, data) if func.find("uris") >= 0: selection.set_uris(data) return True # Handles the conversion from the stored .row to the desired selection data def export_row(self, info, r): # Needs buffering because `data_get` gets called mercilessly along the dragging path if info in self.buf: return self.buf[info] # Prepare new converter cnv = action.save_playlist(source=r["listformat"], multiply=False) # internal JSON row if info >= 51: buf = 'text', json.dumps(r) # Pass M3U/PLS/XSPF as literal payload elif info >= 20: buf = 'text', cnv.export(urls=[r["url"]], row=r, dest=self.cnv_types[info]) # Direct server URL elif info >= 10: urls = action.convert_playlist(r["url"], r["listformat"], "srv", False, r) #buf = 'uris', urls buf = 'text', urls[0] # Text sources are assumed to understand the literal URL or expect a description block elif info >= 5: buf = 'text', "{url}\n# Title: {title}\n# Homepage: {homepage}\n\n".format(**r) # Create temporary PLS file, because "text/uri-list" is widely misunderstood and just implemented for file:// IRLs else: tmpfn = "{}/{}.{}".format(conf.tmp, re.sub("[^\w-]+", " ", r["title"]), conf.dnd_format) cnv.file(rows=[r], dest=conf.dnd_format, fn=tmpfn) buf = 'uris', ["file://{}".format(tmpfn)] if (info==4) else tmpfn # Keep in type request buffer self.buf[info] = buf return buf # -- DESTINATION, when playlist/file gets dragged in 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 accept = [type[0] for type in self.drag_types if type[0] in context.targets] context.drop_reply(len(accept) > 0, time) if accept: widget.drag_get_data(context, accept[0], time) or True return True # Actual data is being passed, def data_received(self, widget, context, x, y, selection, info, time): log.DND("dest←in: data-receival", info, selection.get_text(), selection.get_uris()) # incoming data data = selection.get_text() urls = selection.get_uris() any = (data or urls) and True # Convert/Add if any: self.import_row(info, urls, data, y) else: log.DND("abort, no urls/text") # Respond context.drop_finish(any, time) context.finish(any, False, time) return True # 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 = [] # Direct/internal row import if data and info >= 51: log.DND("Received row, append, reload") rows += [ json.loads(data) ] # Convertible formats elif data and info >= 5: cnv = action.extract_playlist(data) urls = cnv.format(self.cnv_types[info] if info>=20 else "raw") rows += [ self.imported_row(urls[0]) ] # Extract from playlist files (don't import mp3s into stream lists directly) elif urls: for fn in [re.sub("^\w+://[^/]*", "", fn) for fn in urls if re.match("^(scp|file)://(localhost)?/|/", fn)]: ext = action.probe_playlist_fn_ext(fn) if ext: cnt = open(fn, "rt").read() probe = action.probe_playlist_content(cnt) if ext == probe: cnv = action.extract_playlist(cnt) urls = cnv.format(probe) rows += [ self.imported_row(urls[0], os.path.basename(fn)) ] # Insert and update view if rows: # Inserting at correct row requires deducing index from dnd `y` position cn.streams[cn.current] += rows # Now appending to the liststore directly would be even nicer uikit.do(cn.load, cn.current) if cn.module == "bookmarks": cn.save() #self.parent.streamedit() else: self.parent.status("Unsupported station format. Not imported.") def imported_row(self, url, title=None): return { "title": title or "", "url": url, "homepage": "", "playling": "", "listformat": action.probe_playlist_fn_ext(url) or "href", "format": ",".join(re.findall("ogg|mpeg|mp\d+", url)), "genre": "copy", } |