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

⌈⌋ ⎇ branch:  streamtuner2


Check-in [7ef1553f61]

Overview
Comment:Move __print__ into config, add unified dbg.COLOR codes
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | trunk
Files: files | file ages | folders
SHA1: 7ef1553f61c9abcecfb85ac12064e265f68a5727
User & Date: mario on 2014-04-07 00:33:43
Other Links: manifest | tags
Context
2014-04-27
22:19
Python3 support back into trunk check-in: 9ecea4fb26 user: mario tags: trunk
2014-04-08
21:16
rename http to ahttp to avoid conflict with Python3 modules, change .iteritems and xrange, remove same remaining plain print statements check-in: d3b1418bc6 user: mario tags: py3
2014-04-07
00:33
Move __print__ into config, add unified dbg.COLOR codes check-in: 7ef1553f61 user: mario tags: trunk
2014-04-06
02:16
rename ui.xml to gtk2.xml for parity with gtk3.xml; Gtk3 suddenly works with gi 1.33 (well, lots of errors still, but main window ok) check-in: e7a0fb24c8 user: mario tags: trunk
Changes

Modified _package.epm from [036b8e5906] to [d2e2c94cae].

27
28
29
30
31
32
33
34

35
36
37
38
39
40
41
d 755 root root /usr/share/doc/streamtuner2/contrib		-
f 644 root root /usr/share/doc/streamtuner2/contrib/streamripper_addgenre		./contrib/streamripper_addgenre
f 755 root root /usr/bin/streamtuner2				./st2.py
f 644 root root /usr/share/applications/streamtuner2.desktop	./streamtuner2.desktop
d 755 root root /usr/share/streamtuner2				-
f 644 root root /usr/share/streamtuner2/streamtuner2.png	./streamtuner2.png
f 644 root root /usr/share/pixmaps/streamtuner2.png		./logo.png
f 644 root root /usr/share/streamtuner2/ui.xml			./ui.xml

f 644 root root /usr/share/streamtuner2/pson.py			./pson.py
#f 644 root root /usr/share/streamtuner2/processing.py		./processing.py
f 644 root root /usr/share/streamtuner2/action.py		./action.py
f 644 root root /usr/share/streamtuner2/config.py		./config.py
f 644 root root /usr/share/streamtuner2/http.py			./http.py
f 644 root root /usr/share/streamtuner2/cli.py			./cli.py
f 644 root root /usr/share/streamtuner2/mygtk.py		./mygtk.py







|
>







27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
d 755 root root /usr/share/doc/streamtuner2/contrib		-
f 644 root root /usr/share/doc/streamtuner2/contrib/streamripper_addgenre		./contrib/streamripper_addgenre
f 755 root root /usr/bin/streamtuner2				./st2.py
f 644 root root /usr/share/applications/streamtuner2.desktop	./streamtuner2.desktop
d 755 root root /usr/share/streamtuner2				-
f 644 root root /usr/share/streamtuner2/streamtuner2.png	./streamtuner2.png
f 644 root root /usr/share/pixmaps/streamtuner2.png		./logo.png
f 644 root root /usr/share/streamtuner2/gtk2.xml		./gtk2.xml
f 644 root root /usr/share/streamtuner2/gtk3.xml		./gtk3.xml
f 644 root root /usr/share/streamtuner2/pson.py			./pson.py
#f 644 root root /usr/share/streamtuner2/processing.py		./processing.py
f 644 root root /usr/share/streamtuner2/action.py		./action.py
f 644 root root /usr/share/streamtuner2/config.py		./config.py
f 644 root root /usr/share/streamtuner2/http.py			./http.py
f 644 root root /usr/share/streamtuner2/cli.py			./cli.py
f 644 root root /usr/share/streamtuner2/mygtk.py		./mygtk.py

Modified action.py from [f63b7b9614] to [fc0650246a].

20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#
#


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,







|


<
<
<
<
<
<







20
21
22
23
24
25
26
27
28
29






30
31
32
33
34
35
36
#
#


import re
import os
import http
from config import conf, __print__, dbg
import platform








main = None


#-- media actions                           ---------------------------------------------
#
# implements "play" and "record" methods,

Modified channels/__init__.py from [f76e54fd21] to [92d73c2e69].

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__








<
1
2
3
4
5
6
7
8

#
# encoding: UTF-8
# api: python
# type: R
#


from channels._generic import *

Modified channels/_generic.py from [d30d0357de] to [f927301f5f].

18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#  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)







|







<







18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

33
34
35
36
37
38
39
#  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, __print__, dbg
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)
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
                    #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] = \







|







204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
                    #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__(dbg.ERR, "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] = \
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
            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








|
|
<

<



|










|




|







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
            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__(dbg.PROC, "first_show ", self.module, self.shown)


            if (self.shown != 55555):

            
                # if category tree is empty, initialize it
                if not self.categories:
                    __print__(dbg.PROC, "first_show: reload_categories");
                    #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__(dbg.STAT, "first_show: load current category");
                    self.load(self.current)
                
                # put selection/cursor on last position
                try:
                    __print__(dbg.STAT, "first_show: select last known category treelist position")
                    self.gtk_list.get_selection().select_path(self.shown)
                except:
                    pass
                    
                # this method will only be invoked once
                self.shown = 55555

527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547

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






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

__debug_print__ = __print__











<
<
<
<
<
<
<
<
<
<
<



524
525
526
527
528
529
530











531
532
533

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
















Modified channels/global_key.py from [7c03f6331b] to [3bfb69eda6].

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


import keybinder
from config import conf
import action
import random








<







9
10
11
12
13
14
15

16
17
18
19
20
21
22
#
# 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

Modified channels/internet_radio_org_uk.py from [5a7ebd0c8e] to [4dcc6f65d4].

10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#
#



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




# streams and gui







|







10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#
#



from channels import *
import re
from config import conf, __print__, dbg
import http
from pq import pq




# streams and gui

Modified channels/live365.py from [9e6de8cad1] to [17860a6a46].

8
9
10
11
12
13
14
15
16
17
18
19
20
21
22


# 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







|







8
9
10
11
12
13
14
15
16
17
18
19
20
21
22


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

# python modules
import re
import xml.dom.minidom
from xml.sax.saxutils import unescape as entity_decode, escape as xmlentities
import gtk
import copy
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
            =["']audioQuality.+?>(\d+)\w<.+?
            >DrawListenerStars\((\d+),.+?
            >DrawRatingStars\((\d+),\s+(\d+),.*?
                """, 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[8])
                count = int(row[9])
                ls.append({
                    "launch_id": row[0],
                    "sofo": row[0],  # subscribe-or-fuck-off status flags
                    "state":  (""  if  row[0]=="OK"  else  gtk.STOCK_STOP),
                    "homepage": entity_decode(row[1]),







|


|







114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
            =["']audioQuality.+?>(\d+)\w<.+?
            >DrawListenerStars\((\d+),.+?
            >DrawRatingStars\((\d+),\s+(\d+),.*?
                """, re.X|re.I|re.S|re.M)
#            src="(http://www.live365.com/.+?/stationlogo\w+.jpg)".+?

            # append entries to result list
            __print__( dbg.DATA, html )
            ls = []
            for row in rx.findall(html):
                __print__( dbg.DATA, row )
                points = int(row[8])
                count = int(row[9])
                ls.append({
                    "launch_id": row[0],
                    "sofo": row[0],  # subscribe-or-fuck-off status flags
                    "state":  (""  if  row[0]=="OK"  else  gtk.STOCK_STOP),
                    "homepage": entity_decode(row[1]),

Modified channels/modarchive.py from [ac3a95273a] to [bf829e8b72].

11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#


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













|







11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#


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






107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
            .*?    /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,







|







107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
            .*?    /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__( dbg.DATA, uu )
            entries.append({
                "genre": cat,
                "url": url,
                "id": id,
                "format": self.mime_fmt(fmt) + "+zip",
                "title": title,
                "playing": file,

Modified channels/punkcast.py from [ba986159c3] to [6f838d457b].

9
10
11
12
13
14
15
16
17
18
19
20
21
22
23


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







|







9
10
11
12
13
14
15
16
17
18
19
20
21
22
23


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





# disable plugin per default
if "punkcast" not in vars(conf): 
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
    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"])
            
        












|













82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
    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__( dbg.DATA, uu )
            (url, fmt) = uu
            action.action.play(url, self.mime_fmt(fmt), "url/direct")
            return
        
        # or just open webpage
        action.action.browser(row["homepage"])
            
        





Modified channels/shoutcast.py from [8c7ec7957d] to [3296716516].

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





|




|
<
<
<

<
<
<
<
<
<
<
<
<
<
<
<
<
>
>
>








>

<


<







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
#
# api: streamtuner2
# title: shoutcast
# description: Channel/tab for Shoutcast.com directory
# depends: pq, re, http
# version: 1.3
# 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.



#













# After its recent aquisition the layout got slimmed down considerably. So
# there's not a lot of information to fetch left. And this plugin is now back
# to defaulting to regex extraction instead of HTML parsing & DOM extraction.
#
#
#


import http
import urllib
import re
from config import conf, __print__, dbg
from pq import pq

#from channels import *    # works everywhere but in this plugin(???!)
import channels




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

        # desc
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
        
            
        # 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 = []
            __print__( html )

            # <h2>Radio Genres</h2>
	    rx = re.compile(r'<li((?:\s+id="\d+"\s+class="files")?)><a href="\?action=sub&cat=([\w\s]+)#(\d+)">[\w\s]+</a>', re.S)
            sub = []
            for uu in rx.findall(html):
                __print__(uu)
		(main,name,id) = uu
                name = urllib.unquote(name)

                # main category
                if main:
                    if sub:
                        self.categories.append(sub)
                        sub = []
                    self.categories.append(name)
                else:
                    sub.append(name)

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


            # loop
            entries = []
            next = 0
            max = int(conf.max_streams)
            count = max
            rx_stream = None

            try:
               if (next < max):


                  #/radiolist.cfm?action=sub&string=&cat=Oldies&_cf_containerId=radiolist&_cf_nodebug=true&_cf_nocache=true&_cf_rc=0
                  #/radiolist.cfm?start=19&action=sub&string=&cat=Oldies&amount=18&order=listeners
                  # page
                  url = "http://www.shoutcast.com/radiolist.cfm?action=sub&string=&cat="+ucat+"&order=listeners&amount="+str(count)
                  __print__(url)
                  referer = "http://www.shoutcast.com/?action=sub&cat="+ucat
                  params = {} # "strIndex":"0", "count":str(count), "ajax":"true", "mode":"listeners", "order":"desc" }
                  html = http.ajax(url, params, referer)   #,feedback=self.parent.status)

                  __print__(html)
                  __print__(re.compile("id=(\d+)").findall(html));







                  # regular expressions
                  if 1:  #not conf.get("pyquery") or not pq:

                      # new html
                      """ 
                      <tr>
                         <td width="6%"><a href="#" onClick="window.open('player/?radname=Schlagerhoelle%20%2D%20das%20Paradies%20fr%20Schlager%20%20und%20Discofox&stationid=14687&coding=MP3','radplayer','height=232,width=776')"><img class="icon transition" src="/img/icon-play.png" alt="Play"></a></td>
                         <td width="30%"><a class="transition" href="http://yp.shoutcast.com/sbin/tunein-station.pls?id=14687">Schlagerhoelle - das Paradies fr Schlager  und Discofox</a></td>
                         <td width="12%" style="text-align:left;" width="10%">Oldies</td>







|





|













|













|



















|




|
|

>
>
>
>
>
>
|
|







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
        
            
        # 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 = []
            __print__( dbg.DATA, html )

            # <h2>Radio Genres</h2>
	    rx = re.compile(r'<li((?:\s+id="\d+"\s+class="files")?)><a href="\?action=sub&cat=([\w\s]+)#(\d+)">[\w\s]+</a>', re.S)
            sub = []
            for uu in rx.findall(html):
                __print__( dbg.DATA, uu )
		(main,name,id) = uu
                name = urllib.unquote(name)

                # main category
                if main:
                    if sub:
                        self.categories.append(sub)
                        sub = []
                    self.categories.append(name)
                else:
                    sub.append(name)

            # it's done
            __print__( dbg.PROC, 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__( dbg.ERR, "nocat" )
                return []
            ucat = urllib.quote(cat)


            # loop
            entries = []
            next = 0
            max = int(conf.max_streams)
            count = max
            rx_stream = None

            try:
               if (next < max):


                  #/radiolist.cfm?action=sub&string=&cat=Oldies&_cf_containerId=radiolist&_cf_nodebug=true&_cf_nocache=true&_cf_rc=0
                  #/radiolist.cfm?start=19&action=sub&string=&cat=Oldies&amount=18&order=listeners
                  # page
                  url = "http://www.shoutcast.com/radiolist.cfm?action=sub&string=&cat="+ucat+"&order=listeners&amount="+str(count)
                  __print__(dbg.HTTP, url)
                  referer = "http://www.shoutcast.com/?action=sub&cat="+ucat
                  params = {} # "strIndex":"0", "count":str(count), "ajax":"true", "mode":"listeners", "order":"desc" }
                  html = http.ajax(url, params, referer)   #,feedback=self.parent.status)

                  __print__(dbg.DATA, html)
                  #__print__(re.compile("id=(\d+)").findall(html));


                  # With the new shallow <td> lists it doesn't make much sense to use
                  # the pyquery DOM traversal. There aren't any sensible selectors to
                  # extract values; it's just counting the tags.


                  # regular expressions (default)
                  if not conf.get("pyquery") or not pq:

                      # new html
                      """ 
                      <tr>
                         <td width="6%"><a href="#" onClick="window.open('player/?radname=Schlagerhoelle%20%2D%20das%20Paradies%20fr%20Schlager%20%20und%20Discofox&stationid=14687&coding=MP3','radplayer','height=232,width=776')"><img class="icon transition" src="/img/icon-play.png" alt="Play"></a></td>
                         <td width="30%"><a class="transition" href="http://yp.shoutcast.com/sbin/tunein-station.pls?id=14687">Schlagerhoelle - das Paradies fr Schlager  und Discofox</a></td>
                         <td width="12%" style="text-align:left;" width="10%">Oldies</td>
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
                               \s+  <td [^>]+  >([^<>]+)</td>
                               \s+  <td [^>]+  >(\d+)</td>
                               \s+  <td [^>]+  >(\d+)</td>
                               \s+  <td [^>]+  >(\w+)</td>
                              """,
                              re.S|re.I|re.X
                          )
                      __print__( rx_stream)

                      # extract entries
                      self.parent.status("parsing document...")

                      __print__("loop-rx")
                      for m in rx_stream.findall(html):
                          __print__(m)
                          (id, title, genre, listeners, bitrate, fmt) = m
                          entries += [{
                              "id": id,
                              "url": "http://yp.shoutcast.com/sbin/tunein-station.pls?id=" + id,
                              "title": self.entity_decode(title),
                              #"homepage": http.fix_url(homepage),
                              #"playing": self.entity_decode(playing),
                              "genre": genre,
                              "listeners": int(listeners),
                              #"max": 0, #int(uu[6]),
                              "bitrate": int(bitrate),
                              "format": self.mime_fmt(fmt),
                          }]


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

                          entries.append({
                               "title": div.find("a.transition").text(),
                               "url": div.find("a.transition").attr("href"),
                               "homepage": "",
                               "playing": div.find("td:eq(2)").text(),
                               "listeners": int(div.find("td:eq(4)").text()),
                               "bitrate": int(div.find("td:eq(5)").text()),
                               "format": self.mime_fmt(div.find("td:eq(6)").text()),
                               "max": 0,
                               "genre": cat,
                          })


                  # 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?
                  next = 99999
                     
            except Exception as e:
               __print__(e)
               return entries
            
            #fin
            __print__(entries)
            return entries









|



>
|

|









|



>










<
|
|
|













|



|



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
                               \s+  <td [^>]+  >([^<>]+)</td>
                               \s+  <td [^>]+  >(\d+)</td>
                               \s+  <td [^>]+  >(\d+)</td>
                               \s+  <td [^>]+  >(\w+)</td>
                              """,
                              re.S|re.I|re.X
                          )


                      # extract entries
                      self.parent.status("parsing document...")
                      __print__(dbg.PROC, "channels.shoutcast.update_streams: regex scraping mode")

                      for m in rx_stream.findall(html):
                          #__print__(m)
                          (id, title, genre, listeners, bitrate, fmt) = m
                          entries += [{
                              "id": id,
                              "url": "http://yp.shoutcast.com/sbin/tunein-station.pls?id=" + id,
                              "title": self.entity_decode(title),
                              #"homepage": http.fix_url(homepage),
                              #"playing": self.entity_decode(playing),
                              "genre": genre,
                              "listeners": int(listeners),
                              "max": 0, #int(uu[6]),
                              "bitrate": int(bitrate),
                              "format": self.mime_fmt(fmt),
                          }]


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

                          entries.append({
                               "title": div.find("a.transition").text(),
                               "url": div.find("a.transition").attr("href"),
                               "homepage": "",

                               "listeners": int(div.find("td:eq(3)").text()),
                               "bitrate": int(div.find("td:eq(4)").text()),
                               "format": self.mime_fmt(div.find("td:eq(5)").text()),
                               "max": 0,
                               "genre": cat,
                          })


                  # 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?
                  next = 99999
                     
            except Exception as e:
               __print__(dbg.ERR, e)
               return entries
            
            #fin
            __print__(dbg.DATA, entries)
            return entries


Modified channels/xiph.py from [8ad5a64170] to [6bbc72e8d5].

17
18
19
20
21
22
23
24
25
26
27
28
29
30
31


# 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









|







17
18
19
20
21
22
23
24
25
26
27
28
29
30
31


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

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


86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
                    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):







|







86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
                    else:
                        g[t] = 0
            g = [ [v[1],v[0]] for v in g.items() ]
            g.sort()
            g.reverse()
            for row in g:
                pass
                __print__( dbg.DATA, '        "' + 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):

Modified config.py from [0a6d293594] to [16fe14f094].

26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
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







<







26
27
28
29
30
31
32

33
34
35
36
37
38
39
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
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




















                    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

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


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































|





|





















>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
                    f = open(file, "r")
                else:
                    return # file not found
                # decode
                r = pson.load(f)
                f.close()
                return r
            except Exception as e:
                print("PSON parsing error (in "+name+")", e)
            

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

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


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




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


# error colorization
dbg = type('obj', (object,), {
    "ERR":  "[ERR]",  # red    ERROR
    "INIT": "[INIT]", # red    INIT ERROR
    "PROC": "[PROC]", # green  PROCESS
    "CONF": "[CONF]", # brown  CONFIG DATA
    "UI":   "[UI]",   # blue   USER INTERFACE BEHAVIOUR
    "HTTP": "[HTTP]", # magenta HTTP REQUEST
    "DATA": "[DATA]", # cyan   DATA
    "INFO": "[INFO]", # gray   INFO
    "STAT": "[STATE]", # gray  CONFIG STATE
})


Modified http.py from [68776c8e4a] to [0d4dcfee94].

10
11
12
13
14
15
16
17
18
19



20
21
22
23
24
25
26
27
28
#  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                            ---------------------------------------------










|
|
|
>
>
>

|







10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#  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.
#
            
try:
    import urllib2
    from urllib import urlencode
except:
    import urllib.request as urllib2
    import urllib.parse.urlencode as urlencode
import config
from config import __print__, dbg



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



Modified mygtk.py from [74d7664de2] to [98fa2cab88].

23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#
#




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


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







|
<



|







23
24
25
26
27
28
29
30

31
32
33
34
35
36
37
38
39
40
41
#
#




# debug
from config import __print__, dbg



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

Modified pson.py from [1f7ebfaebc] to [759da668d2].

17
18
19
20
21
22
23




24
25
26
27
28
29
30
#  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")








>
>
>
>







17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#  pygtk-objects crawled into the streams[] lists, because rows
#  might have been queried from the widgets.
#


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

import sys
if sys.version_info > (2, 9):
    unicode = str
    #dict.iteritems = dict.items

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

80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
        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










|









84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
        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 list(obj.items()):
                        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



Modified st2.py from [4696fba60c] to [0b9e0b6b81].

95
96
97
98
99
100
101

102
103
104
105
106
107
108
109
110
111
112
sys.path.insert(0, ".")   # pre-defined directory for modules

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

# custom modules
from config import conf   # initializes itself, so all conf.vars are available right away

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







>



<







95
96
97
98
99
100
101
102
103
104
105

106
107
108
109
110
111
112
sys.path.insert(0, ".")   # pre-defined directory for modules

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

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

import favicon
#from pq import pq



# this represents the main window
# and also contains most application behaviour
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
        current_channel = "bookmarks"    # currently selected channel name (as index in self.channels{})


        # constructor
        def __init__(self):

            # gtkrc stylesheet
            self.load_theme(), gui_startup(1/20)

            # instantiate gtk/glade widgets in current object
            gtk.Builder.__init__(self)
            gtk.Builder.add_from_file(self, conf.find_in_dirs([".", conf.share], ui_file)), gui_startup(2/20)
            # 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(3/20)
            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(17/20)
            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(19/20)
            self.connect_signals(dict( {
                "gtk_main_quit" : self.gtk_main_quit,                # close window
                # treeviews / notebook
                "on_stream_row_activated" : self.on_play_clicked,    # double click in a streams list
                "on_category_clicked": self.on_category_clicked,     # new selection in category list
                "on_notebook_channels_switch_page": self.channel_switch,   # channel notebook tab changed
                "station_context_menu": lambda tv,ev: station_context_menu(tv,ev),







|



|








|


















|


|



|







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
        current_channel = "bookmarks"    # currently selected channel name (as index in self.channels{})


        # constructor
        def __init__(self):

            # gtkrc stylesheet
            self.load_theme(), gui_startup(1/20.0)

            # instantiate gtk/glade widgets in current object
            gtk.Builder.__init__(self)
            gtk.Builder.add_from_file(self, conf.find_in_dirs([".", conf.share], ui_file)), gui_startup(2/20.0)
            # 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(3/20.0)
            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(17/20.0)
            self.current_channel = self.current_channel_gtk()
            try: self.channel().first_show()
            except: __print__(dbg.INIT, "main.__init__: current_channel.first_show() initialization error")

      
            # bind gtk/glade event names to functions
            gui_startup(19/20.0)
            self.connect_signals(dict( {
                "gtk_main_quit" : self.gtk_main_quit,                # close window
                # treeviews / notebook
                "on_stream_row_activated" : self.on_play_clicked,    # double click in a streams list
                "on_category_clicked": self.on_category_clicked,     # new selection in category list
                "on_notebook_channels_switch_page": self.channel_switch,   # channel notebook tab changed
                "station_context_menu": lambda tv,ev: station_context_menu(tv,ev),
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
                "streamedit_open": streamedit.open,
                "streamedit_save": streamedit.save,
                "streamedit_new": streamedit.new,
                "streamedit_cancel": streamedit.cancel,
            }.items() + self.add_signals.items() ))
            
            # actually display main window
            gui_startup(99/100)
            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)









|







219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
                "streamedit_open": streamedit.open,
                "streamedit_save": streamedit.save,
                "streamedit_new": streamedit.new,
                "streamedit_cancel": streamedit.cancel,
            }.items() + self.add_signals.items() ))
            
            # actually display main window
            gui_startup(99/100.0)
            self.win_streamtuner2.show()
            
            # WHY DON'T YOU WANT TO WORK?!
            #self.shoutcast.gtk_list.set_enable_search(True)
            #self.shoutcast.gtk_list.set_search_column(4)


279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
                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]








|
|
|

|







279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
                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__(dbg.PROC, "channel_switch: try .first_show", self.channel().module);
                __print__(self.channel().first_show)
                __print__(self.channel().first_show())
            except:
                __print__(dbg.INIT, "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]

333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
            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)







|





|







333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
            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__(dbg.UI, "dblclick")
                action.browser(self.channel().homepage)            


        # reload stream list in current channel-category
        def on_reload_clicked(self, widget=None, reload=1):
            __print__(dbg.UI, "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)
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
                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())







|







363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
                thread = self.working.pop()
                thread.stop()

        
        # click in category list
        def on_category_clicked(self, widget, event, *more):
            category = self.channel().currentcat()
            __print__(dbg.UI, "on_category_clicked", category, self.current_channel)
            self.on_reload_clicked(None, reload=0)
            pass


        # add current selection to bookmark store
        def bookmark(self, widget):
            self.bookmarks.add(self.row())
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
            
            # 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(2/10 + 7/10 * 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 as e:
                    print("error initializing:", module, ", exception:")
                    import traceback
                    traceback.print_exc()

            # default plugins
            conf.add_plugin_defaults(self.channels["bookmarks"].config, "bookmarks")
            #conf.add_plugin_defaults(self.channels["shoutcast"].config, "shoutcast")








|



|




















|







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
            
            # 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(2/10.0 + 7/10.0 * float(ls.index(module))/len(ls), "loading module "+module)
                                
                # skip module if disabled
                if conf.plugins.get(module, 1) == False:
                    __print__(dbg.STAT, "disabled plugin:", module)
                    continue
                
                # 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 as e:
                    __print__(dbg.INIT, "load_plugin_channels: error initializing:", module, ", exception:")
                    import traceback
                    traceback.print_exc()

            # default plugins
            conf.add_plugin_defaults(self.channels["bookmarks"].config, "bookmarks")
            #conf.add_plugin_defaults(self.channels["shoutcast"].config, "shoutcast")

512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
            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):







|







512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
            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):
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
        
        # 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):







|







772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
        
        # set/load values between gtk window and conf. dict
        def apply(self, config, prefix="config_", save=0):
            for key,val in config.iteritems():
                # map non-alphanumeric chars from config{} to underscores in according gtk widget names
                id = re.sub("[^\w]", "_", key)
                w = main.get_widget(prefix + id)
                __print__(dbg.CONF, "config", ("save" if save else "load"), prefix+id, w, val)
                # recurse into dictionaries, transform: conf.play.audio/mp3 => conf.play_audio_mp3
                if (type(val) == dict):
                    self.apply(val, prefix + id + "_", save)
                # 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):
795
796
797
798
799
800
801







802
803
804
805
806
807
808
809
810
811
        # 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







>
>
>
>
>
>
>


|







795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
        # 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)]
            __print__(dbg.STAT, themes)
            # prepare liststore
            store = gtk.ListStore(gobject.TYPE_STRING)
            self.theme.set_model(store)
            cell = gtk.CellRendererText()
            self.theme.pack_start(cell, True)
            self.theme.add_attribute(cell, "text", 0)
            # add to combobox
            for num,themename in enumerate(themes):
                 store.append([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
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
            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)







|







1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
            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__(dbg.UI, 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)
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





#-- startup progress bar
progresswin, progressbar = 0, 0
def gui_startup(p=0/100, 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)







|











|







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





#-- startup progress bar
progresswin, progressbar = 0, 0
def gui_startup(p=0/100.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)
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178

    # graphical
    if len(sys.argv) < 2:
    
        
        # prepare for threading in Gtk+ callbacks
        gobject.threads_init()
        gui_startup(1/100)
        
        # 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(100/100)
        gtk.main()
        
        
    # invoke command-line interface
    else:
        import cli
        cli.StreamTunerCLI()







|
















|







1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185

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


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