Internet radio browser GUI for music/video streams from various directory services.

āŒˆāŒ‹ āŽ‡ branch:  streamtuner2


Check-in [e0e28edba2]

Overview
Comment:Experiment with a few more target types. Works with a few text editors on STRING. Most just want a uri-list, but can't handle it.
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | trunk
Files: files | file ages | folders
SHA1: e0e28edba26bf906967648092b554b80599875e4
User & Date: mario on 2015-04-19 19:35:57
Other Links: manifest | tags
Context
2015-04-19
22:17
Implement in-application row copying per JSON (info=51, mime=json/vnd.streamtuner2.station). Fixed set_text() bug by using set("STRING",..) atom instead. check-in: 38812e4bbf user: mario tags: trunk
19:35
Experiment with a few more target types. Works with a few text editors on STRING. Most just want a uri-list, but can't handle it. check-in: e0e28edba2 user: mario tags: trunk
19:35
Add plugin defaults. check-in: 6a17061df0 user: mario tags: trunk
Changes

Modified action.py from [820487cd33] to [ceb9b4a4e2].

387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
                    row["url"] = url
                    new_rows.append(row)
                    # Or just allow one stream per station in a playlist entry
                    if not self.multiply:
                        break
            rows = new_rows

        debug(dbg.DATA, "conversion to:", dest, " from:", self.source, "with rows=", rows)

        # call conversion schemes
        converter = getattr(self, dest) or self.pls
        return converter(rows)

    # save directly
    def file(self, rows, dest, fn):







|







387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
                    row["url"] = url
                    new_rows.append(row)
                    # Or just allow one stream per station in a playlist entry
                    if not self.multiply:
                        break
            rows = new_rows

        debug(dbg.DATA, "conversion to:", dest, "  with rows=", rows)

        # call conversion schemes
        converter = getattr(self, dest) or self.pls
        return converter(rows)

    # save directly
    def file(self, rows, dest, fn):
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472


    # SMIL
    def smil(self, rows):
        txt = """<smil>\n<head>\n\t<meta name="title" content="%s"/>\n</head>\n<body>\n\t<seq>\n""" % (rows[0]["title"])
        for row in rows:
            if row.get("url"):
                txt += """\t\t<audio src="%s"/>\n""" % row["url"]
        txt += """\t</seq>\n</body>\n</smil>\n"""
        return txt




# Generate filename for temporary .m3u, if possible with unique id







|







458
459
460
461
462
463
464
465
466
467
468
469
470
471
472


    # SMIL
    def smil(self, rows):
        txt = """<smil>\n<head>\n\t<meta name="title" content="%s"/>\n</head>\n<body>\n\t<seq>\n""" % (rows[0]["title"])
        for row in rows:
            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




# Generate filename for temporary .m3u, if possible with unique id

Modified channels/dnd.py from [2bdcd36106] to [3ecc3ca6e1].

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
57
58

59
60
61
62
63
64
65
66
67
68
69
70
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
113
114
115
116

117



118


119
120
121
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
# 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


# category: ui

#
# 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 tab to move favourites around.









import copy
from config import *
from uikit import *
import action


# Drag and Drop support
class dnd(object):

    module = "dnd"
    meta = plugin_meta()

    # Keeps selected row on starting DND event
    row = None
    # Buffer converted types
    buf = {}

    # Supported type map
    drag_types = [

      ("json/vnd.streamtuner2.station", 0, 51),

      ("audio/x-mpegurl", 0, 20),
      ("application/x-scpls", 0, 21),
      ("application/xspf+xml", 0, 22),

      ("FILE_NAME", 0, 3),





      ("text/uri-list", 0, 4),


      ("STRING", 0, 5),

      ("text/plain", 0, 5),



    ]
    cnv_types = {
       20: "m3u",
       21: "pls",
       22: "xspf",



        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)



    # 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():
            w = module.gtk_list
            # bind SOURCE events
            w.enable_model_drag_source(gtk.gdk.BUTTON1_MASK, self.drag_types, gtk.gdk.ACTION_DEFAULT|gtk.gdk.ACTION_COPY|gtk.gdk.ACTION_MOVE)
            w.connect('drag-begin', self.begin)
            w.connect('drag-data-get', self.data_get)
            # bind DESTINATION events
            w.enable_model_drag_dest(self.drag_types, gtk.gdk.ACTION_DEFAULT|gtk.gdk.ACTION_COPY)
            w.connect('drag-drop', self.drop)#self.drag_types
            w.connect('drag-data-received', self.data_received)



    # -- SOURCE, drag'n'drop from ST2 to elsewhere --

    # Starting to drag a row
    def begin(self, widget, context):
        __print__(dbg.UI, "dndā†source: begin-drag, store current row")
        self.row = self.treelist_row()
        self.buf = {}
        #context.set_icon_stock("gtk-add", 2, 2)
        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)

        return row

        
    # Target window/app requests data for offered drop
    def data_get(self, widget, context, selection, info, time):
        __print__(dbg.UI, "dndā†source: data-get, send and convert to requested target type", info)

        # Start new converter if not buffered (because `data_get` gets called mercilessly along the dragging path)
        if not info in self.buf:
            r = self.row
            cnv = action.save_playlist(source=r["listformat"], multiply=False)





            # Pass M3U/PLS/XSPF as direct content, or internal JSON even
            if info >= 20:
                buf = 'set_text', cnv.export(urls=[r["url"]], row=r, dest=self.cnv_types[info])
            # Create temporary PLS file, because "text/uri-list" is widely misunderstood and just implemented for file:// IRLs

            elif info <= 4:
                fn = "{}/{}.pls".format(conf.tmp, re.sub("[^\w-]+", " ", r["title"]))
                cnv.file(rows=[r], dest="pls", fn=fn)
                if info == 4:
                    fn = ["file://localhost{}".format(fn)]
                buf = 'set_uris', fn

            # Text sources are assumed to understand the literal URL, or expect a description



            else:


                buf = 'set_text', "{url}\n# Title: {title}\n# Homepage: {homepage}".format(**r)

            # Buffer
            self.buf[info] = buf
            
        # Return prepared data
        func, data = self.buf[info]
        if func in ('set_text'):
            selection.set_text(data)
        else:

            selection.set_uris(data)
        return True


                
    # -- DESTINATION, when playlist/url gets dragged in from other app --

    # Just a notification for incoming drop
    def drop(self, widget, context, x, y, time):
        __print__(dbg.UI, "dndā†’dest: drop-probing", context.targets, x, y, time, context.drag_get_selection())
        widget.drag_get_data(context, context.targets[0], time)

        return True

    # Actual data is being passed,
    # now has to be converted and patched into stream rows and channel liststore
    def data_received(self, widget, context, x, y, selection, info, time):
        __print__(dbg.UI, "dndā†’dest: data-receival", x,y,selection, info, time, selection.get_uris(), selection.get_text())

        context.finish(True, False, time)
        return True









>
>

>








>
>
>
>
>
>
>

|

















>

>



>
|
>
>
>
>
>
|
>
>

>

>
>
>





>
>
>










>














|











|

<







>

<



|






>
>
>
>
|
|
|
<
>
|
<
<
<
|
|
>
|
>
>
>

>
>
|

|

|


|
|
<
>


>







|
>





|
>




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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
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
113
114

115
116
117
118
119
120
121
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
# 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: pls, 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 tab to move favourites around.


# mousepad == ['GTK_TEXT_BUFFER_CONTENTS', 'application/x-gtk-text-buffer-rich-text',
#   'UTF8_STRING', 'COMPOUND_TEXT', 'TEXT', 'STRING',
#   'text/plain;charset=utf-8', 'text/plain']
# libreoffice ==# ['text/plain;charset=utf-8', 'UTF8_STRING', 'application/x-openoffice-embed-source-xml;windows_formatname="Star Embed# Source (XML)"', 'text/richtext', 'text/html',
#    'application/x-openoffice-objectdescriptor-xml;windows_formatname="Star Object Descriptor (XML)";classname="8BC6B165-B1B2-4EDD-aa47-dae2ee689dd6";typename="LibreOffice 4.4 Textdokument";viewaspect="1";width="16999";height="2995";posx="5347";posy="5347"']


import copy
from config import conf, __print__, dbg, json
from uikit import *
import action


# Drag and Drop support
class dnd(object):

    module = "dnd"
    meta = plugin_meta()

    # Keeps selected row on starting DND event
    row = None
    # Buffer converted types
    buf = {}

    # Supported type map
    drag_types = [
      # internal
      ("json/vnd.streamtuner2.station", 0, 51),
      # literal exports
      ("audio/x-mpegurl", 0, 20),
      ("application/x-scpls", 0, 21),
      ("application/xspf+xml", 0, 22),
      ("application/smil", 0, 23),
      ("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),
      # filename, file:// IRL
      ("FILE_NAME", 0, 3),
      ("text/uri-list", 0, 4),
    ]
    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)


    # 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():
            w = module.gtk_list
            # bind SOURCE events
            w.enable_model_drag_source(gtk.gdk.BUTTON1_MASK, self.drag_types, gtk.gdk.ACTION_DEFAULT|gtk.gdk.ACTION_COPY|gtk.gdk.ACTION_MOVE)
            w.connect('drag-begin', self.begin)
            w.connect('drag-data-get', self.data_get)
            # bind DESTINATION events
            w.enable_model_drag_dest(self.drag_types, gtk.gdk.ACTION_DEFAULT|gtk.gdk.ACTION_COPY)
            w.connect('drag-drop', self.drop)
            w.connect('drag-data-received', self.data_received)



    # -- SOURCE, drag'n'drop from ST2 to elsewhere --

    # Starting to drag a row
    def begin(self, widget, context):
        __print__(dbg.UI, "dndā†source: 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):
        __print__(dbg.UI, "dndā†source: data-get, send and convert to requested target type", info, selection.get_target())

        # Start new converter if not buffered (because `data_get` gets called mercilessly along the dragging path)
        if not info in self.buf:
            r = self.row
            cnv = action.save_playlist(source=r["listformat"], multiply=False)

            # internal JSON row
            info = 5
            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 prepared data
        func, data = self.buf[info]
        if func.find("text") >= 0:
            selection.set_text(data, len(data))

        if func.find("uris") >= 0:
            selection.set_uris(data)
        return True


                
    # -- DESTINATION, when playlist/url gets dragged in from other app --

    # Just a notification for incoming drop
    def drop(self, widget, context, x, y, time):
        __print__(dbg.UI, "dndā†’dest: drop-probing", context.targets, x, y, time, context.drag_get_selection())
        widget.drag_get_data(context, "STRING", time)#context.targets[0], time)
        context.drop_reply(True, time)
        return True

    # Actual data is being passed,
    # now has to be converted and patched into stream rows and channel liststore
    def data_received(self, widget, context, x, y, selection, info, time):
        __print__(dbg.UI, "dndā†’dest: data-receival", info, selection.get_target(), selection.get_uris(), selection.get_text())
        context.drop_finish(True, time)
        context.finish(True, False, time)
        return True