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

⌈⌋ ⎇ branch:  streamtuner2


Diff

Differences From Artifact [cb67ede034]:

To Artifact [1c74200e99]:


1
2
3
4
5
6
7
8
9
10
11
1
2
3

4
5
6
7
8
9
10



-







#
# 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:
27
28
29
30
31
32
33
34
35

36


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


33
34
35
36
37
38
39
40
41
42
43







-
-
+

+
+







import inspect


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



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




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


        # start
177
178
179
180
181
182
183
184

185
186
187
188
189
190
191
177
178
179
180
181
182
183

184
185
186
187
188
189
190
191







-
+







                else:
                    return # file not found
                # decode
                r = json.load(f)
                f.close()
                return r
            except Exception as e:
                print(dbg.ERR, "PSON parsing error (in "+name+")", e)
                print(dbg.ERR, "JSON parsing error (in "+name+")", e)
            

        # recursive dict update
        def update(self, with_new_data):
            for key,value in with_new_data.items():
                if type(value) == dict:
                    self[key].update(value)
208
209
210
211
212
213
214
215

216
217
218
219
220
221
222
223
224

225
226
227
228





229
230
231
232
233
234
235







236
237
238

239



240
241
242
243
244
















245


246
247
248






249
250
251
252



253
254
255






















256
257
258
259
260
261
262

263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281





282
283
284

285
208
209
210
211
212
213
214

215









216
217



218
219
220
221
222
223






224
225
226
227
228
229
230
231
232
233
234
235
236
237
238





239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257



258
259
260
261
262
263




264
265
266
267


268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295

296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313


314
315
316
317
318
319


320








-
+
-
-
-
-
-
-
-
-
-
+

-
-
-
+
+
+
+
+

-
-
-
-
-
-
+
+
+
+
+
+
+



+

+
+
+
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+
+
-
-
-
+
+
+
+
+
+
-
-
-
-
+
+
+

-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+






-
+

















-
-
+
+
+
+
+

-
-
+
-
                    return d+"/"+file



# Plugin meta data extraction
#
# Extremely crude version for Python and streamtuner2 plugin usage.
# Doesn't check top-level comment coherency.
# Fetches module source, or reads from filename / out of zip package.
# 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=""):
def plugin_meta(fn=None, src=None, frame=1):

    # filename of caller
    if not fn:
        fn = inspect.getfile(sys._getframe(frame))
    # get source directly from caller
    if not src and not fn:
        module = inspect.getmodule(sys._getframe(frame))
        fn = inspect.getsourcefile(module)
        src = inspect.getcomments(module)

    # 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)
    # within zip archive or dir?
    elif fn:
        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 = {
        "id": re.sub("\.\w+$", "", os.path.basename(fn or "")),
        "fn": fn,
        "title": fn, "description": "no description", "config": [],
        "type": "module", "api": "python", "doc": ""
    }
        "id": os.path.basename(fn).replace(".py", "")
    }
    # extraction
    for field in rx_meta.findall(src):
        meta[field[0]] = rx_lines.sub("", field[1])

    # extract coherent comment block, split doc section
    src = rx.comment.search(src)
    if not src:
        __print__(dbg.ERR, "Couldn't read source meta information", fn)
        return meta
    src = src.group(0)
    src = rx.hash.sub("", src).strip()
    if src.find("\n\n") > 0:
        src, meta["doc"] = src.split("\n\n", 1)
    

    # split into dict
    for field in rx.keyval.findall(src):
        meta[field[0]] = field[1].strip()
    meta["config"] = plugin_meta_config(meta.get("config") or "")

    return meta

    # unpack config: structures
    meta["config"] = [
        dict([field for field in rx_fields.findall(entry)])
# unpack config: structures
def plugin_meta_config(str):
    config = []
    for entry in rx.config.findall(str):
        opt = { "type": None, "name": None, "description": "", "value": None }
        for field in rx.options.findall(entry):
        for entry in rx_config.findall(meta.get("config", ""))
    ]
        
    return meta
            opt[field[0]] = (field[1] or field[2] or field[3] or "").strip()
        config.append(opt)
    return config



# Comment extraction regexps
class rx:
    zipfn   = re.compile(r"""
        ^ (.+  \.(?:zip|pyz|pyzw|pyzip)        # zip-wrapping extensions
        (?:\.py)? ) /(\w.*) $
    """, re.X)
    comment = re.compile(r"""(^ {0,4}#.*\n)+""", re.M)
    hash    = re.compile(r"""(^ {0,4}# *)""", re.M)
    keyval  = re.compile(r"""
        ^([\w-]+):(.*$(?:\n(?![\w-]+:).+$)*)      # plain key:value lines
    """, re.M|re.X)
    config  = re.compile(r"""
        [\{\<] (.+?) [\}\>]                    # JSOL/YAML scheme {...} dicts
    """, re.X)
    options = re.compile(r"""
        ["':$]?   (\w+)  ["']?                 # key or ":key" or '$key'
        \s* [:=] \s*                           # "=" or ":"
     (?:  "  ([^"]*)  " 
       |  '  ([^']*)  '                        #  "quoted" or 'singl' values
       |     ([^,]*)                           #  or unquoted literals
     )
    """, re.X)




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


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


   
#-- actually fill global conf instance


   

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