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

⌈⌋ ⎇ branch:  streamtuner2


Check-in [c71701ad73]

Overview
Comment:Use fractions instead of floats, to reduce ambiguity for version:_raw_ meta update
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | trunk
Files: files | file ages | folders
SHA1: c71701ad732aac3d82f5b54d96a8bb3e28c6923a
User & Date: mario on 2014-04-05 23:33:29
Other Links: manifest | tags
Context
2014-04-05
23:33
bump User-Agent version number check-in: 9bbdaaf8f4 user: mario tags: trunk
23:33
Use fractions instead of floats, to reduce ambiguity for version:_raw_ meta update check-in: c71701ad73 user: mario tags: trunk
23:32
use new `version` command line tool check-in: 62b4121741 user: mario tags: trunk
Changes

Modified st2.py from [0743e4b8fe] to [3646b64fc8].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#!/usr/bin/env python
# encoding: UTF-8
# api: python
# type: application
# title: streamtuner2
# description: directory browser for internet radio / audio streams
# depends: gtk, pygtk, xml.dom.minidom, threading, lxml, pyquery, kronos
# version: 2.1.0
# author: mario salzer
# license: public domain
# url: http://freshmeat.net/projects/streamtuner2
# config: <env name="http_proxy" value="" description="proxy for HTTP access" />  <env name="XDG_CONFIG_HOME" description="relocates user .config subdirectory" />
# category: multimedia
# 
#







|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#!/usr/bin/env python
# encoding: UTF-8
# api: python
# type: application
# title: streamtuner2
# description: directory browser for internet radio / audio streams
# depends: gtk, pygtk, xml.dom.minidom, threading, lxml, pyquery, kronos
# version: 2.0.9.5
# author: mario salzer
# license: public domain
# url: http://freshmeat.net/projects/streamtuner2
# config: <env name="http_proxy" value="" description="proxy for HTTP access" />  <env name="XDG_CONFIG_HOME" description="relocates user .config subdirectory" />
# category: multimedia
# 
#
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
176
177
178
        current_channel = "bookmarks"    # currently selected channel name (as index in self.channels{})


        # constructor
        def __init__(self):

            # gtkrc stylesheet
            self.load_theme(), gui_startup(0.05)

            # 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(0.10)
            # 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(0.15)
            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(0.90)
            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(0.95)
            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),







|



|








|


















|






|







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
176
177
178
        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),
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
                "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(0.99)
            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)









|







218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
                "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)


433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
            sbar_cid = self.get_widget("statusbar").get_context_id("messages")
            # remove text
            while ((not text) and (type(text)==str) and len(sbar_msg)):
                sbar_msg.pop()
                mygtk.do(lambda:self.statusbar.pop(sbar_cid))
            # progressbar
            if (type(text)==float):
                if (text >= 1.0):  # completed
                    mygtk.do(lambda:self.progress.hide())
                else:  # show percentage
                    mygtk.do(lambda:self.progress.show() or self.progress.set_fraction(text))
                    if (text <= 0.0):  # unknown state
                        mygtk.do(lambda:self.progress.pulse())
            # add text
            elif (type(text)==str):
                sbar_msg.append(1)
                mygtk.do(lambda:self.statusbar.push(sbar_cid, text))
            pass








|



|







433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
            sbar_cid = self.get_widget("statusbar").get_context_id("messages")
            # remove text
            while ((not text) and (type(text)==str) and len(sbar_msg)):
                sbar_msg.pop()
                mygtk.do(lambda:self.statusbar.pop(sbar_cid))
            # progressbar
            if (type(text)==float):
                if (text >= 999/1000):  # completed
                    mygtk.do(lambda:self.progress.hide())
                else:  # show percentage
                    mygtk.do(lambda:self.progress.show() or self.progress.set_fraction(text))
                    if (text <= 0):  # unknown state
                        mygtk.do(lambda:self.progress.pulse())
            # add text
            elif (type(text)==str):
                sbar_msg.append(1)
                mygtk.do(lambda:self.statusbar.push(sbar_cid, text))
            pass

459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
            
            # 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(0.2 + 0.7 * 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







|







459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
            
            # 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
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
                        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, 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")







|







483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
                        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")
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
            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):







|







511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
            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):
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555


# auxiliary window: about dialog
class AboutStreamtuner2:
        # about us
        def __init__(self):
            a = gtk.AboutDialog()
            a.set_version("2.0.9")
            a.set_name("streamtuner2")
            a.set_license("Public Domain\n\nNo Strings Attached.\nUnrestricted distribution,\nmodification, use.")
            a.set_authors(["Mario Salzer <http://mario.include-once.org/>\n\nConcept based on streamtuner 0.99.99 from\nJean-Yves Lefort, of which some code remains\nin the Google stations plugin.\n<http://www.nongnu.org/streamtuner/>\n\nMyOggRadio plugin based on cooperation\nwith Christian Ehm. <http://ehm-edv.de/>"])
            a.set_website("http://milki.include-once.org/streamtuner2/")
            a.connect("response", lambda a, ok: ( a.hide(), a.destroy() ) )
            a.show()
            

            
# right click in streams/stations TreeView







|


|







538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555


# auxiliary window: about dialog
class AboutStreamtuner2:
        # about us
        def __init__(self):
            a = gtk.AboutDialog()
            a.set_version("2.0.9.5")
            a.set_name("streamtuner2")
            a.set_license("Public Domain\n\nNo Strings Attached.\nUnrestricted distribution,\nmodification, use.")
            a.set_authors(["Mario Salzer <http://mario.include-once.org/>\n\nConcept based on streamtuner 0."+"99."+"99 from\nJean-Yves Lefort, of which some code remains\nin the Google stations plugin.\n<http://www.nongnu.org/streamtuner/>\n\nMyOggRadio plugin based on cooperation\nwith Christian Ehm. <http://ehm-edv.de/>"])
            a.set_website("http://milki.include-once.org/streamtuner2/")
            a.connect("response", lambda a, ok: ( a.hide(), a.destroy() ) )
            a.show()
            

            
# right click in streams/stations TreeView
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
class bookmarks(GenericChannel):


        # desc
        api = "streamtuner2"
        module = "bookmarks"
        title = "bookmarks"
        version = 0.4
        base_url = "file:.config/streamtuner2/bookmarks.json"
        listformat = "*/*"

        
        # i like this
        config = [
            {"name":"like_my_bookmarks", "type":"boolean", "value":0, "description":"I like my bookmarks"},







|







926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
class bookmarks(GenericChannel):


        # desc
        api = "streamtuner2"
        module = "bookmarks"
        title = "bookmarks"
        version = 4/10
        base_url = "file:.config/streamtuner2/bookmarks.json"
        listformat = "*/*"

        
        # i like this
        config = [
            {"name":"like_my_bookmarks", "type":"boolean", "value":0, "description":"I like my bookmarks"},
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112





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

    global progresswin,progressbar
    if not progresswin:

        # GtkWindow "progresswin"
        progresswin = gtk.Window()
        progresswin.set_property("title", "streamtuner2")







|







1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112





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

    # graphical
    if len(sys.argv) < 2:
    
        
        # prepare for threading in Gtk+ callbacks
        gobject.threads_init()
        gui_startup(0.05)
        
        # 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(1.00)
        gtk.main()
        
        
    # invoke command-line interface
    else:
        import cli
        cli.StreamTunerCLI()







|
















|







1146
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

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