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

⌈⌋ branch:  streamtuner2


Diff

Differences From Artifact [af23ce772a]:

To Artifact [ca9c88608a]:


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







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














-
+






#
# encoding: UTF-8
# api: streamtuner2
# type: class
# title: global config object
# description: reads ~/.config/streamtuner/*.json files
# config:
#    { arg: -d,     type: str,      name: plugin[],  description: omit plugin from initialization  }
#    { arg: --gtk3, type: boolean,  name: gtk3,      description: use gtk3 interface }
#    { arg: -D,     type: boolean,  name: debug,     description: enable debug messages on console }
#    { arg: action, type: str*,     name: action[],  description: commandline actions }
#    { arg: -x,     type: boolean,  name: exit,      description: terminate right away }
#    { 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 }
# version: 2.5
# 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 __print__, 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
83
84
85
86
87
88
89

90
91
92
93
94
95
96
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100







+






        # store defaults in file
        else:
            self.save("settings")
            self.firstrun = 1
        
        # add argv
        self.args = self.init_args(argparse.ArgumentParser())
        self.apply_args(self.args)


    # some defaults
    def defaults(self):
        self.play = {
           "audio/mpeg": self.find_player(),
           "audio/ogg": self.find_player(),
167
168
169
170
171
172
173
174



175
176
177
178
179
180
181
171
172
173
174
175
176
177

178
179
180
181
182
183
184
185
186
187







-
+
+
+






            os.makedirs(self.dir)
       

    # store some configuration list/dict into a file                
    def save(self, name="settings", data=None, gz=0, nice=0):
        name = name + ".json"
        if (data is None):
            data = dict(self.__dict__)  # ANOTHER WORKAROUND: typecast to plain dict(), else json filter_data sees it as object and str()s it
            data = vars(self)
            if "args" in data:
                data.pop("args")
            nice = 1
        # check for subdir
        if (name.find("/") > 0):
            subdir = name[0:name.find("/")]
            subdir = self.dir + "/" + subdir
            if (not os.path.exists(subdir)):
                os.mkdir(subdir)
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
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

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







+
-
+
+


-
-
+
+
-
-
-
-
+


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








-
+


-
+
+
+
+
+

-
+



-
+

-
-
+
+

-
-
+
+

-
+

+
+
+
+
+
+
+






                     netrc = parser(self.xdg() + "/netrc").hosts
            except:
                pass
        for server in varhosts:
            if server in netrc:
                return netrc[server]


    # Use config: definitions for argv extraction
    # 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)
            #print kwargs
            if [kwargs for kwargs in [self.argparse_map(opt)]]:
                #print kwargs
            if kwargs:
                args = kwargs["args"]
                del kwargs["args"]
                ap.add_argument(*args, **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
        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 list
    # 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|\[\]|store|append|const", opt["type"])
        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]
        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 is highly fragile with combinations of named params
        # Populate partially - ArgumentParser has aversions to many parameter combinations
        kwargs = {
            "args": args,
            "dest": name[0] if not name[0] in args else None,
            "action": "append" if "[]" in (naming+typing) else ("store_true" if "bool" in typing else "store"),
            "action": self.sw( {is_arr: "append"}, {is_bool and false_b: "store_false"}, {is_bool: "store_true"}, {1: "store"} ),
            "nargs": nargs[0],
            "default": opt["value"],
           # "type":  int if "int" in typing else bool if "bool" in typing else str,
            "default": opt.get("default") or opt["value"],
            "type":  self.sw( {is_bool: None}, {"int" in typing: int}, {"bool" in typing: bool}, {1: str} ),
            "choices": opt["select"].split("|") if "select" in opt else None,
           # "required": "required" in opt,
            "help": opt["description"] or "",
            "required": "required" in opt or None,
            "help": opt["description"] if not "hidden" in opt else argparse.SUPPRESS,
        }
        return dict((k,w) for k,w in kwargs.items() if w is not None)
        return {k:w for k,w in kwargs.items() if w is not None}


    # Shorthand switch, returns first value for cond==true from list of {cond:val} arguments
    def sw(self, *args):
        for pair in args:
            [(cond,val)] = pair.items()
            if cond:
                return val


# 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)
432
433
434
435
436
437
438
439

440
441
442
443
444
445
446
472
473
474
475
476
477
478

479
480
481
482
483
484
485
486







-
+










# wrapper for all print statements
def __print__(*args):
    if "debug" in conf and conf.debug or args[0] == dbg.ERR:
        print(" ".join([str(a) for a in args]))
        print(" ".join([str(a) for a in args]), file=sys.stderr)


# error colorization
dbg = type('obj', (object,), {
    "ERR":  r"[ERR]",  # red    ERROR
    "INIT": r"[INIT]", # red    INIT ERROR
    "PROC": r"[PROC]", # green  PROCESS