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
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 *
from channels import __print__
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
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)
            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)
            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)
            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)
            gui_startup(17/20.0)
            self.current_channel = self.current_channel_gtk()
            try: self.channel().first_show()
            except: print("channel .first_show() initialization error")
            except: __print__(dbg.INIT, "main.__init__: current_channel.first_show() initialization error")

      
            # bind gtk/glade event names to functions
            gui_startup(19/20)
            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
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)
            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
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())
                __print__(dbg.PROC, "channel_switch: try .first_show", self.channel().module);
                __print__(self.channel().first_show)
                __print__(self.channel().first_show())
            except:
                print("channel .first_show() initialization error")
                __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
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")
                __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__("reload", reload, self.current_channel, self.channels[self.current_channel], self.channel().current)
            __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
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)
            __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
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)
                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__("disabled plugin:", module)
                    __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("error initializing:", module, ", exception:")
                    __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
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"
                    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
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)
                __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
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):
                 self.theme.append_text(themename)
                 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
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(category, self.streams.keys())
            __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
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, msg="streamtuner2 is starting"):
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("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
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)
        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)
        gui_startup(100/100.0)
        gtk.main()
        
        
    # invoke command-line interface
    else:
        import cli
        cli.StreamTunerCLI()