Index: action.py
==================================================================
--- action.py
+++ action.py
@@ -326,11 +326,26 @@
if decode in ("xml", "*"):
urls = [xmlunescape(url) for url in urls]
if decode in ("json", "*"):
urls = [url.replace("\\/", "/") for url in urls]
# only uniques
- return list(set(urls))
+ uniq = []
+ urls = [uniq.append(u) for u in urls if not u in uniq]
+ return uniq
+
+ # Try to capture common title schemes
+ def title(self):
+ t = re.search(r"""(?:
+ ^Title\d*=(.+)
+ | ^\#EXTINF[-:\d,]*(.+)
+ |
([^<>]+)
+ | (?i)Title[\W]+(.+)
+ )""", self.src, re.X|re.M)
+ for i in range(1,10):
+ if t and t.group(i):
+ return t.group(i)
+
# Only look out for URLs, not local file paths, nor titles
extr_urls = (
("pls", (r"(?im) ^ \s*File\d* \s*=\s* (\w+://[^\s]+) ", None)),
("m3u", (r" (?m) ^( \w+:// [^#\n]+ )", None)),
Index: channels/dnd.py
==================================================================
--- channels/dnd.py
+++ channels/dnd.py
@@ -1,11 +1,11 @@
# encoding: UTF-8
# api: streamtuner2
-# title: Drag and Drop
-# description: Move streams/stations from and to other applications.
+# title: Drag and Drop (experimental)
+# description: Copy streams/stations from and to other applications.
# depends: uikit
-# version: 0.1
+# 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 entry." }
# category: ui
# priority: experimental
@@ -13,11 +13,12 @@
# 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.
+# Which perhaps should even be constrained to just the bookmarks
+# tab.
import copy
from config import conf, json, log
from uikit import *
@@ -82,10 +83,12 @@
("TEXT", 0, 5),
("STRING", 0, 5),
("UTF8_STRING", 0, 5),
("text/plain", 0, 5),
]
+
+ # Map target/`info` integers to action. module identifiers
cnv_types = {
20: "m3u",
21: "pls",
22: "xspf",
23: "smil",
@@ -137,12 +140,13 @@
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"))
+ row.update({"_origin": [cn.module, cn.current, cn.rowno()]}) # internal: origin channel+genre+rowid
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)
@@ -210,14 +214,12 @@
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")
+ 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
@@ -225,47 +227,54 @@
# 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")
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]) ]
+ rows += [ self.imported_row(urls[0], cnv.title()) ]
- # Extract from playlist files (don't import mp3s into stream lists directly)
+ # Extract from playlist files, either passed as text/uri-list or FILE_NAME
elif urls:
- for fn in [re.sub("^\w+://[^/]*", "", fn) for fn in urls if re.match("^(scp|file)://(localhost)?/|/", fn)]:
+ for fn in [re.sub("^\w+://[^/]*", "", fn) for fn in urls or [data] if re.match("^(scp|file)://(localhost)?/|/", fn)]:
ext = action.probe_playlist_fn_ext(fn)
- if ext:
+ if ext: # don't import mp3s into stream lists directly
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)) ]
+ rows += [ self.imported_row(urls[0], cnv.title() or 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
+ 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(cn.load, cn.current)
+ 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()
else:
self.parent.status("Unsupported station format. Not imported.")
+ # Stub row for dragged entries.
+ # Which is a workaround for the missing full playlist conversion and literal URL input
def imported_row(self, url, title=None):
return {
"title": title or "",
"url": url,
"homepage": "",