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

⌈⌋ ⎇ branch:  streamtuner2


Check-in [c0702405f8]

Overview
Comment:prepare for gtk3
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | trunk
Files: files | file ages | folders
SHA1: c0702405f83c670c7760d4f30bb91a0ff59476b4
User & Date: mario on 2014-01-06 22:45:37
Other Links: manifest | tags
Context
2014-04-05
23:17
Fix regex parsing for new sparse shoutcast.com check-in: 3a5e6068b9 user: mario tags: trunk
2014-01-06
22:45
prepare for gtk3 check-in: c0702405f8 user: mario tags: trunk
2014-01-05
03:30
fixed channels/timer gtk signal_connect handling, st2main now provides an amendable slot dict check-in: 41405a6488 user: mario tags: trunk
Changes

Modified channels/global_key.py from [71e77fdc54] to [7c03f6331b].

9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#
# Binds a key to global desktop (F13 = left windows key). On keypress
# it switches the currently playing radio station to another one in
# bookmarks list.
#
# Valid key names are for example F9, <Ctrl>G, <Alt>R, <Super>N
#



import keybinder
from config import conf
import action
import random








|







9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#
# Binds a key to global desktop (F13 = left windows key). On keypress
# it switches the currently playing radio station to another one in
# bookmarks list.
#
# Valid key names are for example F9, <Ctrl>G, <Alt>R, <Super>N
#
return


import keybinder
from config import conf
import action
import random

Modified config.py from [3cd51eb7d8] to [8ca1d32093].

168
169
170
171
172
173
174
175







176
177
178
179
180
181


        def update(self, with_new_data):
            for key,value in with_new_data.iteritems():
                if type(value) == dict:
                    self[key].update(value)
                else:
                    self[key] = value
            # descends into sub-dicts instead of wiping them with subkeys
             








   
#-- actually fill global conf instance
conf = ConfigDict()











|
>
>
>
>
>
>
>






>
>
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
        def update(self, with_new_data):
            for key,value in with_new_data.iteritems():
                if type(value) == dict:
                    self[key].update(value)
                else:
                    self[key] = value
            # descends into sub-dicts instead of wiping them with subkeys

             
        # check for existing filename in directory list
        def find_in_dirs(self, dirs, file):
            for d in dirs:
                if os.path.exists(d+"/"+file):
                    return d+"/"+file


   
#-- actually fill global conf instance
conf = ConfigDict()




Modified help/streamtuner2.1 from [17a0ee5d37] to [de31364a43].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29




30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
.\" this is one of the nanoweb man pages
.\" (many thanks to the manpage howto!)
.\"
.TH streamtuner2 "July 2010" "BSD/Linux" "User Manuals"
.SH NAME
streamtuner2 \- Browser for internet radio stations
.SH SYNOPSIS
.B streamtuner2
.I command
[
.BI channel ,...
] [
.IB title
]

.SH DESCRIPTION
Streamtuner2 is a graphical application for browsing through internet
radio station directories, like
.BR Shoutcast.com " and " Xiph.org " or " Internet-Radio.org.uk .
It is written in Python and easy to extend. And besides the grapical
interface, has a commandline interface.

.SH OPTIONS

.B Display data from cache

.TP
.BI help
Prints out a summary of available commands.




.TP
.BI stream " channel title"
Searches for a station with the given title. Either looks in a single
channel, or scans all plugins.
.TP
.BI url " channel title"
Prints out only the streaming URL.
.TP
.BI play " " [ channel ] " title"
Invokes the configured audio player.

.PP
.B Load data from directory service

.TP
.BI categories " channelname"
Returns a nested JSON list of all categories/genres.
.TP
.BI category " ""channelname"" ""Category"""
Prints out a JSON list of the genre. Each entry constains title, url and



|




















<
<



>
>
>
>












|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24


25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
.\" this is one of the nanoweb man pages
.\" (many thanks to the manpage howto!)
.\"
.TH streamtuner2 "January 2014" "BSD/Linux" "User Manuals"
.SH NAME
streamtuner2 \- Browser for internet radio stations
.SH SYNOPSIS
.B streamtuner2
.I command
[
.BI channel ,...
] [
.IB title
]

.SH DESCRIPTION
Streamtuner2 is a graphical application for browsing through internet
radio station directories, like
.BR Shoutcast.com " and " Xiph.org " or " Internet-Radio.org.uk .
It is written in Python and easy to extend. And besides the grapical
interface, has a commandline interface.

.SH OPTIONS



.TP
.BI help
Prints out a summary of available commands.

.PP
.B Cached data

.TP
.BI stream " channel title"
Searches for a station with the given title. Either looks in a single
channel, or scans all plugins.
.TP
.BI url " channel title"
Prints out only the streaming URL.
.TP
.BI play " " [ channel ] " title"
Invokes the configured audio player.

.PP
.B Instantly retrieve data from directory service

.TP
.BI categories " channelname"
Returns a nested JSON list of all categories/genres.
.TP
.BI category " ""channelname"" ""Category"""
Prints out a JSON list of the genre. Each entry constains title, url and

Modified mygtk.py from [29ba27c9de] to [42f942998a].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27


28







29
30
31



32
33
34
35

36
37








38
39
40
41
42
43
44
#
# encoding: UTF-8
# api: python
# type: functions
# title: mygtk helper functions
# description: simplify usage of some gtk widgets
# version: 1.5
# author: mario
# license: public domain
#
#
# Wrappers around gtk methods. The TreeView method .columns() allows
# to fill a treeview. It adds columns and data rows with a mapping
# dictionary (which specifies many options and data positions).
#
# The .tree() method is a trimmed-down variant of that, creates a
# single column, but has threaded entries.
#
# With the methodes .app_state() and .app_restore() named gtk widgets
# can be queried for attributes. The methods return a saveable dict,
# which contain current layout options for a few Widget types. Saving
# and restoring must be handled elsewhere.
#
#


# gtk modules


import pygtk







import gtk
import gtk.glade
import gobject



import os.path
import copy



def __print__(*args):
        print(" ".join([str(a) for a in args]))









# simplified gtk constructors               ---------------------------------------------
class mygtk:


             
        #-- fill a treeview






|




















>
>
|
>
>
>
>
>
>
>
|
|
|
>
>
>




>


>
>
>
>
>
>
>
>







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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
56
57
58
59
60
61
62
63
64
65
#
# encoding: UTF-8
# api: python
# type: functions
# title: mygtk helper functions
# description: simplify usage of some gtk widgets
# version: 1.6
# author: mario
# license: public domain
#
#
# Wrappers around gtk methods. The TreeView method .columns() allows
# to fill a treeview. It adds columns and data rows with a mapping
# dictionary (which specifies many options and data positions).
#
# The .tree() method is a trimmed-down variant of that, creates a
# single column, but has threaded entries.
#
# With the methodes .app_state() and .app_restore() named gtk widgets
# can be queried for attributes. The methods return a saveable dict,
# which contain current layout options for a few Widget types. Saving
# and restoring must be handled elsewhere.
#
#


# gtk modules
gtk = 0   # 0=gtk2, else gtk3
if gtk:
    from gi import pygtkcompat as pygtk
    pygtk.enable() 
    pygtk.enable_gtk(version='3.0')
    from gi.repository import Gtk as gtk
    from gi.repository import GObject as gobject
    from gi.repository import GdkPixbuf
    ui_file = "gtk3.xml"
if not gtk:
    import pygtk
    import gtk
    import gobject
    ui_file = "ui.xml"

# filesystem
import os.path
import copy


# debug
def __print__(*args):
        print(" ".join([str(a) for a in args]))


try:
  empty_pixbuf = gtk.gdk.pixbuf_new_from_data("\0\0\0\0",gtk.gdk.COLORSPACE_RGB,True,8,1,1,4)
except:
  empty_pixbuf = GdkPixbuf.Pixbuf.new_from_data("\0\0\0\0", GdkPixbuf.Colorspace.RGB, True, 8, 1, 1, 4, None, None)



# simplified gtk constructors               ---------------------------------------------
class mygtk:


             
        #-- fill a treeview
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

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

                # prepare for missing values, and special variable types
                defaults = {
                    str: "",
                    unicode: u"",
                    bool: False,
                    int: 0,
                    gtk.gdk.Pixbuf: gtk.gdk.pixbuf_new_from_data("\0\0\0\0",gtk.gdk.COLORSPACE_RGB,True,8,1,1,4)
                }
                if gtk.gdk.Pixbuf in vartypes:
                    pix_entry = vartypes.index(gtk.gdk.Pixbuf) 
                
                # sort data into gtk liststore array
                for row in entries:

                    # generate ordered list from dictionary, using rowmap association
                    row = [   row.get( skey , defaults[vartypes[i]] )   for i,skey   in enumerate(rowmap)   ]
                    
                    # autotransform string -> gtk image object
                    if (pix_entry and type(row[pix_entry]) == str):
                        row[pix_entry] = (  gtk.gdk.pixbuf_new_from_file(row[pix_entry])  if  os.path.exists(row[pix_entry])  else  defaults[gtk.gdk.Pixbuf]  )

                    try:
                        # add
                        ls.append(row)   # had to be adapted for real TreeStore (would require additional input for grouping/level/parents)

                    except:
                        # brute-force typecast
                        ls.append(  [va  if ty==gtk.gdk.Pixbuf  else ty(va)   for va,ty in zip(row,vartypes)]  )

                # apply array to widget
                widget.set_model(ls)
                return ls
                
            pass




        #-- treeview for categories
        #
        # simple two-level treeview display in one column
        # with entries = [main,[sub,sub], title,[...],...]
        #
        @staticmethod     
        def tree(widget, entries, title="category", icon=gtk.STOCK_DIRECTORY):

            # list types
            ls = gtk.TreeStore(str, str)

            # add entries
            for entry in entries:
                if (type(entry) == str):
                    main = ls.append(None, [entry, icon])
                else:
                    for sub_title in entry:
                        ls.append(main, [sub_title, icon])

            # just one column
            tvcolumn = gtk.TreeViewColumn(title);
            widget.append_column(tvcolumn)

            # inner display: icon & string
            pix = gtk.CellRendererPixbuf()
            txt = gtk.CellRendererText()

            # position
            tvcolumn.pack_start(pix, expand=False)
            tvcolumn.pack_end(txt, expand=True)

            # select array content source in treestore

            tvcolumn.set_attributes(pix, stock_id=1)
            tvcolumn.set_attributes(txt, text=0)
            # finalize
            widget.set_model(ls)
            tvcolumn.set_sort_column_id(0)

            #tvcolumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
            #tvcolumn.set_fixed_width(125])
            widget.set_search_column(0)
            #widget.expand_all()
            #widget.expand_row("3", False)
            #print(widget.row_expanded("3"))

            return ls




        #-- save window size and widget properties
        #







|




















|

















>


>







>



>



>



>

>
|
|



>


<



>







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
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
239
240
241
242
243

                # prepare for missing values, and special variable types
                defaults = {
                    str: "",
                    unicode: u"",
                    bool: False,
                    int: 0,
                    gtk.gdk.Pixbuf: empty_pixbuf
                }
                if gtk.gdk.Pixbuf in vartypes:
                    pix_entry = vartypes.index(gtk.gdk.Pixbuf) 
                
                # sort data into gtk liststore array
                for row in entries:

                    # generate ordered list from dictionary, using rowmap association
                    row = [   row.get( skey , defaults[vartypes[i]] )   for i,skey   in enumerate(rowmap)   ]
                    
                    # autotransform string -> gtk image object
                    if (pix_entry and type(row[pix_entry]) == str):
                        row[pix_entry] = (  gtk.gdk.pixbuf_new_from_file(row[pix_entry])  if  os.path.exists(row[pix_entry])  else  defaults[gtk.gdk.Pixbuf]  )

                    try:
                        # add
                        ls.append(row)   # had to be adapted for real TreeStore (would require additional input for grouping/level/parents)

                    except:
                        # brute-force typecast
                        ls.append( [va  if ty==gtk.gdk.Pixbuf  else ty(va)   for va,ty in zip(row,vartypes)]  )

                # apply array to widget
                widget.set_model(ls)
                return ls
                
            pass




        #-- treeview for categories
        #
        # simple two-level treeview display in one column
        # with entries = [main,[sub,sub], title,[...],...]
        #
        @staticmethod     
        def tree(widget, entries, title="category", icon=gtk.STOCK_DIRECTORY):

            # list types
            ls = gtk.TreeStore(str, str)

            # add entries
            for entry in entries:
                if (type(entry) == str):
                    main = ls.append(None, [entry, icon])
                else:
                    for sub_title in entry:
                        ls.append(main, [sub_title, icon])

            # just one column
            tvcolumn = gtk.TreeViewColumn(title);
            widget.append_column(tvcolumn)

            # inner display: icon & string
            pix = gtk.CellRendererPixbuf()
            txt = gtk.CellRendererText()

            # position
            tvcolumn.pack_start(pix, expand=False)
            tvcolumn.pack_end(txt, expand=True)

            # select array content source in treestore
            tvcolumn.add_attribute(pix, "stock_id", 1)
            tvcolumn.add_attribute(txt, "text", 0)

            # finalize
            widget.set_model(ls)
            tvcolumn.set_sort_column_id(0)
            widget.set_search_column(0)
            #tvcolumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
            #tvcolumn.set_fixed_width(125])

            #widget.expand_all()
            #widget.expand_row("3", False)
            #print(widget.row_expanded("3"))

            return ls




        #-- save window size and widget properties
        #

Modified st2.py from [26d82d3fb9] to [0743e4b8fe].

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
# 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.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
# 
#
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
try:
    from processing import Process as Thread
except:
    from threading import Thread
    Thread.stop = lambda self: None

# gtk modules
import pygtk
import gtk
import gtk.glade
import gobject


# custom modules
sys.path.insert(0, "/usr/share/streamtuner2")   # pre-defined directory for modules
sys.path.insert(0, ".")   # pre-defined directory for modules
from config import conf   # initializes itself, so all conf.vars are available right away
from mygtk import mygtk   # gtk treeview
import http
import action  # needs workaround... (action.main=main)
from channels import *
from channels import __print__
import favicon
#from pq import pq








|
<
<
<






<







87
88
89
90
91
92
93
94



95
96
97
98
99
100

101
102
103
104
105
106
107
try:
    from processing import Process as Thread
except:
    from threading import Thread
    Thread.stop = lambda self: None

# gtk modules
from mygtk import pygtk, gtk, gobject, ui_file, mygtk





# custom modules
sys.path.insert(0, "/usr/share/streamtuner2")   # pre-defined directory for modules
sys.path.insert(0, ".")   # pre-defined directory for modules
from config import conf   # initializes itself, so all conf.vars are available right away

import http
import action  # needs workaround... (action.main=main)
from channels import *
from channels import __print__
import favicon
#from pq import pq

133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
        def __init__(self):

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

            # instantiate gtk/glade widgets in current object
            gtk.Builder.__init__(self)
            ui_file = [i for i in sum([[i, conf.share+"/"+i] for i in ["ui.xml", "st2.gtk"]], []) if os.path.exists(i)][0];
            gtk.Builder.add_from_file(self, 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),







<
|







129
130
131
132
133
134
135

136
137
138
139
140
141
142
143
        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),
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
270
271
272

273
274
275
276
277
278
279
            # WHY DON'T YOU WANT TO WORK?!
            #self.shoutcast.gtk_list.set_enable_search(True)
            #self.shoutcast.gtk_list.set_search_column(4)


          

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


                
        # returns the currently selected directory/channel object
        def channel(self):
            #try:
                return self.channels[self.current_channel]
            #except Exception,e:
            #    print(e)
            #    self.notebook_channels.set_current_page(0)
            #    self.current_channel = "bookmarks"
            #    return self.channels["bookmarks"]

            
        def current_channel_gtk(self):
            i = self.notebook_channels.get_current_page()
            try: return self.channel_names[i]
            except: return "bookmarks"


        # notebook tab clicked
        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







|
|
|





>




















>





>







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
270
271
272
273
274
275
276
277
            # 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)
                


                
        # returns the currently selected directory/channel object
        def channel(self):
            #try:
                return self.channels[self.current_channel]
            #except Exception,e:
            #    print(e)
            #    self.notebook_channels.set_current_page(0)
            #    self.current_channel = "bookmarks"
            #    return self.channels["bookmarks"]

            
        def current_channel_gtk(self):
            i = self.notebook_channels.get_current_page()
            try: return self.channel_names[i]
            except: return "bookmarks"


        # notebook tab clicked
        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
287
288
289
290
291
292
293
294
295
296
297
298
299

300
301
302
303

304
305
306
307

308
309
310
311
312
313

314
315
316
317
318
319
320
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
370
371
372
373
374
375
376

377
378
379
380
381

382
383
384
385

386
387
388
389
390
391
392
393
394

395
396
397
398

399
400
401
402
403
404
405

406
407
408
409
410
411
412
                print("try: .first_show", self.channel().module);
                print(self.channel().first_show)
                print(self.channel().first_show())
            except:
                print("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]


        # 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)
                favicon.download_playing(row)


        # streamripper
        def on_record_clicked(self, widget):
            row = self.row()
            action.record(row.get("url"), "audio/mp3", "url/direct", row=row)


        # browse stream
        def on_homepage_stream_clicked(self, widget):
            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")
                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)
            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)
        def thread(self, target, *args):
            thread = Thread(target=target, args=args)
            thread.start()
            self.working.append(thread)


        # stop reload/update threads
        def on_stop_clicked(self, widget):
            while self.working:
                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)
            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 a file            
        def save_as(self, widget):
            row = self.row()
            default_fn = row["title"] + ".m3u"
            fn = mygtk.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 an entry
        def delete_entry(self, w):
            n = self.rowno()
            del self.channel().stations()[ n ]
            self.channel().switch()
            self.channel().save()


        # stream right click
        def station_context_menu(self, treeview, event):
            return station_context_menu(treeview, event) # wrapper to the static function
            









<
<




>




>




>






>








>





>





>






>








>






>






>







>












>





>




>









>




>







>







285
286
287
288
289
290
291


292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
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
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
                print("try: .first_show", self.channel().module);
                print(self.channel().first_show)
                print(self.channel().first_show())
            except:
                print("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]


        # 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)
                favicon.download_playing(row)


        # streamripper
        def on_record_clicked(self, widget):
            row = self.row()
            action.record(row.get("url"), "audio/mp3", "url/direct", row=row)


        # browse stream
        def on_homepage_stream_clicked(self, widget):
            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")
                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)
            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)
        def thread(self, target, *args):
            thread = Thread(target=target, args=args)
            thread.start()
            self.working.append(thread)


        # stop reload/update threads
        def on_stop_clicked(self, widget):
            while self.working:
                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)
            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 a file            
        def save_as(self, widget):
            row = self.row()
            default_fn = row["title"] + ".m3u"
            fn = mygtk.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 an entry
        def delete_entry(self, w):
            n = self.rowno()
            del self.channel().stations()[ n ]
            self.channel().switch()
            self.channel().save()


        # stream right click
        def station_context_menu(self, treeview, event):
            return station_context_menu(treeview, event) # wrapper to the static function
            


430
431
432
433
434
435
436

437
438
439
440
441
442
443
                    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


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

            # find plugin files
            ls = os.listdir(conf.share + "/channels/")
            ls = [fn[:-3] for fn in ls if re.match("^[a-z][\w\d_]+\.py$", fn)]







>







444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
                    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


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

            # find plugin files
            ls = os.listdir(conf.share + "/channels/")
            ls = [fn[:-3] for fn in ls if re.match("^[a-z][\w\d_]+\.py$", fn)]
476
477
478
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
                    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")


        # store window/widget states (sizes, selections, etc.)
        def app_state(self, widget):
            # gtk widget states
            widgetnames = ["win_streamtuner2", "toolbar", "notebook_channels", ] \
                        + [id+"_list" for id in self.channel_names] + [id+"_cat" for id in self.channel_names]
            conf.save("window", mygtk.app_state(wTree=self, widgetnames=widgetnames), nice=1)
            # object vars
            channelopts = {} #dict([(id, {"current":self.channels[id].current}) for id in self.channel_names])
            for id in self.channels.keys():
                if (self.channels[id]):
                    channelopts[id] = {"current":self.channels[id].current}
            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):
            if conf.auto_save_appstate:
                self.app_state(widget)
            gtk.main_quit()








>













>









>







491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
                    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")


        # store window/widget states (sizes, selections, etc.)
        def app_state(self, widget):
            # gtk widget states
            widgetnames = ["win_streamtuner2", "toolbar", "notebook_channels", ] \
                        + [id+"_list" for id in self.channel_names] + [id+"_cat" for id in self.channel_names]
            conf.save("window", mygtk.app_state(wTree=self, widgetnames=widgetnames), nice=1)
            # object vars
            channelopts = {} #dict([(id, {"current":self.channels[id].current}) for id in self.channel_names])
            for id in self.channels.keys():
                if (self.channels[id]):
                    channelopts[id] = {"current":self.channels[id].current}
            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):
            if conf.auto_save_appstate:
                self.app_state(widget)
            gtk.main_quit()

563
564
565
566
567
568
569

570
571
572
573

574
575
576
577
578
579
580
""" allows to use self. and main. almost interchangably """



# aux win: search dialog (keeps search text in self.q)
# and also: quick search textbox (uses main.q instead)
class search (auxiliary_window):


        # show search dialog   
        def menu_search(self, w):
            self.search_dialog.show();


        # hide dialog box again
        def cancel(self, *args):
            self.search_dialog.hide()
            return True  # stop any other gtk handlers
            #self.search_dialog.hide() #if conf.hide_searchdialog
            







>




>







581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
""" allows to use self. and main. almost interchangably """



# aux win: search dialog (keeps search text in self.q)
# and also: quick search textbox (uses main.q instead)
class search (auxiliary_window):


        # show search dialog   
        def menu_search(self, w):
            self.search_dialog.show();


        # hide dialog box again
        def cancel(self, *args):
            self.search_dialog.hide()
            return True  # stop any other gtk handlers
            #self.search_dialog.hide() #if conf.hide_searchdialog
            
624
625
626
627
628
629
630

631
632
633
634
635
636
637
            main.channels["bookmarks"].streams["search"] = entries   # we have to set it here, else .currentcat() might reset it 
            main.bookmarks.load("search")
            
            
        # live search on directory server homepages
        def server_query(self, w):
            "unimplemented"

            
        # don't search at all, open a web browser
        def google(self, w):
            self.cancel()
            action.browser("http://www.google.com/search?q=" + self.search_full.get_text())









>







644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
            main.channels["bookmarks"].streams["search"] = entries   # we have to set it here, else .currentcat() might reset it 
            main.bookmarks.load("search")
            
            
        # live search on directory server homepages
        def server_query(self, w):
            "unimplemented"

            
        # don't search at all, open a web browser
        def google(self, w):
            self.cancel()
            action.browser("http://www.google.com/search?q=" + self.search_full.get_text())


682
683
684
685
686
687
688

689
690
691
692
693
694
695
696
697

698
699
700
701
702
703
704
705
706
707

708
709
710
711
712
713
714
715

716
717
718
719
720
721
722
723
724
725
726
727
728
729
730

731
732
733
734
735
736
737
738

739
740
741
742

743
744
745
746
747
748
749
# instantiates itself




# aux win: stream data editing dialog
class streamedit (auxiliary_window):


        # show stream data editing dialog
        def open(self, mw):
            row = main.row()
            for name in ("title", "playing", "genre", "homepage", "url", "favicon", "format", "extra"):
                w = main.get_widget("streamedit_" + name) 
                if w:
                    w.set_text((str(row.get(name)) if row.get(name) else ""))
            self.win_streamedit.show()


        # copy widget contents to stream
        def save(self, w):
            row = main.row()
            for name in ("title", "playing", "genre", "homepage", "url", "favicon", "format", "extra"):
               w = main.get_widget("streamedit_" + name)
               if w:
                   row[name] = w.get_text()
            main.channel().save()
            self.cancel(w)

            
        # add a new list entry, update window
        def new(self, w):
            s = main.channel().stations()
            s.append({"title":"new", "url":"", "format":"audio/mp3", "genre":"", "listeners":1});
            main.channel().switch() # update display
            main.channel().gtk_list.get_selection().select_path(str(len(s)-1)); # set cursor to last row
            self.open(w)


        # hide window
        def cancel(self, *w):
            self.win_streamedit.hide()
            return True

streamedit = streamedit()
# instantiates itself





# aux win: settings UI
class config_dialog (auxiliary_window):


        # display win_config, pre-fill text fields from global conf. object
        def open(self, widget):
            self.add_plugins()
            self.apply(conf.__dict__, "config_", 0)
            #self.win_config.modify_bg(gtk.STATE_NORMAL, gtk.gdk.color_parse('#443399'))
            self.combobox_theme()
            self.win_config.show()


        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)







>









>










>








>















>








>




>







703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
# instantiates itself




# aux win: stream data editing dialog
class streamedit (auxiliary_window):


        # show stream data editing dialog
        def open(self, mw):
            row = main.row()
            for name in ("title", "playing", "genre", "homepage", "url", "favicon", "format", "extra"):
                w = main.get_widget("streamedit_" + name) 
                if w:
                    w.set_text((str(row.get(name)) if row.get(name) else ""))
            self.win_streamedit.show()


        # copy widget contents to stream
        def save(self, w):
            row = main.row()
            for name in ("title", "playing", "genre", "homepage", "url", "favicon", "format", "extra"):
               w = main.get_widget("streamedit_" + name)
               if w:
                   row[name] = w.get_text()
            main.channel().save()
            self.cancel(w)

            
        # add a new list entry, update window
        def new(self, w):
            s = main.channel().stations()
            s.append({"title":"new", "url":"", "format":"audio/mp3", "genre":"", "listeners":1});
            main.channel().switch() # update display
            main.channel().gtk_list.get_selection().select_path(str(len(s)-1)); # set cursor to last row
            self.open(w)


        # hide window
        def cancel(self, *w):
            self.win_streamedit.hide()
            return True

streamedit = streamedit()
# instantiates itself





# aux win: settings UI
class config_dialog (auxiliary_window):


        # display win_config, pre-fill text fields from global conf. object
        def open(self, widget):
            self.add_plugins()
            self.apply(conf.__dict__, "config_", 0)
            #self.win_config.modify_bg(gtk.STATE_NORMAL, gtk.gdk.color_parse('#443399'))
            self.combobox_theme()
            self.win_config.show()


        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)
757
758
759
760
761
762
763

764
765
766
767
768
769
770
771
772
773
774
775
776
777
778

779
780
781
782
783
784

785
786
787
788
789
790
791
                elif (w and type(w)==gtk.Entry):
                    w.set_text(str(val))
                elif (w and save):
                    config[key] = w.get_active()
                elif (w):
                    w.set_active(bool(val))
            pass


        # 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)]
            # add to combobox
            for num,themename in enumerate(themes):
                 self.theme.append_text(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
        def apply_theme(self):
            if self.theme.get_active() >= 0:
                conf.theme = self.theme.get_model()[ self.theme.get_active()][0]
                main.load_theme()


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








>















>






>







785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
                elif (w and type(w)==gtk.Entry):
                    w.set_text(str(val))
                elif (w and save):
                    config[key] = w.get_active()
                elif (w):
                    w.set_active(bool(val))
            pass


        # 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)]
            # add to combobox
            for num,themename in enumerate(themes):
                 self.theme.append_text(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
        def apply_theme(self):
            if self.theme.get_active() >= 0:
                conf.theme = self.theme.get_model()[ self.theme.get_active()][0]
                main.load_theme()


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

811
812
813
814
815
816
817

818
819
820
821
822
823
824
                            self.add_( "config_"+opt["name"], cb )
                        else:
                            self.add_( "config_"+opt["name"], gtk.Entry(), opt["description"] )

                # spacer 
                self.add_( "filler_pl_"+name, gtk.HSeparator() )
            self.once = 1


        # put gtk widgets into config dialog notebook
        def add_(self, id, w, label=None, color=""):
            w.set_property("visible", True)
            main.widgets[id] = w
            if label:
                w.set_width_chars(10)







>







842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
                            self.add_( "config_"+opt["name"], cb )
                        else:
                            self.add_( "config_"+opt["name"], gtk.Entry(), opt["description"] )

                # spacer 
                self.add_( "filler_pl_"+name, gtk.HSeparator() )
            self.once = 1


        # put gtk widgets into config dialog notebook
        def add_(self, id, w, label=None, color=""):
            w.set_property("visible", True)
            main.widgets[id] = w
            if label:
                w.set_width_chars(10)
888
889
890
891
892
893
894

895
896
897
898
899
900
901
902

903
904
905
906
907
908

909
910
911
912
913
914

915
916
917
918
919
920
921
922
923

924
925
926
927
928


929
930
931
932
933
934


935
936
937
938
939
940
941
# This module lists static content from ~/.config/streamtuner2/bookmarks.json;
# its data list is queried by other plugins to add 'star' icons.
#
# Some feature extensions inject custom categories[] into streams{}
# e.g. "search" adds its own category once activated, as does the "timer" plugin.
#
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"},
        ]


        # content
        categories = ["favourite", ]
        current = "favourite"
        default = "favourite"
        streams = {"favourite":[], "search":[], "scripts":[], "timer":[], }
        

        # cache list, to determine if a PLS url is bookmarked
        urls = []


        # this channel does not actually retrieve/parse data from anywhere
        def update_categories(self):
            pass
        def update_streams(self, cat):
            return self.streams.get(cat, [])

            
        # initial display
        def first_show(self):
            if not self.streams["favourite"]:
                self.cache()


        # all entries just come from "bookmarks.json"
        def cache(self):
            # stream list
            cache = conf.load(self.module)
            if (cache):
                self.streams = cache


        # save to cache file
        def save(self):
            conf.save(self.module, self.streams, nice=1)


        # checks for existence of an URL in bookmarks store,
        # this method is called by other channel modules' display() method







>








>






>

|




>









>





>
>






>
>







920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
# This module lists static content from ~/.config/streamtuner2/bookmarks.json;
# its data list is queried by other plugins to add 'star' icons.
#
# Some feature extensions inject custom categories[] into streams{}
# e.g. "search" adds its own category once activated, as does the "timer" plugin.
#
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"},
        ]


        # content
        categories = ["favourite", ]  # timer, links, search, and links show up as needed
        current = "favourite"
        default = "favourite"
        streams = {"favourite":[], "search":[], "scripts":[], "timer":[], }
        

        # cache list, to determine if a PLS url is bookmarked
        urls = []


        # this channel does not actually retrieve/parse data from anywhere
        def update_categories(self):
            pass
        def update_streams(self, cat):
            return self.streams.get(cat, [])

            
        # initial display
        def first_show(self):
            if not self.streams["favourite"]:
                self.cache()


        # all entries just come from "bookmarks.json"
        def cache(self):
            # stream list
            cache = conf.load(self.module)
            if (cache):
                self.streams = cache


        # save to cache file
        def save(self):
            conf.save(self.module, self.streams, nice=1)


        # checks for existence of an URL in bookmarks store,
        # this method is called by other channel modules' display() method
971
972
973
974
975
976
977


978
979
980
981
982
983
984


        # select a category in treeview
        def add_category(self, cat):
            if cat not in self.categories: # add category if missing
                self.categories.append(cat)
                self.display_categories()


        # change cursor
        def set_category(self, cat):
            self.add_category(cat)
            self.gtk_cat.get_selection().select_path(str(self.categories.index(cat)))
            return self.currentcat()
            
            







>
>







1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027


        # select a category in treeview
        def add_category(self, cat):
            if cat not in self.categories: # add category if missing
                self.categories.append(cat)
                self.display_categories()


        # change cursor
        def set_category(self, cat):
            self.add_category(cat)
            self.gtk_cat.get_selection().select_path(str(self.categories.index(cat)))
            return self.currentcat()
            
            
1056
1057
1058
1059
1060
1061
1062

1063
1064

1065
1066
1067
1068
1069
1070
1071
1072
1073
1074

1075
1076
1077
1078
1079
1080
1081

1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095




#-- 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")
        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("decorated", False)
        progresswin.set_property("visible", True)

        # GtkProgressBar "progressbar"
        progressbar = gtk.ProgressBar()
        progressbar.set_property("visible", True)
        progressbar.set_property("show_text", True)
        progressbar.set_property("text", msg)
        progresswin.add(progressbar)
        progresswin.show_all()

    try:
      if p<1:
        progressbar.set_fraction(p)
        progressbar.set_property("text", msg)
        while gtk.events_pending(): gtk.main_iteration(False)
      else:
        progresswin.destroy()
    except: return




#-- run main                                ---------------------------------------------
if __name__ == "__main__":







>


>










>







>






|







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
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142




#-- 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")
        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("decorated", False)
        progresswin.set_property("visible", True)

        # GtkProgressBar "progressbar"
        progressbar = gtk.ProgressBar()
        progressbar.set_property("visible", True)
        progressbar.set_property("show_text", True)
        progressbar.set_property("text", msg)
        progresswin.add(progressbar)
        progresswin.show_all()

    try:
      if p<1:
        progressbar.set_fraction(p)
        progressbar.set_property("text", msg)
        while gtk.events_pending(): gtk.main_iteration(False)
      else:
        progresswin.hide()
    except: return




#-- run main                                ---------------------------------------------
if __name__ == "__main__":

Modified ui.xml from [39008dd4a6] to [5efe534670].

1708
1709
1710
1711
1712
1713
1714











1715




1716
1717
1718
1719
1720
1721
1722
                        <property name="shadow_type">none</property>
                        <child>
                          <object class="GtkVBox" id="plugin_options">
                            <property name="visible">True</property>
                            <property name="can_focus">False</property>
                            <property name="spacing">10</property>
                            <child>











                              <placeholder/>




                            </child>
                            <child>
                              <placeholder/>
                            </child>
                            <child>
                              <placeholder/>
                            </child>







>
>
>
>
>
>
>
>
>
>
>
|
>
>
>
>







1708
1709
1710
1711
1712
1713
1714
1715
1716
1717
1718
1719
1720
1721
1722
1723
1724
1725
1726
1727
1728
1729
1730
1731
1732
1733
1734
1735
1736
1737
                        <property name="shadow_type">none</property>
                        <child>
                          <object class="GtkVBox" id="plugin_options">
                            <property name="visible">True</property>
                            <property name="can_focus">False</property>
                            <property name="spacing">10</property>
                            <child>
                              <object class="GtkLabel" id="filler_pl_options_info">
                                <property name="visible">True</property>
                                <property name="can_focus">False</property>
                                <property name="xalign">1</property>
                                <property name="yalign">0</property>
                                <property name="ypad">2</property>
                                <property name="label" translatable="yes">You can enable &lt;i&gt;channels&lt;/i&gt; and &lt;i&gt;plugins&lt;/i&gt; here. Changes take effect after restarting streamtuner2.</property>
                                <property name="use_markup">True</property>
                                <property name="justify">right</property>
                                <property name="wrap">True</property>
                              </object>
                              <packing>
                                <property name="expand">True</property>
                                <property name="fill">True</property>
                                <property name="position">0</property>
                              </packing>
                            </child>
                            <child>
                              <placeholder/>
                            </child>
                            <child>
                              <placeholder/>
                            </child>
2465
2466
2467
2468
2469
2470
2471

2472
2473
2474
2475
2476
2477
2478
                              <object class="GtkImageMenuItem" id="imagemenuitem8">
                                <property name="label">gtk-find</property>
                                <property name="visible">True</property>
                                <property name="can_focus">False</property>
                                <property name="use_action_appearance">False</property>
                                <property name="use_underline">True</property>
                                <property name="use_stock">True</property>

                                <signal name="activate" handler="search_open" swapped="no"/>
                              </object>
                            </child>
                            <child>
                              <object class="GtkSeparatorMenuItem" id="separatormenuitem4">
                                <property name="visible">True</property>
                                <property name="can_focus">False</property>







>







2480
2481
2482
2483
2484
2485
2486
2487
2488
2489
2490
2491
2492
2493
2494
                              <object class="GtkImageMenuItem" id="imagemenuitem8">
                                <property name="label">gtk-find</property>
                                <property name="visible">True</property>
                                <property name="can_focus">False</property>
                                <property name="use_action_appearance">False</property>
                                <property name="use_underline">True</property>
                                <property name="use_stock">True</property>
                                <accelerator key="f" signal="activate" modifiers="GDK_CONTROL_MASK"/>
                                <signal name="activate" handler="search_open" swapped="no"/>
                              </object>
                            </child>
                            <child>
                              <object class="GtkSeparatorMenuItem" id="separatormenuitem4">
                                <property name="visible">True</property>
                                <property name="can_focus">False</property>