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

⌈⌋ ⎇ branch:  streamtuner2


Check-in [ea81d1ad5d]

Overview
Comment:2.0.8.5
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | trunk
Files: files | file ages | folders
SHA1: ea81d1ad5d247a6c1f2d305206a7eed513c59f1e
User & Date: mario on 2012-01-09 03:45:04
Other Links: manifest | tags
Context
2012-01-09
03:45
gnome help files check-in: c2c3526ac3 user: mario tags: trunk
03:45
2.0.8.5 check-in: ea81d1ad5d user: mario tags: trunk
03:42
initial empty check-in check-in: 0732311dbe user: mario tags: trunk
Changes

Added action.py version [a384a5d7ee].

































































































































































































































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
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
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
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
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
#
# encoding: UTF-8
# api: streamtuner2
# type: functions
# title: play/record actions
# description: Starts audio applications, guesses MIME types for URLs
#
#
#  Multimedia interface for starting audio players or browser.
#
#
#  Each channel plugin has a .listtype which describes the linked
#  audio playlist format. It's audio/x-scpls mostly, seldomly m3u,
#  but sometimes url/direct if the entry[url] directly leads to the
#  streaming server.
#  As fallback there is a regex which just looks for URLs in the
#  given resource (works for m3u/pls/xspf/asx/...). There is no
#  actual url "filename" extension guessing.
#
#
#


import re
import os
import http
from config import conf
import platform


#from channels import __print__
def __print__(*args):
    if conf.debug:
        print(" ".join([str(a) for a in args]))


main = None


#-- media actions                           ---------------------------------------------
#
# implements "play" and "record" methods,
# but also "browser" for web URLs
#        
class action:

        # streamlink formats
        lt = {"asx":"video/x-ms-asf", "pls":"audio/x-scpls", "m3u":"audio/x-mpegurl", "xspf":"application/xspf+xml", "href":"url/http", "ram":"audio/x-pn-realaudio", "smil":"application/smil"}
        # media formats
        mf = {"mp3":"audio/mp3", "ogg":"audio/ogg", "aac":"audio/aac"}


        # web
        @staticmethod
        def browser(url):
            __print__( conf.browser )
            os.system(conf.browser + " '" + action.quote(url) + "' &")
            
        # os shell cmd escaping
        @staticmethod
        def quote(s):
            return "%r" % s


        # calls player for stream url and format
        @staticmethod
        def play(url, audioformat="audio/mp3", listformat="text/x-href"):
            if (url):
                url = action.url(url, listformat)
            if (audioformat):
                if audioformat == "audio/mpeg":
                    audioformat = "audio/mp3"  # internally we use the more user-friendly moniker
                cmd = conf.play.get(audioformat, conf.play.get("*/*", "vlc %u"))
                __print__( "play", url, cmd )
            try:
                action.run( action.interpol(cmd, url) )
            except:
                pass
        
        @staticmethod
        def run(cmd):
            __print__( cmd )
            os.system(cmd + (" &" if platform.system()!="Windows" else "")) 


        # streamripper
        @staticmethod
        def record(url, audioformat="audio/mp3", listformat="text/x-href", append="", row={}):
            __print__( "record", url )
            cmd = conf.record.get(audioformat, conf.record.get("*/*", None))
            try: action.run( action.interpol(cmd, url, row) + append )
            except: pass


        # save as .m3u
        @staticmethod
        def save(row, fn, listformat="audio/x-scpls"):
            # modify stream url
            row["url"] = action.url(row["url"], listformat)
            stream_urls = action.extract_urls(row["url"], listformat)
            # output format
            if (re.search("\.m3u", fn)):
                txt = "#M3U\n"
                for url in stream_urls:
                    txt += http.fix_url(url) + "\n"
            # output format
            elif (re.search("\.pls", fn)):
                txt = "[playlist]\n" + "numberofentries=1\n"
                for i,u in enumerate(stream_urls):
                    i = str(i + 1)
                    txt += "File"+i + "=" + u + "\n"
                    txt += "Title"+i + "=" + row["title"] + "\n"
                    txt += "Length"+i + "=-1\n"
                txt += "Version=2\n"
            # output format
            elif (re.search("\.xspf", fn)):
                txt = '<?xml version="1.0" encoding="UTF-8"?>' + "\n"
                txt += '<?http header="Content-Type: application/xspf+xml" ?>' + "\n"
                txt += '<playlist version="1" xmlns="http://xspf.org/ns/0/">' + "\n"
                for attr,tag in [("title","title"), ("homepage","info"), ("playing","annotation"), ("description","annotation")]:
                    if row.get(attr):
                        txt += "  <"+tag+">" + xmlentities(row[attr]) + "</"+tag+">\n"
                txt += "  <trackList>\n"
                for u in stream_urls:
                    txt += '	<track><location>' + xmlentities(u) + '</location></track>' + "\n"
                txt += "  </trackList>\n</playlist>\n"
            # output format
            elif (re.search("\.json", fn)):
                row["stream_urls"] = stream_urls
                txt = str(row)   # pseudo-json (python format)
            # output format
            elif (re.search("\.asx", fn)):
                txt = "<ASX version=\"3.0\">\n"			\
                    + " <Title>" + xmlentities(row["title"]) + "</Title>\n"	\
                    + " <Entry>\n"				\
                    + "  <Title>" + xmlentities(row["title"]) + "</Title>\n"	\
                    + "  <MoreInfo href=\"" + row["homepage"] + "\"/>\n"	\
                    + "  <Ref href=\"" + stream_urls[0] + "\"/>\n"		\
                    + " </Entry>\n</ASX>\n"
            # output format
            elif (re.search("\.smil", fn)):
                txt = "<smil>\n<head>\n  <meta name=\"title\" content=\"" + xmlentities(row["title"]) + "\"/>\n</head>\n"	\
                    + "<body>\n  <seq>\n    <audio src=\"" + stream_urls[0] + "\"/>\n  </seq>\n</body>\n</smil>\n"
            # unknown
            else:
                txt = ""
            # write
            if txt:
                f = open(fn, "wb")
                f.write(txt)
                f.close()
            pass


        # replaces instances of %u, %l, %pls with urls / %g, %f, %s, %m, %m3u or local filenames
        @staticmethod
        def interpol(cmd, url, row={}):
            # inject other meta fields
            if row:
                for field in row:
                    cmd = cmd.replace("%"+field, "%r" % row.get(field))
            # add default if cmd has no %url placeholder
            if cmd.find("%") < 0:
                cmd = cmd + " %m3u"
            # standard placeholders
            if (re.search("%(url|pls|[ulr])", cmd)):
                cmd = re.sub("%(url|pls|[ulr])", action.quote(url), cmd)
            if (re.search("%(m3u|[fgm])", cmd)):
                cmd = re.sub("%(m3u|[fgm])", action.quote(action.m3u(url)), cmd)
            if (re.search("%(srv|[ds])", cmd)):
                cmd = re.sub("%(srv|[ds])", action.quote(action.srv(url)), cmd)
            return cmd


        # eventually transforms internal URN/IRI to URL
        @staticmethod
        def url(url, listformat):
            if (listformat == "audio/x-scpls"):
                url = url
            elif (listformat == "text/x-urn-streamtuner2-script"):
                url = main.special.stream_url(url)
            else:
                url = url
            return url

            
        # download a .pls resource and extract urls
        @staticmethod
        def pls(url):
            text = http.get(url)
            __print__( "pls_text=", text )
            return re.findall("\s*File\d*\s*=\s*(\w+://[^\s]+)", text, re.I)
            # currently misses out on the titles            
            
        # get a single direct ICY stream url (extract either from PLS or M3U)
        @staticmethod
        def srv(url):
            return action.extract_urls(url)[0]


        # retrieve real stream urls from .pls or .m3u links
        @staticmethod
        def extract_urls(pls, listformat="__not_used_yet__"):
            # extract stream address from .pls URL
            if (re.search("\.pls", pls)):       #audio/x-scpls
                return action.pls(pls)
            elif (re.search("\.asx", pls)):	#video/x-ms-asf
                return re.findall("<Ref\s+href=\"(http://.+?)\"", http.get(pls))
            elif (re.search("\.m3u|\.ram|\.smil", pls)):	#audio/x-mpegurl
                return re.findall("(http://[^\s]+)", http.get(pls), re.I)
            else:  # just assume it was a direct mp3/ogg streamserver link
                return [ (pls if pls.startswith("/") else http.fix_url(pls)) ]
            pass


        # generate filename for temporary .m3u, if possible with unique id
        @staticmethod
        def tmp_fn(pls):
            # use shoutcast unique stream id if available
            stream_id = re.search("http://.+?/.*?(\d+)", pls, re.M)
            stream_id = stream_id and stream_id.group(1) or "XXXXXX"
            try:
                channelname = main.current_channel
            except:
                channelname = "unknown"
            return (conf.tmp+"/streamtuner2."+channelname+"."+stream_id+".m3u", len(stream_id) > 3 and stream_id != "XXXXXX")
            
        # check if there are any urls in a given file
        @staticmethod
        def has_urls(tmp_fn):
            if os.path.exists(tmp_fn):
                return open(tmp_fn, "r").read().find("http://") > 0
            
        
        # create a local .m3u file from it
        @staticmethod
        def m3u(pls):
        
            # temp filename
            (tmp_fn, unique) = action.tmp_fn(pls)
            # does it already exist?
            if tmp_fn and unique and conf.reuse_m3u and action.has_urls(tmp_fn):
                return tmp_fn

            # download PLS
            __print__( "pls=",pls )
            url_list = action.extract_urls(pls)
            __print__( "urls=", url_list )

            # output URL list to temporary .m3u file
            if (len(url_list)):
                #tmp_fn = 
                f = open(tmp_fn, "w")
                f.write("#M3U\n")
                f.write("\n".join(url_list) + "\n")
                f.close()
                # return path/name of temporary file
                return tmp_fn
            else:
                __print__( "error, there were no URLs in ", pls )
                raise "Empty PLS"

        # open help browser                
        @staticmethod
        def help(*args):
        
            os.system("yelp /usr/share/doc/streamtuner2/help/ &")
            #or action.browser("/usr/share/doc/streamtuner2/")

#class action


Added channels/__init__.py version [f76e54fd21].



















>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
#
# encoding: UTF-8
# api: python
# type: R
#


from channels._generic import *
from channels._generic import __print__

Added channels/_generic.py version [e213956f5f].

































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
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
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
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
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
280
281
282
283
284
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
427
428
429
430
431
432
433
434
435
436
437
438
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
465
466
467
468
469
470
471
472
473
474
475
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
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
#
# encoding: UTF-8
# api: streamtuner2
# type: class
# title: channel objects
# description: base functionality for channel modules
# version: 1.0
# author: mario
# license: public domain
#
#
#  GenericChannel implements the basic GUI functions and defines
#  the default channel data structure. It implements base and
#  fallback logic for all other channel implementations.
#
#  Built-in channels derive directly from generic. Additional
#  channels don't have a pre-defined Notebook tab in the glade
#  file. They derive from the ChannelPlugins class instead, which
#  adds the required gtk Widgets manually.
#


import gtk
from mygtk import mygtk
from config import conf
import http
import action
import favicon
import os.path
import xml.sax.saxutils
import re
import copy



# dict==object
class struct(dict):
        def __init__(self, *xargs, **kwargs):
                self.__dict__ = self
                self.update(kwargs)
                [self.update(x) for x in xargs]
        pass


# generic channel module                            ---------------------------------------
class GenericChannel(object):

        # desc
        api = "streamtuner2"
        module = "generic"
        title = "GenericChannel"
        version = 1.0
        homepage = "http://milki.inlcude-once.org/streamtuner2/"
        base_url = ""
        listformat = "audio/x-scpls"
        audioformat = "audio/mp3" # fallback value
        config = []

        # categories
        categories = ["empty", ]
        current = ""
        default = "empty"
        shown = None     # last selected entry in stream list, also indicator if notebook tab has been selected once / stream list of current category been displayed yet

        # gui + data
        streams = {}      #meta information dicts
        liststore = {}    #gtk data structure
        gtk_list = None   #gtk widget
        gtk_cat = None    #gtk widget

        # mapping of stream{} data into gtk treeview/treestore representation
        datamap = [
           # coltitle   width	[ datasrc key, type, renderer, attrs ]	[cellrenderer2], ...
           ["",		20,	["state",	str,  "pixbuf",	{}],	],
           ["Genre",	65,	['genre',	str,	"t",	{}],	],
           ["Station Title",275,["title",	str,    "text",	{"strikethrough":11, "cell-background":12, "cell-background-set":13}],  ["favicon",gtk.gdk.Pixbuf,"pixbuf",{"width":20}], ],
           ["Now Playing",185,	["playing",	str,	"text",	{"strikethrough":11}],	],
           ["Listeners", 45,	["listeners",	int,	"t",	{"strikethrough":11}],	],
#           ["Max",	45,	["max",		int,	"t",	{}],	],
           ["Bitrate",	35,	["bitrate",	int,	"t",	{}],	],
           ["Homepage",	160,	["homepage",	str,	"t",	{"underline":10}],	],
           [False,	25,	["---url",	str,	"t",	{"strikethrough":11}],	],
           [False,	0,	["---format",	str,	None,	{}],	],
           [False,	0,	["favourite",	bool,	None,	{}],	],
           [False,	0,	["deleted",	bool,	None,	{}],	],
           [False,	0,	["search_col",	str,	None,	{}],	],
           [False,	0,	["search_set",	bool,	None,	{}],	],
        ]        
        rowmap = []   # [state,genre,title,...] field enumeration still needed separately
        titles = {}   # for easier adapting of column titles in datamap
        
        # 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

            # only if streamtuner2 is run in graphical mode        
            if (parent):
                self.cache()
                self.gui(parent)
            pass
            
            
        # called before application shutdown
        # some plugins might override this, to save their current streams[] data
        def shutdown(self):
            pass
        #__del__ = shutdown
            
            
        # returns station entries from streams[] for .current category
        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
            pass

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

            # save reference to main window/glade API
            self.parent = parent
            self.gtk_list = parent.get_widget(self.module+"_list")
            self.gtk_cat = parent.get_widget(self.module+"_cat")
            
            # category tree
            self.display_categories()
            #mygtk.tree(self.gtk_cat, self.categories, title="Category", icon=gtk.STOCK_OPEN);
            
            # update column names
            for field,title in self.titles.iteritems():
                self.update_datamap(field, title=title)
            
            # prepare stream list
            if (not self.rowmap):
                for row in self.datamap:
                    for x in xrange(2, len(row)):
                        self.rowmap.append(row[x][0])

            # load default category
            if (self.current):
                self.load(self.current)
            else:
                mygtk.columns(self.gtk_list, self.datamap, [{}])
                
            # add to main menu
            mygtk.add_menu(parent.channelmenuitems, self.title, lambda w: parent.channel_switch(w, 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:
                    row[0] = title


        # switch stream category,
        # load data,
        # update treeview content
        def load(self, category, force=False):
        
            # get data from cache or download
            if (force or not self.streams.has_key(category)):
                new_streams = self.update_streams(category)
      
                if new_streams:
                
                    # modify
                    [self.postprocess(row) for row in new_streams]
      
                    # don't lose forgotten streams
                    if conf.retain_deleted:
                       self.streams[category] = new_streams + self.deleted_streams(new_streams, self.streams.get(category,[]))
                    else:
                       self.streams[category] = new_streams
      
                    # save in cache
                    self.save()
      
                    # invalidate gtk list cache
                    #if (self.liststore.has_key(category)):
                    #    del self.liststore[category]
      
                else:
                    # parse error
                    self.parent.status("category parsed empty.")
                    self.streams[category] = [{"title":"no contents found on directory server","bitrate":0,"max":0,"listeners":0,"playing":"error","favourite":0,"deleted":0}]
                    print("oooops, parser returned nothing for category " + category)
                    
            # assign to treeview model
            #self.streams[self.default] = []
            #if (self.liststore.has_key(category)):  # was already loded before
            #    self.gtk_list.set_model(self.liststore[category])
            #else:   # currently list is new, had not been converted to gtk array before
            #    self.liststore[category] = \
            mygtk.do(lambda:mygtk.columns(self.gtk_list, self.datamap, self.prepare(self.streams[category])))

            # set pointer
            self.current = category
            pass
            
        # store current streams data
        def save(self):
            conf.save("cache/" + self.module, self.streams, gz=1)


        # called occasionally while retrieving and parsing
        def update_streams_partially_done(self, entries):
            mygtk.do(lambda: mygtk.columns(self.gtk_list, self.datamap, entries))

            
        # finds differences in new/old streamlist, marks deleted with flag
        def deleted_streams(self, new, old):
            diff = []
            new = [row.get("url","http://example.com/") for row in new]
            for row in old:
                if (row.has_key("url") and (row.get("url") not in new)):
                    row["deleted"] = 1
                    diff.append(row)
            return diff

        
        # prepare data for display
        def prepare(self, streams):
            for i,row in enumerate(streams):
                                            # oh my, at least it's working
                                            # at start the bookmarks module isn't fully registered at instantiation in parent.channels{} - might want to do that step by step rather
                                            # then display() is called too early to take effect - load() & co should actually be postponed to when a notebook tab gets selected first
                                            # => might be fixed now, 1.9.8
                # state icon: bookmark star
                if (conf.show_bookmarks and self.parent.channels.has_key("bookmarks") and self.parent.bookmarks.is_in(streams[i].get("url", "file:///tmp/none"))):
                    streams[i]["favourite"] = 1
                
                # state icon: INFO or DELETE
                if (not row.get("state")):
                    if row.get("favourite"):
                        streams[i]["state"] = gtk.STOCK_ABOUT
                    if conf.retain_deleted and row.get("deleted"):
                        streams[i]["state"] = gtk.STOCK_DELETE
                      
                # guess homepage url  
                #self.postprocess(row)
                
                # favicons?
                if conf.show_favicons:
                    homepage_url = row.get("homepage")
                    # check for availability of PNG file, inject local icons/ filename
                    if homepage_url and favicon.available(homepage_url):
                        streams[i]["favicon"] = favicon.file(homepage_url)
                
            return streams

    
        # data preparations directly after reload        
        def postprocess(self, row):

            # remove non-homepages from shoutcast
            if row.get("homepage") and row["homepage"].find("//yp.shoutcast.")>0:
                row["homepage"] = ""
                
            # deduce homepage URLs from title
            # by looking for www.xyz.com domain names
            if not row.get("homepage"):
                url = self.rx_www_url.search(row.get("title", ""))
                if url:
                    url = url.group(0).lower().replace(" ", "")
                    url = (url if url.find("www.") == 0 else "www."+url)
                    row["homepage"] = http.fix_url(url)
            
            return row

            

        # reload current stream from web directory
        def reload(self):
            self.load(self.current, force=1)
        def switch(self):
            self.load(self.current, force=0)
            
            
        # display .current category, once notebook/channel tab is first opened
        def first_show(self):
            print("first_show ", self.module)
            print 1
            print self.shown
            if (self.shown != 55555):
                print 2
            
                # if category tree is empty, initialize it
                if not self.categories:
                    print 3
                    #self.parent.thread(self.reload_categories)
                    print("reload categories");
                    self.reload_categories()
                    self.display_categories()
                    self.current = self.categories.keys()[0]
                    print self.current
                    self.load(self.current)
            
                # load current category
                else:
                    print 4
                    self.load(self.current)
                
                # put selection/cursor on last position
                try:
                    print 5
                    self.gtk_list.get_selection().select_path(self.shown)
                except:
                    pass
                    
                # this method will only be invoked once
                self.shown = 55555


        # update categories, save, and display                
        def reload_categories(self):
        
            # get data and save
            self.update_categories()
            conf.save("cache/categories_"+self.module, self.categories)

            # display outside of this non-main thread            
            mygtk.do(self.display_categories)

        # insert content into gtk category list
        def display_categories(self):
        
            # remove any existing columns
            if self.gtk_cat:
                [self.gtk_cat.remove_column(c) for c in self.gtk_cat.get_columns()]
            # rebuild gtk.TreeView
            mygtk.tree(self.gtk_cat, self.categories, title="Category", icon=gtk.STOCK_OPEN);

            # if it's a short list of categories, there's probably subfolders
            if len(self.categories) < 20:
                self.gtk_cat.expand_all()
                
            # select any first element
            self.gtk_cat.get_selection().select_path("0") #set_cursor
            self.currentcat()

                
        # selected category
        def currentcat(self):
            (model, iter) = self.gtk_cat.get_selection().get_selected()
            if (type(iter) == gtk.TreeIter):
                self.current = model.get_value(iter, 0)
            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.action.play(row["url"], audioformat, listformat)




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

        

        # remove html <tags> from string        
        def strip_tags(self, s):
            return re.sub("<.+?>", "", s)
            
        # convert audio format nick/shortnames to mime types, e.g. "OGG" to "audio/ogg"
        def mime_fmt(self, s):
            # clean string
            s = s.lower().strip()
            # rename
            map = {
                "audio/mpeg":"audio/mp3",  # Note the real mime type is /mpeg, but /mp3 is more understandable in the GUI
                "ogg":"ogg", "ogm":"ogg", "xiph":"ogg", "vorbis":"ogg", "vnd.xiph.vorbis":"ogg",
                "mpeg":"mp3", "mp":"mp3", "mp2":"mp3", "mpc":"mp3", "mps":"mp3",
                "aac+":"aac", "aacp":"aac",
                "realaudio":"x-pn-realaudio", "real":"x-pn-realaudio", "ra":"x-pn-realaudio", "ram":"x-pn-realaudio", "rm":"x-pn-realaudio",
                # yes, we do video
                "flv":"video/flv", "mp4":"video/mp4",
            }
            map.update(action.action.lt)   # list type formats (.m3u .pls and .xspf)
            if map.get(s):
                s = map[s]
            # add prefix:
            if s.find("/") < 1:
                s = "audio/" + s
            #
            return s
        
        # remove SGML/XML entities
        def entity_decode(self, s):
            return xml.sax.saxutils.unescape(s)
        
        # convert special characters to &xx; escapes
        def xmlentities(self, s):
            return xml.sax.saxutils.escape(s)









# channel plugin without glade-pre-defined notebook tab
#
class ChannelPlugin(GenericChannel):

        module = "abstract"
        title = "New Tab"
        version = 0.1


        def gui(self, parent):
        
            # name id
            module = self.module

            if parent:
                # two panes
                vbox = gtk.HPaned()
                vbox.show()
                # category treeview
                sw1 = gtk.ScrolledWindow()
                sw1.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
                sw1.set_property("width_request", 150)
                sw1.show()
                tv1 = gtk.TreeView()
                tv1.set_property("width_request", 75)
                tv1.set_property("enable_tree_lines", True)
                tv1.connect("button_release_event", parent.on_category_clicked)
                tv1.show()
                sw1.add(tv1)
                vbox.pack1(sw1, resize=False, shrink=True)
                # stream list
                sw2 = gtk.ScrolledWindow()
                sw2.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
                sw2.show()
                tv2 = gtk.TreeView()
                tv2.set_property("width_request", 200)
                tv2.set_property("enable_tree_lines", True)
                tv2.connect("row_activated", parent.on_play_clicked)
                tv2.show()
                sw2.add(tv2)
                vbox.pack2(sw2, resize=True, shrink=True)

                # prepare label
                label = gtk.HBox()
                label.set_property("visible", True)
                fn = "/usr/share/streamtuner2/channels/" + self.module + ".png"
                if os.path.exists(fn):
                    icon = gtk.Image()
                    icon.set_property("pixbuf", gtk.gdk.pixbuf_new_from_file(fn))
                    icon.set_property("icon-size", 1)
                    icon.set_property("visible", True)
                    label.pack_start(icon, expand=False, fill=True)
                if self.title:
                    text = gtk.Label(self.title)
                    text.set_property("visible", True)
                    label.pack_start(text, expand=True, fill=True)
                    
                # pack it into an event container to catch double-clicks
                ev_label = gtk.EventBox()
                ev_label.add(label)
                ev_label.connect('event', parent.on_homepage_channel_clicked)

                # add notebook tab
                tab = parent.notebook_channels.append_page(vbox, ev_label)
                
                # to widgets
                self.gtk_cat = tv1
                parent.widgets[module + "_cat"] = tv1
                self.gtk_list = tv2
                parent.widgets[module + "_list"] = tv2
                parent.widgets["v_" + module] = vbox
                parent.widgets["c_" + module] = ev_label
                tv2.connect('button-press-event', parent.station_context_menu)
                
                
                
                # double-click catch
                


            # add module to list            
            #parent.channels[module] = None
            #parent.channel_names.append(module)
            """ -> already taken care of in main.load_plugins() """

            # superclass
            GenericChannel.gui(self, parent)






# wrapper for all print statements
def __print__(*args):
    if conf.debug:
        print(" ".join([str(a) for a in args]))

__debug_print__ = __print__




Added channels/basicch.py version [2ad035d297].



































































































































































































































































































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
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
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
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
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
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305

# api: streamtuner2
# title: basic.ch channel
#
#
#
# Porting ST1 plugin senseless, old parsing method wasn't working any longer. Static
# realaudio archive is not available anymore.
#
# Needs manual initialisation of categories first.
#


import re
import http
from config import conf
from channels import *
from xml.sax.saxutils import unescape














# basic.ch broadcast archive
class basicch (ChannelPlugin):

    # description
    title = "basic.ch"
    module = "basicch"
    homepage = "http://www.basic.ch/"
    version = 0.3
    base = "http://basic.ch/"

    # keeps category titles->urls    
    catmap = {}
    categories = []   #"METAMIX", "reload category tree!", ["menu > channel > reload cat..."]]
    #titles = dict(listeners=False, bitrate=False)
 
    
    # read previous channel/stream data, if there is any
    def cache(self):
        ChannelPlugin.cache(self)
        # catmap
        cache = conf.load("cache/catmap_" + self.module)
        if (cache):
            self.catmap = cache
        pass
    

    # refresh category list
    def update_categories(self):

        html = http.get(self.base + "shows.cfm")

        rx_current = re.compile(r"""
            <a\shref="shows.cfm[?]showid=(\d+)">([^<>]+)</a>
            \s*<span\sclass="smalltxt">([^<>]+)</span><br
        """, re.S|re.X)


        #-- archived shows
        for uu in rx_current.findall(html):
            (id, title, genre) = uu
            self.catmap[title] = {
                "sub_url": self.base + "shows.cfm?showid=" + id,
                "title": title,
                "genre": genre,
                "id": id,
            }

        #-- populate category treee
        self.categories = [
            "METAMIX",
            "shows", [ title for title in self.catmap.keys() ]
        ]
        
        #-- keep catmap as cache-file, it's essential for redisplaying        
        self.save()
        return

    # saves .streams and .catmap
    def save(self):
        ChannelPlugin.save(self)
        conf.save("cache/catmap_" + self.module, self.catmap)


    # download links from dmoz listing
    def update_streams(self, cat, force=0):

        rx_metamix = re.compile("""
            <a\shref="(http://[^<">]+)">(METAMIX[^<]+)</a>
            \s+<span\sclass="smalltxt">([^<>]+)</span><br
        """, re.S|re.X)

        rx_playlist = re.compile("""
            <filename>(http://[^<">]+)</filename>\s*
            <artist>([^<>]+)</artist>\s*
            <title>([^<>]+)</title>
        """, re.S|re.X)

        entries = []
 
        #-- update categories first
        if not len(self.catmap):
            self.update_categories()
    
        #-- frontpage mixes
        if cat == "METAMIX":
            for uu in rx_metamix.findall(http.get(self.base)):
                (url, title, genre) = uu
                entries.append({
                    "genre": genre,
                    "title": title,
                    "url": url,
                    "format": "audio/mp3",
                    "homepage": self.homepage,
                })

        #-- pseudo entry        
        elif cat=="shows":
            entries = [{"title":"shows","homepage":self.homepage+"shows.cfm"}]
            
        #-- fetch playlist.xml
        else:
        
            # category name "Xsound & Ymusic" becomes "Xsoundandymusic"
            id = cat.replace("&", "and").replace(" ", "")
            id = id.lower().capitalize()
            
            catinfo = self.catmap.get(cat, {"id":"", "genre":""})
            
            # extract
            html = http.get(self.base + "playlist/" + id + ".xml")
            for uu in rx_playlist.findall(html):    # you know, we could parse this as proper xml
                (url, artist, title) = uu           # but hey, lazyness works too
                entries.append({
                    "url": url,
                    "title": artist,
                    "playing": title,
                    "genre": catinfo["genre"],
                    "format": "audio/mp3",
                    "homepage": self.base + "shows.cfm?showid=" + catinfo["id"],
                })

        # done    
        return entries
        










# basic.ch broadcast archive
class basicch_old_static: #(ChannelPlugin):

    # description
    title = "basic.ch"
    module = "basicch"
    homepage = "http://www.basic.ch/"
    version = 0.2
    base = "http://basic.ch/"

    # keeps category titles->urls    
    catmap = {}
 
    
    # read previous channel/stream data, if there is any
    def cache(self):
        ChannelPlugin.cache(self)
        # catmap
        cache = conf.load("cache/catmap_" + self.module)
        if (cache):
            self.catmap = cache
        pass
    

    # refresh category list
    def update_categories(self):

        html = http.get(self.base + "downtest.cfm")

        rx_current = re.compile("""
            href="javascript:openWindow.'([\w.?=\d]+)'[^>]+>
            <b>(\w+[^<>]+)</.+?
            <b>(\w+[^<>]+)</.+?
            <a\s+href="(http://[^">]+)"
        """, re.S|re.X)

        rx_archive = re.compile("""
            href="javascript:openWindow.'([\w.?=\d]+)'[^>]+>.+?
            color="000000">(\w+[^<>]+)</.+?
            color="000000">(\w+[^<>]+)</
        """, re.S|re.X)

        archive = []
        previous = []
        current = []
        
        #-- current listings with latest broadcast and archive link
        for uu in rx_current.findall(html):
            self.catmap[uu[1]] = {
                "sub_url": self.base + uu[0],
                "title": uu[1],
                "genre": uu[2],
                "url": uu[3],
            }
            archive.append(uu[1])

        #-- old listings only have archive link
        for uu in rx_archive.findall(html):
            self.catmap[uu[1]] = {
                "sub_url": self.base + uu[0],
                "genre": uu[2],
            }
            previous.append(uu[1])
        
        #-- populate category treee
        self.categories = [
            "current",
            "archive", archive,
            "previous", previous,
        ]
        
        #-- inject streams
        self.streams["current"] = [
             {
                "title": e["title"],
                "url": e["url"],
                "genre": e["genre"],
                "format": "audio/mp3",
                "listeners": 0,
                "max": 100,
                "bitrate": 0,
                "homepage": e["sub_url"],
             }
             for title,e in self.catmap.iteritems() if e.get("url")
        ]

        #-- keep catmap as cache-file, it's essential for redisplaying        
        self.save()
        return

    # saves .streams and .catmap
    def save(self):
        ChannelPlugin.save(self)
        conf.save("cache/catmap_" + self.module, self.catmap)


    # download links from dmoz listing
    def update_streams(self, cat, force=0):
    
        if not self.catmap:
            self.update_categories()
        elif cat=="current":
            self.update_categories()
            return self.streams["current"]
            
        if not self.catmap.get(cat):
            return []
    
        e = self.catmap[cat]
        html = http.get( e["sub_url"] )
        
        rx_archives = re.compile("""
            >(\d\d\.\d\d\.\d\d)</font>.+?
            href="(http://[^">]+|/ram/\w+.ram)"[^>]*>([^<>]+)</a>
            .+?   (>(\w+[^<]*)</)?
        """, re.X|re.S)
        
        entries = []
        
        for uu in rx_archives.findall(html):
            url = uu[1]
            ram = url.find("http://") < 0
            if ram:
                url = self.base + url[1:]
            entries.append({
                "title": uu[0],
                "url": url,
                "playing": uu[2],
                "genre": e["genre"],
                "format": ( "audio/x-pn-realaudio"  if  ram  else  "audio/"+uu[3].lower() ),
                "listeners": 0,
                "max": 1,
                "homepage": e["sub_url"],
            })
            
        return entries
        


Added channels/file.py version [13559afbce].





















































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
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
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
#
# api: streamtuner2
# title: file browser plugin
# description: browses through local files, displays mp3/oggs and m3u/pls files
# version: -0.5
# depends: mutagen, kiwi
# x:
# x:
# x:
# x:
#
#
# Local file browser.
#
#



# modules
import os
import re

from channels import *
from config import conf

# ID3 libraries
try:
    from mutagen import File as get_meta
except:
    try:
        print("just basic ID3 support")
        from ID3 import ID3
        get_meta = lambda fn: dict([(k.lower(),v) for k,v in ID3(fn).iteritems()])
    except:
        print("you are out of luck in regards to mp3 browsing")
        get_meta = lambda *x: {}


# work around mutagens difficult interface
def mutagen_postprocess(d):
    if d.get("TIT2"):
        return {
            "encoder": d["TENC"][0],
            "title": d["TIT2"][0],
            "artist": d["TPE1"][0],
#            "tyer?????????????": d["TYER"][0],
#            "track": d["TRCK"][0],
            "album": d["TALB"][0],
        }
    else:
        return d




# file browser / mp3 directory listings
class file (ChannelPlugin):

    # info
    api = "streamtuner2"
    module = "file"
    title = "file browser"
    version = -0.5
    listtype = "url/file"
    
    
    # data
    config = [
        {"name":"file_browser_dir", "type":"text", "value":os.environ["HOME"]+"/Music, /media/music", "description":"list of directories to scan for audio files"},
        {"name":"file_browser_ext", "type":"text", "value":"mp3,ogg, m3u,pls,xspf, avi,flv,mpg,mp4", "description":"file type filter"},
    ]
    streams = {}
    categories = []
    dir = []
    ext = []
    
    # display
    datamap = [ # coltitle   width	[ datasrc key, type, renderer, attrs ]	[cellrenderer2], ...
           ["",		20,	["state",	str,  "pixbuf",	{}],	],
           ["Genre",	65,	['genre',	str,	"t",	{"editable":8}],	],
           ["File",	160,	["filename",	str,	"t",	{"strikethrough":11, "cell-background":12, "cell-background-set":13}],	],
           ["Title",	205,	["title",	str,    "t",	{"editable":8}], ],
           ["Artist",	125,	["artist",	str,	"t",	{"editable":8}],	],
           ["Album", 	125,	["album",	str,	"t",	{"editable":8}],	],
           ["Bitrate",	35,	["bitrate",	int,	"t",	{}],	],
           ["Format",	50,	["format",	str,	None,	{}],	],
           [False,	0,	["editable",	bool,	None,	{}],	],
           [False,	0,	["favourite",	bool,	None,	{}],	],
           [False,	0,	["deleted",	bool,	None,	{}],	],
           [False,	0,	["search_col",	str,	None,	{}],	],
           [False,	0,	["search_set",	bool,	None,	{}],	],
    ]        
    rowmap = []

    

    # prepare    
    def __init__(self, parent):
    
        # data dirs
        self.dir = [s.strip() for s in conf.file_browser_dir.split(",")]
        self.ext = [s.strip() for s in conf.file_browser_ext.split(",")]
        # first run
        if not self.categories or not self.streams:
            self.scan_dirs()
            
        # draw gtk lists
        ChannelPlugin.__init__(self, parent)
        
        # make editable
        #{editable:8}
        
        # add custom context menu
        #self.gtk_list.connect('button-press-event', self.context_menu)

        
        
    # save list?
    #save = lambda *x: None
    # yeah, give it a try
    
    # don't load cache file
    cache = lambda *x: None

        

    # read dirs
    def scan_dirs(self):
        self.categories = []
    
        # add main directory
        for main in self.dir:
          if os.path.exists(main):
            self.categories.append(main)
            
            # prepare subdirectories list
            sub = []
            self.categories.append(sub)

            # look through            
            for dir, subdirs, files in os.walk(main):
                name = os.path.basename(dir)
                while name in self.categories:
                    name = name + "2"
        
                # files in subdir
                if files:
                    sub.append(name)
                    self.streams[name] = [self.file_entry(fn, dir) for fn in files if self.we_like_that_extension(fn)]
                
            # plant a maindir reference to shortname
            self.streams[main] = self.streams[os.path.basename(main)]


    # extract meta data
    def file_entry(self, fn, dir):
        # basic data
        meta = {
            "title": fn,
            "filename": fn,
            "url": dir + "/" + fn,
            "genre": "",
            "format": self.mime_fmt(fn[-3:]),
            "editable": True,
        }
        # add ID3
        meta.update(mutagen_postprocess(get_meta(dir + "/" + fn) or {}))
        return meta
        
    # check fn for .ext
    def we_like_that_extension(self, fn):
        return fn[-3:] in self.ext
    


    # same as init
    def update_categories(self):
        self.scan_dirs()

        
    # same as init
    def update_streams(self, cat, x=0):
        self.scan_dirs()
        return self.streams.get(os.path.basename(cat))


Added channels/global_key.py version [71e77fdc54].



































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
#
# type: feature
# api: streamtuner2
# title: global keyboard shortcut
# description: allows switching radios in bookmarks list via key press
# version: 0.2
# depends: python-keybinder
#
#
# 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



# register a key
class global_key(object):

    module = "global_key"
    title = "keyboard shortcut"
    version = 0.2
    
    config = [
        dict(name="switch_key", type="text", value="XF86Forward", description="global key for switching radio"),
        dict(name="switch_channel", type="text", value="bookmarks:favourite", description="station list to alternate in"),
        dict(name="switch_random", type="boolean", value=0, description="pick random channel, instead of next"),
    ]
    last = 0


    # register
    def __init__(self, parent):
        self.parent = parent
        try:
            for i,keyname in enumerate(conf.switch_key.split(",")):    # allow multiple keys
                keybinder.bind(keyname, self.switch, ((-1 if i else +1)))   # forward +1 or backward -1
        except:
            print("Key could not be registered")
    
        
    # key event
    def switch(self, num, *any):
        
        # bookmarks, favourite
        channel, cat = conf.switch_channel.split(":")

        # get list
        streams = self.parent.channels[channel].streams[cat]

        # pickrandom
        if conf.switch_random:
            self.last = random.randint(0, len(streams)-1)

        # or iterate over list
        else:
            self.last = self.last + num
            if self.last >= len(streams):
                self.last = 0
            elif self.last < 0:
                self.last = len(streams)-1
            
        # play
        i = self.last
        action.action.play(streams[i]["url"], streams[i]["format"])

        # set pointer in gtk.TreeView
        if self.parent.channels[channel].current == cat:
            self.parent.channels[channel].gtk_list.get_selection().select_path(i)


        

Added channels/google.py version [3f0788927c].













































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
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
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
#
# encoding: ISO-8859-1
# api: streamtuner2
# title: google stations
# description: Looks up web radio stations from DMOZ/Google directory
# depends: channels, re, http
# version: 0.1
# author: Mario, original: Jean-Yves Lefort
#
# This is a plugun from streamtuner1. It has been rewritten for the
# more mundane plugin API of streamtuner2 - reimplementing ST seemed
# to much work.
# Also it has been rewritten to query DMOZ directly. Google required
# the use of fake User-Agents for access, and the structure on DMOZ
# is simpler (even if less HTML-compliant). DMOZ probably is kept
# more up-to-date as well.
# PS: we need to check out //musicmoz.org/
#


# Copyright (c) 2003, 2004 Jean-Yves Lefort
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
# 1. Redistributions of source code must retain the above copyright
#    notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
#    notice, this list of conditions and the following disclaimer in the
#    documentation and/or other materials provided with the distribution.
# 3. Neither the name of Jean-Yves Lefort nor the names of its contributors
#    may be used to endorse or promote products derived from this software
#    without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
# CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
# INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
# MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS
# BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
# TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.


import re, os, gtk
from channels import *
from xml.sax.saxutils import unescape as entity_decode, escape as xmlentities
import http


### constants #################################################################


GOOGLE_DIRECTORY_ROOT	= "http://www.dmoz.org"
CATEGORIES_URL_POSTFIX	= "/Arts/Music/Sound_Files/MP3/Streaming/Stations/"
#GOOGLE_DIRECTORY_ROOT	= "http://directory.google.com"
#CATEGORIES_URL_POSTFIX	= "/Top/Arts/Music/Sound_Files/MP3/Streaming/Stations/"
GOOGLE_STATIONS_HOME	= GOOGLE_DIRECTORY_ROOT + CATEGORIES_URL_POSTFIX

"""<li><a href="/Arts/Music/Sound_Files/MP3/Streaming/Stations/Jazz/"><b>Jazz</b></a>"""
re_category	= re.compile('<a href="(.+)">(<b>)([^:]+?)(</b>)</a>', re.I|re.M)

#re_stream	= re.compile('^<td><font face="arial,sans-serif"><a href="(.*)">(.*)</a>')
#re_description	= re.compile('^<br><font size=-1> (.*?)</font>')
"""<li><a href="http://www.atlantabluesky.com/">Atlanta Blue Sky</a> - Rock and alternative streaming audio. Live real-time requests."""
re_stream_desc	= re.compile('^<li><a href="(.*)">([^<>]+)</a>( - )?([^<>\n\r]+)', re.M|re.I)


######


# Google Stations is actually now DMOZ Stations
class google(ChannelPlugin):

    # description
    title = "Google"
    module = "google"
    homepage = GOOGLE_STATIONS_HOME
    version = 0.2
    
    # config data
    config = [
#        {"name": "theme", "type": "text", "value":"Tactile", "description":"Streamtuner2 theme; no this isn't a google-specific option. But you know, the plugin options are a new toy."},
#        {"name": "flag2", "type": "boolean", "value":1, "description":"oh see, an unused checkbox"}
    ]
    

    # category map
    categories = ['Google/DMOZ Stations', 'Alternative', 'Ambient', 'Classical', 'College', 'Country', 'Dance', 'Experimental', 'Gothic', 'Industrial', 'Jazz', 'Local', 'Lounge', 'Metal', 'New Age', 'Oldies', 'Old-Time Radio', 'Pop', 'Punk', 'Rock', '80s', 'Soundtracks', 'Talk', 'Techno', 'Urban', 'Variety', 'World']
    catmap = [('Google/DMOZ Stations', '__main', '/Arts/Radio/Internet/'), ['Alternative', 'Alternative', '/Arts/Music/Sound_Files/MP3/Streaming/Stations/Rock/Alternative/'], ['Ambient', 'Ambient', '/Arts/Music/Sound_Files/MP3/Streaming/Stations/Ambient/'], ['Classical', 'Classical', '/Arts/Music/Sound_Files/MP3/Streaming/Stations/Classical/'], ['College', 'College', '/Arts/Music/Sound_Files/MP3/Streaming/Stations/College/'], ['Country', 'Country', '/Arts/Music/Sound_Files/MP3/Streaming/Stations/Country/'], ['Dance', 'Dance', '/Arts/Music/Sound_Files/MP3/Streaming/Stations/Dance/'], ['Experimental', 'Experimental', '/Arts/Music/Sound_Files/MP3/Streaming/Stations/Experimental/'], ['Gothic', 'Gothic', '/Arts/Music/Sound_Files/MP3/Streaming/Stations/Rock/Gothic/'], ['Industrial', 'Industrial', '/Arts/Music/Sound_Files/MP3/Streaming/Stations/Rock/Industrial/'], ['Jazz', 'Jazz', '/Arts/Music/Sound_Files/MP3/Streaming/Stations/Jazz/'], ['Local', 'Local', '/Arts/Music/Sound_Files/MP3/Streaming/Stations/Local/'], ['Lounge', 'Lounge', '/Arts/Music/Sound_Files/MP3/Streaming/Stations/Lounge/'], ['Metal', 'Metal', '/Arts/Music/Sound_Files/MP3/Streaming/Stations/Rock/Metal/'], ['New Age', 'New Age', '/Arts/Music/Sound_Files/MP3/Streaming/Stations/New_Age/'], ['Oldies', 'Oldies', '/Arts/Music/Sound_Files/MP3/Streaming/Stations/Rock/Oldies/'], ['Old-Time Radio', 'Old-Time Radio', '/Arts/Radio/Formats/Old-Time_Radio/Streaming_MP3_Stations/'], ['Pop', 'Pop', '/Arts/Music/Sound_Files/MP3/Streaming/Stations/Pop/'], ['Punk', 'Punk', '/Arts/Music/Sound_Files/MP3/Streaming/Stations/Rock/Punk/'], ['Rock', 'Rock', '/Arts/Music/Sound_Files/MP3/Streaming/Stations/Rock/'], ['80s', '80s', '/Arts/Music/Sound_Files/MP3/Streaming/Stations/80s/'], ['Soundtracks', 'Soundtracks', '/Arts/Music/Sound_Files/MP3/Streaming/Stations/Soundtracks/'], ['Talk', 'Talk', '/Arts/Music/Sound_Files/MP3/Streaming/Stations/Talk/'], ['Techno', 'Techno', '/Arts/Music/Sound_Files/MP3/Streaming/Stations/Dance/Techno/'], ['Urban', 'Urban', '/Arts/Music/Sound_Files/MP3/Streaming/Stations/Urban/'], ['Variety', 'Variety', '/Arts/Music/Sound_Files/MP3/Streaming/Stations/Variety/'], ['World', 'World', '/Arts/Music/Sound_Files/MP3/Streaming/Stations/World/']]


    #def __init__(self, parent):
    #    #self.update_categories()
    #    ChannelPlugin.__init__(self, parent)


    # refresh category list
    def update_categories(self):
    
        # interim data structure for categories (label, google-id/name, url)
        categories = [
            ("Google/DMOZ Stations", "__main", "/Arts/Radio/Internet/"),
        ]
        
        # fetch and extract list
        html = http.get(GOOGLE_DIRECTORY_ROOT + CATEGORIES_URL_POSTFIX)

        for row in re_category.findall(html):
            if row:
                name = entity_decode(row[2])
                label = name

                href = entity_decode(row[0])
                if href[0] != "/":
                    href = CATEGORIES_URL_POSTFIX + href
                   
                categories.append([label, name, href])

        # return
        self.catmap = categories
        self.categories = [x[0] for x in categories]
        pass
        # actually saving this into _categories and _catmap.json would be nice
        # ...



    # download links from dmoz listing
    def update_streams(self, cat, force=0):

        # result list
        ls = []    
        
        # get //dmoz.org/HREF for category name
        try:
            (label, name, href) = [x for x in self.catmap if x[0]==cat][0]
        except:
            return ls  # wrong category

        # download
        html = http.get(GOOGLE_DIRECTORY_ROOT + href)
        
        # filter
        for row in re_stream_desc.findall(html):
            
            if row:
                row = {
                    "homepage": entity_decode(row[0]),
                    "title": entity_decode(row[1]),
                    "playing": entity_decode(row[3]),
                }
                ls.append(row)


        # final list for current category
        return ls
        


Added channels/internet_radio_org_uk.py version [5a7ebd0c8e].















































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
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
141
142
143
144
145
146
147
148
149
150
151
#
# api: streamtuner2
# title: internet-radio.org.uk
# description: io channel
# version: 0.1
#
#
# Might become new main plugin
#
#
#



from channels import *
import re
from config import conf
import http
from pq import pq




# streams and gui
class internet_radio_org_uk (ChannelPlugin):


    # description
    title = "InternetRadio"
    module = "internet_radio_org_uk"
    homepage = "http://www.internet-radio.org.uk/"
    version = 0.1
    listformat = "audio/x-scpls"
    
    # settings
    config = [
        {"name":"internetradio_max_pages", "type":"int", "value":5, "description":"How many pages to fetch and read."},
    ]
    

    # category map
    categories = []
    current = ""
    default = ""


    # load genres
    def update_categories(self):
    
        html = http.get(self.homepage)
        rx = re.compile("""<option[^>]+value="/stations/[-+&.\w\s%]+/">([^<]+)</option>""")
        
        self.categories = rx.findall(html)





    # fetch station lists
    def update_streams(self, cat, force=0):    
    
        entries = []
        if cat not in self.categories:
            return []
            
        # regex
        #rx_div = re.compile('<tr valign="top" class="stream">(.+?)</tr>', re.S)
        rx_data = re.compile("""
		(?:M3U|PLS)',\s*'(http://[^']+)'
		.*?
		<br><br>([^\n]*?)</td>
		.*?
		(?:href="(http://[^"]+)"[^>]+target="_blank"[^>]*)?
		>\s*
		<b>\s*(\w[^<]+)[<\n]
		.*?
		playing\s*:\s*([^<\n]+)
		.*?
                (\d+)\s*Kbps
                (?:<br>(\d+)\s*Listeners)?
        """, re.S|re.X)
        #rx_homepage = re.compile('href="(http://[^"]+)"[^>]+target="_blank"')
        rx_pages = re.compile('href="/stations/[-+\w%\d\s]+/page(\d+)">\d+</a>')
        rx_numbers = re.compile("(\d+)")
        self.parent.status("downloading category pages...")


        # multiple pages
        page = 1
        max = int(conf.internetradio_max_pages)
        max = (max if max > 1 else 1)
        while page <= max:
        
            # fetch
            html = http.get(self.homepage + "stations/" + cat.lower().replace(" ", "%20") + "/" + ("page"+str(page) if page>1 else ""))


            # regex parsing?
            if not conf.pyquery:            
                # step through
                for uu in rx_data.findall(html):
                    (url, genre, homepage, title, playing, bitrate, listeners) = uu
                    
                    # transform data
                    entries.append({
                        "url": url,
                        "genre": self.strip_tags(genre),
                        "homepage": http.fix_url(homepage),
                        "title": title,
                        "playing": playing,
                        "bitrate": int(bitrate),
                        "listeners": int(listeners if listeners else 0),
                        "format": "audio/mp3", # there is no stream info on that, but internet-radio.org.uk doesn't seem very ogg-friendly anyway, so we assume the default here
                    })

            # DOM parsing
            else:
                # the streams are arranged in table rows
                doc = pq(html)
                for dir in (pq(e) for e in doc("tr.stream")):
                    
                    bl = dir.find("td[align=right]").text()
                    bl = rx_numbers.findall(str(bl) + " 0 0")
                    
                    entries.append({
                        "title": dir.find("b").text(),
                        "homepage": http.fix_url(dir.find("a.url").attr("href")),
                        "url": dir.find("a").eq(2).attr("href"),
                        "genre": dir.find("td").eq(0).text(),
                        "bitrate": int(bl[0]),
                        "listeners": int(bl[1]),
                        "format": "audio/mp3",
                        "playing": dir.find("td").eq(1).children().remove().end().text()[13:].strip(),
                    })
            
            # next page?
            if str(page+1) not in rx_pages.findall(html):
                max = 0
            else:
                page = page + 1

            # keep listview updated while searching
            self.update_streams_partially_done(entries)
            try: self.parent.status(float(page)/float(max))
            except: """there was a div by zero bug report despite max=1 precautions"""
            
        # fin
        self.parent.status()
        return entries


Added channels/jamendo.py version [0df05c5caf].

































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
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
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160

# api: streamtuner2
# title: jamendo browser
#
# For now this is really just a browser, doesn't utilizt the jamendo API yet.
# Requires more rework of streamtuner2 list display to show album covers.
#


import re
import http
from config import conf
from channels import *
from xml.sax.saxutils import unescape














# jamendo CC music sharing site
class jamendo (ChannelPlugin):

    # description
    title = "Jamendo"
    module = "jamendo"
    homepage = "http://www.jamendo.com/"
    version = 0.2

    base = "http://www.jamendo.com/en/"
    listformat = "url/http"

    categories = []  #"top 100", "reload category tree!", ["menu > channel > reload.."]]
    titles = dict( title="Artist", playing="Album/Song", bitrate=False, listeners=False )
 
    config = [
        {"name":"jamendo_stream_format", "value":"ogg2", "type":"text", "description":"streaming format, 'ogg2' or 'mp31'"}
    ]    
    



    # refresh category list
    def update_categories(self):

        html = http.get(self.base + "tags")

        rx_current = re.compile(r"""
             <a\s[^>]+rel="tag"[^>]+href="(http://www.jamendo.com/\w\w/tag/[\w\d]+)"[^>]*>([\w\d]+)</a>
        """, re.S|re.X)


        #-- categories
        tags = []
        for uu in rx_current.findall(html):
            (href, title) = uu
            tags.append(title)
            
        self.categories = [
           "top 100",
           "radios",
           "tags", tags
        ]
        


    # download links from dmoz listing
    def update_streams(self, cat, force=0):

        entries = []
    
        # top list
        if cat == "top" or cat == "top 100":
            html = http.get(self.base + "top")

	    rx_top = re.compile("""
		<img[^>]+src="(http://imgjam.com/albums/[\w\d]+/\d+/covers/1.\d+.jpg)"
		.*?
		<a\stitle="([^"]+)\s*-\s*([^"]+)"\s+class="track_name"\s+href="(http://www.jamendo.com/\w+/track/(\d+))"
	    """, re.X|re.S)

	    for uu in rx_top.findall(html):
		(cover, title, artist, track, track_id) = uu
		entries.append({
		    "title": artist,
		    "playing": title,
		    "homepage": track,
		    "url": self.track_url(track_id, conf.jamendo_stream_format),
		    "favicon": self.cover(cover),
		    "format": self.stream_mime(),
		})
		
		
        # static
        elif cat == "radios":

            rx = '>(\w+[-/\s]*\w+)</a>.+?/(get2/stream/track/m3u/radio_track_inradioplaylist/[?]order=numradio_asc&radio_id=\d+)"'
            for uu in re.findall(rx, http.get(self.base + "radios")):
                (name, url) = uu
                entries.append({
                    "title": name,
                    "url": self.homepage,
                    "homepage": self.base + "radios",
                })
                

        # genre list            
        else:
            html = http.get(self.base + "tag/" + cat)
            
            rx_tag = re.compile("""
		<a\s+title="([^"]+)\s*-\s*([^"]+)"
		\s+href="(http://www.jamendo.com/\w+/album/(\d+))"\s*>
		\s*<img[^>]+src="(http://imgjam.com/albums/[\w\d]+/\d+/covers/1.\d+.jpg)"
		.*? /tag/([\w\d]+)"
            """, re.X|re.S)

	    for uu in rx_tag.findall(html):
		(artist, title, album, album_id, cover, tag) = uu
		entries.append({
		    "title": artist,
		    "playing": title,
		    "homepage": album,
		    "url": self.track_url(album_id, conf.jamendo_stream_format, "album"),
		    "favicon": self.cover(cover),
		    "genre": tag,
		    "format": self.stream_mime(),
		})
 
        # done    
        return entries
        
        
    # smaller album link
    def cover(self, url):
        return url.replace(".100",".50").replace(".130",".50")
     
    # track id to download url   
    def track_url(self, track_id, fmt="ogg2", track="track", urltype="redirect"):
        # track = "album"
        # fmt = "mp31"
        # urltype = "m3u"
	return "http://api.jamendo.com/get2/stream/"+track+"/"+urltype+"/?id="+track_id+"&streamencoding="+fmt

    # audio/*
    def stream_mime(self):
        if conf.jamendo_stream_format.find("og") >= 0:
            return "audio/ogg"
        else:
            return "audio/mp3"


Added channels/links.py version [692f83efe7].





























































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
66
67
68
69
70
71
72
73
74
75
76
77
78
#
# api: streamtuner2
# title: links to directory services
# description: provides a simple list of homepages for directory services
# version: 0.1
# priority: rare
#
#
# Simply adds a "links" entry in bookmarks tab, where known channels
# and some others are listed with homepage links.
#
#


from channels import *
import copy



# hooks into main.bookmarks
class links (object):

    # plugin info
    module = "links"
    title = "Links"
    version = 0.1
    
    
    # configuration settings
    config = [    ]
    
    # list
    streams = [    ]
    default = {
        "radio.de": "http://www.radio.de/",
        "musicgoal": "http://www.musicgoal.com/",
        "streamfinder": "http://www.streamfinder.com/",
        "last.fm": "http://www.last.fm/",
        "rhapsody (US-only)": "http://www.rhapsody.com/",
        "pandora (US-only)": "http://www.pandora.com/",
        "radiotower": "http://www.radiotower.com/",
        "pirateradio": "http://www.pirateradionetwork.com/",
        "R-L": "http://www.radio-locator.com/",
        "radio station world": "http://radiostationworld.com/",
        "surfmusik.de": "http://www.surfmusic.de/",
    }
    
    
    # prepare gui
    def __init__(self, parent):
      if parent:

        # target channel
        bookmarks = parent.bookmarks
        if not bookmarks.streams.get(self.module):
            bookmarks.streams[self.module] = []
        bookmarks.add_category(self.module)


        # collect links from channel plugins
        for name,channel in parent.channels.iteritems():
          try:
            self.streams.append({
                "favourite": 1,
                "title": channel.title,
                "homepage": channel.homepage,
            })
          except: pass
        for title,homepage in self.default.iteritems():
            self.streams.append({
                "title": title,
                "homepage": homepage,
            })

        # add to bookmarks
        bookmarks.streams[self.module] = self.streams
        
        

Added channels/live365.py version [0058898af0].



































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
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
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161





# streamtuner2 modules
from config import conf
from mygtk import mygtk
import http
from channels import *
from channels import __print__

# python modules
import re
import xml.dom.minidom
from xml.sax.saxutils import unescape as entity_decode, escape as xmlentities
import gtk
import copy
import urllib


# channel live365
class live365(ChannelPlugin):


        # desc
        api = "streamtuner2"
        module = "live365"
        title = "Live365"
        version = 0.1
        homepage = "http://www.live365.com/"
        base_url = "http://www.live365.com/"
        listformat = "url/http"
        mediatype = "audio/mpeg"

        # content
        categories = ['Alternative', ['Britpop', 'Classic Alternative', 'College', 'Dancepunk', 'Dream Pop', 'Emo', 'Goth', 'Grunge', 'Indie Pop', 'Indie Rock', 'Industrial', 'Lo-Fi', 'Modern Rock', 'New Wave', 'Noise Pop', 'Post-Punk', 'Power Pop', 'Punk'], 'Blues', ['Acoustic Blues', 'Chicago Blues', 'Contemporary Blues', 'Country Blues', 'Delta Blues', 'Electric Blues', 'Cajun/Zydeco'], 'Classical', ['Baroque', 'Chamber', 'Choral', 'Classical Period', 'Early Classical', 'Impressionist', 'Modern', 'Opera', 'Piano', 'Romantic', 'Symphony'], 'Country', ['Alt-Country', 'Americana', 'Bluegrass', 'Classic Country', 'Contemporary Bluegrass', 'Contemporary Country', 'Honky Tonk', 'Hot Country Hits', 'Western'], 'Easy Listening', ['Exotica', 'Lounge', 'Orchestral Pop', 'Polka', 'Space Age Pop'], 'Electronic/Dance', ['Acid House', 'Ambient', 'Big Beat', 'Breakbeat', 'Disco', 'Downtempo', "Drum 'n' Bass", 'Electro', 'Garage', 'Hard House', 'House', 'IDM', 'Jungle', 'Progressive', 'Techno', 'Trance', 'Tribal', 'Trip Hop'], 'Folk', ['Alternative Folk', 'Contemporary Folk', 'Folk Rock', 'New Acoustic', 'Traditional Folk', 'World Folk'], 'Freeform', ['Chill', 'Experimental', 'Heartache', 'Love/Romance', 'Music To ... To', 'Party Mix', 'Patriotic', 'Rainy Day Mix', 'Reality', 'Shuffle/Random', 'Travel Mix', 'Trippy', 'Various', 'Women', 'Work Mix'], 'Hip-Hop/Rap', ['Alternative Rap', 'Dirty South', 'East Coast Rap', 'Freestyle', 'Gangsta Rap', 'Old School', 'Turntablism', 'Underground Hip-Hop', 'West Coast Rap'], 'Inspirational', ['Christian', 'Christian Metal', 'Christian Rap', 'Christian Rock', 'Classic Christian', 'Contemporary Gospel', 'Gospel', 'Praise/Worship', 'Sermons/Services', 'Southern Gospel', 'Traditional Gospel'], 'International', ['African', 'Arabic', 'Asian', 'Brazilian', 'Caribbean', 'Celtic', 'European', 'Filipino', 'Greek', 'Hawaiian/Pacific', 'Hindi', 'Indian', 'Japanese', 'Jewish', 'Mediterranean', 'Middle Eastern', 'North American', 'Soca', 'South American', 'Tamil', 'Worldbeat', 'Zouk'], 'Jazz', ['Acid Jazz', 'Avant Garde', 'Big Band', 'Bop', 'Classic Jazz', 'Cool Jazz', 'Fusion', 'Hard Bop', 'Latin Jazz', 'Smooth Jazz', 'Swing', 'Vocal Jazz', 'World Fusion'], 'Latin', ['Bachata', 'Banda', 'Bossa Nova', 'Cumbia', 'Latin Dance', 'Latin Pop', 'Latin Rap/Hip-Hop', 'Latin Rock', 'Mariachi', 'Merengue', 'Ranchera', 'Salsa', 'Tango', 'Tejano', 'Tropicalia'], 'Metal', ['Extreme Metal', 'Heavy Metal', 'Industrial Metal', 'Pop Metal/Hair', 'Rap Metal'], 'New Age', ['Environmental', 'Ethnic Fusion', 'Healing', 'Meditation', 'Spiritual'], 'Oldies', ['30s', '40s', '50s', '60s', '70s', '80s', '90s'], 'Pop', ['Adult Contemporary', 'Barbershop', 'Bubblegum Pop', 'Dance Pop', 'JPOP', 'Soft Rock', 'Teen Pop', 'Top 40', 'World Pop'], 'R&B/Urban', ['Classic R&B', 'Contemporary R&B', 'Doo Wop', 'Funk', 'Motown', 'Neo-Soul', 'Quiet Storm', 'Soul', 'Urban Contemporary'], 'Reggae', ['Contemporary Reggae', 'Dancehall', 'Dub', 'Pop-Reggae', 'Ragga', 'Reggaeton', 'Rock Steady', 'Roots Reggae', 'Ska'], 'Rock', ['Adult Album Alternative', 'British Invasion', 'Classic Rock', 'Garage Rock', 'Glam', 'Hard Rock', 'Jam Bands', 'Prog/Art Rock', 'Psychedelic', 'Rock & Roll', 'Rockabilly', 'Singer/Songwriter', 'Surf'], 'Seasonal/Holiday', ['Anniversary', 'Birthday', 'Christmas', 'Halloween', 'Hanukkah', 'Honeymoon', 'Valentine', 'Wedding'], 'Soundtracks', ['Anime', "Children's/Family", 'Original Score', 'Showtunes'], 'Talk', ['Comedy', 'Community', 'Educational', 'Government', 'News', 'Old Time Radio', 'Other Talk', 'Political', 'Scanner', 'Spoken Word', 'Sports']]
        current = ""
        default = "Pop"
        empty = None
        
        # redefine
        streams = {}
        

        def __init__(self, parent=None):
        
            # override datamap fields  //@todo: might need a cleaner method, also: do we really want the stream data in channels to be different/incompatible?
            self.datamap = copy.deepcopy(self.datamap)
            self.datamap[5][0] = "Rating"
            self.datamap[5][2][0] = "rating"
            self.datamap[3][0] = "Description"
            self.datamap[3][2][0] = "description"
            
            # superclass
            ChannelPlugin.__init__(self, parent)


        # read category thread from /listen/browse.live
        def update_categories(self):
            self.categories = []

            # fetch page
            html = http.get("http://www.live365.com/index.live", feedback=self.parent.status);
            rx_genre = re.compile("""
                href='/genres/([\w\d%+]+)'[^>]*>
                (   (?:<nobr>)?   )
                ( \w[-\w\ /'.&]+ )
                (   (?:</a>)?   )
            """, re.X|re.S)

            # collect
            last = []
            for uu in rx_genre.findall(html):
                (link, sub, title, main) = uu

                # main
                if main and not sub:
                    self.categories.append(title)
                    self.categories.append(last)
                    last = []
                # subcat
                else:
                    last.append(title)

            # don't forget last entries
            self.categories.append(last)



        # extract stream infos
        def update_streams(self, cat, search=""):
        
            # search / url
            if (not search):
                url = "http://www.live365.com/cgi-bin/directory.cgi?genre=" + self.cat2tag(cat) + "&rows=200"  #+"&first=1"
            else:
                url = "http://www.live365.com/cgi-bin/directory.cgi?site=..&searchdesc=" + urllib.quote(search) + "&searchgenre=" + self.cat2tag(cat) + "&x=0&y=0"
            html = http.get(url, feedback=self.parent.status)
            # we only need to download one page, because live365 always only gives 200 results

            # terse format            
            rx = re.compile(r"""
            ['"]Launch\((\d+).*?
            ['"](OK|PM_ONLY|SUBSCRIPTION).*?
            href=['"](http://www.live365.com/stations/\w+)['"].*?
            page['"]>([^<>]*)</a>.*?
            CLASS="genre"[^>]*>(.+?)</TD>.+?
            =["']audioQuality.+?>\w+\s+(\d+)\w<.+?
            >DrawListenerStars\((\d+),.+?
            >DrawRatingStars\((\d+),\s+(\d+),.*?
            ["']station_id=(\d+).+?
            class=["']?desc-link[^>]+>([^<>]*)<
                """, re.X|re.I|re.S|re.M)
#            src="(http://www.live365.com/.+?/stationlogo\w+.jpg)".+?

            # append entries to result list
            __print__( html )
            ls = []
            for row in rx.findall(html):
                __print__( row )
                points = int(row[7])
                count = int(row[8])
                ls.append({
                    "launch_id": row[0],
                    "sofo": row[1],  # subscribe-or-fuck-off status flags
                    "state":  (""  if  row[1]=="OK"  else  gtk.STOCK_STOP),
                    "homepage": entity_decode(row[2]),
                    "title": entity_decode(row[3]),
                    "genre": self.strip_tags(row[4]),
                    "bitrate": int(row[5]),
                    "listeners": int(row[6]),
                    "max": 0,
                    "rating": (points + count**0.4) / (count - 0.001*(count-0.1)),   # prevents division by null, and slightly weights (more votes are higher scored than single votes)
                    "rating_points": points,
                    "rating_count": count,
                    # id for URL:
                    "station_id": row[9],
                    "url": self.base_url + "play/" + row[9],
                    "description": entity_decode(row[10]),
                   #"playing": row[10],
                   # "deleted": row[0] != "OK",
                })
            return ls
            
        # faster if we do it in _update() prematurely
        #def prepare(self, ls):
        #    GenericChannel.prepare(ls)
        #    for row in ls:
        #        if (not row["state"]):
        #            row["state"] = (gtk.STOCK_STOP, "") [row["sofo"]=="OK"]
        #    return ls

        
        # html helpers
        def cat2tag(self, cat):
            return urllib.quote(cat.lower()) #re.sub("[^a-z]", "", 
        def strip_tags(self, s):
            return re.sub("<.+?>", "", s)


Added channels/modarchive.py version [ac3a95273a].



































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129

# api: streamtuner2
# title: modarchive browser
#
#
# Just a genre browser.
#
# MOD files dodn't work with all audio players. And with the default
# download method, it'll receive a .zip archive with embeded .mod file.
# VLC in */* seems to work fine however.
#


import re
import http
from config import conf
from channels import *
from channels import __print__
from xml.sax.saxutils import unescape














# MODs
class modarchive (ChannelPlugin):

    # description
    title = "modarchive"
    module = "modarchive"
    homepage = "http://www.modarchive.org/"
    version = 0.1
    base = "http://modarchive.org/"

    # keeps category titles->urls    
    catmap = {}
    categories = []
 
    
    

    # refresh category list
    def update_categories(self):

        html = http.get("http://modarchive.org/index.php?request=view_genres")

        rx_current = re.compile(r"""
            >\s+(\w[^<>]+)\s+</h1>  |
            <a\s[^>]+query=(\d+)&[^>]+>(\w[^<]+)</a>
        """, re.S|re.X)


        #-- archived shows
        sub = []
        self.categories = []
        for uu in rx_current.findall(html):
            (main, id, subname) = uu
            if main:
                if sub:
                    self.categories.append(sub)
                sub = []
                self.categories.append(main)
            else:
                sub.append(subname)
                self.catmap[subname] = id
        #
        
        #-- keep catmap as cache-file, it's essential for redisplaying        
        self.save()
        return


    # saves .streams and .catmap
    def save(self):
        ChannelPlugin.save(self)
        conf.save("cache/catmap_" + self.module, self.catmap)


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


    # download links from dmoz listing
    def update_streams(self, cat, force=0):

        url = "http://modarchive.org/index.php?query="+self.catmap[cat]+"&request=search&search_type=genre"
        html = http.get(url)
        entries = []
        
        rx_mod = re.compile("""
            href="(http://modarchive.org/data/downloads.php[?]moduleid=(\d+)[#][^"]+)"
            .*?    /formats/(\w+).png"
            .*?    title="([^">]+)">([^<>]+)</a>
            .*?    >Rated</a>\s*(\d+)
        """, re.X|re.S)
        
        for uu in rx_mod.findall(html):
            (url, id, fmt, title, file, rating) = uu
            __print__( uu )
            entries.append({
                "genre": cat,
                "url": url,
                "id": id,
                "format": self.mime_fmt(fmt) + "+zip",
                "title": title,
                "playing": file,
                "listeners": int(rating),
                "homepage": "http://modarchive.org/index.php?request=view_by_moduleid&query="+id,
            })
        
        # done    
        return entries
        

Added channels/musicgoal.py version [1d593a3fc6].













































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
#
# api: streamtuner2
# title: MUSICGOAL channel
# description: musicgoal.com/.de combines radio and podcast listings
# version: 0.1
# status: experimental
# pre-config: <const name="api"/>
#
# Musicgoal.com is a radio and podcast directory. This plugin tries to use
# the new API for accessing listing data.
#
#



# st2 modules
from config import conf
from mygtk import mygtk
import http
from channels import *

# python modules
import re
import json



          
# I wonder what that is for                                             ---------------------------------------
class musicgoal (ChannelPlugin):

        # desc
        module = "musicgoal"
        title = "MUSICGOAL"
        version = 0.1
        homepage = "http://www.musicgoal.com/"
        base_url = homepage
        listformat = "url/direct"

        # settings
        config = [
        ]
        api_podcast = "http://www.musicgoal.com/api/?todo=export&todo2=%s&cat=%s&format=json&id=1000259223&user=streamtuner&pass=tralilala"
        api_radio = "http://www.musicgoal.com/api/?todo=playlist&format=json&genre=%s&id=1000259223&user=streamtuner&pass=tralilala"

        # categories are hardcoded
        podcast = ["Arts", "Management", "Recreation", "Knowledge", "Nutrition", "Books", "Movies & TV", "Music", "News", "Business", "Poetry", "Politic", "Radio", "Science", "Science Fiction", "Religion", "Sport", "Technic", "Travel", "Health", "New"]
        radio = ["Top radios", "Newcomer", "Alternative", "House", "Jazz", "Classic", "Metal", "Oldies", "Pop", "Rock", "Techno", "Country", "Funk", "Hip hop", "R&B", "Reggae", "Soul", "Indian", "Top40", "60s", "70s", "80s", "90s", "Sport", "Various", "Radio", "Party", "Christmas", "Firewall", "Auto DJ", "Audio-aacp", "Audio-ogg", "Video", "MyTop", "New", "World", "Full"]
        categories = ["podcasts/", podcast, "radios/", radio]
        #catmap = {"podcast": dict((i+1,v) for enumerate(self.podcast)), "radio": dict((i+1,v) for enumerate(self.radio))}
        


        # nop
        def update_categories(self):
            pass


        # request json API
        def update_streams(self, cat, search=""):

            # category type: podcast or radio
            if cat in self.podcast:
                grp = "podcast"
                url = self.api_podcast % (grp, self.podcast.index(cat)+1)
            elif cat in self.radio:
                grp = "radio"
                url = self.api_radio % cat.lower().replace(" ","").replace("&","")
            else:
                return []
                
            # retrieve API data
            data = http.ajax(url, None)
            data = json.loads(data)
                
            # tranform datasets            
            if grp == "podcast":
                return [{
                    "genre": cat,
                    "title": row["titel"],
                    "homepage": row["url"],
                    "playing": str(row["typ"]),
                    #"id": row["id"],
                    #"listeners": int(row["2"]),
                    #"listformat": "text/html",
                    "url": "",
                } for row in data]
            else:
                return [{
                    "format": self.mime_fmt(row["ctype"]),
                    "genre": row["genre"] or cat,
                    "url": "http://%s:%s/%s" % (row["host"], row["port"], row["pfad"]),
                    "listformat": "url/direct",
                    "id": row["id"],
                    "title": row["name"],
                    "playing": row["song"],
                    "homepage": row.get("homepage") or row.get("url"),
                } for row in data]




Added channels/myoggradio.py version [37210dae1e].



















































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
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
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
#
# api: streamtuner2
# title: MyOggRadio channel plugin
# description: open source internet radio directory MyOggRadio
# version: 0.5
# config:
#    <var name="myoggradio_login" type="text" value="user:password" description="login account for myoggradio service" />
# priority: standard
# category: channel
# depends: json, StringIO
#
# MyOggRadio is an open source radio station directory. Because this matches
# well with streamtuner2, there's now a project partnership. Shared streams can easily
# be downloaded in this channel plugin. And streamtuner2 users can easily share their
# favourite stations into the MyOggRadio directory.
#
# Beforehand an account needs to be configured in the settings. (Registration
# on myoggradio doesn't require an email address or personal information.)
#



from channels import *
from config import conf
from action import action

import re
import json
from StringIO import StringIO
import copy



# open source radio sharing stie
class myoggradio(ChannelPlugin):

    # description
    title = "MyOggRadio"
    module = "myoggradio"
    homepage = "http://www.myoggradio.org/"
    api = "http://ehm.homelinux.org/MyOggRadio/"
    version = 0.5
    listformat = "url/direct"
    
    # config data
    config = [
        {"name":"myoggradio_login", "type":"text", "value":"user:password", "description":"Account for storing personal favourites."},
        {"name":"myoggradio_morph", "type":"boolean", "value":0, "description":"Convert pls/m3u into direct shoutcast url."},
    ]
    
    # hide unused columns
    titles = dict(playing=False, listeners=False, bitrate=False)
    

    # category map
    categories = ['common', 'personal']
    default = 'common'
    current = 'common'
    
    
    
    # prepare GUI
    def __init__(self, parent):
        ChannelPlugin.__init__(self, parent)
        if parent:
            mygtk.add_menu(parent.extensions, "Share in MyOggRadio", self.share)



    # this is simple, there are no categories
    def update_categories(self):
        pass



    # download links from dmoz listing
    def update_streams(self, cat, force=0):

        # result list
        entries = []
        
        # common
        if (cat == "common"):
            # fetch
            data = http.get(self.api + "common.json")
            entries = json.load(StringIO(data))
            
        # bookmarks
        elif (cat == "personal") and self.user_pw():
            data = http.get(self.api + "favoriten.json?user=" + self.user_pw()[0])
            entries = json.load(StringIO(data))
        
        # unknown
        else:
            self.parent.status("Unknown category")
            pass

        # augment result list
        for i,e in enumerate(entries):
            entries[i]["homepage"] = self.api + "c_common_details.jsp?url="  + e["url"]
            entries[i]["genre"] = cat
        # send back
        return entries
        
        
    
    # upload a single station entry to MyOggRadio
    def share(self, *w):
    
        # get data
        row = self.parent.row()
        if row:
            row = copy.copy(row)
            
            # convert PLS/M3U link to direct ICY stream url
            if conf.myoggradio_morph and self.parent.channel().listformat != "url/direct":
                row["url"] = http.fix_url(action.srv(row["url"]))
                
            # prevent double check-ins
            if row["title"] in (r.get("title") for r in self.streams["common"]):
                pass
            elif row["url"] in (r.get("url") for r in self.streams["common"]):
                pass

            # send
            else:
                self.parent.status("Sharing station URL...")
                self.upload(row)
                sleep(0.5) # artificial slowdown, else user will assume it didn't work
            
        # tell Gtk we've handled the situation
        self.parent.status("Shared '" + row["title"][:30] + "' on MyOggRadio.org")
        return True


    # upload bookmarks
    def send_bookmarks(self, entries=[]):
    
        for e in (entries if entries else parent.bookmarks.streams["favourite"]):
            self.upload(e)
        

    # send row to MyOggRadio
    def upload(self, e, form=0):
        if e:
            login = self.user_pw()
            submit = {
                "user": login[0],    	  # api
                "passwort": login[1], 	  # api
                "url": e["url"],
                "bemerkung": e["title"],
                "genre": e["genre"],
                "typ": e["format"][6:],
                "eintragen": "eintragen", # form
            }

            # just push data in, like the form does
            if form:
                self.login()
                http.ajax(self.api + "c_neu.jsp", submit)

            # use JSON interface
            else:
                http.ajax(self.api + "commonadd.json?" + urllib.urlencode(submit))
    
            
    # authenticate against MyOggRadio
    def login(self):
        login = self.user_pw()    
        if login:
            data = dict(zip(["benutzer", "passwort"], login))
            http.ajax(self.api + "c_login.jsp", data)
            # let's hope the JSESSIONID cookie is kept


    # returns login (user,pw)
    def user_pw(self):
        if conf.myoggradio_login != "user:password":
            return conf.myoggradio_login.split(":")
        else: pass
        




Added channels/punkcast.py version [ba986159c3].













































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102

# api: streamtuner2
# title: punkcast listing
#
#
# Disables itself per default.
# ST1 looked prettier with random images within.
#


import re
import http
from config import conf
import action
from channels import *
from channels import __print__





# disable plugin per default
if "punkcast" not in vars(conf): 
    conf.plugins["punkcast"] = 0









# basic.ch broadcast archive
class punkcast (ChannelPlugin):

    # description
    title = "punkcast"
    module = "punkcast"
    homepage = "http://www.punkcast.com/"
    version = 0.1

    # keeps category titles->urls    
    catmap = {}
    categories = ["list"]
    default = "list"
    current = "list"
 


    # don't do anything
    def update_categories(self):
        pass


    # get list
    def update_streams(self, cat, force=0):

        rx_link = re.compile("""
            <a\shref="(http://punkcast.com/(\d+)/index.html)">
            \s+<img[^>]+ALT="([^<">]+)"
        """, re.S|re.X)

        entries = []
 
        #-- all from frontpage
        for uu in rx_link.findall(http.get(self.homepage)):
            (homepage, id, title) = uu
            entries.append({
                    "genre": "?",
                    "title": title,
                    "playing": "PUNKCAST #"+id,
                    "format": "audio/mp3",
                    "homepage": homepage,
            })

        # done    
        return entries


    # special handler for play
    def play(self, row):
    
        rx_sound = re.compile("""(http://[^"<>]+[.](mp3|ogg|m3u|pls|ram))""")
        html = http.get(row["homepage"])
        
        # look up ANY audio url
        for uu in rx_sound.findall(html):
            __print__( uu )
            (url, fmt) = uu
            action.action.play(url, self.mime_fmt(fmt), "url/direct")
            return
        
        # or just open webpage
        action.action.browser(row["homepage"])
            
        





Added channels/shoutcast.py version [8bd878f25d].





















































































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
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
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
#
# api: streamtuner2
# title: shoutcast
# description: Channel/tab for Shoutcast.com directory
# depends: pq, re, http
# version: 1.2
# author: Mario
# original: Jean-Yves Lefort
#
# Shoutcast is a server software for audio streaming. It automatically spools
# station information on shoutcast.com, which this plugin can read out. But
# since the website format is often changing, we now use PyQuery HTML parsing
# in favour of regular expression (which still work, are faster, but not as
# reliable).
#
# This was previously a built-in channel plugin. It just recently was converted
# from a glade predefined GenericChannel into a ChannelPlugin.
#
#
# NOTES
#
# Just found out what Tunapie uses:
#    http://www.shoutcast.com/sbin/newxml.phtml?genre=Top500
# It's a simpler list format, no need to parse HTML. However, it also lacks
# homepage links. But maybe useful as alternate fallback...
# Also:
#   http://www.shoutcast.com/sbin/newtvlister.phtml?alltv=1
#   http://www.shoutcast.com/sbin/newxml.phtml?search=
#
#
#


import http
import urllib
import re
from pq import pq
from config import conf
#from channels import *    # works everywhere but in this plugin(???!)
import channels
__print__ = channels.__print__



# SHOUTcast data module                                          ----------------------------------------
class shoutcast(channels.ChannelPlugin):

        # desc
        api = "streamtuner2"
        module = "shoutcast"
        title = "SHOUTcast"
        version = 1.2
        homepage = "http://www.shoutcast.com/" 
        base_url = "http://shoutcast.com/"
        listformat = "audio/x-scpls"

        # settings
        config = [
            dict(name="pyquery", type="boolean", value=0, description="Use more reliable PyQuery HTML parsing\ninstead of faster regular expressions."),
            dict(name="debug", type="boolean", value=0, description="enable debug output"),
        ]
        
        # categories
        categories = ['Alternative', ['Adult Alternative', 'Britpop', 'Classic Alternative', 'College', 'Dancepunk', 'Dream Pop', 'Emo', 'Goth', 'Grunge', 'Hardcore', 'Indie Pop', 'Indie Rock', 'Industrial', 'Modern Rock', 'New Wave', 'Noise Pop', 'Power Pop', 'Punk', 'Ska', 'Xtreme'], 'Blues', ['Acoustic Blues', 'Chicago Blues', 'Contemporary Blues', 'Country Blues', 'Delta Blues', 'Electric Blues'], 'Classical', ['Baroque', 'Chamber', 'Choral', 'Classical Period', 'Early Classical', 'Impressionist', 'Modern', 'Opera', 'Piano', 'Romantic', 'Symphony'], 'Country', ['Americana', 'Bluegrass', 'Classic Country', 'Contemporary Bluegrass', 'Contemporary Country', 'Honky Tonk', 'Hot Country Hits', 'Western'], 'Decades', ['30s', '40s', '50s', '60s', '70s', '80s', '90s'], 'Easy Listening', ['Exotica', 'Light Rock', 'Lounge', 'Orchestral Pop', 'Polka', 'Space Age Pop'], 'Electronic', ['Acid House', 'Ambient', 'Big Beat', 'Breakbeat', 'Dance', 'Demo', 'Disco', 'Downtempo', 'Drum and Bass', 'Electro', 'Garage', 'Hard House', 'House', 'IDM', 'Jungle', 'Progressive', 'Techno', 'Trance', 'Tribal', 'Trip Hop'], 'Folk', ['Alternative Folk', 'Contemporary Folk', 'Folk Rock', 'New Acoustic', 'Traditional Folk', 'World Folk'], 'Inspirational', ['Christian', 'Christian Metal', 'Christian Rap', 'Christian Rock', 'Classic Christian', 'Contemporary Gospel', 'Gospel', 'Southern Gospel', 'Traditional Gospel'], 'International', ['African', 'Arabic', 'Asian', 'Bollywood', 'Brazilian', 'Caribbean', 'Celtic', 'Chinese', 'European', 'Filipino', 'French', 'Greek', 'Hindi', 'Indian', 'Japanese', 'Jewish', 'Klezmer', 'Korean', 'Mediterranean', 'Middle Eastern', 'North American', 'Russian', 'Soca', 'South American', 'Tamil', 'Worldbeat', 'Zouk'], 'Jazz', ['Acid Jazz', 'Avant Garde', 'Big Band', 'Bop', 'Classic Jazz', 'Cool Jazz', 'Fusion', 'Hard Bop', 'Latin Jazz', 'Smooth Jazz', 'Swing', 'Vocal Jazz', 'World Fusion'], 'Latin', ['Bachata', 'Banda', 'Bossa Nova', 'Cumbia', 'Latin Dance', 'Latin Pop', 'Latin Rock', 'Mariachi', 'Merengue', 'Ranchera', 'Reggaeton', 'Regional Mexican', 'Salsa', 'Tango', 'Tejano', 'Tropicalia'], 'Metal', ['Black Metal', 'Classic Metal', 'Extreme Metal', 'Grindcore', 'Hair Metal', 'Heavy Metal', 'Metalcore', 'Power Metal', 'Progressive Metal', 'Rap Metal'], 'Misc', [], 'New Age', ['Environmental', 'Ethnic Fusion', 'Healing', 'Meditation', 'Spiritual'], 'Pop', ['Adult Contemporary', 'Barbershop', 'Bubblegum Pop', 'Dance Pop', 'Idols', 'JPOP', 'Oldies', 'Soft Rock', 'Teen Pop', 'Top 40', 'World Pop'], 'Public Radio', ['College', 'News', 'Sports', 'Talk'], 'Rap', ['Alternative Rap', 'Dirty South', 'East Coast Rap', 'Freestyle', 'Gangsta Rap', 'Hip Hop', 'Mixtapes', 'Old School', 'Turntablism', 'West Coast Rap'], 'Reggae', ['Contemporary Reggae', 'Dancehall', 'Dub', 'Ragga', 'Reggae Roots', 'Rock Steady'], 'Rock', ['Adult Album Alternative', 'British Invasion', 'Classic Rock', 'Garage Rock', 'Glam', 'Hard Rock', 'Jam Bands', 'Piano Rock', 'Prog Rock', 'Psychedelic', 'Rockabilly', 'Surf'], 'Soundtracks', ['Anime', 'Kids', 'Original Score', 'Showtunes', 'Video Game Music'], 'Talk', ['BlogTalk', 'Comedy', 'Community', 'Educational', 'Government', 'News', 'Old Time Radio', 'Other Talk', 'Political', 'Scanner', 'Spoken Word', 'Sports', 'Technology'], 'Themes', ['Adult', 'Best Of', 'Chill', 'Eclectic', 'Experimental', 'Female', 'Heartache', 'Instrumental', 'LGBT', 'Party Mix', 'Patriotic', 'Rainy Day Mix', 'Reality', 'Sexy', 'Shuffle', 'Travel Mix', 'Tribute', 'Trippy', 'Work Mix']]
        #["default", [], 'TopTen', [], 'Alternative', ['College', 'Emo', 'Hardcore', 'Industrial', 'Punk', 'Ska'], 'Americana', ['Bluegrass', 'Blues', 'Cajun', 'Folk'], 'Classical', ['Contemporary', 'Opera', 'Symphonic'], 'Country', ['Bluegrass', 'New Country', 'Western Swing'], 'Electronic', ['Acid Jazz', 'Ambient', 'Breakbeat', 'Downtempo', 'Drum and Bass', 'House', 'Trance', 'Techno'], 'Hip Hop', ['Alternative', 'Hardcore', 'New School', 'Old School', 'Turntablism'], 'Jazz', ['Acid Jazz', 'Big Band', 'Classic', 'Latin', 'Smooth', 'Swing'], 'Pop/Rock', ['70s', '80s', 'Classic', 'Metal', 'Oldies', 'Pop', 'Rock', 'Top 40'], 'R&B/Soul', ['Classic', 'Contemporary', 'Funk', 'Smooth', 'Urban'], 'Spiritual', ['Alternative', 'Country', 'Gospel', 'Pop', 'Rock'], 'Spoken', ['Comedy', 'Spoken Word', 'Talk'], 'World', ['African', 'Asian', 'European', 'Latin', 'Middle Eastern', 'Reggae'], 'Other/Mixed', ['Eclectic', 'Film', 'Instrumental']]
        current = ""
        default = "Alternative"
        empty = ""

        
        # redefine
        streams = {}
        
            
        # extracts the category list from shoutcast.com,
        # sub-categories are queried per 'AJAX'
        def update_categories(self):
            html = http.get(self.base_url)
            self.categories = ["default"]
            __print__( html )

            # <h2>Radio Genres</h2>
	    rx_main = re.compile(r'<li class="prigen" id="(\d+)".+?<a href="/radio/([\w\s]+)">[\w\s]+</a></li>', re.S)
	    rx_sub = re.compile(r'<a href="/radio/([\w\s\d]+)">[\w\s\d]+</a></li>')
            for uu in rx_main.findall(html):
                __print__(uu)
		(id,name) = uu
                name = urllib.unquote(name)

                # main category
                self.categories.append(name)

                # sub entries
                html = http.ajax("http://shoutcast.com/genre.jsp", {"genre":name, "id":id})
                __print__(html)
                sub = rx_sub.findall(html)
                self.categories.append(sub)

            # it's done
            __print__(self.categories)
            conf.save("cache/categories_shoutcast", self.categories)
            pass



        #def strip_tags(self, s):
        #    rx = re.compile(""">(\w+)<""")
        #    return " ".join(rx.findall(s))

        # downloads stream list from shoutcast for given category
        def update_streams(self, cat, search=""):

            if (not cat or cat == self.empty):
                __print__("nocat")
                return []
            ucat = urllib.quote(cat)

            
            # new extraction regex
            if not conf.get("pyquery") or not pq:
              rx_stream = re.compile("""
                 <a\s+class="?playbutton\d?[^>]+id="(\d+)".+?
		 <a\s+class="[\w\s]*title[\w\s]*"[^>]+href="(http://[^">]+)"[^>]*>([^<>]+)</a>.+?
                 (?:Recently\s*played|Coming\s*soon|Now\s*playing):\s*([^<]*).+?
                 ners">(\d*)<.+?
                 bitrate">(\d*)<.+?
                 type">([MP3AAC]*)
                 """, re.S|re.I|re.X)
            rx_next = re.compile("""onclick="showMoreGenre""")



            # loop
            entries = []
            next = 0
            max = int(conf.max_streams)
            count = max
            while (next < max):

               # page
               url = "http://www.shoutcast.com/genre-ajax/" + ucat
	       referer = url.replace("/genre-ajax", "/radio")
	       params = { "strIndex":"0", "count":str(count), "ajax":"true", "mode":"listeners", "order":"desc" }
               html = http.ajax(url, params, referer)   #,feedback=self.parent.status)

               __print__(html)

               # regular expressions
               if not conf.get("pyquery") or not pq:
               
                   # extract entries
                   self.parent.status("parsing document...")
                   __print__("loop-rx")
                   for uu in rx_stream.findall(html):
                       (id, homepage, title, playing, ls, bit, fmt) = uu
                       __print__(uu)
                       entries += [{
                           "title": self.entity_decode(title),
                           "url": "http://yp.shoutcast.com/sbin/tunein-station.pls?id=" + id,
                           "homepage": http.fix_url(homepage),
                           "playing": self.entity_decode(playing),
                           "genre": cat, #self.strip_tags(uu[4]),
                           "listeners": int(ls),
                           "max": 0, #int(uu[6]),
                           "bitrate": int(bit),
                           "format": self.mime_fmt(fmt),
                       }]

               # PyQuery parsing
               else:
                   # iterate over DOM
                   for div in (pq(e) for e in pq(html).find("div.dirlist")):

                       entries.append({
                            "title": div.find("a.clickabletitleGenre, div.stationcol a").attr("title"),
                            "url": div.find("a.playbutton, a.playbutton1, a.playimage").attr("href"),
                            "homepage": http.fix_url(div.find("a.playbutton.clickabletitle, a[target=_blank], a.clickabletitleGenre, a.clickabletitle, div.stationcol a, a").attr("href")),
                            "playing": div.find("div.playingtextGenre, div.playingtext").attr("title"),
                            "listeners": int(div.find("div.dirlistners").text()),
                            "bitrate": int(div.find("div.dirbitrate").text()),
                            "format": self.mime_fmt(div.find("div.dirtype").text()),
                            "max": 0,
                            "genre": cat,
                           # "title2": e.find("a.playbutton").attr("name"),
                       })


               # display partial results (not strictly needed anymore, because we fetch just one page)
               self.parent.status()
               self.update_streams_partially_done(entries)
               
               # more pages to load?
               if (re.search(rx_next, html)):
                  next += count
               else:
                  next = 99999
            
            #fin
            __print__(entries)
            return entries


Added channels/timer.py version [c3eb5a36c2].







































































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
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
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
#
# api: streamtuner2
# title: radio scheduler
# description: time play/record events for radio stations
# depends: kronos
# version: 0.5
# config:
# category: features
# priority: optional
# support: unsupported
#
# Okay, while programming this, I missed the broadcast I wanted to hear. Again(!)
# But still this is a useful extension, as it allows recording and playing specific
# stations at a programmed time and interval. It accepts a natural language time
# string when registering a stream. (Via streams menu > extension > add timer)
#
# Programmed events are visible in "timer" under the "bookmarks" channel. Times
# are stored in the description field, and can thus be edited. However, after editing
# times manuall, streamtuner2 must be restarted for the changes to take effect.
#


from channels import *
import kronos
from mygtk import mygtk
from action import action
import copy



# timed events (play/record) within bookmarks tab
class timer:

    # plugin info
    module = "timer"
    title = "Timer"
    version = 0.5
    
    
    # configuration settings
    config = [
    ]
    timefield = "playing"
    
    
    # kronos scheduler list
    sched = None
    
    
    
    
    # prepare gui
    def __init__(self, parent):
      if parent:
          
        # keep reference to main window
        self.parent = parent
        self.bookmarks = parent.bookmarks
        
        # add menu
        mygtk.add_menu(self.parent.extensions, "Add timer for station", self.edit_timer)
        
        # target channel
        if not self.bookmarks.streams.get("timer"):
            self.bookmarks.streams["timer"] = [{"title":"--- timer events ---"}]
        self.bookmarks.add_category("timer")
        self.streams = self.bookmarks.streams["timer"]
        
        # widgets
        parent.signal_autoconnect({
            "timer_ok": self.add_timer,
            "timer_cancel": lambda w,*a: self.parent.timer_dialog.hide() or 1,
        })
        
        # prepare spool
        self.sched = kronos.ThreadedScheduler()
        for row in self.streams:
            try: self.queue(row)
            except Exception,e: print("queuing error", e)
        self.sched.start()


    # display GUI for setting timespec
    def edit_timer(self, *w):
        self.parent.timer_dialog.show()
        self.parent.timer_value.set_text("Fri,Sat 20:00-21:00 play")

    # close dialog,get data
    def add_timer(self, *w):
        self.parent.timer_dialog.hide()
        row = self.parent.row()
        row = copy.copy(row)
        
        # add data
        row["listformat"] = "url/direct" #self.parent.channel().listformat
        if row.get(self.timefield):
            row["title"] = row["title"] + " -- " + row[self.timefield]
        row[self.timefield] = self.parent.timer_value.get_text()
        
        # store
        self.save_timer(row)
    
    
    # store row in timer database
    def save_timer(self, row):
        self.streams.append(row)
        self.bookmarks.save()
        self.queue(row)
        pass
        
        
    # add event to list
    def queue(self, row):
    
        # chk
        if not row.get(self.timefield) or not row.get("url"):
            #print("NO TIME DATA", row)
            return
    
        # extract timing parameters
        _ = row[self.timefield]
        days = self.days(_)
        time = self.time(_)
        duration = self.duration(_)
        
        # which action
        if row[self.timefield].find("rec")>=0:
            activity, action_method = "record", self.record
        else:
            activity, action_method = "play", self.play
        
        # add
        task = self.sched.add_daytime_task(action_method, activity, days, None, time, kronos.method.threaded, [row], {})

        #__print__( "queue",  act, self.sched, (action_method, act, days, None, time, kronos.method.threaded, [row], {}), task.get_schedule_time(True) )
    
    
    
    # converts Mon,Tue,... into numberics 1-7
    def days(self, s):
        weekdays = ["su", "mo", "tu", "we", "th", "fr", "sa", "su"]
        r = []
        for day in re.findall("\w\w+", s.lower()):
            day = day[0:2]
            if day in weekdays:
                r.append(weekdays.index(day))
        return list(set(r))
        
    # get start time 18:00
    def time(self, s):
        r = re.search("(\d+):(\d+)", s)
        return int(r.group(1)), int(r.group(2))
        
    # convert "18:00-19:15" to minutes
    def duration(self, s):
        try:
            r = re.search("(\d+:\d+)\s*(\.\.+|-+)\s*(\d+:\d+)", s)
            start = self.time(r.group(1))
            end = self.time(r.group(3))
            duration = (end[0] - start[0]) * 60 + (end[1] - start[1])
            return int(duration) # in minutes
        except:
            return 0   # no limit
        
    # action wrapper
    def play(self, row, *args, **kwargs):
        action.play(
            url = row["url"],
            audioformat = row.get("format","audio/mp3"), 
            listformat = row.get("listformat","url/direct"),
        )

    # action wrapper
    def record(self, row, *args, **kwargs):
        #print("TIMED RECORD")
        
        # extra params
        duration = self.duration(row.get(self.timefield))
        if duration:
            append = " -a %S.%d.%q -l "+str(duration*60)   # make streamripper record a whole broadcast
        else:
            append = ""

        # start recording
        action.record(
            url = row["url"],
            audioformat = row.get("format","audio/mp3"), 
            listformat = row.get("listformat","url/direct"),
            append = append,
        )
    
    def test(self, row, *args, **kwargs):
        print("TEST KRONOS", row)


Added channels/tv.py version [e09d4b20ac].























































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
#
# api: streamtuner2
# title: shoutcast TV
# description: TV listings from shoutcast
# version: 0.0
# stolen-from: Tunapie.sf.net
#
# As seen in Tunapie, there are still TV listings on Shoutcast. This module
# adds a separate tab for them. Streamtuner2 is centrally and foremost about
# radio listings, so this plugin will remain one of the few exceptions.
#
#   http://www.shoutcast.com/sbin/newtvlister.phtml?alltv=1
#
# Pasing with lxml is dead simple in this case, so we use etree directly
# instead of PyQuery. Like with the Xiph plugin, downloaded streams are simply
# stored in .streams["all"] pseudo-category.
#
# icon: http://cemagraphics.deviantart.com/art/Little-Tv-Icon-96461135

from channels import *
import http
import lxml.etree




# TV listings from shoutcast.com
class tv(ChannelPlugin):

	# desc
	api = "streamtuner2"
	module = "tv"
	title = "TV"
	version = 0.1
	homepage = "http://www.shoutcast.com/" 
	base_url = "http://www.shoutcast.com/sbin/newtvlister.phtml?alltv=1"
	play_url = "http://yp.shoutcast.com/sbin/tunein-station.pls?id="
	listformat = "audio/x-scpls"  # video streams are NSV linked in PLS format

        # settings
        config = [
        ]
        
        # categories
        categories = ["all", "video"]
        current = ""
        default = "all"
        empty = ""

        # redefine
        streams = {}


        # get complete list
        def all(self):
            r = []

            # retrieve
            xml = http.get(self.base_url)
            
            # split up <station> entries
            for station in lxml.etree.fromstring(xml):

                r.append({
                    "title": station.get("name"),
                    "playing": station.get("ct"),
                    "id": station.get("id"),
                    "url": self.play_url + station.get("id"),
                    "format": "video/nsv",
                    "time": station.get("rt"),
                    "extra": station.get("load"),
                    "genre": station.get("genre"),
                    "bitrate": int(station.get("br")),
                    "listeners": int(station.get("lc")),
                })
                              
            return r
         
            
        # genre switch
        def load(self, cat, force=False):
            if force or not self.streams.get("all"):
                self.streams["all"] = self.all()
            ChannelPlugin.load(self, cat, force)
        
            
        # update from the list
        def update_categories(self):

            # update it always here:  #if not self.streams.get("all"):
            self.streams["all"] = self.all()

            # enumerate categories
            c = {"all":100000}
            for row in self.streams["all"]:
                for genre in row["genre"].split(" "):
                    if len(genre)>2 and row["bitrate"]>=200:
                        c[genre] = c.get(genre, 0) + 1
            # append
            self.categories = sorted(c, key=c.get, reverse=True)



        # extract from big list
        def update_streams(self, cat, search=""):

            # autoload only if "all" category is missing
            if not self.streams.get("all"):
                self.streams["all"] = self.all()
    
            # return complete list as-is            
            if cat == "all":
                return self.streams[cat]
                
            # search for category
            else:
                return [row for row in self.streams["all"] if row["genre"].find(cat)>=0]






Added channels/xiph.py version [8ad5a64170].































































































































































































































































































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
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
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
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
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
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
#
# api: streamtuner2
# title: Xiph.org
# description: Xiph/ICEcast radio directory
# version: 0.1
#
#
# Xiph.org maintains the Ogg streaming standard and Vorbis audio compression
# format, amongst others. The ICEcast server is an alternative to SHOUTcast.
# But it turns out, that Xiph lists only MP3 streams, no OGG. And the directory
# is less encompassing than Shoutcast.
#
#
#
#



# streamtuner2 modules
from config import conf
from mygtk import mygtk
import http
from channels import *
from channels import __print__

# python modules
import re
from xml.sax.saxutils import unescape as entity_decode, escape as xmlentities
import xml.dom.minidom



          
# I wonder what that is for                                             ---------------------------------------
class xiph (ChannelPlugin):

        # desc
        api = "streamtuner2"
        module = "xiph"
        title = "Xiph.org"
        version = 0.1
        homepage = "http://dir.xiph.org/"
        base_url = "http://dir.xiph.org/"
        yp = "yp.xml"
        listformat = "url/http"
        config = [
           {"name":"xiph_min_bitrate", "value":64, "type":"int", "description":"minimum bitrate, filter anything below", "category":"filter"}
        ]

        # content
        categories = ["all", [],           ]
        current = ""
        default = "all"
        empty = None
        
        
        # prepare category names
        def __init__(self, parent=None):
            
            self.categories = ["all"]
            self.filter = {}
            for main in self.genres:
                if (type(main) == str):
                    id = main.split("|")
                    self.categories.append(id[0].title())
                    self.filter[id[0]] = main
                else:
                    l = []
                    for sub in main:
                        id = sub.split("|")
                        l.append(id[0].title())
                        self.filter[id[0]] = sub
                    self.categories.append(l)
            
            # GUI
            ChannelPlugin.__init__(self, parent)


        # just counts genre tokens, does not automatically create a category tree from it
        def update_categories(self):
            g = {}
            for row in self.streams["all"]:
                for t in row["genre"].split():
                    if g.has_key(t):
                        g[t] += 1
                    else:
                        g[t] = 0
            g = [ [v[1],v[0]] for v in g.items() ]
            g.sort()
            g.reverse()
            for row in g:
                pass
                __print__( '        "' + row[1] + '", #' + str(row[0]) )


        # xml dom node shortcut to text content
        def x(self, entry, name):
            e = entry.getElementsByTagName(name)
            if (e):
                if (e[0].childNodes):
                    return e[0].childNodes[0].data
                    
        # convert bitrate string to integer
        # (also convert "Quality \d+" to pseudo bitrate)
        def bitrate(self, s):
            uu = re.findall("(\d+)", s)
            if uu:
                br = uu[0]
                if br > 10:
                    return int(br)
                else:
                    return int(br * 25.6)
            else:
                return 0

        # downloads stream list from shoutcast for given category
        def update_streams(self, cat, search=""):

            # there is actually just a single category to download,
            # all else are virtual
            if (cat == "all"):
            
                #-- get data
                yp = http.get(self.base_url + self.yp, 1<<22, feedback=self.parent.status)
                
                #-- extract
                l = []
                for entry in xml.dom.minidom.parseString(yp).getElementsByTagName("entry"):
                    bitrate = self.bitrate(self.x(entry, "bitrate"))
                    if conf.xiph_min_bitrate and bitrate and bitrate >= int(conf.xiph_min_bitrate):
                      l.append({
                        "title": str(self.x(entry, "server_name")),
                        "url": str(self.x(entry, "listen_url")),
                        "format": self.mime_fmt(str(self.x(entry, "server_type"))[6:]),
                        "bitrate": bitrate,
                        "channels": str(self.x(entry, "channels")),
                        "samplerate": str(self.x(entry, "samplerate")),
                        "genre": str(self.x(entry, "genre")),
                        "playing": str(self.x(entry, "current_song")),
                        "listeners": 0,
                        "max": 0,              # this information is in the html view, but not in the yp.xml (seems pretty static, we might as well make it a built-in list)
                        "homepage": "",
                      })
                
            # filter out a single subtree
            else:
                rx = re.compile(self.filter.get(cat.lower(), cat.lower()))
                l = []
                for i,row in enumerate(self.streams["all"]):
                    if rx.search(row["genre"]):
                        l.append(row)
            
            # send back the list 
            return l




        genres = [
            "scanner", #442
            "rock", #305
            [
              "metal|heavy", #36
            ],
            "various", #286
            [
              "mixed", #96
            ],
            "pop", #221
            [
              "top40|top|40|top 40", #32
              "charts|hits", #20+4
              "80s", #68
              "90s", #20
              "disco", #17
              "remixes", #10
            ],
            "electronic|electro", #33
            [
              "dance", #161
              "house", #106
              "trance", #82
              "techno", #72
              "chillout", #16
              "lounge", #12
            ],
            "alternative", #68
            [
              "college", #32
              "student", #20
              "progressive", #20
            ],
            "classical|classic", #58+20
            "live", #57
            "jazz", #42
            [
              "blues", #19
            ],
            "talk|spoken|speak", #41
            [
              "news", #39
              "public", #12
              "info", #5
            ],
            "world|international", #25
            [
              "latin", #34
              "reggae", #12
              "indie", #12
              "folk", #9
              "schlager", #14
              "jungle", #13
              "country", #7
              "russian", #6
            ],
            "hip hop|hip|hop", #34
            [
               "oldschool", #10
               "rap",
            ],
            "ambient", #34
            "adult", #33
           ## "music", #32
            "oldies", #31
            [
              "60s", #2
              "70s", #17
            ],
            "religious", #4
            [
              "christian|bible", #14
            ],
            "rnb|r&b", #12
            [
              "soul", #11
              "funk", #24
              "urban", #11
            ],
            "other", #25
            [
              "deep", #14
              "soft", #12
              "minimal", #12
              "eclectic", #12
              "drum", #12
              "bass", #12
              "experimental", #11
              "hard", #10
              "funky", #10
              "downtempo", #10
              "slow", #9
              "break", #9
              "electronica", #8
              "dub", #8
              "retro", #7
              "punk", #7
              "psychedelic", #7
              "goa", #7
              "freeform", #7
              "c64", #7
              "breaks", #7
              "anime", #7
              "variety", #6
              "psytrance", #6
              "island", #6
              "downbeat", #6
              "underground", #5
              "newage", #5
              "gothic", #5
              "dnb", #5
              "club", #5
              "acid", #5
              "video", #4
              "trip", #4
              "pure", #4
              "industrial", #4
              "groove", #4
              "gospel", #4
              "gadanie", #4
              "french", #4
              "dark", #4
              "chill", #4
              "age", #4
              "wave", #3
              "vocal", #3
              "tech", #3
              "studio", #3
              "relax", #3
              "rave", #3
              "hardcore", #3
              "breakbeat", #3
              "avantgarde", #3
              "swing", #2
              "soundtrack", #2
              "salsa", #2
              "italian", #2
              "independant", #2
              "groovy", #2
              "european", #2
              "darkwave", #2
            ],
        ]

Added cli.py version [a682821821].



























































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
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
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
#
# api: streamtuner2
# title: CLI interface
# description: allows to call streamtuner2 from the commandline
# status: experimental
# version: 0.3
#
#  Returns JSON data when queried. Usually returns cache data, but the
#  "category" module always loads fresh info from the directory servers.
#
#  Not all channel plugins are gtk-free yet. And some workarounds are
#  used here to not upset channel plugins about a missing parent window.
#
#
#


import sys
#from channels import *
import http
import action
from config import conf
import json




# CLI
class StreamTunerCLI (object):


    # plugin info
    title = "CLI interface"
    version = 0.3

    
    # channel plugins
    channel_modules = ["shoutcast", "xiph", "internet_radio_org_uk", "jamendo", "myoggradio", "live365"]
    current_channel = "cli"
    plugins = {} # only populated sparsely by .stream()
    
    
    # start
    def __init__(self):

        # fake init    
        action.action.main = empty_parent()
        action.action.main.current_channel = self.current_channel

        # check if enough arguments, else  help
        if len(sys.argv)<3:
            a = self.help
        # first cmdline arg == action
        else:
            command = sys.argv[1]
            a = self.__getattribute__(command)

        # run
        result = a(*sys.argv[2:])
        if result:
            self.json(result)
        
        
    # show help
    def help(self, *args): 
        print("""
syntax:  streamtuner2 action [channel] "stream title"

    from cache:
          streamtuner2 stream shoutcast frequence
          streamtuner2 dump xiph
          streamtuner2 play "..."
          streamtuner2 url "..."
    load fresh:
          streamtuner2 category shoutcast "Top 40"
          streamtuner2 categories xiph
        """)
        
    # prints stream data from cache
    def stream(self, *args):
 
        # optional channel name, title
        if len(args) > 1:
            (channel_list, title) = args
            channel_list = channel_list.split(",")
        else:
            title = list(args).pop()
            channel_list = self.channel_modules
        
        # walk through channel plugins, categories, rows
        title = title.lower()
        for channel in channel_list:
            self.current_channel = channel
            c = self.channel(channel)
            self.plugins[channel] = c
            c.cache()
            for cat in c.streams:
                for row in c.streams[cat]:
                    if row and row.get("title","").lower().find(title)>=0:
                        return(row)
    
    # just get url
    def url(self, *args):
        row = self.stream(*args)
        if row.get("url"):
            print(row["url"])
            
    # run player
    def play(self, *args):
        row = self.stream(*args)
        if row.get("url"):
            #action.action.play(row["url"], audioformat=row.get("format","audio/mp3"))
            self.plugins[self.current_channel].play(row)
            
    # return cache data 1:1
    def dump(self, channel):
        c = self.channel(channel)
        c.cache()
        return c.streams
        
        
    # load from server
    def category(self, module, cat):
        c = self.channel(module)
        r = c.update_streams(cat)
        [c.postprocess(row) for row in r]
        return r

    # load from server
    def categories(self, module):
        c = self.channel(module)
        c.cache()
        r = c.update_categories()
        if not r:
            r = c.categories
        if c.__dict__.get("empty"):
            del r[0]
        return r
        
        
    # load module
    def channel(self, module):
        plugin = __import__("channels."+module, None, None, [""])
        plugin_class = plugin.__dict__[module]
        p = plugin_class(None)
        p.parent = empty_parent()
        return p
    
    # load all channel modules    
    def channels(self, channels=None):
        if channels:
            channels = channels.split(",")
        else:
            channels = self.channel_modules
        return (self.channel(module) for module in channels)
    
    # pretty print json
    def json(self, dat):    
        print(json.dumps(dat, sort_keys=True, indent=2))
    
    
    
    
    
# trap for some main window calls
class empty_parent (object):    
    channel = {}
    null = lambda *a: None
    status = null
    thread = null


    

Added config.py version [791971aff7].







































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
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
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
#
# encoding: UTF-8
# api: streamtuner2
# type: class
# title: global config object
# description: reads ~/.config/streamtuner/*.json files
#
# In the main application or module files which need access
# to a global conf object, just import this module as follows:
#
#   from config import conf
#
# Here conf is already an instantiation of the underlying
# Config class.
#


import os
import sys
import pson
import gzip


#-- create a single instance of config object
conf = object()



#-- global configuration data               ---------------------------------------------
class ConfigDict(dict):


        # start
        def __init__(self):
        
            # object==dict means conf.var is conf["var"]
            self.__dict__ = self  # let's pray this won't leak memory due to recursion issues

            # prepare
            self.defaults()
            self.xdg()
            
            # runtime
            dirs = ["/usr/share/streamtuner2", "/usr/local/share/streamtuner2", sys.path[0], "."]
            self.share = [d for d in dirs if os.path.exists(d)][0]
            
            # settings from last session
            last = self.load("settings")
            if (last):
                self.update(last)
            # store defaults in file
            else:
                self.save("settings")
                self.firstrun = 1

        # some defaults
        def defaults(self):
            self.browser = "sensible-browser"
            self.play = {
               "audio/mp3": "audacious ",	# %u for url to .pls, %g for downloaded .m3u
               "audio/ogg": "audacious ",
               "audio/aac": "amarok -l ",
               "audio/x-pn-realaudio": "vlc ",
               "audio/*": "totem ",
               "*/*": "vlc %srv",
            }
            self.record = {
               "*/*": "x-terminal-emulator -e streamripper %srv",
                    #  x-terminal-emulator -e streamripper %srv -d /home/***USERNAME***/Musik
            }
            self.plugins = {
                "bookmarks": 1,  # built-in plugins, cannot be disabled
                "shoutcast": 1,
                "punkcast": 0,   # disable per default
            }
            self.tmp = os.environ.get("TEMP", "/tmp")
            self.max_streams = "120"
            self.show_bookmarks = 1
            self.show_favicons = 1
            self.load_favicon = 1
            self.heuristic_bookmark_update = 1
            self.retain_deleted = 1
            self.auto_save_appstate = 1
            self.theme = "" #"MountainDew"
            self.debug = False
            self.channel_order = "shoutcast, xiph, internet_radio_org_uk, jamendo, myoggradio, .."
            self.reuse_m3u = 1
            self.google_homepage = 1

            
        # each plugin has a .config dict list, we add defaults here
        def add_plugin_defaults(self, config, module=""):
        
            # options
            for opt in config:
                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] = 1

        
            
        # http://standards.freedesktop.org/basedir-spec/basedir-spec-0.6.html
        def xdg(self):
            home = os.environ.get("HOME", self.tmp)
            config = os.environ.get("XDG_CONFIG_HOME", home+"/.config")
            
            # storage dir
            self.dir = config + "/" + "streamtuner2"
            
            # create if necessary
            if (not os.path.exists(self.dir)):
                os.makedirs(self.dir)
           

        # store some configuration list/dict into a file                
        def save(self, name="settings", data=None, gz=0, nice=0):
            name = name + ".json"
            if (data == None):
                data = dict(self.__dict__)  # ANOTHER WORKAROUND: typecast to plain dict(), else json filter_data sees it as object and str()s it
                nice = 1
            # check for subdir
            if (name.find("/") > 0):
                subdir = name[0:name.find("/")]
                subdir = self.dir + "/" + subdir
                if (not os.path.exists(subdir)):
                    os.mkdir(subdir)
                    open(subdir+"/.nobackup", "w").close()
            # write                        
            file = self.dir + "/" + name
            # .gz or normal file
            if gz:
                f = gzip.open(file+".gz", "w")
                if os.path.exists(file):
                    os.unlink(file)
            else:
                f = open(file, "w")
            # encode
            pson.dump(data, f, indent=(4 if nice else None))
            f.close()


        # retrieve data from config file            
        def load(self, name):
            name = name + ".json"
            file = self.dir + "/" + name
            try:
                # .gz or normal file
                if os.path.exists(file + ".gz"):
                    f = gzip.open(file + ".gz", "r")
                elif os.path.exists(file):
                    f = open(file, "r")
                else:
                    return # file not found
                # decode
                r = pson.load(f)
                f.close()
                return r
            except (Exception), e:
                print("PSON parsing error (in "+name+")", e)
            

        # recursive dict update
        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()


Added favicon.py version [f5d4a162a1].

































































































































































































































































































































































































































































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
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
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
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
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
280
281
282
283
284
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
#
# encoding: utf-8
# api: python
# title: favicon download
# description: retrieves favicons for station homepages, plus utility code for display preparation
# config:
#    <var name="always_google" value="1" description="always use google favicon to png conversion service" />
#    <var name="only_google" value="1" description="don't try other favicon retrieval methods, if google service fails" />
#    <var name="delete_google_stub" value="1" description="delete placeholder favicons" />
# type: module
#
#
#  This module fetches favicon.ico files and prepares .png images for each domain
#  in the stations list. Homepage URLs are used for this.
#
#  Files end up in:
#     /home/user/.config/streamtuner2/icons/www.example.org.png
#
#  Currently relies on Google conversion service, because urllib+PIL conversion
#  method is still flaky, and a bit slower. Future version might use imagemagick.
#


always_google = 1      # use favicon service for speed
only_google = 1        # if that fails, try our other/slower methods?
delete_google_stub = 1   # don't keep placeholder images
google_placeholder_filesizes = (726,896)


import os, os.path
import urllib
import re
import urlparse
from config import conf
try: from processing import Process as Thread
except: from threading import Thread
import http



# ensure that we don't try to download a single favicon twice per session,
# if it's not available the first time, we won't get it after switching stations back and forth
tried_urls = []




# walk through entries
def download_all(entries):
  t = Thread(target= download_thread, args= ([entries]))
  t.start()
def download_thread(entries):
    for e in entries:
        # try just once
        if e.get("homepage") in tried_urls:
            pass
        # retrieve specific img url as favicon
        elif e.get("img"):
            pass
        # favicon from homepage URL
        elif e.get("homepage"):
            download(e["homepage"])
        # remember
        tried_urls.append(e.get("homepage"))
    pass

# download a single favicon for currently playing station
def download_playing(row):
    if conf.google_homepage and not row.get("homepage"):
        google_find_homepage(row)
    if conf.load_favicon and row.get("homepage"):
        download_all([row])
    pass



#--- unrelated ---
def google_find_homepage(row):
    """ Searches for missing homepage URL via Google. """
    if row.get("url") not in tried_urls:
        tried_urls.append(row.get("url"))

        rx_t = re.compile('^(([^-:]+.?){1,2})')
        rx_u = re.compile('"(http://[^"]+)" class=l')

        # extract first title parts
        title = rx_t.search(row["title"])
        if title:
            title = title.group(0).replace(" ", "%20")
            
            # do a google search
            html = http.ajax("http://www.google.de/search?hl=de&q="+title, None)
            
            # find first URL hit
            url = rx_u.search(html)
            if url:
                row["homepage"] = http.fix_url(url.group(1))
    pass
#-----------------



# extract domain name
def domain(url):
    if url.startswith("http://"):
        return url[7:url.find("/", 8)]  # we assume our URLs are fixed already (http://example.org/ WITH trailing slash!)
    else:
        return "null"

# local filename
def name(url):
    return domain(url) + ".png"
  
# local filename
def file(url):
    icon_dir = conf.dir + "/icons"
    if not os.path.exists(icon_dir):
        os.mkdir(icon_dir)
        open(icon_dir+"/.nobackup", "w").close()
    return icon_dir + "/" + name(url)

# does the favicon exist
def available(url):
    return os.path.exists(file(url))




# download favicon for given URL
def download(url):

  # skip if .png for domain already exists
  if available(url):
    return


  # fastest method, so default to google for now
  if always_google:
      google_ico2png(url)
      if available(url) or only_google:
         return

  try:    # look for /favicon.ico first
    #print("favicon.ico")
    direct_download("http://"+domain(url)+"/favicon.ico", file(url))

  except:
    try:    # extract facicon filename from website <link rel>
      #print("html <rel favicon>")
      html_download(url)

    except:    # fallback
      #print("google ico2png")
      google_ico2png(url)




# retrieve PNG via Google ico2png
def google_ico2png(url):

  #try:
     GOOGLE = "http://www.google.com/s2/favicons?domain="
     (fn, headers) = urllib.urlretrieve(GOOGLE+domain(url), file(url))

     # test for stub image
     if delete_google_stub and (filesize(fn) in google_placeholder_filesizes):
        os.remove(fn)

  
def filesize(fn):
   return os.stat(fn).st_size



# mime magic
def filetype(fn):
   f = open(fn, "rb")
   bin = f.read(4)
   f.close()
   if bin[1:3] == "PNG":
      return "image/png"
   else:
      return "*/*"



# favicon.ico
def direct_download(favicon, fn):

#  try: 
    # URL download
    r = urllib.urlopen(favicon)
    headers = r.info()
    
    # abort on
    if r.getcode() >= 300:
       raise "HTTP error", r.getcode()
    if not headers["Content-Type"].lower().find("image/"):
       raise "can't use text/* content"
       
    # save file
    fn_tmp = fn+".tmp"
    f = open(fn_tmp, "wb")
    f.write(r.read(32768))
    f.close()
        
    # check type
    if headers["Content-Type"].lower()=="image/png" and favicon.find(".png") and filetype(fn)=="image/png":
       pngresize(fn_tmp)
       os.mv(fn_tmp, fn)
    else:
       ico2png(fn_tmp, fn)
       os.remove(fn_tmp)

 # except:
  #  "File not found" and False


  
# peek at URL, download favicon.ico <link rel>
def html_download(url):


  # <link rel>
  #try:
    # download html, look for @href in <link rel=shortcut icon>
    r = urllib.urlopen(url)
    html = r.read(4096)
    r.close()
    rx = re.compile("""<link[^<>]+rel\s*=\s*"?\s*(?:shortcut\s+|fav)?icon[^<>]+href=["'](?P<href>[^<>"']+)["'<>\s].""")
    favicon = "".join(rx.findall(html))
    
    # url or
    if favicon.startswith("http://"):
       None
    # just /pathname
    else:
       favicon = urlparse.urljoin(url, favicon)
       #favicon = "http://" + domain(url) + "/" + favicon

    # download
    direct_download(favicon, file(url))






#
# title: workaround for PIL.Image to preserve the transparency for .ico import
#
# http://stackoverflow.com/questions/987916/how-to-determine-the-transparent-color-index-of-ico-image-with-pil
# http://djangosnippets.org/snippets/1287/
#
# Author: dc
# Posted: January 17, 2009
# Languag: Python
# Django Version: 1.0
# Tags: pil image ico 
# Score: 2 (after 2 ratings)
#
         
import operator
import struct

try:
    from PIL import BmpImagePlugin, PngImagePlugin, Image
except Exception, e:
    print("no PIL", e)
    always_google = 1
    only_google = 1


def load_icon(file, index=None):
    '''
    Load Windows ICO image.

    See http://en.wikipedia.org/w/index.php?oldid=264332061 for file format
    description.
    '''
    if isinstance(file, basestring):
        file = open(file, 'rb')

    try:
        header = struct.unpack('<3H', file.read(6))
    except:
        raise IOError('Not an ICO file')

    # Check magic
    if header[:2] != (0, 1):
        raise IOError('Not an ICO file')

    # Collect icon directories
    directories = []
    for i in xrange(header[2]):
        directory = list(struct.unpack('<4B2H2I', file.read(16)))
        for j in xrange(3):
            if not directory[j]:
                directory[j] = 256

        directories.append(directory)

    if index is None:
        # Select best icon
        directory = max(directories, key=operator.itemgetter(slice(0, 3)))
    else:
        directory = directories[index]

    # Seek to the bitmap data
    file.seek(directory[7])

    prefix = file.read(16)
    file.seek(-16, 1)

    if PngImagePlugin._accept(prefix):
        # Windows Vista icon with PNG inside
        image = PngImagePlugin.PngImageFile(file)
    else:
        # Load XOR bitmap
        image = BmpImagePlugin.DibImageFile(file)
        if image.mode == 'RGBA':
            # Windows XP 32-bit color depth icon without AND bitmap
            pass
        else:
            # Patch up the bitmap height
            image.size = image.size[0], image.size[1] >> 1
            d, e, o, a = image.tile[0]
            image.tile[0] = d, (0, 0) + image.size, o, a

            # Calculate AND bitmap dimensions. See
            # http://en.wikipedia.org/w/index.php?oldid=264236948#Pixel_storage
            # for description
            offset = o + a[1] * image.size[1]
            stride = ((image.size[0] + 31) >> 5) << 2
            size = stride * image.size[1]

            # Load AND bitmap
            file.seek(offset)
            string = file.read(size)
            mask = Image.fromstring('1', image.size, string, 'raw',
                                    ('1;I', stride, -1))

            image = image.convert('RGBA')
            image.putalpha(mask)

    return image




# convert .ico file to .png format
def ico2png(ico, png_fn):
  #print("ico2png", ico, png, image)
  
  try:  # .ico
    image = load_icon(ico, None)
  except:  # automatic img file type guessing
    image = Image.open(ico)
       
  # resize
  if image.size[0] > 16:
    image.resize((16, 16), Image.ANTIALIAS)

  # .png format
  image.save(png_fn, "PNG", quality=98)


# resize an image
def pngresize(fn, x=16, y=16):
  image = Image.open(fn)
  if image.size[0] > x:
    image.resize((x, y), Image.ANTIALIAS)
    image.save(fn, "PNG", quality=98)




#-- test
if __name__ == "__main__":
    import sys
    download(sys.argv[1])


Added http.py version [6932b3bfa3].



























































































































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
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
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
215
216
217
218
219
220
221
#
# encoding: UTF-8
# api: streamtuner2
# type: functions
# title: http download / methods
# description: http utility
# version: 1.3
#
#  Provides a http GET method with gtk.statusbar() callback.
#  And a function to add trailings slashes on http URLs.
#
#  The latter code is pretty much unreadable. But let's put the
#  blame on urllib2, the most braindamaged code in the Python
#  standard library.
#
            

import urllib2
from urllib import urlencode
import config
from channels import __print__



#-- url download                            ---------------------------------------------



#-- chains to progress meter and status bar in main window
feedback = None

# sets either text or percentage, so may take two parameters
def progress_feedback(*args):

  # use reset values if none given
  if not args:
     args = ["", 1.0]

  # send to main win
  if feedback:
    try: [feedback(d) for d in args]
    except: pass




#-- GET
def get(url, maxsize=1<<19, feedback="old"):
    __print__("GET", url)

    # statusbar info
    progress_feedback(url, 0.0)
    
    # read
    content = ""
    f = urllib2.urlopen(url)
    max = 222000  # mostly it's 200K, but we don't get any real information
    read_size = 1
    
    # multiple steps
    while (read_size and len(content) < maxsize):
    
        # partial read
        add = f.read(8192)
        content = content + add
        read_size = len(add)

        # set progress meter
        progress_feedback(float(len(content)) / float(max))

    # done
    
    # clean statusbar
    progress_feedback()
        
    # fin
    __print__(len(content))
    return content





#-- fix invalid URLs
def fix_url(url):
    if url is None:
        url = ""
    if len(url):
        # remove whitespace
        url = url.strip()
        # add scheme
        if (url.find("://") < 0):
            url = "http://" + url
        # add mandatory path
        if (url.find("/", 10) < 0):
            url = url + "/"
    return url




# default HTTP headers for AJAX/POST request
default_headers = {
    "User-Agent": "streamtuner2/0.4 (X11; U; Linux AMD64; en; rv:1.5.0.1) like WinAmp/2.1 but not like Googlebot/2.1", #"Mozilla/5.0 (X11; U; Linux x86_64; de; rv:1.9.2.6) Gecko/20100628 Ubuntu/10.04 (lucid) Firefox/3.6.6",
    "Accept": "*/*;q=0.5, audio/*, url/*",
    "Accept-Language": "en-US,en,de,es,fr,it,*;q=0.1",
    "Accept-Encoding": "gzip,deflate",
    "Accept-Charset": "ISO-8859-1,utf-8;q=0.7,*;q=0.1",
    "Keep-Alive": "115",
    "Connection": "keep-alive",
   #"Content-Length", "56",
   #"Cookie": "s_pers=%20s_getnr%3D1278607170446-Repeat%7C1341679170446%3B%20s_nrgvo%3DRepeat%7C1341679170447%3B; s_sess=%20s_cc%3Dtrue%3B%20s_sq%3Daolshtcst%252Caolsvc%253D%252526pid%25253Dsht%25252520%2525253A%25252520SHOUTcast%25252520Radio%25252520%2525257C%25252520Search%25252520Results%252526pidt%25253D1%252526oid%25253Dfunctiononclick%25252528event%25252529%2525257BshowMoreGenre%25252528%25252529%2525253B%2525257D%252526oidt%25253D2%252526ot%25253DDIV%3B; aolDemoChecked=1.849061",
    "Pragma": "no-cache",
    "Cache-Control": "no-cache",
}



# simulate ajax calls
def ajax(url, post, referer=""):
    
    # request
    headers = default_headers
    headers.update({
        "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
        "X-Requested-With": "XMLHttpRequest",
        "Referer": (referer if referer else url),
    })
    if type(post) == dict:
        post = urlencode(post)
    request = urllib2.Request(url, post, headers)
    
    # open url
    __print__( vars(request) )
    progress_feedback(url, 0.2)
    r = urllib2.urlopen(request)
    
    # get data
    __print__( r.info() )
    progress_feedback(0.5)
    data = r.read()
    progress_feedback()
    return data



# http://techknack.net/python-urllib2-handlers/    
from gzip import GzipFile
from StringIO import StringIO
class ContentEncodingProcessor(urllib2.BaseHandler):
  """A handler to add gzip capabilities to urllib2 requests """

  # add headers to requests
  def http_request(self, req):
    req.add_header("Accept-Encoding", "gzip, deflate")
    return req

  # decode
  def http_response(self, req, resp):
    old_resp = resp
    # gzip
    if resp.headers.get("content-encoding") == "gzip":
        gz = GzipFile(
                    fileobj=StringIO(resp.read()),
                    mode="r"
                  )
        resp = urllib2.addinfourl(gz, old_resp.headers, old_resp.url, old_resp.code)
        resp.msg = old_resp.msg
    # deflate
    if resp.headers.get("content-encoding") == "deflate":
        gz = StringIO( deflate(resp.read()) )
        resp = urllib2.addinfourl(gz, old_resp.headers, old_resp.url, old_resp.code)  # 'class to add info() and geturl() methods to an open file.'
        resp.msg = old_resp.msg
    return resp

# deflate support
import zlib
def deflate(data):   # zlib only provides the zlib compress format, not the deflate format;
  try:               # so on top of all there's this workaround:
    return zlib.decompress(data, -zlib.MAX_WBITS)
  except zlib.error:
    return zlib.decompress(data)







#-- init for later use
if urllib2:

    # config 1
    handlers = [None, None, None]
    
    # base
    handlers[0] = urllib2.HTTPHandler()
    if config.conf.debug:
        handlers[0].set_http_debuglevel(3)
        
    # content-encoding
    handlers[1] = ContentEncodingProcessor()
    
    # store cookies at runtime
    import cookielib
    cj = cookielib.CookieJar()
    handlers[2] = urllib2.HTTPCookieProcessor( cj )
    
    # inject into urllib2
    urllib2.install_opener( urllib2.build_opener(*handlers) )




# alternative function names
AJAX=ajax
POST=ajax
GET=get
URL=fix_url


Added kronos.py version [6ae12b7565].





































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
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
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
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
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
280
281
282
283
284
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
427
428
429
430
431
432
433
434
435
436
437
438
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
465
466
467
468
469
470
471
472
473
474
475
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
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
"""Module that provides a cron-like task scheduler.

This task scheduler is designed to be used from inside your own program.
You can schedule Python functions to be called at specific intervals or
days. It uses the standard 'sched' module for the actual task scheduling,
but provides much more:

* repeated tasks (at intervals, or on specific days)
* error handling (exceptions in tasks don't kill the scheduler)
* optional to run scheduler in its own thread or separate process
* optional to run a task in its own thread or separate process

If the threading module is available, you can use the various Threaded
variants of the scheduler and associated tasks. If threading is not
available, you could still use the forked variants. If fork is also
not available, all processing is done in a single process, sequentially.

There are three Scheduler classes:

    Scheduler    ThreadedScheduler    ForkedScheduler

You usually add new tasks to a scheduler using the add_interval_task or
add_daytime_task methods, with the appropriate processmethod argument
to select sequential, threaded or forked processing. NOTE: it is impossible
to add new tasks to a ForkedScheduler, after the scheduler has been started!
For more control you can use one of the following Task classes
and use schedule_task or schedule_task_abs:

    IntervalTask    ThreadedIntervalTask    ForkedIntervalTask
    SingleTask      ThreadedSingleTask      ForkedSingleTask 
    WeekdayTask     ThreadedWeekdayTask     ForkedWeekdayTask
    MonthdayTask    ThreadedMonthdayTask    ForkedMonthdayTask

Kronos is the Greek God of Time.

Kronos scheduler (c) Irmen de Jong.
This version has been extracted from the Turbogears source repository
and slightly changed to be completely stand-alone again. Also some fixes
have been made to make it work on Python 2.6 (sched module changes).
The version in Turbogears is based on the original stand-alone Kronos.
This is open-source software, released under the MIT Software License:
http://www.opensource.org/licenses/mit-license.php

"""

__version__="2.0"

__all__ = [
    "DayTaskRescheduler",
    "ForkedIntervalTask",
    "ForkedMonthdayTask",
    "ForkedScheduler",
    "ForkedSingleTask",
    "ForkedTaskMixin",
    "ForkedWeekdayTask",
    "IntervalTask",
    "MonthdayTask",
    "Scheduler",
    "SingleTask",
    "Task",
    "ThreadedIntervalTask",
    "ThreadedMonthdayTask",
    "ThreadedScheduler",
    "ThreadedSingleTask",
    "ThreadedTaskMixin",
    "ThreadedWeekdayTask",
    "WeekdayTask",
    "add_interval_task",
    "add_monthday_task",
    "add_single_task",
    "add_weekday_task",
    "cancel",
    "method",
]

import os
import sys
import sched
import time
import traceback
import weakref

class method:
    sequential="sequential"
    forked="forked"
    threaded="threaded"

class Scheduler:
    """The Scheduler itself."""

    def __init__(self):
        self.running=True
        self.sched = sched.scheduler(time.time, self.__delayfunc)

    def __delayfunc(self, delay):
        # This delay function is basically a time.sleep() that is
        # divided up, so that we can check the self.running flag while delaying.
        # there is an additional check in here to ensure that the top item of
        # the queue hasn't changed
        if delay<10:
            time.sleep(delay)
        else:
            toptime = self._getqueuetoptime()
            endtime = time.time() + delay
            period = 5
            stoptime = endtime - period
            while self.running and stoptime > time.time() and \
                self._getqueuetoptime() == toptime:
                time.sleep(period)
            if not self.running or self._getqueuetoptime() != toptime:
                return
            now = time.time()
            if endtime > now:
                time.sleep(endtime - now)

    def _acquire_lock(self):
        pass

    def _release_lock(self):
        pass

    def add_interval_task(self, action, taskname, initialdelay, interval,
            processmethod, args, kw):
        """Add a new Interval Task to the schedule.
        
        A very short initialdelay or one of zero cannot be honored, you will 
        see a slight delay before the task is first executed. This is because
        the scheduler needs to pick it up in its loop.

        """
        if initialdelay < 0 or interval < 1:
            raise ValueError("Delay or interval must be >0")
        # Select the correct IntervalTask class. Not all types may be available!
        if processmethod == method.sequential:
            TaskClass = IntervalTask
        elif processmethod == method.threaded:
            TaskClass = ThreadedIntervalTask
        elif processmethod == method.forked:
            TaskClass = ForkedIntervalTask
        else:
            raise ValueError("Invalid processmethod")
        if not args:
            args = []
        if not kw:
            kw = {}
        task = TaskClass(taskname, interval, action, args, kw)
        self.schedule_task(task, initialdelay)
        return task

    def add_single_task(self, action, taskname, initialdelay, processmethod, 
            args, kw):
        """Add a new task to the scheduler that will only be executed once."""
        if initialdelay < 0:
            raise ValueError("Delay must be >0")
        # Select the correct SingleTask class. Not all types may be available!
        if processmethod == method.sequential:
            TaskClass = SingleTask
        elif processmethod == method.threaded:
            TaskClass = ThreadedSingleTask
        elif processmethod == method.forked:
            TaskClass = ForkedSingleTask
        else:
            raise ValueError("Invalid processmethod")
        if not args:
            args = []
        if not kw:
            kw = {}
        task = TaskClass(taskname, action, args, kw)
        self.schedule_task(task, initialdelay)
        return task

    def add_daytime_task(self, action, taskname, weekdays, monthdays, timeonday, 
            processmethod, args, kw):
        """Add a new Day Task (Weekday or Monthday) to the schedule."""
        if weekdays and monthdays:
            raise ValueError("You can only specify weekdays or monthdays, "
                "not both")
        if not args:
            args = []
        if not kw:
            kw = {}
        if weekdays:
            # Select the correct WeekdayTask class.
            # Not all types may be available!
            if processmethod == method.sequential:
                TaskClass = WeekdayTask
            elif processmethod == method.threaded:
                TaskClass = ThreadedWeekdayTask
            elif processmethod == method.forked:
                TaskClass = ForkedWeekdayTask
            else:
                raise ValueError("Invalid processmethod")
            task=TaskClass(taskname, weekdays, timeonday, action, args, kw)
        if monthdays:
            # Select the correct MonthdayTask class.
            # Not all types may be available!
            if processmethod == method.sequential:
                TaskClass = MonthdayTask
            elif processmethod == method.threaded:
                TaskClass = ThreadedMonthdayTask
            elif processmethod == method.forked:
                TaskClass = ForkedMonthdayTask
            else:
                raise ValueError("Invalid processmethod")
            task=TaskClass(taskname, monthdays, timeonday, action, args, kw)
        firsttime=task.get_schedule_time(True)
        self.schedule_task_abs(task, firsttime)
        return task

    def schedule_task(self, task, delay):
        """Add a new task to the scheduler with the given delay (seconds).
        
        Low-level method for internal use.
        
        """
        if self.running:
            # lock the sched queue, if needed
            self._acquire_lock()
            try:
                task.event = self.sched.enter(delay, 0, task,
                            (weakref.ref(self),) )
            finally:
                self._release_lock()
        else:
            task.event = self.sched.enter(delay, 0, task,
                        (weakref.ref(self),) )

    def schedule_task_abs(self, task, abstime):
        """Add a new task to the scheduler for the given absolute time value.
        
        Low-level method for internal use.
        
        """
        if self.running:
            # lock the sched queue, if needed
            self._acquire_lock()
            try:
                task.event = self.sched.enterabs(abstime, 0, task,
                                    (weakref.ref(self),) )
            finally:
                self._release_lock()
        else:
            task.event = self.sched.enterabs(abstime, 0, task,
                                (weakref.ref(self),) )

    def start(self):
        """Start the scheduler."""
        self._run()

    def stop(self):
        """Remove all pending tasks and stop the Scheduler."""
        self.running = False
        self._clearschedqueue()

    def cancel(self, task):
        """Cancel given scheduled task."""
        self.sched.cancel(task.event)

    if sys.version_info>=(2,6):
        # code for sched module of python 2.6+
        def _getqueuetoptime(self):
            return self.sched._queue[0].time
        def _clearschedqueue(self):
            self.sched._queue[:] = []
    else:
        # code for sched module of python 2.5 and older
        def _getqueuetoptime(self):
            return self.sched.queue[0][0]
        def _clearschedqueue(self):
            self.sched.queue[:] = []                    

    def _run(self):
        # Low-level run method to do the actual scheduling loop.
        while self.running:
            try:
                self.sched.run()
            except Exception,x:
                print >>sys.stderr, "ERROR DURING SCHEDULER EXECUTION",x
                print >>sys.stderr, "".join(
                    traceback.format_exception(*sys.exc_info()))
                print >>sys.stderr, "-" * 20
            # queue is empty; sleep a short while before checking again
            if self.running:
                time.sleep(5)


class Task:
    """Abstract base class of all scheduler tasks"""

    def __init__(self, name, action, args, kw):
        """This is an abstract class!"""
        self.name=name
        self.action=action
        self.args=args
        self.kw=kw

    def __call__(self, schedulerref):
        """Execute the task action in the scheduler's thread."""
        try:
            self.execute()
        except Exception,x:
            self.handle_exception(x)
        self.reschedule(schedulerref())

    def reschedule(self, scheduler):
        """This method should be defined in one of the sub classes!"""
        raise NotImplementedError("You're using the abstract base class 'Task',"
            " use a concrete class instead")

    def execute(self):
        """Execute the actual task."""
        self.action(*self.args, **self.kw)

    def handle_exception(self, exc):
        """Handle any exception that occured during task execution."""
        print >>sys.stderr, "ERROR DURING TASK EXECUTION", exc
        print >>sys.stderr, "".join(traceback.format_exception(*sys.exc_info()))
        print >>sys.stderr, "-" * 20


class SingleTask(Task):
    """A task that only runs once."""

    def reschedule(self, scheduler):
        pass


class IntervalTask(Task):
    """A repeated task that occurs at certain intervals (in seconds)."""

    def __init__(self, name, interval, action, args=None, kw=None):
        Task.__init__(self, name, action, args, kw)
        self.interval = interval

    def reschedule(self, scheduler):
        """Reschedule this task according to its interval (in seconds)."""
        scheduler.schedule_task(self, self.interval)


class DayTaskRescheduler:
    """A mixin class that contains the reschedule logic for the DayTasks."""

    def __init__(self, timeonday):
        self.timeonday = timeonday

    def get_schedule_time(self, today):
        """Calculate the time value at which this task is to be scheduled."""
        now = list(time.localtime())
        if today:
            # schedule for today. let's see if that is still possible
            if (now[3], now[4]) >= self.timeonday:
                # too bad, it will be tomorrow
                now[2] += 1
        else:
            # tomorrow
            now[2] += 1
        # set new time on day (hour,minute)
        now[3], now[4] = self.timeonday
        # seconds
        now[5] = 0
        return time.mktime(now)

    def reschedule(self, scheduler):
        """Reschedule this task according to the daytime for the task.
        
        The task is scheduled for tomorrow, for the given daytime.

        """
        # (The execute method in the concrete Task classes will check
        # if the current day is a day on which the task must run).
        abstime = self.get_schedule_time(False)
        scheduler.schedule_task_abs(self, abstime)


class WeekdayTask(DayTaskRescheduler, Task):
    """A task that is called at specific days in a week (1-7), at a fixed time
    on the day.
    
    """

    def __init__(self, name, weekdays, timeonday, action, args=None, kw=None):
        if type(timeonday) not in (list, tuple) or len(timeonday) != 2:
            raise TypeError("timeonday must be a 2-tuple (hour,minute)")
        if type(weekdays) not in (list, tuple):
            raise TypeError("weekdays must be a sequence of weekday numbers "
                "1-7 (1 is Monday)")
        DayTaskRescheduler.__init__(self, timeonday)
        Task.__init__(self, name, action, args, kw)
        self.days = weekdays

    def execute(self):
        # This is called every day, at the correct time. We only need to
        # check if we should run this task today (this day of the week).
        weekday = time.localtime().tm_wday + 1
        if weekday in self.days:
            self.action(*self.args, **self.kw)


class MonthdayTask(DayTaskRescheduler, Task):
    """A task that is called at specific days in a month (1-31), at a fixed 
    time on the day.
    
    """

    def __init__(self, name, monthdays, timeonday, action, args=None, kw=None):
        if type(timeonday) not in (list, tuple) or len(timeonday) != 2:
            raise TypeError("timeonday must be a 2-tuple (hour,minute)")
        if type(monthdays) not in (list, tuple):
            raise TypeError("monthdays must be a sequence of monthdays numbers "
                "1-31")
        DayTaskRescheduler.__init__(self, timeonday)
        Task.__init__(self, name, action, args, kw)
        self.days = monthdays

    def execute(self):
        # This is called every day, at the correct time. We only need to
        # check if we should run this task today (this day of the month).
        if time.localtime().tm_mday in self.days:
            self.action(*self.args, **self.kw)


try:
    import threading

    class ThreadedScheduler(Scheduler):
        """A Scheduler that runs in its own thread."""

        def __init__(self):
            Scheduler.__init__(self)
            # we require a lock around the task queue
            self._lock = threading.Lock()

        def start(self):
            """Splice off a thread in which the scheduler will run."""
            self.thread = threading.Thread(target=self._run)
            self.thread.setDaemon(True)
            self.thread.start()
            
        def stop(self):
            """Stop the scheduler and wait for the thread to finish."""
            Scheduler.stop(self)
            try:
                self.thread.join()
            except AttributeError:
                pass

        def _acquire_lock(self):
            """Lock the thread's task queue."""
            self._lock.acquire()
            
        def _release_lock(self):
            """Release the lock on th ethread's task queue."""
            self._lock.release()


    class ThreadedTaskMixin:
        """A mixin class to make a Task execute in a separate thread."""

        def __call__(self, schedulerref):
            """Execute the task action in its own thread."""
            threading.Thread(target=self.threadedcall).start()
            self.reschedule(schedulerref())

        def threadedcall(self):
            # This method is run within its own thread, so we have to
            # do the execute() call and exception handling here.
            try:
                self.execute()
            except Exception,x:
                self.handle_exception(x)

    class ThreadedIntervalTask(ThreadedTaskMixin, IntervalTask):
        """Interval Task that executes in its own thread."""
        pass

    class ThreadedSingleTask(ThreadedTaskMixin, SingleTask):
        """Single Task that executes in its own thread.""" 
        pass

    class ThreadedWeekdayTask(ThreadedTaskMixin, WeekdayTask):
        """Weekday Task that executes in its own thread."""
        pass

    class ThreadedMonthdayTask(ThreadedTaskMixin, MonthdayTask):
        """Monthday Task that executes in its own thread."""
        pass

except ImportError:
    # threading is not available
    pass


if hasattr(os, "fork"):
    import signal

    class ForkedScheduler(Scheduler):
        """A Scheduler that runs in its own forked process."""

        def __del__(self):
            if hasattr(self, "childpid"):
                os.kill(self.childpid, signal.SIGKILL)

        def start(self):
            """Fork off a new process in which the scheduler will run."""
            pid = os.fork()
            if pid == 0:
                # we are the child
                signal.signal(signal.SIGUSR1, self.signalhandler)
                self._run()
                os._exit(0)
            else:
                # we are the parent
                self.childpid = pid
                # can no longer insert in the scheduler queue
                del self.sched

        def stop(self):
            """Stop the scheduler and wait for the process to finish."""
            os.kill(self.childpid, signal.SIGUSR1)
            os.waitpid(self.childpid, 0)

        def signalhandler(self, sig, stack):
            Scheduler.stop(self)


    class ForkedTaskMixin:
        """A mixin class to make a Task execute in a separate process."""

        def __call__(self, schedulerref):
            """Execute the task action in its own process."""
            pid = os.fork()
            if pid == 0:
                # we are the child
                try:
                    self.execute()
                except Exception,x:
                    self.handle_exception(x)
                os._exit(0)
            else:
                # we are the parent
                self.reschedule(schedulerref())


    class ForkedIntervalTask(ForkedTaskMixin, IntervalTask):
        """Interval Task that executes in its own process."""
        pass

    class ForkedSingleTask(ForkedTaskMixin, SingleTask):
        """Single Task that executes in its own process."""
        pass

    class ForkedWeekdayTask(ForkedTaskMixin, WeekdayTask):
        """Weekday Task that executes in its own process."""
        pass

    class ForkedMonthdayTask(ForkedTaskMixin, MonthdayTask):
        """Monthday Task that executes in its own process."""
        pass



if __name__=="__main__":
    def testaction(arg):
        print ">>>TASK",arg,"sleeping 3 seconds"
        time.sleep(3)
        print "<<<END_TASK",arg

    s=ThreadedScheduler()
    s.add_interval_task( testaction, "test action 1", 0, 4, method.threaded, ["task 1"], None )
    s.start()
    
    print "Scheduler started, waiting 15 sec...."
    time.sleep(15)
    
    print "STOP SCHEDULER"
    s.stop()
    
    print "EXITING"

Added mygtk.py version [8d9a2e155e].

























































































































































































































































































































































































































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
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
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
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
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
280
281
282
283
284
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
#
# 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



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


             
        #-- fill a treeview
        #
        # Adds treeviewcolumns/cellrenderers and liststore from a data dictionary.
        # Its datamap and the table contents can be supplied in one or two steps.
        # When new data gets applied, the columns aren't recreated.
        #
        # The columns are created according to the datamap, which describes cell
        # mapping and layout. Columns can have multiple cellrenderers, but usually
        # there is a direct mapping to a data source key from entries.
        #
        # datamap = [  #  title   width    dict-key    type,  renderer,  attrs  
        #               ["Name",   150,  ["titlerow",   str,    "text",    {} ]  ],
        #               [False,     0,   ["interndat",  int,     None,     {} ]  ],
        #               ["Desc",   200,  ["descriptn",  str,    "text",    {} ],  ["icon",str,"pixbuf",{}]  ],
        #
        # An according entries list then would contain a dictionary for each row:
        #   entries = [ {"titlerow":"first", "interndat":123}, {"titlerow":"..."}, ]
        # Keys not mentioned in the datamap get ignored, and defaults are applied
        # for missing cols. All values must already be in the correct type however.
        #
        @staticmethod
        def columns(widget, datamap=[], entries=[], pix_entry=False, typecast=0):

            # create treeviewcolumns?
            if (not widget.get_column(0)):
                # loop through titles
                datapos = 0
                for n_col,desc in enumerate(datamap):
                                    
                    # check for title
                    if (type(desc[0]) != str):
                        datapos += 1  # if there is none, this is just an undisplayed data column
                        continue
                    # new tvcolumn
                    col = gtk.TreeViewColumn(desc[0])  # title
                    col.set_resizable(True)
                    # width
                    if (desc[1] > 0):
                        col.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
                        col.set_fixed_width(desc[1])

                    # loop through cells
                    for var in xrange(2, len(desc)):
                        cell = desc[var]
                        # cell renderer
                        if (cell[2] == "pixbuf"):
                            rend = gtk.CellRendererPixbuf()  # img cell
                            if (cell[1] == str):
                                cell[3]["stock_id"] = datapos  # for stock icons
                                expand = False
                            else:
                                pix_entry = datapos
                                cell[3]["pixbuf"] = datapos
                        else:
                            rend = gtk.CellRendererText()    # text cell
                            cell[3]["text"] = datapos
                            #col.set_sort_column_id(datapos)  # only on textual cells
   
                        # attach cell to column
                        col.pack_end(rend, expand=cell[3].get("expand",True))
                        # apply attributes
                        for attr,val in cell[3].iteritems():
                            col.add_attribute(rend, attr, val)
                        # next
                        datapos += 1

                    # add column to treeview
                    widget.append_column(col)
                # finalize widget
                widget.set_search_column(5)   #??
                widget.set_search_column(4)   #??
                widget.set_search_column(3)   #??
                widget.set_search_column(2)   #??
                widget.set_search_column(1)   #??
                #widget.set_reorderable(True)
               
            # add data?
            if (entries):
                #- expand datamap            
                vartypes = []  #(str, str, bool, str, int, int, gtk.gdk.Pixbuf, str, int)
                rowmap = []    #["title", "desc", "bookmarked", "name", "count", "max", "img", ...]
                if (not rowmap):
                    for desc in datamap:
                        for var in xrange(2, len(desc)):
                            vartypes.append(desc[var][1])  # content types
                            rowmap.append(desc[var][0])    # dict{} column keys in entries[] list
                # create gtk array storage
                ls = gtk.ListStore(*vartypes)   # could be a TreeStore, too

                # 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(  [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
        #
        # needs a list of widgetnames
        # e.g. pickle.dump(mygtk.app_state(...), open(os.environ["HOME"]+"/.config/app_winstate", "w"))
        #
        @staticmethod
        def app_state(wTree, widgetnames=["window1", "treeview2", "vbox17"]):
            r = {} # restore array
            for wn in widgetnames:
                r[wn] = {}
                w = wTree.get_widget(wn)
                t = type(w)
#                print(wn, w, t)
                # extract different information from individual widget types
                if t == gtk.Window:
                    r[wn]["size"] = list(w.get_size())
                    #print("WINDOW SIZE", list(w.get_size()), r[wn])
                if t == gtk.Widget:
                    r[wn]["name"] = w.get_name()
                # gtk.TreeView
                if t == gtk.TreeView:
                    r[wn]["columns:width"] = []
                    for col in w.get_columns():
                        r[wn]["columns:width"].append( col.get_width() )
                    # - Rows
                    r[wn]["rows:expanded"] = []
                    for i in xrange(0,50):
                        if w.row_expanded(str(i)):
                            r[wn]["rows:expanded"].append(i)
                    # - selected
                    (model, paths) = w.get_selection().get_selected_rows()
                    if paths:
                        r[wn]["row:selected"] = paths[0]
                # gtk.Toolbar
                if t == gtk.Toolbar:
                    r[wn]["icon_size"] = int(w.get_icon_size())
                    r[wn]["style"] = int(w.get_style())
                # gtk.Notebook
                if t == gtk.Notebook:
                    r[wn]["page"] = w.get_current_page()
            #print(r)
            return r


        #-- restore window and widget properties
        #
        # requires only the previously saved widget state dict
        #
        @staticmethod
        def app_restore(wTree, r=None):
            for wn in r.keys():  # widgetnames
                w = wTree.get_widget(wn)
                if (not w):
                    continue
                t = type(w)
                for method,args in r[wn].iteritems():
                    # gtk.Window
                    if method == "size":
                        w.resize(args[0], args[1])
                    # gtk.TreeView
                    if method == "columns:width":
                        for i,col in enumerate(w.get_columns()):
                            if (i < len(args)):
                                col.set_fixed_width(args[i])
                    #  - Rows
                    if method == "rows:expanded":
                        w.collapse_all()
                        for i in args:
                            w.expand_row(str(i), False)                        
                    #  - selected
                    if method == "row:selected":
                        w.get_selection().select_path(tuple(args))
                    # gtk.Toolbar
                    if method == "icon_size":
                        w.set_icon_size(args)
                    if method == "style":
                        w.set_style(args)
                    # gtk.Notebook
                    if method == "page":
                        w.set_current_page(args)

            pass



        #-- Save-As dialog
        #
        @staticmethod
        def save_file(title="Save As", parent=None, fn="", formats=[("*","*")]):
            c = gtk.FileChooserDialog(title, parent, action=gtk.FILE_CHOOSER_ACTION_SAVE, buttons=(gtk.STOCK_CANCEL, 0, gtk.STOCK_SAVE, 1))
            # params
            if fn:
                c.set_current_name(fn)
                fn = ""
            for fname,ftype in formats:
                f = gtk.FileFilter()
                f.set_name(fname)
                f.add_pattern(ftype)
                c.add_filter(f)
            # display
            if c.run():
                fn = c.get_filename()  # return filaname
            c.destroy()
            return fn
        
        
        
        # pass updates from another thread, ensures that it is called just once
        @staticmethod
        def do(lambda_func):
            gobject.idle_add(lambda: lambda_func() and False)


        # adds background color to widget,
        # eventually wraps it into a gtk.Window, if it needs a container
        @staticmethod
        def bg(w, color="", where=["bg"]):
            """ this method should be called after widget creation, and before .add()ing it to container """
            if color:
                # wrap unstylable widgets into EventBox
                if not isinstance(w, gtk.Window):
                    wrap = gtk.EventBox()
                    wrap.add(w)
                    wrap.set_property("visible", True)
                    w = wrap
                # copy style object, modify settings
                s = w.get_style().copy()
                c = w.get_colormap().alloc_color(color)
                for state in (gtk.STATE_NORMAL, gtk.STATE_SELECTED):
                    s.bg[state] = c
                w.set_style(s)
                # probably redundant, but better safe than sorry:
                w.modify_bg(gtk.STATE_NORMAL, c)
            # return modified or wrapped widget
            return w


        @staticmethod
        def add_menu(menuwidget, label, action):
            m = gtk.MenuItem(label)
            m.connect("activate", action)
            m.show()
            menuwidget.add(m)
            

        # gtk.messagebox
        @staticmethod
        def msg(text, style=gtk.MESSAGE_INFO, buttons=gtk.BUTTONS_CLOSE):
            m = gtk.MessageDialog(None, 0, style, buttons, message_format=text)
            m.show()
            m.connect("response", lambda *w: m.destroy())
            


Added pq.py version [774d8a07cf].





























































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
#
# type: interface
# api: python
# title: PyQuery pq
# description: shortcut to PyQuery w/ extensions
#
#


import config



# load pyquery
try:

    from pyquery import PyQuery as pq

    # pq.each_pq = lambda self,func:  self.each(   lambda i,html: func( pq(html, parser="html") )   )


except Exception, e:

    # disable use
    pq = None
    config.conf.pyquery = False

    # error hint
    print("LXML is missing\n", e)
    print("\n")
    print("Please install the packages python-lxml and python-pyquery from your distributions software manager.\n")


    # let's invoke packagekit?
    """
    try:
         import packagekit.client
         pkc = packagekit.client.PackageKitClient()
         pkc.install_packages([pkc.search_name(n) for n in ["python-lxml", "python-pyquery"]])
         

    except:
        print("no LXML")
    """


Added pson.py version [1f7ebfaebc].

































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
#
# encoding: UTF-8
# api: python
# type: functions
# title: json emulation
# description: simplify usage of some gtk widgets
# version: 1.7
# author: mario
# license: public domain
#
#
#  This module provides the JSON api. If the python 2.6 module
#  isn't available, it provides an emulation using str() and
#  eval() and Python notation. (The representations are close.)
#
#  Additionally it filters out any left-over objects. Sometimes
#  pygtk-objects crawled into the streams[] lists, because rows
#  might have been queried from the widgets.
#


#-- reading and writing json (for the config module)  ----------------------------------


# try to load the system module first
try:
        from json import dump as json_dump, load as json_load
except:
        print("no native Python JSON module")


#except:
    # pseudo-JSON implementation
    # - the basic python data types dict,list,str,int are mostly identical to json
    # - therefore a basic str() conversion is enough for writing
    # - for reading the more bothersome eval() is used
    # - it's however no severe security problem here, because we're just reading
    #   local config files (written by us) and accept no data from outside / web
    # NOTE: This code is only used, if the Python json module (since 2.6) isn't there.


# store object in string representation into filepointer
def dump(obj, fp, indent=0):

        obj = filter_data(obj)
        
        try:
                return json_dump(obj, fp, indent=indent, sort_keys=(indent and indent>0))
        except:
                return fp.write(str(obj))
                # .replace("'}, ", "'},\n ")    # add whitespace
              	# .replace("', ", "',\n "))
              	# .replace("': [{'", "':\n[\n{'")
        pass


# load from filepointer, decode string into dicts/list
def load(fp):
        try:
                #print("try json")
                r = json_load(fp)
                r = filter_data(r)   # turn unicode() strings back into str() - pygtk does not accept u"strings"
        except:
                #print("fall back on pson")
                fp.seek(0)
                r = eval(fp.read(1<<27))   # max 128 MB
                # print("fake json module: in python variable dump notation")

        if r == None:
                r = {}
        return r


# removes any objects, turns unicode back into str
def filter_data(obj):
        if type(obj) in (int, float, bool, str):
                return obj
#        elif type(obj) == str:   #->str->utf8->str
#                return str(unicode(obj))
        elif type(obj) == unicode:
                return str(obj)
        elif type(obj) in (list, tuple, set):
                obj = list(obj)
                for i,v in enumerate(obj):
                        obj[i] = filter_data(v)
        elif type(obj) == dict:
                for i,v in obj.iteritems():
                        i = filter_data(i)
                        obj[i] = filter_data(v)
        else:
                print("invalid object in data, converting to string: ", type(obj), obj)
                obj = str(obj)
        return obj



Added st2.py version [70adb4776e].



























































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
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
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
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
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
280
281
282
283
284
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
427
428
429
430
431
432
433
434
435
436
437
438
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
465
466
467
468
469
470
471
472
473
474
475
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
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
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
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
778
779
780
781
782
783
784
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
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
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
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
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
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
1096
1097
1098
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
#!/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.8
# 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
# 
#
#
# Streamtuner2 is a GUI browser for internet radio directories. Various
# providers can be added, and streaming stations are usually grouped into
# music genres or categories. It starts external audio players for stream
# playing and streamripper for recording broadcasts.
#
# It's an independent rewrite of streamtuner1 in a scripting language. So
# it can be more easily extended and fixed. The use of PyQuery for HTML
# parsing makes this simpler and more robust.
#
# Stream lists are stored in JSON cache files.
#
#
#


""" project status """
#
# Cumulative development time is two months now, but the application
# runs mostly stable already. The GUI interfaces are workable.
# There haven't been any optimizations regarding memory usage and
# performance. The current internal API is acceptable. Documentation is
# coming up.
#
#  current bugs:
#   - audio- and list-format support is not very robust / needs better API
#   - lots of GtkWarning messages
#   - not all keyboard shortcuts work
#   - in-list search doesn't work in our treeviews (???)
#   - JSON files are only trouble: loading of data files might lead to more
#     errors now, even if pson module still falls back on old method
#     (unicode strings from json.load are useless to us, require typecasts)
#     (nonsupport of tuples led to regression in mygtk.app_restore)
#     (sometimes we receive 8bit-content, which the json module can't save)
#
#  features:
#   - treeview lists are created from datamap[] structure and stream{} dicts
#   - channel categories are built-in defaults (can be freshened up however)
#   - config vars and cache data get stored as JSON in ~/.config/streamtuner2/
#
#  missing:
#   - localization
#
#  security notes:
#   - directory scrapers use fragile regular expressions - which is probably
#     not a security risk, but might lead to faulty data
#   - MEDIUM: little integrity checking for .pls / .m3u references and files
#   - minimal XML/SGML entity decoding (-> faulty data)
#   - MEDIUM: if system json module is not available, pseudo-json uses eval()
#     to read the config data -> limited risk, since it's only local files
#   - HIGH RISK: no verification of downloaded favicon image files (ico/png),
#     as they are passed to gtk.gdk.Pixbuf (OTOH data pre-filtered by Google)
#   - MEDIUM: audio players / decoders are easily affected by buffer overflows
#     from corrupt mp3/stream data, and streamtuner2 executes them
#      - but since that's the purpose -> no workaround
#
#  still help wanted on:
#   - any of the above
#   - new plugins (local file viewer)
#   - nicer logo (or donations accepted to consult graphics designer)
#



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

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

# 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



# this represents the main window
# and also contains most application behaviour
main = None
class StreamTunerTwo(gtk.glade.XML):


        # object containers
        widgets = {}     # non-glade widgets (the manually instantiated ones)
        channels = {}    # channel modules
        features = {}    # non-channel plugins
        working = []     # threads

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

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

            # instantiate gtk/glade widgets in current object
            gtk.glade.XML.__init__(self, ("st2.glade" if os.path.exists("st2.glade") else conf.share+"/st2.glade")), gui_startup(0.10)
            # manual gtk operations
            self.extensionsCTM.set_submenu(self.extensions)  # duplicates Station>Extension menu into stream context menu

            # initialize channels
            self.channels = {
              "bookmarks": bookmarks(parent=self),   # this the remaining built-in channel
              "shoutcast": None,#shoutcast(parent=self),
            }
            gui_startup(0.15)
            self.load_plugin_channels()   # append other channel modules / plugins


            # load application state (widget sizes, selections, etc.)
            try:
                winlayout = conf.load("window")
                if (winlayout):
                    mygtk.app_restore(self, winlayout)
                # selection values
                winstate = conf.load("state")
                if (winstate):
                    for id in winstate.keys():
                        self.channels[id].current = winstate[id]["current"]
                        self.channels[id].shown = winlayout[id+"_list"].get("row:selected", 0)   # actually just used as boolean flag (for late loading of stream list), selection bar has been positioned before already
            except:
                pass # fails for disabled/reordered plugin channels

            # display current open channel/notebook tab
            gui_startup(0.90)
            self.current_channel = self.current_channel_gtk()
            try: self.channel().first_show()
            except: print("channel .first_show() initialization error")

      
            # bind gtk/glade event names to functions
            gui_startup(0.95)
            self.signal_autoconnect({
                "gtk_main_quit" : self.gtk_main_quit,                # close window
                # treeviews / notebook
                "on_stream_row_activated" : self.on_play_clicked,    # double click in a streams list
                "on_category_clicked": self.on_category_clicked,     # new selection in category list
                "on_notebook_channels_switch_page": self.channel_switch,   # channel notebook tab changed
                "station_context_menu": lambda tv,ev: station_context_menu(tv,ev),
                # toolbar
                "on_play_clicked" : self.on_play_clicked,
                "on_record_clicked": self.on_record_clicked,
                "on_homepage_stream_clicked": self.on_homepage_stream_clicked,
                "on_reload_clicked": self.on_reload_clicked,
                "on_stop_clicked": self.on_stop_clicked,
                "on_homepage_channel_clicked" : self.on_homepage_channel_clicked,
                "double_click_channel_tab": self.on_homepage_channel_clicked,
                # menu
                "menu_toolbar_standard": lambda w: (self.toolbar.unset_style(), self.toolbar.unset_icon_size()),
                "menu_toolbar_style_icons": lambda w: (self.toolbar.set_style(gtk.TOOLBAR_ICONS)),
                "menu_toolbar_style_both": lambda w: (self.toolbar.set_style(gtk.TOOLBAR_BOTH)),
                "menu_toolbar_size_small": lambda w: (self.toolbar.set_icon_size(gtk.ICON_SIZE_SMALL_TOOLBAR)),
                "menu_toolbar_size_medium": lambda w: (self.toolbar.set_icon_size(gtk.ICON_SIZE_DND)),
                "menu_toolbar_size_large": lambda w: (self.toolbar.set_icon_size(gtk.ICON_SIZE_DIALOG)),
                # else
                "menu_properties": config_dialog.open,
                "config_cancel": config_dialog.hide,
                "config_save": config_dialog.save,
                "update_categories": self.update_categories,
                "update_favicons": self.update_favicons,
                "app_state": self.app_state,
                "bookmark": self.bookmark,
                "save_as": self.save_as,
                "menu_about": lambda w: AboutStreamtuner2(),
                "menu_help": action.action.help,
                "menu_onlineforum": lambda w: action.browser("http://sourceforge.net/projects/streamtuner2/forums/forum/1173108"),
               # "menu_bugreport": lambda w: BugReport(),
                "menu_copy": self.menu_copy,
                "delete_entry": self.delete_entry,
                "quicksearch_set": search.quicksearch_set,
                "search_open": search.menu_search,
                "search_go": search.start,
                "search_srv": search.start,
                "search_google": search.google,
                "search_cancel": search.cancel,
                "true": lambda w,*args: True,
                "streamedit_open": streamedit.open,
                "streamedit_save": streamedit.save,
                "streamedit_new": streamedit.new,
                "streamedit_cancel": streamedit.cancel,
            })
            
            # actually display main window
            gui_startup(0.99)
            self.win_streamtuner2.show()
            
            # WHY DON'T YOU WANT TO WORK?!
            #self.shoutcast.gtk_list.set_enable_search(True)
            #self.shoutcast.gtk_list.set_search_column(4)


          

        #-- 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_widget(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.glade.XML.get_widget(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
                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]
            
            # if first selected, load current category
            try:
                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
            




        # shortcut to statusbar
        # (hacked to work from within threads, circumvents the statusbar msg pool actually)
        def status(self, text="", sbar_msg=[]):
            # init
            sbar_cid = self.get_widget("statusbar").get_context_id("messages")
            # remove text
            while ((not text) and (type(text)==str) and len(sbar_msg)):
                sbar_msg.pop()
                mygtk.do(lambda:self.statusbar.pop(sbar_cid))
            # progressbar
            if (type(text)==float):
                if (text >= 1.0):  # completed
                    mygtk.do(lambda:self.progress.hide())
                else:  # show percentage
                    mygtk.do(lambda:self.progress.show() or self.progress.set_fraction(text))
                    if (text <= 0.0):  # unknown state
                        mygtk.do(lambda:self.progress.pulse())
            # add text
            elif (type(text)==str):
                sbar_msg.append(1)
                mygtk.do(lambda:self.statusbar.push(sbar_cid, text))
            pass

        # 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)]
            
            # resort with tab order
            order = [module.strip() for module in conf.channel_order.lower().replace(".","_").replace("-","_").split(",")]
            ls = [module for module in (order) if (module in ls)] + [module for module in (ls) if (module not in order)]

            # step through
            for module in ls:
                gui_startup(0.2 + 0.7 * float(ls.index(module))/len(ls), "loading module "+module)
                                
                # skip module if disabled
                if conf.plugins.get(module, 1) == False:
                    __print__("disabled plugin:", module)
                    continue
                
                # load plugin
                try:
                    plugin = __import__("channels."+module, None, None, [""])
                    plugin_class = plugin.__dict__[module]
                
                    # load .config settings from plugin
                    conf.add_plugin_defaults(plugin_class.config, module)

                    # add and initialize channel
                    if issubclass(plugin_class, GenericChannel):
                        self.channels[module] = plugin_class(parent=self)
                        if module not in self.channel_names:  # skip (glade) built-in channels
                            self.channel_names.append(module)
                    # other plugin types
                    else:
                        self.features[module] = plugin_class(parent=self)
                    
                except Exception, e:
                    print("error initializing:", module)
                    print(e)

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






                




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

            
# right click in streams/stations TreeView
def station_context_menu(treeview, event):
            # right-click ?
            if event.button >= 3:
                path = treeview.get_path_at_pos(int(event.x), int(event.y))[0]
                treeview.grab_focus()
                treeview.set_cursor(path, None, False)
                main.streamactions.popup(None, None, None, event.button, event.time)
                return None
            # we need to pass on to normal left-button signal handler
            else:
                return False
# this works better as callback function than as class - because of False/Object result for event trigger




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



# 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
            
            
        # perform search
        def start(self, *w):
            self.cancel()
            
            # prepare variables
            self.q = self.search_full.get_text().lower()
            entries = []
            main.bookmarks.streams["search"] = []
            
            # which fields?
            fields = ["title", "playing", "genre", "homepage", "url", "extra", "favicon", "format"]
            if not self.search_in_all.get_active():
                fields = [f for f in fields if (main.get_widget("search_in_"+f) and main.get_widget("search_in_"+f).get_active())]
            # channels?
            channels = main.channel_names[:]
            if not self.search_channel_all.get_active():
                channels = [c for c in channels if main.get_widget("search_channel_"+c).get_active()]
            
            # step through channels
            for c in channels:
                if main.channels[c] and main.channels[c].streams:  # skip disabled plugins

                    # categories
                    for cat in main.channels[c].streams.keys():

                        # stations
                        for row in main.channels[c].streams[cat]:
                    
                            # assemble text fields
                            text = " ".join([row.get(f, " ") for f in fields])
                        
                            # compare
                            if text.lower().find(self.q) >= 0:

                                # add result
                                entries.append(row)

            
            # display "search" in "bookmarks"
            main.channel_switch(None, "bookmarks", 0)
            main.bookmarks.set_category("search")
            # insert data and show
            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())


        # search text edited in text entry box
        def quicksearch_set(self, w, *eat, **up):
            
            # keep query string
            main.q = self.search_quick.get_text().lower()

            # get streams
            c = main.channel()
            rows = c.stations()
            col = c.rowmap.index("search_col") # this is the gtk.ListStore index # which contains the highlighting color

            # callback to compare (+highlight) rows
            m = c.gtk_list.get_model()
            m.foreach(self.quicksearch_treestore, (rows, main.q, col, col+1))
        search_set = quicksearch_set
            
            
            
        # callback that iterates over whole gtk treelist,
        # looks for search string and applies TreeList color and flag if found
        def quicksearch_treestore(self, model, path, iter, extra_data):
            i = path[0]
            (rows, q, color, flag) = extra_data

            # compare against interesting content fields:
            text = rows[i].get("title", "") + " " + rows[i].get("homepage", "")
            # config.quicksearch_fields
            text = text.lower()

            # simple string match (probably doesn't need full search expression support)
            if len(q) and text.find(q) >= 0:
               model.set_value(iter, color, "#fe9")  # highlighting color
               model.set_value(iter, flag, True) # background-set flag
            # color = 12 in liststore, flag = 13th position
            else:
               model.set_value(iter, color, "")   # for some reason the cellrenderer colors get applied to all rows, even if we specify an iter (=treelist position?, not?)
               model.set_value(iter, flag, False)   # that's why we need the secondary -set option

            #??
            return False



search = search()
# 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)
                __print__("config_save", save, prefix+id, w, val)
                # recurse into dictionaries, transform: conf.play.audio/mp3 => conf.play_audio_mp3
                if (type(val) == dict):
                    self.apply(val, prefix + id + "_", save)
                # load or set gtk.Entry text field
                elif (w and save and type(w)==gtk.Entry):
                    config[key] = w.get_text()
                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

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

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

                # look up individual plugin options, if loaded
                if self.channels.get(name) or self.features.get(name):
                    c = self.channels.get(name) or self.features.get(name)
                    for opt in c.config:

                        # default values are already in conf[] dict (now done in conf.add_plugin_defaults)
                            
                        # display checkbox or text entry
                        if opt["type"] == "boolean":
                            cb = gtk.CheckButton(opt["description"])
                            #cb.set_line_wrap(True)
                            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)
                label = gtk.Label(label)
                label.set_property("visible", True)
                label.set_line_wrap(True) 
                label.set_size_request(250, -1)
                vbox = gtk.HBox(homogeneous=False, spacing=10)
                vbox.set_property("visible", True)
                vbox.pack_start(w, expand=False, fill=False)
                vbox.pack_start(label, expand=True, fill=True)
                w = vbox
            if color:
                w = mygtk.bg(w, color)
            self.plugin_options.pack_start(w)

        
        # save config
        def save(self, widget):
            self.apply(conf.__dict__, "config_", 1)
            self.apply_theme()
            conf.save(nice=1)
            self.hide()
                  
config_dialog = config_dialog()
# instantiates itself





























# class GenericChannel:
#
#   is in channels/__init__.py
#





#-- favourite lists                                            ------------------------------------------
#
# 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
        def is_in(self, url, once=1):
            if (not self.urls):
                self.urls = [row.get("url","urn:x-streamtuner2:no") for row in self.streams["favourite"]]
            return url in self.urls


        # called from main window / menu / context menu,
        # when bookmark is to be added for a selected stream entry
        def add(self, row):
        
            # normalize data (this row originated in a gtk+ widget)
            row["favourite"] = 1
            if row.get("favicon"):
               row["favicon"] = favicon.file(row.get("homepage"))
            if not row.get("listformat"):
                row["listformat"] = main.channel().listformat
               
            # append to storage
            self.streams["favourite"].append(row)
            self.save()
            self.load(self.default)
            self.urls.append(row["url"])


        # simplified gtk TreeStore display logic (just one category for the moment, always rebuilt)
        def load(self, category, force=False):
            #self.liststore[category] = \
#            print(category, self.streams.keys())
            mygtk.columns(self.gtk_list, self.datamap, self.prepare(self.streams.get(category,[])))


        # 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()
            
            
        # update bookmarks from freshly loaded streams data
        def heuristic_update(self, updated_channel, updated_category):

            if not conf.heuristic_bookmark_update: return
            save = 0
            fav = self.streams["favourite"]
        
            # First we'll generate a list of current bookmark stream urls, and then
            # remove all but those from the currently UPDATED_channel + category.
            # This step is most likely redundant, but prevents accidently re-rewriting
            # stations that are in two channels (=duplicates with different PLS urls).
            check = {"http//": "[row]"}
            check = dict((row["url"],row) for row in fav)
            # walk through all channels/streams
            for chname,channel in main.channels.iteritems():
                for cat,streams in channel.streams.iteritems():

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

                    # remove unchanged urls/rows
                    else:
                        unchanged_urls = (row.get("url") for row in streams)
                        for url in unchanged_urls:
                            if url in check:
                                del check[url]
                                # directory duplicates could unset the check list here,
                                # so we later end up doing a deep comparison


            # now the real comparison,
            # where we compare station titles and homepage url to detect if a bookmark is an old entry
            for row in freshened_streams:
                url = row.get("url")
                
                # empty entry (google stations), or stream still in current favourites
                if not url or url in check:
                    pass

                # need to search
                else:
                    title = row.get("title")
                    homepage = row.get("homepage")
                    for i,old in enumerate(fav):

                        # skip if new url already in streams
                        if url == old.get("url"):
                            pass   # This is caused by channel duplicates with identical PLS links.
                        
                        # on exact matches (but skip if url is identical anyway)
                        elif title == old["title"] and homepage == old.get("homepage",homepage):
                            # update stream url
                            fav[i]["url"] = url
                            save = 1
                            
                        # more text similarity heuristics might go here
                        else:
                            pass
            
            # if there were changes
            if save: self.save()













#-- 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__":

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

    # graphical
    if len(sys.argv) < 2:
    
        
        # prepare for threading in Gtk+ callbacks
        gobject.threads_init()
        gui_startup(0.05)
        
        # prepare main window
        main = StreamTunerTwo()
        
        # module coupling
        action.main = main      # action (play/record) module needs a reference to main window for gtk interaction and some URL/URI callbacks
        action = action.action  # shorter name
        http.feedback = main.status  # http module gives status feedbacks too
        
        # first invocation
        if (conf.get("firstrun")):
            config_dialog.open(None)
            del conf.firstrun


        # run
        gui_startup(1.00)
        gtk.main()
        
        
    # invoke command-line interface
    else:
        import cli
        cli.StreamTunerCLI()




#
#
#
#