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

⌈⌋ ⎇ branch:  streamtuner2


Check-in [65a0b18268]

Overview
Comment:Move concrete row(), rowno(), selected(), model_iter() and play(), record() implementations from main into channels.GenericModule.
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | trunk
Files: files | file ages | folders
SHA1: 65a0b182684cd6d004c6fe8bc9e0a22070ca3da9
User & Date: mario on 2015-04-10 10:49:56
Other Links: manifest | tags
Context
2015-04-10
10:50
Removed custom pls conversion code, now uses action. playlist conversion tools. check-in: c683b52003 user: mario tags: trunk
10:49
Move concrete row(), rowno(), selected(), model_iter() and play(), record() implementations from main into channels.GenericModule. check-in: 65a0b18268 user: mario tags: trunk
2015-04-09
14:52
Update notes on python-requests >= 2.0.0 required now (streams=True). And fix reference to `icon.png` now. check-in: 45c45d5755 user: mario tags: trunk
Changes

Modified channels/__init__.py from [ae8e7ee1ba] to [e46cdc65d0].

102
103
104
105
106
107
108




109
110
111
112
113
114
115
    # for empty grouping / categories
    placeholder = [dict(genre="./.", title="Subcategory placeholder", playing="./.", url="none:", listeners=0, bitrate=0, homepage="", state="gtk-folder")]
    empty_stub = [dict(genre="./.", title="No categories found (HTTP error)", playing="Try Channel→Reload Categories later..", url="none:", listeners=0, bitrate=0, homepage="", state="gtk-stop")]
    
    # regex            
    rx_www_url = re.compile("""(www(\.\w+[\w-]+){2,}|(\w+[\w-]+[ ]?\.)+(com|FM|net|org|de|PL|fr|uk))""", re.I)






    # constructor
    def __init__(self, parent=None):
    
        #self.streams = {}
        self.gtk_list = None
        self.gtk_cat = None







>
>
>
>







102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
    # for empty grouping / categories
    placeholder = [dict(genre="./.", title="Subcategory placeholder", playing="./.", url="none:", listeners=0, bitrate=0, homepage="", state="gtk-folder")]
    empty_stub = [dict(genre="./.", title="No categories found (HTTP error)", playing="Try Channel→Reload Categories later..", url="none:", listeners=0, bitrate=0, homepage="", state="gtk-stop")]
    
    # regex            
    rx_www_url = re.compile("""(www(\.\w+[\w-]+){2,}|(\w+[\w-]+[ ]?\.)+(com|FM|net|org|de|PL|fr|uk))""", re.I)



    #--------------------------- initialization --------------------------------


    # constructor
    def __init__(self, parent=None):
    
        #self.streams = {}
        self.gtk_list = None
        self.gtk_cat = None
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
        self.parent = stub_parent(None)

        # only if streamtuner2 is run in graphical mode        
        if (parent):
            self.cache()
            self.gui(parent)
        pass
        
        
    # These are all implemented in main (where they don't belong!)
    def stations(self):
        return self.streams.get(self.current, [])
    def rowno(self):
        pass
    def row(self):
        pass
    

    # read previous channel/stream data, if there is any
    def cache(self):
        # stream list
        cache = conf.load("cache/" + self.module)
        if (cache):
            self.streams = cache
        # categories
        cache = conf.load("cache/categories_" + self.module)
        if (cache):
            self.categories = cache
        # catmap (optional)
        cache = conf.load("cache/catmap_" + self.module)
        if (cache):
            self.catmap = cache
        pass

        
    # initialize Gtk widgets / data objects
    def gui(self, parent):
        #print(self.module + ".gui()")

        # save reference to main window/glade API







<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<







129
130
131
132
133
134
135


























136
137
138
139
140
141
142
        self.parent = stub_parent(None)

        # only if streamtuner2 is run in graphical mode        
        if (parent):
            self.cache()
            self.gui(parent)
        pass



























        
    # initialize Gtk widgets / data objects
    def gui(self, parent):
        #print(self.module + ".gui()")

        # save reference to main window/glade API
185
186
187
188
189
190
191






























































192
193
194
195
196
197
198
            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_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:







>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>







163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
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
            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_by_name(self.module) or 1)


    # Statusbar stub (defers to parent/main window, if in GUI mode)
    def status(self, *v):
        if self.parent: self.parent.status(*v)
        else: __print__(dbg.INFO, "status():", *v)


        
    #--------------------- streams/model data accesss ---------------------------
        
    # Get list of stations in current category
    def stations(self):
        return self.streams.get(self.current, [])

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

    # Return ListStore object and Iterator for currently selected row in gtk.TreeView station list
    def model_iter(self):
        return self.gtk_list.get_selection().get_selected()

    # Currently selected entry in stations list, return complete data dict
    def row(self):
        return self.stations() [self.rowno()]
        
    # Fetches a single varname from currently selected station entry
    def selected(self, name="url"):
        return self.row().get(name)
    
    # Inject status icon into currently selected row (used by main.bookmark() call)
    def row_icon(self, gtkIcon = gtk.STOCK_ABOUT):
        try:
            # Updates gtk_list store, set icon in current display.
            # Since it is used by bookmarks, would be reshown with next display() anyhow,
            # and there's no need to invalidate the ls cache, because that's referenced by model anyhow.
            (model,iter) = self.model_iter()
            model.set_value(iter, 0, gtkIcon)
        except:
             pass

    

    #------------------------ base implementations -----------------------------

    # read previous channel/stream data, if there is any
    def cache(self):
        # stream list
        cache = conf.load("cache/" + self.module)
        if (cache):
            self.streams = cache
        # categories
        cache = conf.load("cache/categories_" + self.module)
        if (cache):
            self.categories = cache
        # catmap (optional)
        cache = conf.load("cache/catmap_" + self.module)
        if (cache):
            self.catmap = cache
        pass

        
    # 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:
439
440
441
442
443
444
445
446
447
448

449
450
451
452
453
454
455
456



457








458
459
460
461
462
463
464
        return self.current




    #--------------------------- actions ---------------------------------

    # invoke action.play,
    # can be overridden to provide channel-specific "play" alternative
    def play(self, row):

        if row.get("url"):

            # parameters
            audioformat = row.get("format", self.audioformat)
            listformat = row.get("listformat", self.listformat)

            # invoke audio player
            action.play(row["url"], audioformat, listformat)















    #--------------------------- utility functions -----------------------

    








|
|
|
>
|
|
<


<

|
>
>
>

>
>
>
>
>
>
>
>







479
480
481
482
483
484
485
486
487
488
489
490
491

492
493

494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
        return self.current




    #--------------------------- actions ---------------------------------

    # Invoke action.play() for current station.
    # Can be overridden to provide channel-specific "play" alternative
    def play(self):
        row = self.row()
        if row:
            # playlist and audio type

            audioformat = row.get("format", self.audioformat)
            listformat = row.get("listformat", self.listformat)

            # invoke audio player
            action.play(row["url"], audioformat, listformat, row)
        else:
            self.status("No station selected for playing.")
        return row

    # Start streamripper/youtube-dl/etc
    def record(self):
        row = self.row()
        if row:
            audioformat = row.get("format", self.audioformat)
            listformat = row.get("listformat", self.listformat)
            action.record(row.get("url"), audioformat, listformat, row=row)
        return row



    #--------------------------- utility functions -----------------------

    

Modified st2.py from [eacd2e641f] to [4120fbf826].

66
67
68
69
70
71
72
73
74
75
76
77
78

79
80
81
82
83
84
85


# This represents the main window, dispatches Gtk events,
# and shares most application behaviour with the channel modules.
class StreamTunerTwo(gtk.Builder):

    # object containers
    widgets = {}     # non-glade widgets (the manually instantiated ones)
    channels = {}    # channel modules
    features = {}    # non-channel plugins
    working = []     # threads
    hooks = {
        "play": [favicon.download_playing],  # observers queue here

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









|





>







66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86


# This represents the main window, dispatches Gtk events,
# and shares most application behaviour with the channel modules.
class StreamTunerTwo(gtk.Builder):

    # object containers
    widgets = {}     # non-glade widgets (any manually instantiated ones)
    channels = {}    # channel modules
    features = {}    # non-channel plugins
    working = []     # threads
    hooks = {
        "play": [favicon.download_playing],  # observers queue here
        "record": [],
        "init": [],
        "config_load": [],
        "config_save": [],
    }
    meta = plugin_meta()


258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286

287
288
289
290
291
292
293

294
295

296
297
298
299
300
301
302
        self.notebook_channels.set_current_page(self.channel_names.index(name))

    # Mirror selected channel tab into main window title
    def update_title(self):
        self.win_streamtuner2.set_title("Streamtuner2 - %s" % self.channel().meta.get("title"))


    # 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
    def row(self):
        return self.channel().stations() [self.rowno()]

        
    # return ListStore object and Iterator for currently selected row in gtk.TreeView station list
    def model_iter(self):
        return self.channel().gtk_list.get_selection().get_selected()
        
    # Fetches a single varname from currently selected station entry
    def selected(self, name="url"):
        return self.row().get(name)


            
    # Play button
    def on_play_clicked(self, widget, event=None, *args):

        row = self.row()
        if row:
            self.channel().play(row)
            [callback(row) for callback in self.hooks["play"]]

    # Recording: invoke streamripper for current stream URL
    def on_record_clicked(self, widget):

        row = self.row()
        action.record(row.get("url"), row.get("format", "audio/mpeg"), "url/direct", row=row)


    # Open stream homepage in web browser
    def on_homepage_stream_clicked(self, widget):
        url = self.selected("homepage")
        if url and len(url): action.browser(url)
        else: self.status("No homepage URL present.")








<
<
<
<
|
<

|
|
<
<
<
<
<
|







>
|
<
|
|



>
|
<
>







259
260
261
262
263
264
265




266

267
268
269





270
271
272
273
274
275
276
277
278
279

280
281
282
283
284
285
286

287
288
289
290
291
292
293
294
        self.notebook_channels.set_current_page(self.channel_names.index(name))

    # Mirror selected channel tab into main window title
    def update_title(self):
        self.win_streamtuner2.set_title("Streamtuner2 - %s" % self.channel().meta.get("title"))






    # Channel: row{} dict for current station

    def row(self):
        return self.channel().row()
        





    # Channel: fetch single varname from station row{} dict
    def selected(self, name="url"):
        return self.row().get(name)


            
    # Play button
    def on_play_clicked(self, widget, event=None, *args):
        self.status("Starting player...")
        row = self.channel().play()

        self.status("")
        [callback(row) for callback in self.hooks["play"]]

    # Recording: invoke streamripper for current stream URL
    def on_record_clicked(self, widget):
        self.status("Recording station...")
        row = self.channel().record()

        [callback(row) for callback in self.hooks["record"]]

    # Open stream homepage in web browser
    def on_homepage_stream_clicked(self, widget):
        url = self.selected("homepage")
        if url and len(url): action.browser(url)
        else: self.status("No homepage URL present.")

329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359


360
361
362
363
364
365
366
367
368
369

370
371
372
373
374
375
376
377
378
379
        __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())
        # code to update current list (set icon just in on-screen liststore, it would be updated with next display() anyhow - and there's no need to invalidate the ls cache, because that's referenced by model anyhow)
        try:
            (model,iter) = self.model_iter()
            model.set_value(iter, 0, gtk.STOCK_ABOUT)
        except:
            pass
        # refresh bookmarks tab
        self.bookmarks.load(self.bookmarks.default)

    # Reload category tree
    def update_categories(self, widget):
        Thread(target=self.channel().reload_categories).start()

    # Menu invocation: refresh favicons for all stations in current streams category
    def update_favicons(self, widget):
        entries = self.channel().stations()
        favicon.download_all(entries)

    # Save stream to file (.m3u)
    def save_as(self, widget):
        row = self.row()
        default_fn = row["title"] + ".m3u"
        fn = uikit.save_file("Save Stream", None, default_fn, [(".m3u","*m3u"),(".pls","*pls"),(".xspf","*xspf"),(".smil","*smil"),(".asx","*asx"),("all files","*")])
        if fn:


            action.save(row, fn)
        pass

    # Save current stream URL into clipboard
    def menu_copy(self, w):
        gtk.clipboard_get().set_text(self.selected("url"))

    # Remove a stream entry
    def delete_entry(self, w):
        n = self.rowno()

        del self.channel().stations()[ n ]
        self.channel().switch()
        self.channel().save()

    # Alternative Notebook channel tabs between TOP and LEFT position
    def switch_notebook_tabs_position(self, w, pos):
        self.notebook_channels.set_tab_pos(pos);
        









<
<
<
|
<
<


















>
>
|








|
>
|
|
|







321
322
323
324
325
326
327



328


329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
        __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())



        self.channel().row_icon(gtk.STOCK_ABOUT)


        # refresh bookmarks tab
        self.bookmarks.load(self.bookmarks.default)

    # Reload category tree
    def update_categories(self, widget):
        Thread(target=self.channel().reload_categories).start()

    # Menu invocation: refresh favicons for all stations in current streams category
    def update_favicons(self, widget):
        entries = self.channel().stations()
        favicon.download_all(entries)

    # Save stream to file (.m3u)
    def save_as(self, widget):
        row = self.row()
        default_fn = row["title"] + ".m3u"
        fn = uikit.save_file("Save Stream", None, default_fn, [(".m3u","*m3u"),(".pls","*pls"),(".xspf","*xspf"),(".smil","*smil"),(".asx","*asx"),("all files","*")])
        if fn:
            source = row.get("listformat", self.channel().listformat)
            dest = (re.findall("\.(m3u|pls|xspf|jspf|json|smil|asx|wpl)8?$", fn) or ["pls"])[0]
            action.save_playlist(source=source, multiply=True).save(rows=[row], fn=fn, dest=dest)
        pass

    # Save current stream URL into clipboard
    def menu_copy(self, w):
        gtk.clipboard_get().set_text(self.selected("url"))

    # Remove a stream entry
    def delete_entry(self, w):
        cn = self.channel()
        n = cn.rowno()
        del cn.stations()[ n ]
        cn.switch()
        cn.save()

    # Alternative Notebook channel tabs between TOP and LEFT position
    def switch_notebook_tabs_position(self, w, pos):
        self.notebook_channels.set_tab_pos(pos);