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

⌈⌋ ⎇ branch:  streamtuner2


Check-in [acaea4439d]

Overview
Comment:Implement plugin meta data extraction in config.plugin_meta() instead of channels.__init__
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | trunk
Files: files | file ages | folders
SHA1: acaea4439df7b4577156aaf10d79af1855a73549
User & Date: mario on 2015-03-28 07:26:35
Other Links: manifest | tags
Context
2015-03-28
07:27
Move bookmarks channel out of main, add proper plugin description. (Can't be disabled, is still a core plugin, and manually imported anyway.) check-in: b9dc5e172c user: mario tags: trunk
07:26
Implement plugin meta data extraction in config.plugin_meta() instead of channels.__init__ check-in: acaea4439d user: mario tags: trunk
07:25
Disable some debugging, move gui_startup() to mygtk collection, allow markup for mygtk.label() text. check-in: 164043075d user: mario tags: trunk
Changes

Modified channels/_generic.py from [e9222413a9] to [d7e715823c].

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
#  file. They derive from the ChannelPlugins class instead, which
#  adds the required gtk Widgets manually.
#


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



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


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

        # desc
        module = "generic"

        title = "GenericChannel"
        homepage = "http://fossil.include-once.org/streamtuner2/"
        base_url = ""
        listformat = "audio/x-scpls"
        audioformat = "audio/mpeg" # fallback value
        config = []
        has_search = False







|







>
















>







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
#  file. They derive from the ChannelPlugins class instead, which
#  adds the required gtk Widgets manually.
#


import gtk
from mygtk import mygtk
from config import *
import ahttp as http
import action
import favicon
import os.path
import xml.sax.saxutils
import re
import copy
import inspect


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


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

        # desc
        module = "generic"
        meta = {}
        title = "GenericChannel"
        homepage = "http://fossil.include-once.org/streamtuner2/"
        base_url = ""
        listformat = "audio/x-scpls"
        audioformat = "audio/mpeg" # fallback value
        config = []
        has_search = False
96
97
98
99
100
101
102


103
104
105
106
107
108
109

        # constructor
        def __init__(self, parent=None):
        
            #self.streams = {}
            self.gtk_list = None
            self.gtk_cat = None



            # only if streamtuner2 is run in graphical mode        
            if (parent):
                self.cache()
                self.gui(parent)
            pass
            







>
>







98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113

        # constructor
        def __init__(self, parent=None):
        
            #self.streams = {}
            self.gtk_list = None
            self.gtk_cat = None
            self.meta = plugin_meta(inspect.getsourcefile(inspect.getmodule(self)))
            self.config = self.meta["config"]

            # only if streamtuner2 is run in graphical mode        
            if (parent):
                self.cache()
                self.gui(parent)
            pass
            

Modified config.py from [59ff23a453] to [cb67ede034].

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
#
# encoding: UTF-8
# api: streamtuner2

# type: class
# title: global config object
# description: reads ~/.config/streamtuner/*.json files

#
# In the main application or module files which need access
# to a global conf object, just import this module as follows:
#
#   from config import conf
#
# Here conf is already an instantiation of the underlying
# Config class.
#


import os
import sys
import json
import gzip
import platform





# export symbols
__all__ = ["conf", "__print__", "dbg"]



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





>



>
















>
>
>



|







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
#
# encoding: UTF-8
# api: streamtuner2
#  .2
# type: class
# title: global config object
# description: reads ~/.config/streamtuner/*.json files
# config: {type:var, name:z, description:v}
#
# In the main application or module files which need access
# to a global conf object, just import this module as follows:
#
#   from config import conf
#
# Here conf is already an instantiation of the underlying
# Config class.
#


import os
import sys
import json
import gzip
import platform
import re
import zipfile
import inspect


# export symbols
__all__ = ["conf", "__print__", "dbg", "plugin_meta"]



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


42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
            self.__dict__ = self  # let's pray this won't leak memory due to recursion issues

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







<
|







47
48
49
50
51
52
53

54
55
56
57
58
59
60
61
            self.__dict__ = self  # let's pray this won't leak memory due to recursion issues

            # prepare
            self.defaults()
            self.xdg()
            
            # runtime

            self.share = os.path.dirname(__file__)
            
            # settings from last session
            last = self.load("settings")
            if (last):
                self.update(last)
                self.migrate()
            # store defaults in file
201
202
203
204
205
206
207















































208
209
210
211
212
213
214
        def find_in_dirs(self, dirs, file):
            for d in dirs:
                if os.path.exists(d+"/"+file):
                    return d+"/"+file



















































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









>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>







205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
        def find_in_dirs(self, dirs, file):
            for d in dirs:
                if os.path.exists(d+"/"+file):
                    return d+"/"+file



# Plugin meta data extraction
#
# Extremely crude version for Python and streamtuner2 plugin usage.
# Doesn't check top-level comment coherency.
# But supports plugins within python zip archives.
#
rx_zipfn  = re.compile(r"""^(.+\.(?:zip|pyz|pyzw|pyzip)(?:\.py)?)/(\w.*)$""")
rx_meta   = re.compile(r"""^ {0,4}# *([\w-]+): *(.+(\n *#  +(?![\w-]+:).+)*)$""", re.M)  # Python comments only
rx_lines  = re.compile(r"""\n *# """)        # strip multi-line prefixes
rx_config = re.compile(r"""[\{\<](.+?)[\}\>]""")     # extract only from JSOL/YAML scheme
rx_fields = re.compile(r"""["']?(\w+)["']?\s*[:=]\s*["']?([^,]+)(?<!["'])""")  # simple key: value entries
#
def plugin_meta(fn=None, frame=1, src=""):

    # filename of caller
    if not fn:
        fn = inspect.getfile(sys._getframe(frame))

    # within zip archive?
    zip = rx_zipfn.match(fn)
    if zip and zipfile.is_zipfile(zip.group(1)):
        src = zipfile.ZipFile(zip.group(1), "r").read(zip.group(2))
    else:
        src = open(fn).read(4096)

    # defaults
    meta = {
        "fn": fn,
        "id": os.path.basename(fn).replace(".py", "")
    }
    # extraction
    for field in rx_meta.findall(src):
        meta[field[0]] = rx_lines.sub("", field[1])

    # unpack config: structures
    meta["config"] = [
        dict([field for field in rx_fields.findall(entry)])
        for entry in rx_config.findall(meta.get("config", ""))
    ]
        
    return meta







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


227
228
229
230
231
232
233
234
235
236
237


   
#-- actually fill global conf instance
conf = ConfigDict()
if conf:
    __print__(dbg.PROC, "ConfigDict() initialized")












<
<
<
278
279
280
281
282
283
284
285





   
#-- actually fill global conf instance
conf = ConfigDict()
if conf:
    __print__(dbg.PROC, "ConfigDict() initialized")