Index: channels/pluginmanager2.py
==================================================================
--- channels/pluginmanager2.py
+++ channels/pluginmanager2.py
@@ -1,15 +1,16 @@
# encoding: UTF-8
# api: streamtuner2
# title: User Plugin Manager Ⅱ
# description: Downloads new plugins, or updates them.
-# version: 0.1
+# version: 0.2
# 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)" }
+# { name: plugin_repos, type: text, value: "http://fossil.include-once.org/repo.json/streamtuner2/contrib/*.py, http://fossil.include-once.org/repo.json/streamtuner2/channels/*.py", description: "Plugin repository JSON source references." }
+# { name: plugin_auto, type: boolean, value: 1, description: Apply plugin activation/disabling without restart. }
# priority: extra
# support: experimental
#
# Scans for new plugins from the repository server, using
# a common-repo.json list. Compares new against installed
@@ -16,13 +17,15 @@
# 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.
+# Further enables direct activation of existing channel
+# plugins, often without restarting streamtuner2.
#
+# Actually rather trivial. The Gtk interface building just
+# makes this handler look complicated.
import imp
import config
import pkgutil
@@ -52,22 +55,23 @@
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)
+ parent.hooks["config_save"].append(self.clean_config_vboxen)
# 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
+ # Register user config dir "~/.config/streamtuner2/plugins" for module loading
sys.path.insert(0, conf.dir)
+
+ # Let channels.* package load modules from two directories
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:
@@ -110,56 +114,101 @@
# Add plugin list
def refresh(self, *w):
- # fetch plugins
+ # Fetch repository JSON list
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")
+ # Clean up placeholders in vbox
+ vbox = self.vbox
+ for c in vbox.get_children()[3:]:
+ vbox.remove(c)
+
+ # Query existing plugins
+ have = {name: plugin_meta(module=name) for name in module_list()}
+ # Attach available downloads
+ for newpl in meta:
+ id = newpl.get("$name")
if id.find("__") == 0: # skip __init__.py
continue
if have.get(id):
- if p.get("version") == have[id]["version"]:
+ if have[id]["version"] >= newpl.get("version"):
continue;
- self.add_plugin(p)
- # some filler
+ self.add_plugin(newpl)
+
+ # Readd some filler labels
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 = "{title} {version}\n{description}\n{type}/{category}".format(**p)
- self.add_(b, text, markup=1)
+ p = self.update_p(p)
+ text = "$title, "\
+ "version: $version, "\
+ "type: $type "\
+ "category: $category\n"\
+ "$description\n"\
+ "$extras, view src"
+ self.add_(b, safe_format(text, **p), markup=1)
+
+
+ # Add placeholder fields
+ def update_p(self, p):
+ extras = ["{}: {}".format(n, p[n]) for n in ("status", "support", "author", "depends") if p.get(n)]
+ p["extras"] = " ".join(["💁"] + extras)
+ p["file"] = p["$file"]
+ for field in ("version", "title", "description", "type", "category"):
+ p.setdefault(field, "-")
+ return p
# 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))
+
+ # Empty out [channels] and [feature] tab in configdialog, so it rereads them
+ def clean_config_vboxen(self, *w):
+ self.parent.configwin.first_open = 1
+ for vbox in [self.parent.plugin_options, self.parent.feature_options]:
+ for c in vbox.get_children()[1:]:
+ vbox.remove(c)
+
# Activate/deactivate changed plugins
def activate_plugins(self, *w):
- pass
+ if not conf.plugin_auto:
+ return
+ p = self.parent
+ for name,act in conf.plugins.items():
+
+ # disable channel plugin
+ if not act and name in p.channels:
+ p.notebook_channels.remove_page(p.channel_names.index(name))
+ del p.channels[name]
+
+ # feature plugins usually have to many hooks
+ if not act and name in p.features:
+ log.WARN("Cannot disable feature plugin '{}'.".format(name))
+ p.status("Disabling feature plugins requires a restart.")
+
+ # just let main load any new plugins
+ p.load_plugin_channels()
+
+# Alternative to .format(), with keys possibly being absent
+from string import Template
+def safe_format(str, **kwargs):
+ return Template(str).safe_substitute(**kwargs)