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

βŒˆβŒ‹ βŽ‡ branch:  streamtuner2


Check-in [ce0e9149db]

Overview
Comment:Add conf.nothreads flag and --nt cmdline flag, to prevent Gtk3 idle update race conditions if need be. (Still flaky for initial startups.)
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | trunk
Files: files | file ages | folders
SHA1: ce0e9149db46cf2296f451c2168e19d29d550d67
User & Date: mario on 2015-04-24 00:00:44
Other Links: manifest | tags
Context
2015-04-24
04:59
Disable update_streams_partially_done() for Gtk3, as that's the main cause of memory corruption (despite being run in idle loop). check-in: 1edde401ce user: mario tags: trunk
00:00
Add conf.nothreads flag and --nt cmdline flag, to prevent Gtk3 idle update race conditions if need be. (Still flaky for initial startups.) check-in: ce0e9149db user: mario tags: trunk
2015-04-23
19:31
Use distinct /tmp/streamtuner2/ directory for temporary pls/m3u/xspf files (also for DND). And have action. module reuse them, based on numeric row{} hash. check-in: 7411543862 user: mario tags: trunk
Changes

Modified channels/__init__.py from [bddc9139b6] to [7050519949].

442
443
444
445
446
447
448

449
450
451
452
453
454
455
456
457
458
459
460
461
462

        # display outside of this non-main thread
        uikit.do(self.display_categories)


    # insert content into gtk category list
    def display_categories(self):

    
        # rebuild gtk.TreeView
        log.UI("display_categoris: tree→gtk_cat")
        uikit.tree(self.gtk_cat, self.categories, title="Category", icon=gtk.STOCK_OPEN)

        # if it's a short list of categories, there's probably subfolders
        log.UI("display_categoris: expand_all")
        if len(self.categories) < 20:
            self.gtk_cat.expand_all()
            
        # select any first element
        self.gtk_cat.get_selection().select_path("0") #set_cursor
        self.currentcat()








>


<



<







442
443
444
445
446
447
448
449
450
451

452
453
454

455
456
457
458
459
460
461

        # display outside of this non-main thread
        uikit.do(self.display_categories)


    # insert content into gtk category list
    def display_categories(self):
        log.UI(self.module+".display_categories()", "mk tree, expand_all, select first path, currentcat")
    
        # rebuild gtk.TreeView

        uikit.tree(self.gtk_cat, self.categories, title="Category", icon=gtk.STOCK_OPEN)

        # if it's a short list of categories, there's probably subfolders

        if len(self.categories) < 20:
            self.gtk_cat.expand_all()
            
        # select any first element
        self.gtk_cat.get_selection().select_path("0") #set_cursor
        self.currentcat()

Modified channels/jamendo.py from [c57db677e5] to [e32838ce56].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# encoding: UTF-8
# api: streamtuner2
# title: Jamendo
# description: A license-free music collection and artist hub.
# type: channel
# version: 2.2
# category: radio
# url: http://jamendo.com/
# depends: json
# config: 
#    { name: jamendo_stream_format, value: ogg,  type: select,  select: "ogg=Ogg Vorbis, 112kbit/s|mp32=MP3, 192kbit/s VBR|mp31=MP3, 96kbit/s|flac=Xiph FLAC, ≳600kbit/s",  description: "Default streaming audio format for tracks, albums and playlists. Radios are just MP3." }
#    { name: jamendo_image_size,    value: 50,   type: select,  select: "25=25px|35=35px|50=50px|55=55px|60=60px|65=65px|70=70px|75=75px|85=85px|100=100px|130=130px|150=150px|200=200px|300=300px",  description: "Preview images size (height and width) for albums or tracks." }
#    { name: jamendo_count,         value: 1,    type:text,     description: "How many result sets (200 entries each) to retrieve." }
# priority: default
# png:
#   iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABHNCSVQICAgIfAhkiAAAAwNJREFUOI1lk0toXGUYhp/v/8+ZzDQzmdZMZ8YSkxIQCWTTYsALLsQsUsWFQgJeEJQqIigUvBK0FBQULIJKMEKrFjdOW9xoW1AbRdRCEYtd2GCxxUQbk8bMrTNnzpn//1xMLYLv6l2877N7oBcB0GMY
#   IAUY/p9w/glCrVzby3+LVqaxM4dx+inQV7KIHcKYAbzG4FYgqcmudYBAF+jKnT2QAExPYysVHCdKRQgewwRT0B1FfQboYsLLeE7hk4Oo/iD3rFit4GQGRCsYwJMt7yQVfrS6Go3HUQtvskiwCWM82qlijGdbaUsbTV5G/X65+y+rCzgBRI+VbsSmvvltqVkuX78t6rvpKWtvuMsgOfHtJia66OPz
#   86564UtDkA2Lm/VJYF6mLgXo50X0xNAHy4cK2jk+diVe+1FVVX/57oIuL66qqmriVF23rZ1TjyTLH2YT/WJ4TY+XRgAMKqNgJ41vebbvTgeFneo6Tb567zPmHn+LxdPnCIxHTFrt2CtB/8BWqtVOAewDWgGDmHG0u1VtztjC7SLqRSUkM5Clsd5ibs/7nDzyLd51xfQPq82PKS5SsHfQAEMsIZG3










|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# encoding: UTF-8
# api: streamtuner2
# title: Jamendo
# description: A license-free music collection and artist hub.
# type: channel
# version: 2.2
# category: radio
# url: http://jamendo.com/
# depends: json
# config: 
#    { name: jamendo_stream_format, value: ogg,  type: select,  select: "ogg=Ogg Vorbis, 112kbit/s|mp32=MP3, 192kbit/s VBR|mp31=MP3, 96kbit/s|flac=Xiph FLAC, ≳600kbit/s",  description: "Audio format for tracks, albums, playlists." }
#    { name: jamendo_image_size,    value: 50,   type: select,  select: "25=25px|35=35px|50=50px|55=55px|60=60px|65=65px|70=70px|75=75px|85=85px|100=100px|130=130px|150=150px|200=200px|300=300px",  description: "Preview images size (height and width) for albums or tracks." }
#    { name: jamendo_count,         value: 1,    type:text,     description: "How many result sets (200 entries each) to retrieve." }
# priority: default
# png:
#   iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABHNCSVQICAgIfAhkiAAAAwNJREFUOI1lk0toXGUYhp/v/8+ZzDQzmdZMZ8YSkxIQCWTTYsALLsQsUsWFQgJeEJQqIigUvBK0FBQULIJKMEKrFjdOW9xoW1AbRdRCEYtd2GCxxUQbk8bMrTNnzpn//1xMLYLv6l2877N7oBcB0GMY
#   IAUY/p9w/glCrVzby3+LVqaxM4dx+inQV7KIHcKYAbzG4FYgqcmudYBAF+jKnT2QAExPYysVHCdKRQgewwRT0B1FfQboYsLLeE7hk4Oo/iD3rFit4GQGRCsYwJMt7yQVfrS6Go3HUQtvskiwCWM82qlijGdbaUsbTV5G/X65+y+rCzgBRI+VbsSmvvltqVkuX78t6rvpKWtvuMsgOfHtJia66OPz
#   86564UtDkA2Lm/VJYF6mLgXo50X0xNAHy4cK2jk+diVe+1FVVX/57oIuL66qqmriVF23rZ1TjyTLH2YT/WJ4TY+XRgAMKqNgJ41vebbvTgeFneo6Tb567zPmHn+LxdPnCIxHTFrt2CtB/8BWqtVOAewDWgGDmHG0u1VtztjC7SLqRSUkM5Clsd5ibs/7nDzyLd51xfQPq82PKS5SsHfQAEMsIZG3

Modified config.py from [9fc88f2a2d] to [1bfcbd774c].

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

14
15
16
17
18
19
20
21
#
# 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 }

# version: 2.6
# 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 *
#













>
|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#
# 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 *
#
116
117
118
119
120
121
122

123
124
125
126
127
128
129
             # core plugins, cannot be disabled anyway
            "bookmarks": 1,
            "search": 1,
            "streamedit": 1,
            "configwin": 1,
        }
        self.tmp = os.environ.get("TEMP", "/tmp") + "/streamtuner2"

        self.max_streams = "500"
        self.show_bookmarks = 1
        self.show_favicons = 1
        self.load_favicon = 1
        self.heuristic_bookmark_update = 0
        self.retain_deleted = 0
        self.auto_save_appstate = 1







>







117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
             # core plugins, cannot be disabled anyway
            "bookmarks": 1,
            "search": 1,
            "streamedit": 1,
            "configwin": 1,
        }
        self.tmp = os.environ.get("TEMP", "/tmp") + "/streamtuner2"
        self.nothreads = 0
        self.max_streams = "500"
        self.show_bookmarks = 1
        self.show_favicons = 1
        self.load_favicon = 1
        self.heuristic_bookmark_update = 0
        self.retain_deleted = 0
        self.auto_save_appstate = 1
283
284
285
286
287
288
289

290
291
292
293
294
295
296
                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








>







285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
                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

485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502

503
504
505
506
507
508
509
    def log_print(self, *args, **kwargs):
        # debug level
        method = self.method.upper()
        if method != "ERR":
            if "debug" in conf and not conf.debug:
                return
        # color/prefix
        method = r"[{}[{}]".format(self.colors.get(method, "47m"), method)
        # output
        print(method + " " + " ".join([str(a) for a in args]), file=sys.stderr)

    # Colors
    colors = {
        "ERR":  "31m", # red    ERROR
        "INIT": "31m", # red    INIT ERROR
        "PROC": "32m", # green  PROCESS
        "CONF": "33m", # brown  CONFIG DATA
        "UI":   "34m", # blue   USER INTERFACE BEHAVIOUR

        "HTTP": "35m", # magenta HTTP REQUEST
        "DATA": "36m", # cyan   DATA
        "INFO": "37m", # gray   INFO
        "STAT": "37m", # gray   CONFIG STATE
    }

# instantiate right away







|










>







488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
    def log_print(self, *args, **kwargs):
        # debug level
        method = self.method.upper()
        if method != "ERR":
            if "debug" in conf and not conf.debug:
                return
        # color/prefix
        method = r"[{}[{}]".format(self.colors.get(method.split("_")[0], "47m"), method)
        # output
        print(method + " " + " ".join([str(a) for a in args]), file=sys.stderr)

    # Colors
    colors = {
        "ERR":  "31m", # red    ERROR
        "INIT": "31m", # red    INIT ERROR
        "PROC": "32m", # green  PROCESS
        "CONF": "33m", # brown  CONFIG DATA
        "UI":   "34m", # blue   USER INTERFACE BEHAVIOUR
        "UIKIT":"38;5;222;48;5;235m", # THREAD/UIKIT/IDLE TASKS
        "HTTP": "35m", # magenta HTTP REQUEST
        "DATA": "36m", # cyan   DATA
        "INFO": "37m", # gray   INFO
        "STAT": "37m", # gray   CONFIG STATE
    }

# instantiate right away

Modified gtk3.xml.gz from [b485718e2f] to [540b2b2ee4].

cannot compute difference between binary files

Modified st2.py from [9776e658be] to [d0ecae043d].

292
293
294
295
296
297
298


299
300
301
302
303
304
305
        self.thread(
                       #@TODO: should get a wrapper, for HTTP errors, and optionalize bookamrks
            lambda: (  self.channel().load(category,reload), reload and self.bookmarks.heuristic_update(self.current_channel,category)  )
        )

    # Thread a function, add to worker pool (for utilizing stop button)
    def thread(self, target, *args):


        thread = Thread(target=target, args=args)
        thread.start()
        self.working.append(thread)


    # Click in category list
    def on_category_clicked(self, widget, event, *more):







>
>







292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
        self.thread(
                       #@TODO: should get a wrapper, for HTTP errors, and optionalize bookamrks
            lambda: (  self.channel().load(category,reload), reload and self.bookmarks.heuristic_update(self.current_channel,category)  )
        )

    # Thread a function, add to worker pool (for utilizing stop button)
    def thread(self, target, *args):
        if conf.nothreads:
            return target(*args)
        thread = Thread(target=target, args=args)
        thread.start()
        self.working.append(thread)


    # Click in category list
    def on_category_clicked(self, widget, event, *more):

Modified uikit.py from [2a34911ea4] to [cf9953748a].

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
            c.set_current_name(re.sub(r"\.(m3u|pls|xspf|jspf|asx|json|smil|wpl)$", ext.strip("*"), fn))
        
    

    # Spool gtk update calls from non-main threads (optional immediate=1 flag to run task next, not last)
    @staticmethod
    def do(callback, *args, **kwargs):

        pos = kwargs.get("immediate", -1)
        if pos and pos >= 0:
            del kwargs["immediate"]
            pos = 0



        if uikit.idle_run:
            log.UIKIT_DORUN(str(callback))
            callback(*args, **kwargs)

        else:
            log.UIKIT_SPOOL(str(callback))
            uikit.idle_tasks.insert(pos, [lambda: callback(*args, **kwargs), str(callback)])
            gobject.idle_add(uikit.idle_do)
    
    # Collect tasks to perform in gtk.main loop
    idle_tasks = []
    idle_run = False
    
    # Execute UI updating tasks in order
    @staticmethod
    def idle_do():
        uikit.idle_run = True
        if uikit.idle_tasks:
            task, callback = uikit.idle_tasks.pop(0)
            log.UIKIT_EXEC(callback)
            task()
        uikit.idle_run = False
        return len(uikit.idle_tasks) > 0
        


    # adds background color to widget,
    # eventually wraps it into a gtk.Window, if it needs a container
    @staticmethod







>
|
<


>
>
>
|
|

>

|
|




|




|

|
|

|







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
            c.set_current_name(re.sub(r"\.(m3u|pls|xspf|jspf|asx|json|smil|wpl)$", ext.strip("*"), fn))
        
    

    # Spool gtk update calls from non-main threads (optional immediate=1 flag to run task next, not last)
    @staticmethod
    def do(callback, *args, **kwargs):
        name = inspect.getsource(callback).strip() if callback.__name__=='<lambda>' else str(callback)
        if kwargs.get("immediate"):

            del kwargs["immediate"]
            pos = 0
        else:
            pos = -1
        # Run callback right away
        if uikit.in_idle or conf.nothreads:
            log.UIKIT_RUN_NOW(name)
            callback(*args, **kwargs)
        # Spool them for Gtk idle handling
        else:
            log.UIKIT_SPOOL(name)
            uikit.idle_tasks.insert(pos, [lambda: callback(*args, **kwargs), name])
            gobject.idle_add(uikit.idle_do)
    
    # Collect tasks to perform in gtk.main loop
    idle_tasks = []
    in_idle = False
    
    # Execute UI updating tasks in order
    @staticmethod
    def idle_do():
        uikit.in_idle = True
        if uikit.idle_tasks:
            task, name = uikit.idle_tasks.pop(0)
            log.UIKIT_EXEC(name)
            task()
        uikit.in_idle = False
        return len(uikit.idle_tasks) > 0
        


    # adds background color to widget,
    # eventually wraps it into a gtk.Window, if it needs a container
    @staticmethod