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

⌈⌋ ⎇ branch:  streamtuner2


Diff

Differences From Artifact [0b9e0b6b81]:

To Artifact [1db7abd798]:


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






|
|







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: pygtk | pygi, threading, pyquery, kronos, requests
# version: 2.0.9.7
# 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
# 
#
28
29
30
31
32
33
34

35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
#
#


""" project status """
#
# The application runs mostly stable. The GUI interfaces are workable.

# There haven't been any optimizations regarding memory usage and
# performance. The current internal API is acceptable. Documentation is
# coming up.
#
#  current bugs:
#   - audio- and list-format support is not very robust / needs better API
#   - lots of GtkWarning messages
#   - not all keyboard shortcuts work
#   - in-list search doesn't work in our treeviews (???)
#   - JSON files are only trouble: loading of data files might lead to more
#     errors now, even if pson module still falls back on old method
#     (unicode strings from json.load are useless to us, require typecasts)
#     (nonsupport of tuples led to regression in mygtk.app_restore)
#     (sometimes we receive 8bit-content, which the json module can't save)
#
#  features:
#   - treeview lists are created from datamap[] structure and stream{} dicts
#   - channel categories are built-in defaults (can be freshened up however)
#   - config vars and cache data get stored as JSON in ~/.config/streamtuner2/
#
#  missing:







>

|
<



<

<
<
<
<
<
<







28
29
30
31
32
33
34
35
36
37

38
39
40

41






42
43
44
45
46
47
48
#
#


""" project status """
#
# The application runs mostly stable. The GUI interfaces are workable.
# It's supposed to run on Gtk2 and Gtk3. Python3 support is still WIP.
# There haven't been any optimizations regarding memory usage and
# performance. The current internal API is vastly undocumented.

#
#  current bugs:
#   - audio- and list-format support is not very robust / needs better API

#   - not all keyboard shortcuts work






#
#  features:
#   - treeview lists are created from datamap[] structure and stream{} dicts
#   - channel categories are built-in defaults (can be freshened up however)
#   - config vars and cache data get stored as JSON in ~/.config/streamtuner2/
#
#  missing:
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
107
108
109
110
111
112
113
114
#     to read the config data -> limited risk, since it's only local files
#   - HIGH RISK: no verification of downloaded favicon image files (ico/png),
#     as they are passed to gtk.gdk.Pixbuf (OTOH data pre-filtered by Google)
#   - MEDIUM: audio players / decoders are easily affected by buffer overflows
#     from corrupt mp3/stream data, and streamtuner2 executes them
#      - but since that's the purpose -> no workaround
#
#  still help wanted on:
#   - any of the above
#   - new plugins (local file viewer)
#   - nicer logo (or donations accepted to consult graphics designer)
#



# standard modules
import sys
import os, os.path
import re
import copy
import urllib

# threading or processing module
try:
    from processing import Process as Thread
except:
    from threading import Thread
    Thread.stop = lambda self: None

# add library path
sys.path.insert(0, "/usr/share/streamtuner2")   # pre-defined directory for modules

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
main = None
class StreamTunerTwo(gtk.Builder):







<
<
<
<
<







<
<










>
|


|




|



<







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
#     to read the config data -> limited risk, since it's only local files
#   - HIGH RISK: no verification of downloaded favicon image files (ico/png),
#     as they are passed to gtk.gdk.Pixbuf (OTOH data pre-filtered by Google)
#   - MEDIUM: audio players / decoders are easily affected by buffer overflows
#     from corrupt mp3/stream data, and streamtuner2 executes them
#      - but since that's the purpose -> no workaround
#








# standard modules
import sys
import os, os.path
import re



# threading or processing module
try:
    from processing import Process as Thread
except:
    from threading import Thread
    Thread.stop = lambda self: None

# add library path
sys.path.insert(0, "/usr/share/streamtuner2")   # pre-defined directory for modules
sys.path.insert(0, "/usr/share/streamtuner2/bundle")   # external libraries
sys.path.insert(0, ".")   # development module path

# gtk modules
from mygtk import pygtk, gtk, gobject, ui_file, mygtk, ver as GTK_VER

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




# this represents the main window
# and also contains most application behaviour
main = None
class StreamTunerTwo(gtk.Builder):
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
            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),
                # toolbar







|







152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
            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( list({
                "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),
                # toolbar
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
                "search_google": search.google,
                "search_cancel": search.cancel,
                "true": lambda w,*args: True,
                "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)


          

        #-- Shortcut for glade.get_widget()
        # Allows access to widgets as direct attributes instead of using .get_widget()
        # Also looks in self.channels[] for the named channel plugins
        def __getattr__(self, name):
            if (self.channels.has_key(name)):
                return self.channels[name]     # like self.shoutcast
            else:
                return self.get_object(name)   # or gives an error if neither exists


        # custom-named widgets are available from .widgets{} not via .get_widget()
        def get_widget(self, name):
            if self.widgets.has_key(name):
                return self.widgets[name]
            else:
                return gtk.Builder.get_object(self, name)
                


                







|
















|







|







202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
                "search_google": search.google,
                "search_cancel": search.cancel,
                "true": lambda w,*args: True,
                "streamedit_open": streamedit.open,
                "streamedit_save": streamedit.save,
                "streamedit_new": streamedit.new,
                "streamedit_cancel": streamedit.cancel,
            }.items() ) + list( 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)


          

        #-- Shortcut for glade.get_widget()
        # Allows access to widgets as direct attributes instead of using .get_widget()
        # Also looks in self.channels[] for the named channel plugins
        def __getattr__(self, name):
            if (name in self.channels):
                return self.channels[name]     # like self.shoutcast
            else:
                return self.get_object(name)   # or gives an error if neither exists


        # custom-named widgets are available from .widgets{} not via .get_widget()
        def get_widget(self, name):
            if name in self.widgets:
                return self.widgets[name]
            else:
                return gtk.Builder.get_object(self, name)
                


                
521
522
523
524
525
526
527

528


529
530
531
532
533
534
535
                        gtk.rc_parse(f)
                pass


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

                self.app_state(widget)


            gtk.main_quit()













>
|
>
>







507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
                        gtk.rc_parse(f)
                pass


        # end application and gtk+ main loop
        def gtk_main_quit(self, widget, *x):
            if conf.auto_save_appstate:
                try:  # doesn't work with gtk3 yet (probably just hooking at the wrong time)
                    self.app_state(widget)
                except:
                    None
            gtk.main_quit()






556
557
558
559
560
561
562
563




564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
# right click in streams/stations TreeView
def station_context_menu(treeview, event):
            # right-click ?
            if event.button >= 3:
                path = treeview.get_path_at_pos(int(event.x), int(event.y))[0]
                treeview.grab_focus()
                treeview.set_cursor(path, None, False)
                main.streamactions.popup(None, None, None, event.button, event.time)




                return None
            # we need to pass on to normal left-button signal handler
            else:
                return False
# this works better as callback function than as class - because of False/Object result for event trigger




# encapsulates references to gtk objects AND properties in main window
class auxiliary_window(object):
        def __getattr__(self, name):
            if main.__dict__.has_key(name):
                return main.__dict__[name]
            elif StreamTunerTwo.__dict__.has_key(name):
                return StreamTunerTwo.__dict__[name]
            else:
                return main.get_widget(name)
""" allows to use self. and main. almost interchangably """










|
>
>
>
>












|

|







545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
# right click in streams/stations TreeView
def station_context_menu(treeview, event):
            # right-click ?
            if event.button >= 3:
                path = treeview.get_path_at_pos(int(event.x), int(event.y))[0]
                treeview.grab_focus()
                treeview.set_cursor(path, None, False)
                main.streamactions.popup(
                      parent_menu_shell=None, parent_menu_item=None, func=None,
                      button=event.button, activate_time=event.time,
                      data=None
                )
                return None
            # we need to pass on to normal left-button signal handler
            else:
                return False
# this works better as callback function than as class - because of False/Object result for event trigger




# encapsulates references to gtk objects AND properties in main window
class auxiliary_window(object):
        def __getattr__(self, name):
            if name in main.__dict__:
                return main.__dict__[name]
            elif name in StreamTunerTwo.__dict__:
                return StreamTunerTwo.__dict__[name]
            else:
                return main.get_widget(name)
""" allows to use self. and main. almost interchangably """



768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
        def hide(self, *args):
            self.win_config.hide()
            return True

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







|







761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
        def hide(self, *args):
            self.win_config.hide()
            return True

        
        # set/load values between gtk window and conf. dict
        def apply(self, config, prefix="config_", save=0):
            for key,val in config.items():
                # 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)
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838

        # add configuration setting definitions from plugins
        once = 0
        def add_plugins(self):
            if self.once:
                return

            for name,enabled in conf.plugins.iteritems():

                # add plugin load entry
                if name:
                    label = ("enable ⎗ %s channel" if self.channels.get(name) else "use ⎗ %s plugin")
                    cb =  gtk.ToggleButton(label=label % name)
                    self.add_( "config_plugins_"+name, cb )#, label=None, color="#ddd" )








|







817
818
819
820
821
822
823
824
825
826
827
828
829
830
831

        # add configuration setting definitions from plugins
        once = 0
        def add_plugins(self):
            if self.once:
                return

            for name,enabled in conf.plugins.items():

                # add plugin load entry
                if name:
                    label = ("enable ⎗ %s channel" if self.channels.get(name) else "use ⎗ %s plugin")
                    cb =  gtk.ToggleButton(label=label % name)
                    self.add_( "config_plugins_"+name, cb )#, label=None, color="#ddd" )

1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
            # First we'll generate a list of current bookmark stream urls, and then
            # remove all but those from the currently UPDATED_channel + category.
            # This step is most likely redundant, but prevents accidently re-rewriting
            # stations that are in two channels (=duplicates with different PLS urls).
            check = {"http//": "[row]"}
            check = dict((row["url"],row) for row in fav)
            # walk through all channels/streams
            for chname,channel in main.channels.iteritems():
                for cat,streams in channel.streams.iteritems():

                    # keep the potentially changed rows
                    if (chname == updated_channel) and (cat == updated_category):
                        freshened_streams = streams

                    # remove unchanged urls/rows
                    else:







|
|







1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
            # First we'll generate a list of current bookmark stream urls, and then
            # remove all but those from the currently UPDATED_channel + category.
            # This step is most likely redundant, but prevents accidently re-rewriting
            # stations that are in two channels (=duplicates with different PLS urls).
            check = {"http//": "[row]"}
            check = dict((row["url"],row) for row in fav)
            # walk through all channels/streams
            for chname,channel in main.channels.items():
                for cat,streams in channel.streams.items():

                    # keep the potentially changed rows
                    if (chname == updated_channel) and (cat == updated_category):
                        freshened_streams = streams

                    # remove unchanged urls/rows
                    else:
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
#-- run main                                ---------------------------------------------
if __name__ == "__main__":

    #-- global configuration settings
    "conf = Config()"       # already happened with "from config import conf"

    # 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









|












|







1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
#-- run main                                ---------------------------------------------
if __name__ == "__main__":

    #-- global configuration settings
    "conf = Config()"       # already happened with "from config import conf"

    # graphical
    if len(sys.argv) < 2 or "--gtk3" in sys.argv:
    
        
        # 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
        ahttp.feedback = main.status  # http module gives status feedbacks too
        
        # first invocation
        if (conf.get("firstrun")):
            config_dialog.open(None)
            del conf.firstrun