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

⌈⌋ ⎇ branch:  streamtuner2


Check-in [ea924e3c27]

Overview
Comment:Introduce FeaturePlugin as new base class for channels and all other plugins. Pre-defines the meta, module attributes and calls init2().
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | trunk
Files: files | file ages | folders
SHA1: ea924e3c27911b844b8b6159392ce90b29cb609c
User & Date: mario on 2017-01-05 21:23:23
Other Links: manifest | tags
Context
2017-01-05
21:33
Fix `links` plugin format: attribute; make it understood by channel.play() that a homepage-only row triggers the web browser. check-in: f48ad79aa1 user: mario tags: trunk
21:23
Introduce FeaturePlugin as new base class for channels and all other plugins. Pre-defines the meta, module attributes and calls init2(). check-in: ea924e3c27 user: mario tags: trunk
21:22
Detect more absent variables/login, introduce UI delay on submission. check-in: 9eeccf1f29 user: mario tags: trunk
Changes

Modified channels/__init__.py from [c58402c723] to [4a4e01489e].

1
2
3
4
5
6
7

8
9
10
11
12
13
14
1
2
3
4
5
6

7
8
9
10
11
12
13
14






-
+







# encoding: UTF-8
# api: streamtuner2
# type: class
# category: ui
# title: Channel plugins
# description: Base implementation for channels and feature plugins
# version: 1.6
# version: 1.7
# license: public domain
# author: mario
# url: http://fossil.include-once.org/streamtuner2/
# pack:
#    *.py
# config: -
# priority: core
34
35
36
37
38
39
40
41
42


43
44
45
46
47


















































48
49

50
51
52
53
54
55
56
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







-
-
+
+





+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

-
+







import re
import copy
import inspect


# Only export plugin classes and a few utility functions
__all__ = [
    "GenericChannel", "ChannelPlugin", "use_rx", "mime_fmt",
    "entity_decode", "strip_tags", "nl", "unhtml", "to_int"
    "FeaturePlugin", "GenericChannel", "ChannelPlugin", "use_rx", "mime_fmt",
    "stub_parent", "entity_decode", "strip_tags", "nl", "unhtml", "to_int"
]
__path__.insert(0, conf.dir + "/plugins")



# Base class for plugins (features or channels)
class FeaturePlugin(object):

    # plugin meta and object references
    module = ""
    meta = { "config": [] }
    parent = None     # main window

    # minimum setup for ChannelPlugins and feature hooks
    def __init__(self, parent, *k, **kw):

        # set up meta infos
        self.parent = parent
        self.module = self.__class__.__name__
        self.meta = plugin_meta(src = inspect.getcomments(inspect.getmodule(self)))
        self.config = self.meta.get("config", [])
        self.title = self.meta.get("title", self.module)

        # add default options values to config.conf.* dict
        conf.add_plugin_defaults(self.meta, self.module)

        # implicit action handler registration
        if hasattr(self, "resolve_urn"):
            action.handler["urn:%s" % self.module] = self.resolve_urn

        # secondary init function
        self.init2(parent)

    # optionally to be overriden by plugins (run after base __init__)
    def init2(self, parent, *k, **kw):
        pass

    # Statusbar stub (defers to parent/main window, if in GUI mode)
    def status(self, *args, **kw):
        if self.parent:
            self.parent.status(*args, **kw)
        else:
            log.INFO("status():", *v)
        
    # Statusbar with highlighting and default icon
    def warn(self, text, *args, **kw):
        if isinstance(text, (str, unicode)) and "status_color" in conf:
            text = "<span background='%s'>%s</span>" % (conf.status_color, text)
            kw["markup"] = 1
        if not "icon" in kw:
            kw["icon"] = "gtk-dialog-warning"
        self.status(text, *args, **kw)



# Generic channel module
class GenericChannel(object):
class GenericChannel(FeaturePlugin):

    # control attributes
    meta = { "config": [] }
    base_url = ""
    listformat = "pls"
    audioformat = "audio/mpeg" # fallback value
    has_search = False
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
117
118
119
120
121
122
123

124
125
126
127
128
129
130







-







    gtk_list = None   # Gtk widget for station treeview
    gtk_cat = None    # Gtk widget for category columns
    ls = None         # ListStore for station treeview
    rowmap = None     # Preserve streams-datamap
    pix_entry = None  # ListStore entry that contains favicon
    img_resize = None  # Rescale `img` references to icon size
    fixed_size = [24,24]  # Default height+width for favicons
    parent = None     # reference to main window

    # mapping of stream{} data into gtk treeview/treestore representation
    datamap = [
       # coltitle   width	[ datasrc key, type, renderer, attrs ]	[cellrenderer2], ...
       ["",		20,	["state",	str,  "pixbuf",	{}],	],
       ["Genre",	65,	['genre',	str,	"t",	{}],	],
       ["Station Title",275,	["title",	str,    "text",	{"strikethrough":11, "cell-background":12, "cell-background-set":13}],  ["favicon", gtk.gdk.Pixbuf, "pixbuf", {}], ],
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
173
174
175
176
177
178
179





180



181



182


183
184
185
186
187
188
189







-
-
-
-
-
+
-
-
-
+
-
-
-
+
-
-








    # constructor
    def __init__(self, parent=None):
    
        #self.streams = {}
        self.gtk_list = None
        self.gtk_cat = None
        self.module = self.__class__.__name__
        self.meta = plugin_meta(src = inspect.getcomments(inspect.getmodule(self)))
        self.config = self.meta.get("config", [])
        self.title = self.meta.get("title", self.module)

        
        # add default options values to config.conf.* dict
        conf.add_plugin_defaults(self.meta, self.module)
        
        # base init (meta infos, parent reference, init2)
        # extra init function
        if hasattr(self, "init2"):
            self.init2(parent)
        FeaturePlugin.__init__(self, parent)
        if hasattr(self, "resolve_urn"):
            action.handler["urn:%s" % self.module] = self.resolve_urn
        
        # Only if streamtuner2 is run in graphical mode        
        if (parent):
            # Update/display stream processors
            if not self.prepare_filters:
                self.prepare_filters += [
                    self.prepare_filter_icons,
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
229
230
231
232
233
234
235








236
237
238
239
240
241
242







-
-
-
-
-
-
-
-







    def columns(self, entries=None):
        self.ls, self.rowmap, self.pix_entry = uikit.columns(
            self.gtk_list, self.datamap, entries,
            show_favicons=True, fixed_size=self.fixed_size
        )
        # no longer using `conf.show_favicons`


    # Statusbar stub (defers to parent/main window, if in GUI mode)
    def status(self, *args, **kw):
        if self.parent: self.parent.status(*args, **kw)
        else: log.INFO("status():", *v)
    def warn(self, *args, **kw):
        self.status(*args, icon="gtk-dialog-warning", **kw)


        
    #--------------------- streams/model data accesss ---------------------------


    # Traverse category TreeModel to set current, expand parent nodes
    def select_current(self, name):
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
616
617
618
619
620
621
622









623
624
625
626
627
628
629







-
-
-
-
-
-
-
-
-







            listformat = row.get("listformat", self.listformat)
            action.record(row, audioformat, listformat)
        return row



    

        
    








# channel plugin without glade-pre-defined notebook tab
#
class ChannelPlugin(GenericChannel):

    module = "abstract"
678
679
680
681
682
683
684


685
686
687
688
689
690
691
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715







+
+








        # try to initialize superclass now, before adding to channel tabs
        GenericChannel.gui(self, parent)

        # add notebook tab
        tab = parent.notebook_channels.insert_page_menu(vbox, ev_label, plain_label, -1)
        parent.notebook_channels.set_tab_reorderable(vbox, True)





# WORKAROUND for direct channel module imports,
# eases instantiations without GUI a little,
# reducing module dependencies (conf. / ahttp. / channels. / parent.) would be better
def stub_parent(object):

Modified channels/jamendo.py from [0db1a39017] to [d440527930].

320
321
322
323
324
325
326
327

328
329
330
331
332
333
334
320
321
322
323
324
325
326

327
328
329
330
331
332
333
334







-
+







                    #"url": "http://api.jamendo.com/get2/stream/track/xspf/?album_id=%s&streamencoding=ogg2&n=all&from=app-%s" % (e["id"], self.cid),
                    #"format": "audio/ogg",
                    #"listformat": "xspf",
                    "url": "http://api.jamendo.com/v3.0/tracks?client_id={}&audioformat={}&album_id={}".format(self.cid, fmt, e["id"]),
                    "listformat": "jamj",
                    "format": fmt_mime,
                })
		

        # Feeds (News)
        elif cat == "feeds":
            for e in self.api(method="feeds", order="date_start_desc", target="notlogged"):
              if e.get("joinid") and e.get("subtitle"):
                entries.append({
                    "genre": e["type"],
                    "title": e["title"]["en"],