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

⌈⌋ ⎇ branch:  streamtuner2


Changes On Branch 7911337325807d54

Changes In Branch py3 Through [7911337325] Excluding Merge-Ins

This is equivalent to a diff from 7ef1553f61 to 7911337325

2014-04-27
22:19
Python3 support back into trunk check-in: 9ecea4fb26 user: mario tags: trunk
2014-04-08
21:53
fix dict + dict back into list join check-in: d09e020ecf user: mario tags: py3
21:50
more Python3 syntax fixes, introduce compat2and3 module check-in: 7911337325 user: mario tags: py3
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

Modified _package.epm from [d2e2c94cae] to [789f724b9f].

33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
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
f 644 root root /usr/share/streamtuner2/favicon.py		./favicon.py
f 644 root root /usr/share/streamtuner2/kronos.py		./kronos.py
f 644 root root /usr/share/streamtuner2/pq.py			./pq.py
#-- channels
d 755 root root /usr/share/streamtuner2/channels		-







|







33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
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/ahttp.py		./ahttp.py
f 644 root root /usr/share/streamtuner2/cli.py			./cli.py
f 644 root root /usr/share/streamtuner2/mygtk.py		./mygtk.py
f 644 root root /usr/share/streamtuner2/favicon.py		./favicon.py
f 644 root root /usr/share/streamtuner2/kronos.py		./kronos.py
f 644 root root /usr/share/streamtuner2/pq.py			./pq.py
#-- channels
d 755 root root /usr/share/streamtuner2/channels		-

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

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


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


main = None









|







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


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


main = None


77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
                pass

        
        # exec wrapper
        @staticmethod
        def run(cmd):
            if conf.windows:
 	        os.system("start \"%s\"")
 	    else:
                os.system(cmd + " &")


        # streamripper
        @staticmethod
        def record(url, audioformat="audio/mp3", listformat="text/x-href", append="", row={}):
            __print__( "record", url )







|
|







77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
                pass

        
        # exec wrapper
        @staticmethod
        def run(cmd):
            if conf.windows:
                os.system("start \"%s\"")
            else:
                os.system(cmd + " &")


        # streamripper
        @staticmethod
        def record(url, audioformat="audio/mp3", listformat="text/x-href", append="", row={}):
            __print__( "record", url )

Added ahttp.py version [11eca44aed].











































































































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
#
# encoding: UTF-8
# api: streamtuner2
# type: functions
# title: http download / methods
# description: http utility
# version: 1.4
#
#  Provides a http GET method with gtk.statusbar() callback.
#  And a function to add trailings slashes on http URLs.
#
#


from compat2and3 import urllib2, urlencode, urlparse, cookielib, StringIO, xrange
from gzip import GzipFile
from config import conf, __print__, dbg


#-- 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/2.1 (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/    
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 conf.debug:
        handlers[0].set_http_debuglevel(3)
        
    # content-encoding
    handlers[1] = ContentEncodingProcessor()
    
    # store cookies at runtime
    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


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

19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#  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








|







19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#  adds the required gtk Widgets manually.
#


import gtk
from mygtk import mygtk
from config import conf, __print__, dbg
import ahttp as http
import action
import favicon
import os.path
import xml.sax.saxutils
import re
import copy

145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
            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, [{}])







|





|







145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
            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 list(self.titles.items()):
                self.update_datamap(field, title=title)
            
            # prepare stream list
            if (not self.rowmap):
                for row in self.datamap:
                    for x in range(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, [{}])
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193

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







|







179
180
181
182
183
184
185
186
187
188
189
190
191
192
193

        # switch stream category,
        # load data,
        # update treeview content
        def load(self, category, force=False):
        
            # get data from cache or download
            if (force or not category in self.streams):
                new_streams = self.update_streams(category)
      
                if new_streams:
                
                    # modify
                    [self.postprocess(row) for row in new_streams]
      
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

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







|













|







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

            
        # 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 (url in row 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 "bookmarks" in self.parent.channels 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"):
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325

            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)
                







<



|







307
308
309
310
311
312
313

314
315
316
317
318
319
320
321
322
323
324

            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)

                    self.reload_categories()
                    self.display_categories()
                    self.current = self.categories.keys()[0]
                    __print__(dbg.STAT, self.current)
                    self.load(self.current)
            
                # load current category
                else:
                    __print__(dbg.STAT, "first_show: load current category");
                    self.load(self.current)
                

Modified channels/basicch.py from [2ad035d297] to [c507665eb7].

8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 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











|







8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# realaudio archive is not available anymore.
#
# Needs manual initialisation of categories first.
#


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




Modified channels/google.py from [3f0788927c] to [36a6ee6c2d].

47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
# 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/"







|







47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
# 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 ahttp as http


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


GOOGLE_DIRECTORY_ROOT	= "http://www.dmoz.org"
CATEGORIES_URL_POSTFIX	= "/Arts/Music/Sound_Files/MP3/Streaming/Stations/"

Modified channels/internet_radio_org_uk.py from [4dcc6f65d4] to [6c77259f65].

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



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




# streams and gui
class internet_radio_org_uk (ChannelPlugin):







|







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



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




# streams and gui
class internet_radio_org_uk (ChannelPlugin):

Modified channels/jamendo.py from [0df05c5caf] to [9d3d3ac039].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 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














|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 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 ahttp as http
from config import conf
from channels import *
from xml.sax.saxutils import unescape




Modified channels/links.py from [692f83efe7] to [e2e193464a].

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







|







|









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
        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.items():
          try:
            self.streams.append({
                "favourite": 1,
                "title": channel.title,
                "homepage": channel.homepage,
            })
          except: pass
        for title,homepage in self.default.items():
            self.streams.append({
                "title": title,
                "homepage": homepage,
            })

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

Modified channels/live365.py from [17860a6a46] to [10562bd5f9].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# api: st2
# title: live365 channel
#
# 2.0.9 fixed by Abhisek Sanyal
#




# 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












|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# api: st2
# title: live365 channel
#
# 2.0.9 fixed by Abhisek Sanyal
#




# streamtuner2 modules
from config import conf
from mygtk import mygtk
import ahttp as 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

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

8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 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 config import __print__, dbg
from xml.sax.saxutils import unescape










|







8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 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 ahttp as http
from config import conf
from channels import *
from config import __print__, dbg
from xml.sax.saxutils import unescape



Modified channels/musicgoal.py from [1d593a3fc6] to [faf1e3671e].

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



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

# python modules
import re
import json









|







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



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

# python modules
import re
import json


Modified channels/myoggradio.py from [37210dae1e] to [6c4dac0144].

22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

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








|







22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

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

import re
import json
from compat2and3 import StringIO
import copy



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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 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 config import __print__, dbg














|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

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


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



Modified channels/shoutcast.py from [3296716516] to [dd8d75b19e].

14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# 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








|







14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# 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 ahttp as 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

62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
        # 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 = []







|



|







62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
        # 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 = []

Modified channels/timer.py from [c50adafaed] to [73a61c99cb].

16
17
18
19
20
21
22

23
24
25
26
27
28
29
#
# 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 manually, 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









>







16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#
# 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 manually, streamtuner2 must be restarted for the changes to take effect.
#


from config import __print__, dbg
from channels import *
import kronos
from mygtk import mygtk
from action import action
import copy


72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
            "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")







|







73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
            "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 as e: __print__(dbg.ERR, "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")

Modified channels/tv.py from [e09d4b20ac] to [2d1fa5a0ac].

14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# 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):







|







14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# 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 ahttp as http
import lxml.etree




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

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

15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#



# 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







|







15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#



# streamtuner2 modules
from config import conf
from mygtk import mygtk
import ahttp as 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

Modified cli.py from [a682821821] to [ea53579a96].

13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#
#
#


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











|







13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#
#
#


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




Added compat2and3.py version [022ad69c97].





















































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
#
# encoding: UTF-8
# api: python 
# type: functions
# title: Python2 and Python3 compatibility
# version: 0.1
#
#  Renames some Python3 modules into their Py2 equivalent.
#  Slim local alternative to `six` module.
#


import sys


# Python 2
if sys.version_info < (3,0):

    # version tags
    PY2 = 1
    PY3 = 0

    # basic functions
    xrange = xrange
    range = xrange

    # urllib modules
    import urllib
    import urllib2
    from urllib import urlencode
    import urlparse
    import cookielib
    
    # filesys
    from StringIO import StringIO


# Python 3
else:

    # version tags
    PY2 = 0
    PY3 = 1

    # basic functions
    xrange = range

    # urllib modules
    import urllib.request as urllib
    import urllib.request as urllib2
    from urllib.parse import urlencode
    import urllib.parse as urlparse
    from http import cookiejar as cookielib
    
    # filesys
    from io import StringIO

    

Modified favicon.py from [f5d4a162a1] to [983556d34c].

24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
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 = []








|

<



|







24
25
26
27
28
29
30
31
32

33
34
35
36
37
38
39
40
41
42
43
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
from compat2and3 import xrange, urllib
import re

from config import conf
try: from processing import Process as Thread
except: from threading import Thread
import ahttp



# 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 = []

85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104

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







|




|







84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103

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



# extract domain name
def domain(url):
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
#  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()
        







|

|







190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
#  try: 
    # URL download
    r = urllib.urlopen(favicon)
    headers = r.info()
    
    # abort on
    if r.getcode() >= 300:
       raise Error("HTTP error" + r.getcode())
    if not headers["Content-Type"].lower().find("image/"):
       raise Error("can't use text/* content")
       
    # save file
    fn_tmp = fn+".tmp"
    f = open(fn_tmp, "wb")
    f.write(r.read(32768))
    f.close()
        
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
    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))










|







231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
    favicon = "".join(rx.findall(html))
    
    # url or
    if favicon.startswith("http://"):
       None
    # just /pathname
    else:
       favicon = ahttp.urlparse.urljoin(url, favicon)
       #favicon = "http://" + domain(url) + "/" + favicon

    # download
    direct_download(favicon, file(url))



262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
#
         
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):
    '''







|







261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
#
         
import operator
import struct

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


def load_icon(file, index=None):
    '''

Deleted http.py version [0d4dcfee94].

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



#-- 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/2.1 (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


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
































































































































































































































































































































































































































































Modified kronos.py from [6ae12b7565] to [24a87e43ef].

270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
            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)







|







270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
            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 as 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)
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
        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")







|







294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
        self.args=args
        self.kw=kw

    def __call__(self, schedulerref):
        """Execute the task action in the scheduler's thread."""
        try:
            self.execute()
        except Exception as 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")
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
            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):







|







462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
            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 as x:
                self.handle_exception(x)

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

    class ThreadedSingleTask(ThreadedTaskMixin, SingleTask):
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
        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())









|







529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
        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 as x:
                    self.handle_exception(x)
                os._exit(0)
            else:
                # we are the parent
                self.reschedule(schedulerref())


Modified mygtk.py from [98fa2cab88] to [91f32ed114].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#
# encoding: UTF-8
# api: python
# type: functions
# title: mygtk helper functions
# description: simplify usage of some gtk widgets
# version: 1.6
# author: mario
# license: public domain
#
#
# Wrappers around gtk methods. The TreeView method .columns() allows
# to fill a treeview. It adds columns and data rows with a mapping
# dictionary (which specifies many options and data positions).






|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
#
# encoding: UTF-8
# api: python
# type: functions
# title: mygtk helper functions
# description: simplify usage of some gtk widgets
# version: 1.7
# 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).
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




# 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
    ui_file = "gtk3.xml"
    __print__(gtk)
    __print__(gobject)
if not gtk:

    import pygtk
    import gtk
    import gobject
    ui_file = "gtk2.xml"

# filesystem
import os.path
import copy


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









>
>
>
>

>
>
>
>
>
>
>
>
>
>
|
<
|







|
|
<
>





<
<
<
<







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




# debug
from config import __print__, dbg

# filesystem
import os.path
import copy
import sys

if sys.version_info[0] >= 3:
    unicode = str


# gtk version
ver = 2   # 2=gtk2, 3=gtk3
if "--gtk3" in sys.argv:
    ver = 3
if sys.version_info >= (3, 0):
    ver = 3
# load gtk modules

if ver==3:
    from gi import pygtkcompat as pygtk
    pygtk.enable() 
    pygtk.enable_gtk(version='3.0')
    from gi.repository import Gtk as gtk
    from gi.repository import GObject as gobject
    from gi.repository import GdkPixbuf
    ui_file = "gtk3.xml"
    __print__(dbg.PROC, gtk)
    __print__(dbg.PROC, gobject)

else:
    import pygtk
    import gtk
    import gobject
    ui_file = "gtk2.xml"






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


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

                        __print__(cell)
                    # add column to treeview
                    widget.append_column(col)







|


















|







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
                    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 range(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 list(cell[3].items()):
                            col.add_attribute(rend, attr, val)
                        # next
                        datapos += 1

                        __print__(cell)
                    # add column to treeview
                    widget.append_column(col)
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
            # 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
                __print__(vartypes)
                __print__(rowmap)








|







154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
            # 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 range(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
                __print__(vartypes)
                __print__(rowmap)

Modified pq.py from [774d8a07cf] to [9ad9a3d426].

15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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)







|







15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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 as e:

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

    # error hint
    print("LXML is missing\n", e)

Modified st2.py from [0b9e0b6b81] to [9090947823].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#!/usr/bin/env python
# encoding: UTF-8
# api: python
# type: application
# title: streamtuner2
# description: directory browser for internet radio / audio streams
# depends: gtk, pygtk, xml.dom.minidom, threading, lxml, pyquery, kronos
# version: 2.0.9.5
# author: mario salzer
# license: public domain
# url: http://freshmeat.net/projects/streamtuner2
# config: <env name="http_proxy" value="" description="proxy for HTTP access" />  <env name="XDG_CONFIG_HOME" description="relocates user .config subdirectory" />
# category: multimedia
# 
#







|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#!/usr/bin/env python
# encoding: UTF-8
# api: python
# type: application
# title: streamtuner2
# description: directory browser for internet radio / audio streams
# depends: gtk, pygtk, xml.dom.minidom, threading, lxml, pyquery, kronos
# version: 2.0.9.6
# 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
# 
#
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110

# 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










|







96
97
98
99
100
101
102
103
104
105
106
107
108
109
110

# 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 ahttp
import action  # needs workaround... (action.main=main)
from channels import *
import favicon
#from pq import pq



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
                "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,
            }.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)


          

        #-- Shortcut for glade.get_widget()
        # Allows access to widgets as direct attributes instead of using .get_widget()
        # Also looks in self.channels[] for the named channel plugins
        def __getattr__(self, name):
            if (self.channels.has_key(name)):
                return self.channels[name]     # like self.shoutcast
            else:
                return self.get_object(name)   # or gives an error if neither exists


        # custom-named widgets are available from .widgets{} not via .get_widget()
        def get_widget(self, name):
            if self.widgets.has_key(name):
                return self.widgets[name]
            else:
                return gtk.Builder.get_object(self, name)
                


                







|
















|







|







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
                "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,
            }.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)


          

        #-- Shortcut for glade.get_widget()
        # Allows access to widgets as direct attributes instead of using .get_widget()
        # Also looks in self.channels[] for the named channel plugins
        def __getattr__(self, name):
            if (name in self.channels):
                return self.channels[name]     # like self.shoutcast
            else:
                return self.get_object(name)   # or gives an error if neither exists


        # custom-named widgets are available from .widgets{} not via .get_widget()
        def get_widget(self, name):
            if name in self.widgets:
                return self.widgets[name]
            else:
                return gtk.Builder.get_object(self, name)
                


                
521
522
523
524
525
526
527

528


529
530
531
532
533
534
535
                        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()













>
|
>
>







521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
                        gtk.rc_parse(f)
                pass


        # end application and gtk+ main loop
        def gtk_main_quit(self, widget, *x):
            if conf.auto_save_appstate:
                try:  # doesn't work with gtk3 yet (probably just hooking at the wrong time)
                    self.app_state(widget)
                except:
                    None
            gtk.main_quit()






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









|












|







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
1179
#-- run main                                ---------------------------------------------
if __name__ == "__main__":

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

    # graphical
    if len(sys.argv) < 2 or "--gtk3" in sys.argv:
    
        
        # 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
        ahttp.feedback = main.status  # http module gives status feedbacks too
        
        # first invocation
        if (conf.get("firstrun")):
            config_dialog.open(None)
            del conf.firstrun