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

⌈⌋ ⎇ branch:  streamtuner2


Check-in [d3b1418bc6]

Overview
Comment:rename http to ahttp to avoid conflict with Python3 modules, change .iteritems and xrange, remove same remaining plain print statements
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | py3
Files: files | file ages | folders
SHA1: d3b1418bc6a9ef41accbc1954941659a0295ebf4
User & Date: mario on 2014-04-08 21:16:12
Other Links: branch diff | manifest | tags
Context
2014-04-08
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
Changes

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

33
34
35
36
37
38
39
40

41
42
43
44
45
46
47
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/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
19
20
21
22
23
24
25

26
27
28
29
30
31
32
33







-
+







#
#
#


import re
import os
import http
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
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("start \"%s\"")
            else:
                os.system(cmd + " &")


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

Modified ahttp.py from [0d4dcfee94] to [2dbe950381].

9
10
11
12
13
14
15
16



17
18
19




20
21

22
23
24





25

26
27
28
29
30
31
32
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







-
+
+
+



+
+
+
+


+
-
-
-
+
+
+
+
+

+







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


# Python 2.x            
try:
    import urllib2
    from urllib import urlencode
    import urlparse
    import cookielib
    from StringIO import StringIO
# Python 3.x
except:
    import urllib.request as urllib2
    from urllib.parse import urlencode
    import urllib.parse.urlencode as urlencode
import config
from config import __print__, dbg
    import urllib.parse as urlparse
    from http import cookiejar as cookielib
    from io import StringIO

from gzip import GzipFile

from config import conf, __print__, dbg


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



#-- chains to progress meter and status bar in main window
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
154
155
156
157
158
159
160


161
162
163
164
165
166
167







-
-







    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
194
195
196
197
198
199
200
201

202
203
204
205
206
207
208
209
210
211
212
213
214
215
202
203
204
205
206
207
208

209
210
211
212
213
214
215

216
217
218
219
220
221
222







-
+






-







if urllib2:

    # config 1
    handlers = [None, None, None]
    
    # base
    handlers[0] = urllib2.HTTPHandler()
    if config.conf.debug:
    if 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) )


Modified channels/_generic.py from [f927301f5f] to [5d7f7dc2ae].

19
20
21
22
23
24
25
26

27
28
29
30
31
32
33
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 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
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():
            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 xrange(2, len(row)):
                    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, [{}])
247
248
249
250
251
252
253
254

255
256
257
258
259
260
261
247
248
249
250
251
252
253

254
255
256
257
258
259
260
261







-
+







        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"))):
                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
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)
                    print("reload categories");
                    self.reload_categories()
                    self.display_categories()
                    self.current = self.categories.keys()[0]
                    print self.current
                    __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
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
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
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
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
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
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
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
import ahttp as http
from config import conf
from channels import *
from xml.sax.saxutils import unescape




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
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
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
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
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
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
import ahttp as http
from channels import *

# python modules
import re
import json


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
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
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 [0c3da6eedb].

14
15
16
17
18
19
20
21

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

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

14
15
16
17
18
19
20
21

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

20
21
22
23
24
25
26
27







-
+







#
#
#


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




Modified favicon.py from [f5d4a162a1] to [997ecd51ce].

26
27
28
29
30
31
32
33
34
35
36
37

38
39
40
41
42
43
44
26
27
28
29
30
31
32

33
34
35

36
37
38
39
40
41
42
43







-



-
+







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
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
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 = http.ajax("http://www.google.de/search?hl=de&q="+title, None)
            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"] = http.fix_url(url.group(1))
                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
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 "HTTP error", r.getcode()
       raise Error("HTTP error" + r.getcode())
    if not headers["Content-Type"].lower().find("image/"):
       raise "can't use text/* content"
       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
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 = urlparse.urljoin(url, favicon)
       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
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, e:
except Exception as e:
    print("no PIL", e)
    always_google = 1
    only_google = 1


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

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

1
2
3
4
5
6
7

8
9
10
11
12
13
14
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
# 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
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
# gtk modules
# load gtk modules
gtk = 0   # 0=gtk2, else gtk3
if gtk:
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__(gtk)
    __print__(gobject)
    __print__(dbg.PROC, gtk)
    __print__(dbg.PROC, gobject)
if not gtk:
else:
    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)


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
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 xrange(2, len(desc)):
                    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 cell[3].iteritems():
                        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
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 xrange(2, len(desc)):
                        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 st2.py from [0b9e0b6b81] to [1b132a9fda].

1
2
3
4
5
6
7
8

9
10
11
12
13
14
15
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
# 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
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 ahttp
import action  # needs workaround... (action.main=main)
from channels import *
import favicon
#from pq import pq



233
234
235
236
237
238
239
240

241
242
243
244
245
246
247
248

249
250
251
252
253
254
255
233
234
235
236
237
238
239

240
241
242
243
244
245
246
247

248
249
250
251
252
253
254
255







-
+







-
+








          

        #-- 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)):
            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 self.widgets.has_key(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
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
                self.app_state(widget)
                    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
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:
    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
        http.feedback = main.status  # http module gives status feedbacks too
        ahttp.feedback = main.status  # http module gives status feedbacks too
        
        # first invocation
        if (conf.get("firstrun")):
            config_dialog.open(None)
            del conf.firstrun