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

⌈⌋ ⎇ branch:  streamtuner2


Diff

Differences From Artifact [4696fba60c]:

To Artifact [0b9e0b6b81]:


95
96
97
98
99
100
101

102
103
104
105
106
107
108
109
110
111
112
sys.path.insert(0, ".")   # pre-defined directory for modules

# gtk modules
from mygtk import pygtk, gtk, gobject, ui_file, mygtk

# custom modules
from config import conf   # initializes itself, so all conf.vars are available right away

import http
import action  # needs workaround... (action.main=main)
from channels import *
from channels import __print__
import favicon
#from pq import pq



# this represents the main window
# and also contains most application behaviour







>



<







95
96
97
98
99
100
101
102
103
104
105

106
107
108
109
110
111
112
sys.path.insert(0, ".")   # pre-defined directory for modules

# gtk modules
from mygtk import pygtk, gtk, gobject, ui_file, mygtk

# custom modules
from config import conf   # initializes itself, so all conf.vars are available right away
from config import __print__, dbg
import http
import action  # needs workaround... (action.main=main)
from channels import *

import favicon
#from pq import pq



# this represents the main window
# and also contains most application behaviour
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
        current_channel = "bookmarks"    # currently selected channel name (as index in self.channels{})


        # constructor
        def __init__(self):

            # gtkrc stylesheet
            self.load_theme(), gui_startup(1/20)

            # instantiate gtk/glade widgets in current object
            gtk.Builder.__init__(self)
            gtk.Builder.add_from_file(self, conf.find_in_dirs([".", conf.share], ui_file)), gui_startup(2/20)
            # manual gtk operations
            self.extensionsCTM.set_submenu(self.extensions)  # duplicates Station>Extension menu into stream context menu

            # initialize channels
            self.channels = {
              "bookmarks": bookmarks(parent=self),   # this the remaining built-in channel
              "shoutcast": None,#shoutcast(parent=self),
            }
            gui_startup(3/20)
            self.load_plugin_channels()   # append other channel modules / plugins


            # load application state (widget sizes, selections, etc.)
            try:
                winlayout = conf.load("window")
                if (winlayout):
                    mygtk.app_restore(self, winlayout)
                # selection values
                winstate = conf.load("state")
                if (winstate):
                    for id in winstate.keys():
                        self.channels[id].current = winstate[id]["current"]
                        self.channels[id].shown = winlayout[id+"_list"].get("row:selected", 0)   # actually just used as boolean flag (for late loading of stream list), selection bar has been positioned before already
            except:
                pass # fails for disabled/reordered plugin channels

            # display current open channel/notebook tab
            gui_startup(17/20)
            self.current_channel = self.current_channel_gtk()
            try: self.channel().first_show()
            except: print("channel .first_show() initialization error")

      
            # bind gtk/glade event names to functions
            gui_startup(19/20)
            self.connect_signals(dict( {
                "gtk_main_quit" : self.gtk_main_quit,                # close window
                # treeviews / notebook
                "on_stream_row_activated" : self.on_play_clicked,    # double click in a streams list
                "on_category_clicked": self.on_category_clicked,     # new selection in category list
                "on_notebook_channels_switch_page": self.channel_switch,   # channel notebook tab changed
                "station_context_menu": lambda tv,ev: station_context_menu(tv,ev),







|



|








|


















|


|



|







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
        current_channel = "bookmarks"    # currently selected channel name (as index in self.channels{})


        # constructor
        def __init__(self):

            # gtkrc stylesheet
            self.load_theme(), gui_startup(1/20.0)

            # instantiate gtk/glade widgets in current object
            gtk.Builder.__init__(self)
            gtk.Builder.add_from_file(self, conf.find_in_dirs([".", conf.share], ui_file)), gui_startup(2/20.0)
            # manual gtk operations
            self.extensionsCTM.set_submenu(self.extensions)  # duplicates Station>Extension menu into stream context menu

            # initialize channels
            self.channels = {
              "bookmarks": bookmarks(parent=self),   # this the remaining built-in channel
              "shoutcast": None,#shoutcast(parent=self),
            }
            gui_startup(3/20.0)
            self.load_plugin_channels()   # append other channel modules / plugins


            # load application state (widget sizes, selections, etc.)
            try:
                winlayout = conf.load("window")
                if (winlayout):
                    mygtk.app_restore(self, winlayout)
                # selection values
                winstate = conf.load("state")
                if (winstate):
                    for id in winstate.keys():
                        self.channels[id].current = winstate[id]["current"]
                        self.channels[id].shown = winlayout[id+"_list"].get("row:selected", 0)   # actually just used as boolean flag (for late loading of stream list), selection bar has been positioned before already
            except:
                pass # fails for disabled/reordered plugin channels

            # display current open channel/notebook tab
            gui_startup(17/20.0)
            self.current_channel = self.current_channel_gtk()
            try: self.channel().first_show()
            except: __print__(dbg.INIT, "main.__init__: current_channel.first_show() initialization error")

      
            # bind gtk/glade event names to functions
            gui_startup(19/20.0)
            self.connect_signals(dict( {
                "gtk_main_quit" : self.gtk_main_quit,                # close window
                # treeviews / notebook
                "on_stream_row_activated" : self.on_play_clicked,    # double click in a streams list
                "on_category_clicked": self.on_category_clicked,     # new selection in category list
                "on_notebook_channels_switch_page": self.channel_switch,   # channel notebook tab changed
                "station_context_menu": lambda tv,ev: station_context_menu(tv,ev),
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
                "streamedit_open": streamedit.open,
                "streamedit_save": streamedit.save,
                "streamedit_new": streamedit.new,
                "streamedit_cancel": streamedit.cancel,
            }.items() + self.add_signals.items() ))
            
            # actually display main window
            gui_startup(99/100)
            self.win_streamtuner2.show()
            
            # WHY DON'T YOU WANT TO WORK?!
            #self.shoutcast.gtk_list.set_enable_search(True)
            #self.shoutcast.gtk_list.set_search_column(4)









|







219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
                "streamedit_open": streamedit.open,
                "streamedit_save": streamedit.save,
                "streamedit_new": streamedit.new,
                "streamedit_cancel": streamedit.cancel,
            }.items() + self.add_signals.items() ))
            
            # actually display main window
            gui_startup(99/100.0)
            self.win_streamtuner2.show()
            
            # WHY DON'T YOU WANT TO WORK?!
            #self.shoutcast.gtk_list.set_enable_search(True)
            #self.shoutcast.gtk_list.set_search_column(4)


279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
                self.notebook_channels.set_current_page(self.channel_names.index(page))
            # notebook invocation:
            else: #if type(page_num) == int:
                self.current_channel = self.channel_names[page_num]
            
            # if first selected, load current category
            try:
                print("try: .first_show", self.channel().module);
                print(self.channel().first_show)
                print(self.channel().first_show())
            except:
                print("channel .first_show() initialization error")


        # convert ListStore iter to row number
        def rowno(self):
            (model, iter) = self.model_iter()
            return model.get_path(iter)[0]








|
|
|

|







279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
                self.notebook_channels.set_current_page(self.channel_names.index(page))
            # notebook invocation:
            else: #if type(page_num) == int:
                self.current_channel = self.channel_names[page_num]
            
            # if first selected, load current category
            try:
                __print__(dbg.PROC, "channel_switch: try .first_show", self.channel().module);
                __print__(self.channel().first_show)
                __print__(self.channel().first_show())
            except:
                __print__(dbg.INIT, "channel .first_show() initialization error")


        # convert ListStore iter to row number
        def rowno(self):
            (model, iter) = self.model_iter()
            return model.get_path(iter)[0]

333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
            url = self.selected("homepage")             
            action.browser(url)

             
        # browse channel
        def on_homepage_channel_clicked(self, widget, event=2):
            if event == 2 or event.type == gtk.gdk._2BUTTON_PRESS:
                __print__("dblclick")
                action.browser(self.channel().homepage)            


        # reload stream list in current channel-category
        def on_reload_clicked(self, widget=None, reload=1):
            __print__("reload", reload, self.current_channel, self.channels[self.current_channel], self.channel().current)
            category = self.channel().current
            self.thread(
                lambda: (  self.channel().load(category,reload), reload and self.bookmarks.heuristic_update(self.current_channel,category)  )
            )

            
        # thread a function, add to worker pool (for utilizing stop button)







|





|







333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
            url = self.selected("homepage")             
            action.browser(url)

             
        # browse channel
        def on_homepage_channel_clicked(self, widget, event=2):
            if event == 2 or event.type == gtk.gdk._2BUTTON_PRESS:
                __print__(dbg.UI, "dblclick")
                action.browser(self.channel().homepage)            


        # reload stream list in current channel-category
        def on_reload_clicked(self, widget=None, reload=1):
            __print__(dbg.UI, "reload", reload, self.current_channel, self.channels[self.current_channel], self.channel().current)
            category = self.channel().current
            self.thread(
                lambda: (  self.channel().load(category,reload), reload and self.bookmarks.heuristic_update(self.current_channel,category)  )
            )

            
        # thread a function, add to worker pool (for utilizing stop button)
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
                thread = self.working.pop()
                thread.stop()

        
        # click in category list
        def on_category_clicked(self, widget, event, *more):
            category = self.channel().currentcat()
            __print__("on_category_clicked", category, self.current_channel)
            self.on_reload_clicked(None, reload=0)
            pass


        # add current selection to bookmark store
        def bookmark(self, widget):
            self.bookmarks.add(self.row())







|







363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
                thread = self.working.pop()
                thread.stop()

        
        # click in category list
        def on_category_clicked(self, widget, event, *more):
            category = self.channel().currentcat()
            __print__(dbg.UI, "on_category_clicked", category, self.current_channel)
            self.on_reload_clicked(None, reload=0)
            pass


        # add current selection to bookmark store
        def bookmark(self, widget):
            self.bookmarks.add(self.row())
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
            
            # resort with tab order
            order = [module.strip() for module in conf.channel_order.lower().replace(".","_").replace("-","_").split(",")]
            ls = [module for module in (order) if (module in ls)] + [module for module in (ls) if (module not in order)]

            # step through
            for module in ls:
                gui_startup(2/10 + 7/10 * float(ls.index(module))/len(ls), "loading module "+module)
                                
                # skip module if disabled
                if conf.plugins.get(module, 1) == False:
                    __print__("disabled plugin:", module)
                    continue
                
                # load plugin
                try:
                    plugin = __import__("channels."+module, None, None, [""])
                    plugin_class = plugin.__dict__[module]
                
                    # load .config settings from plugin
                    conf.add_plugin_defaults(plugin_class.config, module)

                    # add and initialize channel
                    if issubclass(plugin_class, GenericChannel):
                        self.channels[module] = plugin_class(parent=self)
                        if module not in self.channel_names:  # skip (glade) built-in channels
                            self.channel_names.append(module)
                    # other plugin types
                    else:
                        self.features[module] = plugin_class(parent=self)
                    
                except Exception as e:
                    print("error initializing:", module, ", exception:")
                    import traceback
                    traceback.print_exc()

            # default plugins
            conf.add_plugin_defaults(self.channels["bookmarks"].config, "bookmarks")
            #conf.add_plugin_defaults(self.channels["shoutcast"].config, "shoutcast")








|



|




















|







460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
            
            # resort with tab order
            order = [module.strip() for module in conf.channel_order.lower().replace(".","_").replace("-","_").split(",")]
            ls = [module for module in (order) if (module in ls)] + [module for module in (ls) if (module not in order)]

            # step through
            for module in ls:
                gui_startup(2/10.0 + 7/10.0 * float(ls.index(module))/len(ls), "loading module "+module)
                                
                # skip module if disabled
                if conf.plugins.get(module, 1) == False:
                    __print__(dbg.STAT, "disabled plugin:", module)
                    continue
                
                # load plugin
                try:
                    plugin = __import__("channels."+module, None, None, [""])
                    plugin_class = plugin.__dict__[module]
                
                    # load .config settings from plugin
                    conf.add_plugin_defaults(plugin_class.config, module)

                    # add and initialize channel
                    if issubclass(plugin_class, GenericChannel):
                        self.channels[module] = plugin_class(parent=self)
                        if module not in self.channel_names:  # skip (glade) built-in channels
                            self.channel_names.append(module)
                    # other plugin types
                    else:
                        self.features[module] = plugin_class(parent=self)
                    
                except Exception as e:
                    __print__(dbg.INIT, "load_plugin_channels: error initializing:", module, ", exception:")
                    import traceback
                    traceback.print_exc()

            # default plugins
            conf.add_plugin_defaults(self.channels["bookmarks"].config, "bookmarks")
            #conf.add_plugin_defaults(self.channels["shoutcast"].config, "shoutcast")

512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
            conf.save("state", channelopts, nice=1)


        # apply gtkrc stylesheet
        def load_theme(self):
            if conf.get("theme"):
                for dir in (conf.dir, conf.share, "/usr/share"):
                    f = dir + "/themes/" + conf.theme + "/gtk-2"+".0/gtkrc"
                    if os.path.exists(f):
                        gtk.rc_parse(f)
                pass


        # end application and gtk+ main loop
        def gtk_main_quit(self, widget, *x):







|







512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
            conf.save("state", channelopts, nice=1)


        # apply gtkrc stylesheet
        def load_theme(self):
            if conf.get("theme"):
                for dir in (conf.dir, conf.share, "/usr/share"):
                    f = dir + "/themes/" + conf.theme + "/gtk-2.0/gtkrc"
                    if os.path.exists(f):
                        gtk.rc_parse(f)
                pass


        # end application and gtk+ main loop
        def gtk_main_quit(self, widget, *x):
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
        
        # set/load values between gtk window and conf. dict
        def apply(self, config, prefix="config_", save=0):
            for key,val in config.iteritems():
                # map non-alphanumeric chars from config{} to underscores in according gtk widget names
                id = re.sub("[^\w]", "_", key)
                w = main.get_widget(prefix + id)
                __print__("config_save", save, prefix+id, w, val)
                # recurse into dictionaries, transform: conf.play.audio/mp3 => conf.play_audio_mp3
                if (type(val) == dict):
                    self.apply(val, prefix + id + "_", save)
                # load or set gtk.Entry text field
                elif (w and save and type(w)==gtk.Entry):
                    config[key] = w.get_text()
                elif (w and type(w)==gtk.Entry):







|







772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
        
        # set/load values between gtk window and conf. dict
        def apply(self, config, prefix="config_", save=0):
            for key,val in config.iteritems():
                # map non-alphanumeric chars from config{} to underscores in according gtk widget names
                id = re.sub("[^\w]", "_", key)
                w = main.get_widget(prefix + id)
                __print__(dbg.CONF, "config", ("save" if save else "load"), prefix+id, w, val)
                # recurse into dictionaries, transform: conf.play.audio/mp3 => conf.play_audio_mp3
                if (type(val) == dict):
                    self.apply(val, prefix + id + "_", save)
                # load or set gtk.Entry text field
                elif (w and save and type(w)==gtk.Entry):
                    config[key] = w.get_text()
                elif (w and type(w)==gtk.Entry):
795
796
797
798
799
800
801







802
803
804
805
806
807
808
809
810
811
        # fill combobox
        def combobox_theme(self):
           # self.theme.combo_box_new_text()
            # find themes
            themedirs = (conf.share+"/themes", conf.dir+"/themes", "/usr/share/themes")
            themes = ["no theme"]
            [[themes.append(e) for e in os.listdir(dir)] for dir in themedirs if os.path.exists(dir)]







            # add to combobox
            for num,themename in enumerate(themes):
                 self.theme.append_text(themename)
                 if conf.theme == themename:
                     self.theme.set_active(num)
            # erase this function, so it only ever gets called once
            self.combobox_theme = lambda: None


        # retrieve currently selected value







>
>
>
>
>
>
>


|







795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
        # fill combobox
        def combobox_theme(self):
           # self.theme.combo_box_new_text()
            # find themes
            themedirs = (conf.share+"/themes", conf.dir+"/themes", "/usr/share/themes")
            themes = ["no theme"]
            [[themes.append(e) for e in os.listdir(dir)] for dir in themedirs if os.path.exists(dir)]
            __print__(dbg.STAT, themes)
            # prepare liststore
            store = gtk.ListStore(gobject.TYPE_STRING)
            self.theme.set_model(store)
            cell = gtk.CellRendererText()
            self.theme.pack_start(cell, True)
            self.theme.add_attribute(cell, "text", 0)
            # add to combobox
            for num,themename in enumerate(themes):
                 store.append([themename])
                 if conf.theme == themename:
                     self.theme.set_active(num)
            # erase this function, so it only ever gets called once
            self.combobox_theme = lambda: None


        # retrieve currently selected value
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
            self.load(self.default)
            self.urls.append(row["url"])


        # simplified gtk TreeStore display logic (just one category for the moment, always rebuilt)
        def load(self, category, force=False):
            #self.liststore[category] = \
#            print(category, self.streams.keys())
            mygtk.columns(self.gtk_list, self.datamap, self.prepare(self.streams.get(category,[])))


        # select a category in treeview
        def add_category(self, cat):
            if cat not in self.categories: # add category if missing
                self.categories.append(cat)







|







1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
            self.load(self.default)
            self.urls.append(row["url"])


        # simplified gtk TreeStore display logic (just one category for the moment, always rebuilt)
        def load(self, category, force=False):
            #self.liststore[category] = \
            __print__(dbg.UI, category, self.streams.keys())
            mygtk.columns(self.gtk_list, self.datamap, self.prepare(self.streams.get(category,[])))


        # select a category in treeview
        def add_category(self, cat):
            if cat not in self.categories: # add category if missing
                self.categories.append(cat)
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125





#-- startup progress bar
progresswin, progressbar = 0, 0
def gui_startup(p=0/100, msg="streamtuner2 is starting"):

    global progresswin,progressbar
    if not progresswin:

        # GtkWindow "progresswin"
        progresswin = gtk.Window()
        progresswin.set_property("title", "streamtuner2")
        progresswin.set_property("default_width", 300)
        progresswin.set_property("width_request", 300)
        progresswin.set_property("default_height", 30)
        progresswin.set_property("height_request", 30)
        progresswin.set_property("window_position", "center")
        progresswin.set_property("decorated", False)
        progresswin.set_property("visible", True)

        # GtkProgressBar "progressbar"
        progressbar = gtk.ProgressBar()
        progressbar.set_property("visible", True)
        progressbar.set_property("show_text", True)







|











|







1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132





#-- startup progress bar
progresswin, progressbar = 0, 0
def gui_startup(p=0/100.0, msg="streamtuner2 is starting"):

    global progresswin,progressbar
    if not progresswin:

        # GtkWindow "progresswin"
        progresswin = gtk.Window()
        progresswin.set_property("title", "streamtuner2")
        progresswin.set_property("default_width", 300)
        progresswin.set_property("width_request", 300)
        progresswin.set_property("default_height", 30)
        progresswin.set_property("height_request", 30)
        #progresswin.set_property("window_position", "center")
        progresswin.set_property("decorated", False)
        progresswin.set_property("visible", True)

        # GtkProgressBar "progressbar"
        progressbar = gtk.ProgressBar()
        progressbar.set_property("visible", True)
        progressbar.set_property("show_text", True)
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178

    # graphical
    if len(sys.argv) < 2:
    
        
        # prepare for threading in Gtk+ callbacks
        gobject.threads_init()
        gui_startup(1/100)
        
        # prepare main window
        main = StreamTunerTwo()
        
        # module coupling
        action.main = main      # action (play/record) module needs a reference to main window for gtk interaction and some URL/URI callbacks
        action = action.action  # shorter name
        http.feedback = main.status  # http module gives status feedbacks too
        
        # first invocation
        if (conf.get("firstrun")):
            config_dialog.open(None)
            del conf.firstrun


        # run
        gui_startup(100/100)
        gtk.main()
        
        
    # invoke command-line interface
    else:
        import cli
        cli.StreamTunerCLI()







|
















|







1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185

    # graphical
    if len(sys.argv) < 2:
    
        
        # prepare for threading in Gtk+ callbacks
        gobject.threads_init()
        gui_startup(1/100.0)
        
        # prepare main window
        main = StreamTunerTwo()
        
        # module coupling
        action.main = main      # action (play/record) module needs a reference to main window for gtk interaction and some URL/URI callbacks
        action = action.action  # shorter name
        http.feedback = main.status  # http module gives status feedbacks too
        
        # first invocation
        if (conf.get("firstrun")):
            config_dialog.open(None)
            del conf.firstrun


        # run
        gui_startup(100/100.0)
        gtk.main()
        
        
    # invoke command-line interface
    else:
        import cli
        cli.StreamTunerCLI()