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

⌈⌋ ⎇ branch:  streamtuner2


Check-in [18b4da567f]

Overview
Comment:Got rid of static main.channel_names[] list. Instead now querying Gtk notebook_channels widget for current page name ordering. More plugin defaults added to ConfigDict.defaults(), as workaround for not preparsing inactive plugins on startup.
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | trunk
Files: files | file ages | folders
SHA1: 18b4da567fd5299b0cc426437ac83e35cc254d20
User & Date: mario on 2015-04-03 17:35:55
Other Links: manifest | tags
Context
2015-04-03
17:36
pngopt/pngadv/pngcrushed logo banner/icon. check-in: de4406f6c3 user: mario tags: trunk
17:35
Got rid of static main.channel_names[] list. Instead now querying Gtk notebook_channels widget for current page name ordering. More plugin defaults added to ConfigDict.defaults(), as workaround for not preparsing inactive plugins on startup. check-in: 18b4da567f user: mario tags: trunk
17:33
Split out pack: specifier into `bin` and separately for help/ pages. check-in: 97fd6a532a user: mario tags: trunk
Changes

Modified channels/__init__.py from [4b9f08e907] to [af174af9c9].

177
178
179
180
181
182
183
184
185


186
187
188
189
190
191
192
177
178
179
180
181
182
183


184
185
186
187
188
189
190
191
192







-
-
+
+







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

        
    # make private copy of .datamap and modify field (title= only ATM)
    def update_datamap(self, search="name", title=None):
        if self.datamap == GenericChannel.datamap:
            self.datamap = copy.deepcopy(self.datamap)
        for i,row in enumerate(self.datamap):
            if row[2][0] == search:

Modified config.py from [43829d8ff1] to [31dd5cfc2a].

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
115


116
117
118
119
120
121
122
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
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







-
-
-
-
-
-
+
+
+
+
+
+


-
-
+
+

+

+
-
+



+


+
+
+
+
+
+
+
+
+
-
-
-
+
+
+
+
+
+
+
+
+
+
-
-
-
+
+







            self.save("settings")
            self.firstrun = 1


    # some defaults
    def defaults(self):
        self.play = {
           "audio/mpeg": "audacious ",	# %u for url to .pls, %g for downloaded .m3u
           "audio/ogg": "audacious ",
           "audio/*": "audacious ",
           "video/youtube": "totem $(youtube-dl -g %srv)",
           "video/*": "vlc --one-instance %srv",
           "url/http": "sensible-browser",
           "audio/mpeg": self.find_player(),
           "audio/ogg": self.find_player(),
           "audio/*": self.find_player(),
           "video/youtube": self.find_player(typ="video") + " $(youtube-dl -g %srv)",
           "video/*": self.find_player(typ="video", default="vlc"),
           "url/http": self.find_player(typ="browser"),
        }
        self.record = {
           "audio/*": "xterm -e streamripper %srv",   # -d /home/***USERNAME***/Musik
           "video/youtube": "xterm -e \"youtube-dl %srv\"",
           "audio/*": self.find_player(typ="xterm") + " -e streamripper %srv",   # -d /home/***USERNAME***/Musik
           "video/youtube": self.find_player(typ="xterm") + " -e \"youtube-dl %srv\"",
        }
        # these presets are a temporary workaround, until `priority:` is checked before module loading
        self.plugins = {
             # core plugins, cannot be disabled anyway
            "bookmarks": 1, # built-in plugin, cannot be disabled
            "bookmarks": 1,
            "search": 1,
            "streamedit": 1,
            "configwin": 1,
            # standard channels
            "shoutcast": 1,
            "xiph": 1,
            "myoggradio": 1,
            "internet_radio": 1,
            "surfmusik": 1,
            "jamendo": 1,
            "icast": 1,
            "itunes": 1,
            # disabled per default
            "radiobrowser": 0,
            "youtube": 0,
            "modarchive": 0, # disable per default
            "file": 0,      # disable per default
            "punkcast": 0,  # disable per default
            "modarchive": 0,
            "live365": 0,
            "radiotray": 0,
            "timer": 0,
            "history": 0,
            "global_key": 0,
            "useragentswitcher": 0,
            # obsolete / removed
            "file": 0,
            "punkcast": 0,
            "history": 0,
            "basicch": 0,   # ceased
            "tv": 0,        # ceased
            "basicch": 0,
            "tv": 0,
        }
        self.tmp = os.environ.get("TEMP", "/tmp")
        self.max_streams = "500"
        self.show_bookmarks = 1
        self.show_favicons = 1
        self.load_favicon = 1
        self.heuristic_bookmark_update = 0
140
141
142
143
144
145
146







147





148
149
150
151
152
153
154
158
159
160
161
162
163
164
165
166
167
168
169
170
171

172
173
174
175
176
177
178
179
180
181
182
183







+
+
+
+
+
+
+
-
+
+
+
+
+







            if ("name" in opt) and ("value" in opt) and (opt["name"] not in vars(self)):
                self.__dict__[opt["name"]] = opt["value"]

        # plugin state
        if module and module not in conf.plugins:
             conf.plugins[module] = meta.get("priority") in ("core", "builtin", "default", "standard")

    # look at system binaries for standard audio players
    def find_player(self, typ="audio", default="xdg-open"):
        players = {
           "audio": ["audacious %g", "audacious2", "exaile %p", "xmms2", "banshee", "amarok %g", "clementine", "qmmp", "quodlibet", "aqualung", "mp3blaster %g", "vlc --one-instance %srv", "totem"],
           "video": ["parole", "umplayer", "xnoise", "gxine", "totem", "vlc --one-instance", "smplayer", "gnome-media-player", "xine", "bangarang"],
           "browser": ["opera", "midori", "sensible-browser"],
           "xterm": ["xfce4-terminal", "x-termina-emulator", "gnome-terminal", "xterm", "rxvt"],
    
        }
        for bin in players[typ]:
            if find_executable(bin.split()[0]):
                return bin
        return default
        
    # http://standards.freedesktop.org/basedir-spec/basedir-spec-0.6.html
    def xdg(self, path="/streamtuner2"):
        home = os.environ.get("HOME", self.tmp)
        config = os.environ.get("XDG_CONFIG_HOME", os.environ.get("APPDATA", home+"/.config"))
        
        # storage dir
270
271
272
273
274
275
276
277
278


279
280
281
282
283
284
285
299
300
301
302
303
304
305


306
307
308
309
310
311
312
313
314







-
-
+
+







#
def module_list():

    # Should list plugins within zips as well as local paths
    ls = pkgutil.iter_modules([conf.share+"/channels", "channels"])
    ls = [name for loader,name,ispkg in ls]
    
    # resort with tab order
    order = [module.strip() for module in conf.channel_order.lower().replace(".","_").replace("-","_").split(",")]
    # resort according to configured channel tab order
    order = re.findall("\w+", conf.channel_order.lower())
    ls = [module for module in (order) if (module in ls)] + [module for module in (ls) if (module not in order)]

    return ls



# Plugin meta data extraction

Modified st2.py from [f737d4ecc1] to [1e504cb8ac].

12
13
14
15
16
17
18
19

20
21

22
23

24
25
26
27
28
29
30
12
13
14
15
16
17
18

19
20

21


22
23
24
25
26
27
28
29







-
+

-
+
-
-
+







# url: http://freshcode.club/projects/streamtuner2
# config:  
#   { type: env, name: http_proxy, description: proxy for HTTP access }
#   { type: env, name: XDG_CONFIG_HOME, description: relocates user .config subdirectory }
# category: sound
# depends: pygtk | gi, threading, requests, pyquery, lxml
# id: streamtuner2
# pack: *.py, gtk*.xml, bin=/usr/bin/streamtuner2, channels/__init__.py, bundle/*.py,
# pack: *.py, gtk*.xml, bin, channels/__init__.py, bundle/*.py, CREDITS, help/index.page,
#   streamtuner2.desktop=/usr/share/applications/, README=/usr/share/doc/streamtuner2/,
#   NEWS.gz=/usr/share/doc/streamtuner2/changelog.gz, help/streamtuner2.1=/usr/share/man/man1/,
#   help/streamtuner2.1=/usr/share/man/man1/, NEWS.gz=/usr/share/doc/streamtuner2/changelog.gz,
#   help/*page=/usr/share/doc/streamtuner2/help/, help/img/*=/usr/share/doc/streamtuner2/help/img/,
#   logo.png=/usr/share/pixmaps/streamtuner2.png,
#   logo.png=/usr/share/pixmaps/streamtuner2.png
# architecture: all
#
# Streamtuner2 is a GUI for browsing internet radio directories, music
# collections, and video services - grouped by genres or categories.
# It runs your preferred audio player, and streamripper for recording.
#
# It's an independent rewrite of streamtuner1. Being written in Python,
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
82
83
84
85
86
87
88

89

90
91
92
93
94
95
96







-

-







        "config_load": [],
        "config_save": [],
    }
    meta = plugin_meta()


    # status variables
    channel_names = ["bookmarks"]    # order of channel notebook tabs
    current_channel = "bookmarks"    # currently selected channel name (as index in self.channels{})


    # constructor
    def __init__(self):
        
        # Load stylesheet, instantiate GtkBuilder in self, menu and logo hooks
        gui_startup(0/20.0), uikit.load_theme(conf.get("theme"))
        gui_startup(1/20.0), gtk.Builder.__init__(self)
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

256
257
258
259
260
261
262








263
264
265
266
267
268
269
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
256
257
258
259
260
261
262
263
264
265
266
267
268
269







-
-
+
+
+



+
+
+
+
+
+
-
+

-
-
+
+
-
-
-
+
+

-
-
-
-
+
-
-
-
-
+


-
-
-
-
-
+
+
+
+
+
+
+
+








    # 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)
            
    # returns the currently selected directory/channel object (remembered position)

            
    # Returns the currently selected directory/channel object (remembered position)
    def channel(self):
        return self.channels[self.current_channel]

    # List of module titles for channel tabs
    @property
    def channel_names(self):
        n = self.notebook_channels
        return [n.get_menu_label_text(n.get_nth_page(i)) for i in xrange(0, n.get_n_pages())]

    # returns the currently selected directory/channel object (from gtk)
    # Returns the currently selected directory/channel object (from gtk)
    def current_channel_gtk(self):
        i = self.notebook_channels.get_current_page()
        try: return self.channel_names[i]
        return self.channel_names[self.notebook_channels.get_current_page()]
    
        except: return "bookmarks"

    # Notebook tab clicked
        
    # Notebook tab has been clicked (receives numeric page_num), but *NOT* yet changed (visually).
    def channel_switch(self, notebook, page, page_num=0, *args):

        # can be called from channelmenu as well:
        if type(page) == str:
            self.current_channel = page
        self.current_channel = notebook.get_menu_label_text(notebook.get_nth_page(page_num))
            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]
        __print__(dbg.UI, "main.channel_switch():", "set current_channel :=", self.current_channel)
        
        # if first selected, load current category
        try:
            __print__(dbg.PROC, "channel_switch: try .first_show", self.channel().module);
            self.channel().first_show()
        except:
            __print__(dbg.INIT, "channel .first_show() initialization error")
        __print__(dbg.STAT, "TRY", "main.channel_switch(): ", self.current_channel + ".first_show()")
        try: self.channel().first_show()
        except: __print__(dbg.INIT, ".first_show() initialization error")

    # Invoked from the menu instead, uses module name instead of numeric tab id
    def channel_switch_by_name(self, name):
        self.notebook_channels.set_current_page(self.channel_names.index(name))


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

    # Currently selected entry in stations list, return complete data dict
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
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







-
+


















-
-







    # load plugins from /usr/share/streamtuner2/channels/
    def load_plugin_channels(self):

        # initialize plugin modules (pre-ordered)
        ls = module_list()
        for module in ls:
            gui_startup(4/20.0 + 13.5/20.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
            # or if it's a built-in (already imported)
            elif module in self.features or module in self.channels:
                continue
            
            # load plugin
            try:
                plugin = __import__("channels."+module, globals(), None, [""])
                #print [name for name,c in inspect.getmembers(plugin) if inspect.isclass(c)]
                plugin_class = plugin.__dict__[module]
                plugin_obj = plugin_class(parent=self)

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