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
# encoding: UTF-8
# api: streamtuner2
# type: class
# category: ui
# title: Channel plugins
# description: Base implementation for channels and feature plugins
# version: 1.6
# license: public domain
# author: mario
# url: http://fossil.include-once.org/streamtuner2/
# pack:
#    *.py
# config: -
# priority: core






|







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.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
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"
]
__path__.insert(0, conf.dir + "/plugins")





















































# Generic channel module
class GenericChannel(object):

    # control attributes
    meta = { "config": [] }
    base_url = ""
    listformat = "pls"
    audioformat = "audio/mpeg" # fallback value
    has_search = False







|
|




>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>


|







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__ = [
    "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(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
    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", {}], ],







<







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


    # 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

    # 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)
        
        # extra init function
        if hasattr(self, "init2"):
            self.init2(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,







<
<
<
<
<
<
<

|
|
<
<
<







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







        
        # base init (meta infos, parent reference, init2)
        FeaturePlugin.__init__(self, parent)



        
        # 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
    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):







<
<
<
<
<
<
<
<







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`










        
    #--------------------- 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
            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"







<
<
<
<
<
<
<
<
<







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

        # 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):







>
>







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
                    #"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"],







|







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"],