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

⌈⌋ ⎇ branch:  streamtuner2


Diff

Differences From Artifact [08c3ab8bab]:

To Artifact [f719a01f46]:


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
# encoding: UTF-8
# api: streamtuner2
# type: base
# category: ui
# title: Channel plugins
# description: Base implementation for channels and feature plugins
# version: 1.2
# license: public domain
# author: mario
# url: http://fossil.include-once.org/streamtuner2/
# pack:
#    bookmarks.py configwin.py streamedit.py history.py search.py links.py 
#    icast.py internet_radio.py itunes.py jamendo.py live365.py global_key.py
#    modarchive.py myoggradio.py punkcast.py radiobrowser.py radiotray.py
#    shoutcast.py surfmusik.py timer.py tunein.py xiph.py youtube.py
#    exportcat.py useragentswitcher.py
# config: -
# priority: core
#
#
# Just exports GenericChannel and ChannelPlugin.
# GenericChannel implements the basic GUI functions and defines
# the default channel data structure. It implements base and
# fallback logic for all other channel implementations.

#
# Built-in channels derive directly from generic. Additional
# channels don't have a pre-defined Notebook tab in the glade
# file. They derive from the ChannelPlugins class instead, which
# adds the required gtk Widgets manually.
#
# Makes module scanning available.  Checks for conf.share, so
# should pick up /usr/share/streamtuner2/channels/*.py plugins
# as well as local ./channels/*.* - Needs rework for in-zip
# searching.
#

import gtk
from uikit import uikit
from config import *
import ahttp as http
import action
import favicon


|



|












<
<

|
|
>

<
|
|
|
|
<
<
<
<
<







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
# encoding: UTF-8
# api: streamtuner2
# type: class
# category: ui
# title: Channel plugins
# description: Base implementation for channels and feature plugins
# version: 1.3
# license: public domain
# author: mario
# url: http://fossil.include-once.org/streamtuner2/
# pack:
#    bookmarks.py configwin.py streamedit.py history.py search.py links.py 
#    icast.py internet_radio.py itunes.py jamendo.py live365.py global_key.py
#    modarchive.py myoggradio.py punkcast.py radiobrowser.py radiotray.py
#    shoutcast.py surfmusik.py timer.py tunein.py xiph.py youtube.py
#    exportcat.py useragentswitcher.py
# config: -
# priority: core
#


# GenericChannel implements the basic GUI functions and defines
# the default channel data structure. It implements fallback logic
# for all other channel implementations. Only `bookmarks` uses it
# directly.
#

# All other plugins don't have a pre-defined Notebook tab in the
# GtkBuilder description. They derive from ChannelPlugins therefore,
# which constructs and registers the required gtk widgets manually.







import gtk
from uikit import uikit
from config import *
import ahttp as http
import action
import favicon
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


# generic channel module                            ---------------------------------------
class GenericChannel(object):

    # desc
    meta = { "config": [] }

    homepage = "http://fossil.include-once.org/streamtuner2/"
    base_url = ""
    listformat = "pls"
    audioformat = "audio/mpeg" # fallback value
    config = []
    has_search = False

    # categories
    categories = ["empty", ]
    catmap = {}
    current = ""
    default = "empty"
    shown = None     # last selected entry in stream list, also indicator if notebook tab has been selected once / stream list of current category been displayed yet

    # gui + data
    streams = {}      #meta information dicts
    liststore = {}    #gtk data structure
    gtk_list = None   #gtk widget
    gtk_cat = None    #gtk widget

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







>




<





|
<



|
<
|
|







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


# generic channel module                            ---------------------------------------
class GenericChannel(object):

    # desc
    meta = { "config": [] }
    config = []
    homepage = "http://fossil.include-once.org/streamtuner2/"
    base_url = ""
    listformat = "pls"
    audioformat = "audio/mpeg" # fallback value

    has_search = False

    # categories
    categories = ["empty", ]
    catmap = {}
    current = None

    shown = None     # last selected entry in stream list, also indicator if notebook tab has been selected once / stream list of current category been displayed yet

    # gui + data
    streams = {}      # Station list dict, associates each genre to a list of stream rows

    gtk_list = None   # Gtk widget for station treeview
    gtk_cat = None    # Gtk widget for category columns

    # 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", {}], ],
99
100
101
102
103
104
105

106
107
108
109
110
111
112
    ]
    rowmap = []   # [state,genre,title,...] field enumeration still needed separately
    titles = {}   # for easier adapting of column titles in datamap

    # for empty grouping / categories
    placeholder = [dict(genre="./.", title="Subcategory placeholder", playing="./.", url="none:", listeners=0, bitrate=0, homepage="", state="gtk-folder")]
    empty_stub = [dict(genre="./.", title="No categories found (HTTP error)", playing="Try Channel→Reload Categories later..", url="none:", listeners=0, bitrate=0, homepage="", state="gtk-stop")]

    
    # regex            
    rx_www_url = re.compile("""(www(\.\w+[\w-]+){2,}|(\w+[\w-]+[ ]?\.)+(com|FM|net|org|de|PL|fr|uk))""", re.I)



    #--------------------------- initialization --------------------------------







>







90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
    ]
    rowmap = []   # [state,genre,title,...] field enumeration still needed separately
    titles = {}   # for easier adapting of column titles in datamap

    # for empty grouping / categories
    placeholder = [dict(genre="./.", title="Subcategory placeholder", playing="./.", url="none:", listeners=0, bitrate=0, homepage="", state="gtk-folder")]
    empty_stub = [dict(genre="./.", title="No categories found (HTTP error)", playing="Try Channel→Reload Categories later..", url="none:", listeners=0, bitrate=0, homepage="", state="gtk-stop")]
    nothing_found = [dict(genre="./.", title="No contents found on directory server", playing="Notice", listeners=0, bitrate=0, state="gtk-info")]
    
    # regex            
    rx_www_url = re.compile("""(www(\.\w+[\w-]+){2,}|(\w+[\w-]+[ ]?\.)+(com|FM|net|org|de|PL|fr|uk))""", re.I)



    #--------------------------- initialization --------------------------------
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
        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)
        
        # stub for ST2 main window / dispatcher
        self.parent = stub_parent(None)

        # only if streamtuner2 is run in graphical mode        
        if (parent):
            self.cache()
            self.gui(parent)
        pass




        
    # initialize Gtk widgets / data objects
    def gui(self, parent):
        #print(self.module + ".gui()")

        # save reference to main window/glade API
        self.parent = parent
        self.gtk_list = parent.get_widget(self.module+"_list")
        self.gtk_cat = parent.get_widget(self.module+"_cat")
        
        # category tree
        self.display_categories()
        #uikit.tree(self.gtk_cat, self.categories, title="Category", icon=gtk.STOCK_OPEN);
        
        # update column names
        for field,title in list(self.titles.items()):
            self.update_datamap(field, title=title)
        
        # prepare stream list
        if (not self.rowmap):
            for row in self.datamap:
                for x in range(2, len(row)):
                    self.rowmap.append(row[x][0])

        # load default category
        #if (self.current):
        #    self.load(self.current)
        #else:
        if True:
            uikit.columns(self.gtk_list, self.datamap, [])
            
        # add to main menu
        uikit.add_menu([parent.channelmenuitems], self.meta["title"], lambda w: parent.channel_switch_by_name(self.module) or 1)


    # Statusbar stub (defers to parent/main window, if in GUI mode)
    def status(self, *v):
        if self.parent: self.parent.status(*v)







<
<
<
|



|
>
>
>




<








<











<
<
<
<
|
|
|







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



        # Only if streamtuner2 is run in graphical mode        
        if (parent):
            self.cache()
            self.gui(parent)

        # Stub for ST2 main window / dispatcher
        else:
            self.parent = stub_parent(None)

        
    # initialize Gtk widgets / data objects
    def gui(self, parent):


        # save reference to main window/glade API
        self.parent = parent
        self.gtk_list = parent.get_widget(self.module+"_list")
        self.gtk_cat = parent.get_widget(self.module+"_cat")
        
        # category tree
        self.display_categories()

        
        # update column names
        for field,title in list(self.titles.items()):
            self.update_datamap(field, title=title)
        
        # prepare stream list
        if (not self.rowmap):
            for row in self.datamap:
                for x in range(2, len(row)):
                    self.rowmap.append(row[x][0])





        # Initialize stations TreeView
        uikit.columns(self.gtk_list, self.datamap, [])
        
        # add to main menu
        uikit.add_menu([parent.channelmenuitems], self.meta["title"], lambda w: parent.channel_switch_by_name(self.module) or 1)


    # Statusbar stub (defers to parent/main window, if in GUI mode)
    def status(self, *v):
        if self.parent: self.parent.status(*v)
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
    # load data,
    # update treeview content
    def load(self, category, force=False):

        # get data from cache or download
        if (force or not category in self.streams):
            __print__(dbg.PROC, "load", "update_streams")
            self.parent.status("Updating streams...")
            self.parent.status(-0.1)
            if category == "empty":
                new_streams = self.empty_stub
            else:
                new_streams = self.update_streams(category)
  
            if new_streams:








|
|







231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
    # load data,
    # update treeview content
    def load(self, category, force=False):

        # get data from cache or download
        if (force or not category in self.streams):
            __print__(dbg.PROC, "load", "update_streams")
            self.status("Updating streams...")
            self.status(-0.1)
            if category == "empty":
                new_streams = self.empty_stub
            else:
                new_streams = self.update_streams(category)
  
            if new_streams:

281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
  
                # invalidate gtk list cache
                #if (self.liststore.has_key(category)):
                #    del self.liststore[category]
  
            else:
                # parse error
                self.parent.status("category parsed empty.")
                self.streams[category] = [{"title":"no contents found on directory server","bitrate":0,"max":0,"listeners":0,"playing":"error","favourite":0,"deleted":0}]
                __print__(dbg.INFO, "Oooops, parser returned nothing for category " + category)
                
        # assign to treeview model
        #self.streams[self.default] = []
        #if (self.liststore.has_key(category)):  # was already loded before
        #    self.gtk_list.set_model(self.liststore[category])
        #else:   # currently list is new, had not been converted to gtk array before
        #    self.liststore[category] = \
        uikit.do(lambda:uikit.columns(self.gtk_list, self.datamap, self.prepare(self.streams[category])))

        # set pointer
        self.current = category
        self.parent.status("")
        self.parent.status(1.0)
        pass
        
    # store current streams data
    def save(self):
        conf.save("cache/" + self.module, self.streams, gz=1)









|
|



<








|
|







267
268
269
270
271
272
273
274
275
276
277
278

279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
  
                # invalidate gtk list cache
                #if (self.liststore.has_key(category)):
                #    del self.liststore[category]
  
            else:
                # parse error
                self.status("Category parsed empty.")
                self.streams[category] = self.nothing_found
                __print__(dbg.INFO, "Oooops, parser returned nothing for category " + category)
                
        # assign to treeview model

        #if (self.liststore.has_key(category)):  # was already loded before
        #    self.gtk_list.set_model(self.liststore[category])
        #else:   # currently list is new, had not been converted to gtk array before
        #    self.liststore[category] = \
        uikit.do(lambda:uikit.columns(self.gtk_list, self.datamap, self.prepare(self.streams[category])))

        # set pointer
        self.current = category
        self.status("")
        self.status(1.0)
        pass
        
    # store current streams data
    def save(self):
        conf.save("cache/" + self.module, self.streams, gz=1)


404
405
406
407
408
409
410

411

412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429

430

431


432
433
434
435

436
437
438
439
440
441










442
443
444
445
446
447
448
        if self.current == category:
            self.reload()
    
        
    # display .current category, once notebook/channel tab is first opened
    def first_show(self):


        if (self.shown != 55555):

            __print__(dbg.PROC, self.module+".first_show()")
        
            # if category tree is empty, initialize it
            if not self.categories:
                __print__(dbg.PROC, self.module+"first_show: reload_categories");
                #self.parent.thread(self.reload_categories)
                try:
                    self.reload_categories()
                except:
                    __print__(dbg.ERR, "HTTP error or extraction failure.")
                    self.categories = ["empty"]
                self.display_categories()
                self.current = self.categories.keys()[0]
                __print__(dbg.STAT, "Use first category as current =", self.current)
                self.load(self.current)
        
            # load current category
            else:

                __print__(dbg.STAT, self.module+".first_show(): load current category =", self.current);

                self.load(self.current)


            
            # put selection/cursor on last position
            try:
                __print__(dbg.STAT, self.module+".first_show()", "select last known category treelist position =", self.shown)

                self.gtk_list.get_selection().select_path(self.shown)
            except:
                pass
                
            # this method will only be invoked once
            self.shown = 55555












    # update categories, save, and display                
    def reload_categories(self):
    
        # get data and save
        self.update_categories()







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







389
390
391
392
393
394
395
396
397
398
399
400
401
402
403

404
405
406
407
408
409



410
411

412
413
414
415
416
417
418
419

420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
        if self.current == category:
            self.reload()
    
        
    # display .current category, once notebook/channel tab is first opened
    def first_show(self):

        # Already processed
        if (self.shown == 55555):
            return
        __print__(dbg.PROC, self.module, "→ first_show()", ", current=", self.current, ", categories=", len(self.categories))
    
        # if category tree is empty, initialize it
        if not self.categories:
            __print__(dbg.PROC, self.module, "→ first_show() → reload_categories()");

            try:
                self.reload_categories()
            except:
                __print__(dbg.ERR, "HTTP error or extraction failure.")
                self.categories = ["empty"]
            self.display_categories()




        # Select first category

        self.current = self.str_from_struct(self.categories) or None
        __print__(dbg.STAT, self.module, "→ first_show(); use first category as current =", self.current)
        try:
            self.load(self.current)
        except:
            pass
    
        # put selection/cursor on last position

        __print__(dbg.STAT, self.module+".first_show()", "select last known category treelist position =", self.shown)
        try:
            self.gtk_list.get_selection().select_path(self.shown)
        except:
            pass
            
        # Invoke only once
        self.shown = 55555


    # Retrieve first list value, or key from dict (-- used to get first category on init)
    def str_from_struct(self, d):
        if isinstance(d, (str)):
            return d
        elif isinstance(d, (dict)):
            return self.str_from_struct(d.keys()) or self.str_from_struct(d.values())
        elif isinstance(d, (list, tuple)):
            return d[0] if len(d) else None


    # update categories, save, and display                
    def reload_categories(self):
    
        # get data and save
        self.update_categories()
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
            "ogg":"ogg", "ogm":"ogg", "xiph":"ogg", "vorbis":"ogg", "vnd.xiph.vorbis":"ogg",
            "mp3":"mpeg", "mp":"mpeg", "mp2":"mpeg", "mpc":"mpeg", "mps":"mpeg",
            "aac+":"aac", "aacp":"aac",
            "realaudio":"x-pn-realaudio", "real":"x-pn-realaudio", "ra":"x-pn-realaudio", "ram":"x-pn-realaudio", "rm":"x-pn-realaudio",
            # yes, we do video
            "flv":"video/flv", "mp4":"video/mp4",
        }
        map.update(action.lt)   # list type formats (.m3u .pls and .xspf)
        if map.get(s):
            s = map[s]
        # add prefix:
        if s.find("/") < 1:
            s = "audio/" + s
        #
        return s







|







524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
            "ogg":"ogg", "ogm":"ogg", "xiph":"ogg", "vorbis":"ogg", "vnd.xiph.vorbis":"ogg",
            "mp3":"mpeg", "mp":"mpeg", "mp2":"mpeg", "mpc":"mpeg", "mps":"mpeg",
            "aac+":"aac", "aacp":"aac",
            "realaudio":"x-pn-realaudio", "real":"x-pn-realaudio", "ra":"x-pn-realaudio", "ram":"x-pn-realaudio", "rm":"x-pn-realaudio",
            # yes, we do video
            "flv":"video/flv", "mp4":"video/mp4",
        }
        #map.update(action.listfmt_t)   # list type formats (.m3u .pls and .xspf)
        if map.get(s):
            s = map[s]
        # add prefix:
        if s.find("/") < 1:
            s = "audio/" + s
        #
        return s