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

⌈⌋ ⎇ branch:  streamtuner2


pluginmanager2.py at [8e8a9dd020]

File channels/pluginmanager2.py artifact ca1e100e6f part of check-in 8e8a9dd020


# encoding: UTF-8
# api: streamtuner2
# title: User Plugin Manager â…¡
# description: Downloads new plugins, or updates them.
# version: 0.1
# type: hook
# category: config
# depends: uikit, config, pluginconf
# config:
#   { name: plugin_repos, type: text, value: "http://fossil.include-once.org/plugins.php/streamtuner2/contrib/*.py, http://fossil.include-once.org/plugins.php/streamtuner2/channels/*.py", description: "Plugin sources (common-repo.json)" }
# priority: extra
# support: experimental
#
# Scans for new plugins from the repository server, using
# a common-repo.json list. Compares new against installed
# plugins, and permits to update or download new ones.
#
# User plugins go into ~/.config/streamtuner2/channels/
# and will be picked up in favour of system-installed ones.
#
# Further enables direct activation of existing plugins
# without restarting streamtuner2.
#


import imp
import config
import pkgutil
from channels import __path__ as channels__path__
import os
from config import *
from uikit import *
import ahttp
import json
import compat2and3


# Plugin manager
class pluginmanager2(object):

    module = "pluginmanager2"
    meta = plugin_meta()
    parent = None
    vbox = None


    # Hook up
    def __init__(self, parent):

        # main references
        self.parent = parent
        conf.add_plugin_defaults(self.meta, self.module)
        
        # config dialog
        parent.hooks["config_load"].append(self.add_config_tab)
        parent.hooks["config_save"].append(self.activate_plugins)
        
        # prepare user plugin directory
        conf.plugin_dir = conf.dir + "/plugins"
        if not os.path.exists(conf.plugin_dir):
            os.mkdir(conf.plugin_dir)
            open(conf.plugin_dir + "/__init__.py", "w").close()
        
        # register config dir for module loading
        sys.path.insert(0, conf.dir)
        channels__path__.insert(0, conf.plugin_dir)
        # config.plugin_base.append("plugins")
        # = pkgutil.extend_path(config.__path__, config.__name__)


    # Craft new config dialog notebook tab
    def add_config_tab(self, *w):
        if self.vbox:
            return

        # Notebook tab = label, content = vbox in scrolledwindow
        w = self.parent.config_notebook
        self.vbox = gtk.VBox(True, 5)
        vp = gtk.Viewport()
        vp.add(self.vbox)
        sw = gtk.ScrolledWindow()
        sw.add(vp)  # ScrolledWindow → Viewport → VBox
        # label
        label = gtk.EventBox()
        label.add(gtk.Label(" 📦 Add "))
        label.show_all()
        sw.show_all()
        # add page
        tab = w.insert_page_menu(sw, label, label, -1)
        
        # Prepare some text
        self.add_(uikit.label("\n<b><big>Install or update plugins</big></b>", size=520, markup=1))
        self.add_(uikit.label("You can update existing plugins, or install new contrib/ channels. User plugins reside in ~/.config/streamtuner2/plugins/ and can even be modified there (such as setting a custom # color: entry).\n", size=520, markup=1))
        self.add_(self.button("Refresh", stock="gtk-refresh", cb=self.refresh), "Show available plugins from repository\nhttp://fossil.include-once.org/streamtuner2/")
        self.add_(gtk.image_new_from_stock("gtk-info", gtk.ICON_SIZE_LARGE_TOOLBAR), "While plugins are generally compatible across releases, newer versions may also require to update the streamtuner2 core setup.\n Please note that plugin installation is rather experimental. It still requires a restart of ST2 to activate them.")
        for i in range(1,10):
            self.add_(uikit.label(""))


    # Append to vbox
    def add_(self, w, label=None, markup=0):
        w = uikit.wrap(w=w, label=label, align=10, label_size=400, label_markup=1)
        self.vbox.add(w)

    # Create button, connect click signal    
    def button(self, label, stock=None, cb=None):
        b = gtk.Button(label, stock=stock)
        b.connect("clicked", cb)
        return b

    
    # Add plugin list
    def refresh(self, *w):

        # fetch plugins
        meta = []
        for url in re.split("[\s,]+", conf.plugin_repos.strip()):
            if re.match("https?://", url):
                d = ahttp.get(url, encoding='utf-8') or []
                meta += json.loads(d)
                self.parent.status()
        
        # clean up vbox
        vbox = self.vbox
        for i,c in enumerate(vbox.get_children()):
            if i>=3:
                vbox.remove(c)
        
        # query existing plugins
        have = {name: plugin_meta(module=name) for name in module_list()}
        # add plugins
        for p in meta:
            id = p.get("$name")
            if id.find("__") == 0:   # skip __init__.py
                continue
            if have.get(id):
                if p.get("version") == have[id]["version"]:
                    continue;
            self.add_plugin(p)
        # some filler
        for i in range(1,3):
            self.add_(uikit.label(""))


    # Entry for plugin list
    def add_plugin(self, p):
        b = self.button("Install", stock="gtk-save", cb=lambda *w:self.install(p))
        text = "<b>{title}</b> <small>{version}</small>\n<small>{description}</small>\n{type}/{category}".format(**p)
        self.add_(b, text, markup=1)
    

    # Download a plugin
    def install(self, p):
        src = ahttp.get(p["$file"], encoding="utf-8")
        with open("{}/{$name}.py".format(conf.plugin_dir, **p), "w") as f:
            f.write(src)
        self.parent.status("Plugin '{$name}.py' installed.".format(**p))


    # Activate/deactivate changed plugins
    def activate_plugins(self, *w):
        pass