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

⌈⌋ ⎇ branch:  streamtuner2


Diff

Differences From Artifact [8817626a3c]:

To Artifact [28c3c558fd]:


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
#
# encoding: UTF-8
# api: streamtuner2
# type: class
# title: global config object
# description: reads ~/.config/streamtuner/*.json files
# config:
#    { arg: -d,     type: str,      name: disable[], description: Omit plugin from initialization.  }
#    { arg: -e,     type: str,      name: enable[],  description: Add channel plugin.  }
#    { arg: --gtk3, type: boolean,  name: gtk3,      description: Start with Gtk3 interface. }
#    { arg: -D,     type: boolean,  name: debug,     description: Enable debug messages on console }
#    { arg: action, type: str *,    name: action[],  description: CLI interface commands. }
#    { arg: -x,     type: boolean,  name: exit,      hidden: 1 }
#    { arg: --nt,   type: boolean,  name: nothreads, description: Disable threading/gtk_idle UI. }
# version: 2.7
# priority: core

#

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

# Also provides the logging function log.TYPE(...) and basic

# plugin handling code: plugin_meta() and module_list(),


# and the relative get_data() alias (files from pyzip/path).

#




from __future__ import print_function
import os
import sys
import json
import gzip
import platform
import re
from compat2and3 import gzip_decode, find_executable
import zlib
import zipfile
import inspect
import pkgutil
import argparse




# export symbols
__all__ = ["conf", "log", "plugin_meta", "module_list", "get_data", "find_executable"]


#-- create a stub 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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61

# encoding: UTF-8
# api: streamtuner2
# type: class
# title: global config object
# description: reads ~/.config/streamtuner/*.json files
# config:
#    { arg: -d,     type: str,      name: disable[], description: Omit plugin from initialization.  }
#    { arg: -e,     type: str,      name: enable[],  description: Add channel plugin.  }
#    { arg: --gtk3, type: boolean,  name: gtk3,      description: Start with Gtk3 interface. }
#    { arg: -D,     type: boolean,  name: debug,     description: Enable debug messages on console }
#    { arg: action, type: str *,    name: action[],  description: CLI interface commands. }
#    { arg: -x,     type: boolean,  name: exit,      hidden: 1 }
#    { arg: --nt,   type: boolean,  name: nothreads, description: Disable threading/gtk_idle UI. }
# version: 2.7
# priority: core
# depends: pluginconf >= 0.1, os, json, re, zlib, pkgutil
#
# Ties together the global conf.* object. It's typically used
# in the main application and modules with:

#
#   from config import *
#
# The underlying ConfigDict class is already instantiated and
# imported as `conf` then.
#
# With .save() or .load() it handles storage as JSON. Both
# utility functions are also used for other cache files.
# More specific config stores are available per .netrc(),
# and .init_args().
#
# Whereas plugin utility code is available per plugin_meta(),
# module_list(), and get_data(). There's a prepared function
# for add_plugin_config() on initialization.
#
# Also provides a simple logging interface with log.TYPE(...),
# which is also pre-instantiated.


from __future__ import print_function
import os
import sys
import json
import gzip
import platform
import re
from compat2and3 import gzip_decode, find_executable
import zlib
import zipfile
import inspect
import pkgutil
import argparse
from pluginconf import plugin_meta, module_list, get_data
import pluginconf


# export symbols
__all__ = ["conf", "log", "plugin_meta", "module_list", "get_data", "find_executable"]


#-- create a stub instance of config object
conf = object()
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
        self.playlist_asis = 0
        self.google_homepage = 0
        self.windows = platform.system()=="Windows"
        self.pyquery = 1
        self.debug = 0

        
    # each plugin has a .config dict list, we add defaults here
    def add_plugin_defaults(self, meta, module=""):
    
        # options
        config = meta.get("config", [])
        for opt in config:
            if ("name" in opt) and ("value" in opt) and (opt["name"] not in vars(self)):
                self.__dict__[opt["name"]] = opt["value"]

        # plugin state
        if module and module not in conf.plugins:
             conf.plugins[module] = meta.get("priority") in ("core", "builtin", "always", "default", "standard")


    # look at system binaries for standard audio players
    def find_player(self, typ="audio", default="xdg-open"):
        players = {
           "audio": ["audacious %m3u", "audacious2", "exaile %pls", "xmms2", "banshee", "amarok %pls", "clementine", "qmmp", "quodlibet", "aqualung", "mp3blaster %m3u", "vlc --one-instance", "totem"],
           "video": ["umplayer", "xnoise", "gxine", "totem", "vlc --one-instance", "parole", "smplayer", "gnome-media-player", "xine", "bangarang"],







|
|
|
<
<
<
<
<
<
<
<
<







144
145
146
147
148
149
150
151
152
153









154
155
156
157
158
159
160
        self.playlist_asis = 0
        self.google_homepage = 0
        self.windows = platform.system()=="Windows"
        self.pyquery = 1
        self.debug = 0

        
    # Add plugin names and default config: options from each .meta
    def add_plugin_defaults(self, meta, name):
        pluginconf.add_plugin_defaults(self, self.plugins, meta, name)











    # look at system binaries for standard audio players
    def find_player(self, typ="audio", default="xdg-open"):
        players = {
           "audio": ["audacious %m3u", "audacious2", "exaile %pls", "xmms2", "banshee", "amarok %pls", "clementine", "qmmp", "quodlibet", "aqualung", "mp3blaster %m3u", "vlc --one-instance", "totem"],
           "video": ["umplayer", "xnoise", "gxine", "totem", "vlc --one-instance", "parole", "smplayer", "gnome-media-player", "xine", "bangarang"],
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
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
            if server in netrc:
                return netrc[server]


    # Use config:-style definitions for argv extraction,
    # such as: { arg: -D, name: debug, type: bool }
    def init_args(self, ap):
        for opt in plugin_meta(frame=0).get("config"):
            kwargs = self.argparse_map(opt)
            if kwargs:
                #print kwargs
                ap.add_argument(*kwargs.pop("args"), **kwargs)
        return ap.parse_args()


    # Copy args fields into conf. dict
    def apply_args(self, args):
        self.debug = args.debug
        self.nothreads = args.nothreads
        if args.exit:
            sys.exit(1)
        for p_id in (args.disable or []):
            self.plugins[p_id] = 0
        for p_id in (args.enable or []):
            self.plugins[p_id] = 1


    # Transform config: description into quirky ArgumentParser args.
    #
    # · An option entry requires an arg: parameter - unlike regular plugin options:
    #     { arg: -i, name: input[], type: str, description: input files }
    # · Where list elements are indicated by appending `[]` to names, or `*`onto type
    #   specifiers (alternatively `?`, `+` or a numeric count).
    # · Types `str` or `int` and `bool` are recognized (bool with false/true optionals).
    # · Entries can also carry a `hidden: 1` or `required: 1` attribute.
    # · And `help:` is an alias to `description:`
    # · Same for `default:` instead of the normal `value:`
    # · And `type: select` utilizes the `select: a|b|c` format as uaual.
    # · ArgParsers const=, metavar= flag, or type=file are not aliased here.
    #
    def argparse_map(self, opt):
        if not ("arg" in opt and opt["name"] and opt["type"]):
            return {}

        # Extract --flag names
        args = opt["arg"].split() + re.findall("-+\w+", opt["name"])

        # Prepare mapping options
        typing = re.findall("bool|str|\[\]|const|false|true", opt["type"])
        naming = re.findall("\[\]", opt["name"])
        name   = re.findall("(?<!-)\\b\\w+", opt["name"])
        nargs  = re.findall("\\b\d+\\b|[\?\*\+]", opt["type"]) or [None]
        is_arr = "[]" in (naming + typing) and nargs == [None]
        is_bool= "bool" in typing
        false_b = "false" in typing or opt["value"] in ("0", "false")
        #print "\nname=", name, "is_arr=", is_arr, "is_bool=", is_bool, "bool_d=", false_b, "naming=", naming, "typing=", typing

        # Populate partially - ArgumentParser has aversions to many parameter combinations
        kwargs = dict(
            args     = args,
            dest     = name[0] if not name[0] in args else None,
            action   = is_arr and "append"  or  is_bool and false_b and "store_false"  or  is_bool and "store_true"  or  "store",
            nargs    = nargs[0],
            default  = opt.get("default") or opt["value"],
            type     = None if is_bool  else  ("int" in typing and int  or  "bool" in typing and bool  or  str),
            choices  = opt["select"].split("|") if "select" in opt else None,
            required = "required" in opt or None,
            help     = opt["description"] if not "hidden" in opt else argparse.SUPPRESS
        )
        return {k:w for k,w in kwargs.items() if w is not None}


# Retrieve content from install path or pyzip archive (alias for pkgutil.get_data)
#
def get_data(fn, decode=False, gz=False, file_base="config"):
    try:
        bin = pkgutil.get_data(file_base, fn)
        if gz:
            bin = gzip_decode(bin)
        if decode:
            return bin.decode("utf-8", errors='ignore')
        else:
            return str(bin)
    except:
        log.WARN("get_data() didn't find:", fn)


# Search through ./channels/ and get module basenames.
# (Reordering channel tabs is now done by uikit.apply_state.)
#
def module_list(plugin_base="channels"):

    # Should list plugins within zips as well as local paths
    ls = pkgutil.iter_modules([plugin_base, conf.share+"/"+plugin_base, conf.dir+"/plugins"])
    return [name for loader,name,ispkg in ls]



# Plugin meta data extraction
#
# Extremely crude version for Python and streamtuner2 plugin usage.
# But can fetch from different sources:
#  · fn= to read from literal files, out of a .pyzip package
#  · src= to extract from pre-read script code
#  · module= utilizes pkgutil to read 
#  · frame= automatically extract comment header from caller
#
def plugin_meta(fn=None, src=None, module=None, frame=1, plugin_base=["channels", "plugins"]):

    # try via pkgutil first
    if module:
       fn = module
       for base in plugin_base:
           try:
               src = pkgutil.get_data(base, fn+".py")
               if src: break
           except:
               continue  # plugin_meta_extract() will print a notice later

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

    # real filename/path
    elif fn and os.path.exists(fn):
        src = open(fn).read(4096)

    # assume it's within a zip
    elif fn:
        intfn = ""
        while fn and len(fn) and not os.path.exists(fn):
            fn, add = os.path.split(fn)
            intfn = add + "/" + intfn
        if len(fn) >= 3 and intfn and zipfile.is_zipfile(fn):
            src = zipfile.ZipFile(fn, "r").read(intfn.strip("/"))
            
    if not src:
        src = ""
    if type(src) is not str:
        src = src.decode("utf-8", errors='replace')

    return plugin_meta_extract(src, fn)


# Actual comment extraction logic
def plugin_meta_extract(src="", fn=None, literal=False):

    # defaults
    meta = {
        "id": os.path.splitext(os.path.basename(fn or "")),
        "fn": fn,
        "title": fn, "description": "no description", "config": [],
        "type": "module", "api": "python", "doc": ""
    }

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

    # key:value fields 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
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):
            opt[field[0]] = (field[1] or field[2] or field[3] or "").strip()
        config.append(opt)
    return config

# Comment extraction regexps
class rx:
    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)



# Simplified print wrapper: `log.err(...)`
class log_printer(object):

    # Wrapper
    method = None







|
|


















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







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
321
322
323
324
            if server in netrc:
                return netrc[server]


    # Use config:-style definitions for argv extraction,
    # such as: { arg: -D, name: debug, type: bool }
    def init_args(self, ap):
        for opt in plugin_meta(frame=1).get("config"):
            kwargs = pluginconf.argparse_map(opt)
            if kwargs:
                #print kwargs
                ap.add_argument(*kwargs.pop("args"), **kwargs)
        return ap.parse_args()


    # Copy args fields into conf. dict
    def apply_args(self, args):
        self.debug = args.debug
        self.nothreads = args.nothreads
        if args.exit:
            sys.exit(1)
        for p_id in (args.disable or []):
            self.plugins[p_id] = 0
        for p_id in (args.enable or []):
            self.plugins[p_id] = 1























































































































































































# Simplified print wrapper: `log.err(...)`
class log_printer(object):

    # Wrapper
    method = None
534
535
536
537
538
539
540



541





542
543
544
545
546
        "STAT": "37m",          # gray   CONFIG STATE
    }

# instantiate right away
log = log_printer()











   

#-- populate global conf instance
conf = ConfigDict()
log.PROC("ConfigDict() initialized")







>
>
>

>
>
>
>
>
|

<
<
<
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374



        "STAT": "37m",          # gray   CONFIG STATE
    }

# instantiate right away
log = log_printer()


# populate global conf instance
conf = ConfigDict()
log.PROC("ConfigDict() initialized")

# tie in pluginconf.*
pluginconf.log_WARN = log.WARN
pluginconf.log_ERR = log.ERR
pluginconf.module_base = "config"
pluginconf.plugin_base = ["channels", conf.share + "/channels", conf.dir + "/plugins"]