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 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 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 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220
|
# encoding: UTF-8
# api: streamtuner2
# title: User Plugin Manager Ⅱ
# description: Downloads new plugins, or updates them.
# version: 0.5
# type: hook
# category: config
# depends: uikit >= 1.9, config >= 2.7, streamtuner2 >= 2.1.8, pluginconf < 1.0
# config:
# { 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.", hidden: 1 }
# { name: plugin_auto, type: boolean, value: 1, description: Apply plugin activation/disabling without restart. }
# priority: extra
# png:
# iVBORw0KGgoAAAANSUhEUgAAACAAAAAgBAMAAACBVGfHAAAAJ1BMVEUAAABNYQVcdAx1iTB6mQ6KsQGUsTCUuxGmyDKvzEK51FnB13LP3pnSUwRYAAAAAXRSTlMAQObYZgAAAAFiS0dEAIgFHUgAAAAJcEhZcwAACxMAAAsT
# AQCanBgAAAAHdElNRQffBQ4EMidI8LXfAAABGUlEQVQoz32NPU7DQBCFx4aOxosIIOzGlkWRiogGkBssRfQoK/cxRBwAaw6AhJcLoAwtlTdlKtiSMj4U++PIBiReM/O+mXkD0ImdMxhqp23bsyHINkp9/Qu8TLXBj5BMrfsp
# 08rk2hT7KtIf2o1cmWJzIqU+pVVD9i6SW9Ev0BC9D0FDSzEExouyB0QkLGAQunMSL7IuwVNB+EZu/Bw/lLAnJ1fa6nOBT3CZQ0irDzsWiHo/gLFwqhFxbl6Me+/AiXhdmnUHfIBDMb+onccZnE7AXwTHncfH5CYHSGALeHGH
# tybGAr7gFS8KC464Vs7Se84dGGlvmvQPqAZgFoM/7QYwQqx0fIoFuo0DE3oNuzbbgP1EKwDPlPgb5NefnUHe3aAAAAAASUVORK5CYII=
# support: stable
#
# 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 channel
# plugins, often without restarting streamtuner2.
#
# Actually rather trivial. The Gtk interface building just
# makes this handler look complicated.
import imp
import config
import pluginconf
import pkgutil
from channels import __path__ as channels__path__
import os
from config import *
from uikit import *
import ahttp
import json
import compat2and3
from xml.sax.saxutils import escape as html_escape
# 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)
parent.hooks["config_save"].append(self.clean_config_vboxen)
# prepare user plugin directory
conf.plugin_dir = conf.dir + "/plugins"
plugin_dir_stub = "{}/__init__.py".format(conf.plugin_dir)
if not os.path.exists(conf.plugin_dir):
os.mkdir(conf.plugin_dir)
if not os.path.exists(plugin_dir_stub):
open(plugin_dir_stub, "a").close()
# 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)
# 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.")
for i in range(1,10):
self.add_(uikit.label(""))
# 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 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 placeholders in vbox
_ = [self.vbox.remove(c) for c in self.vbox.get_children()[3:]]
# Attach available downloads after checking dependencies
# e.g. newpl["depends"] = "streamtuner2 < 2.2.0, config >= 2.5"
dep = pluginconf.dependency()
for newpl in meta:
if dep.valid(newpl, log.DEBUG_VALIDITY) and dep.depends(newpl, log.DEBUG_DEPENDS):
self.add_plugin(newpl)
else:
log.DEBUG("plugin fails dependencies:", newpl)
# Readd some filler labels
_ = [self.add_(uikit.label("")) for i in range(1,3)]
# Append to vbox
def add_(self, w, label=None, markup=0, align=10, label_size=400):
w = uikit.wrap(w=w, label=label, align=align, label_size=label_size, label_markup=markup)
self.vbox.add(w)
# Entry for plugin list
def add_plugin(self, p):
b = self.button("Install", stock="gtk-save", cb=lambda *w:self.install(p))
p = self.update_p(p)
text = "<b>$title</b>, "\
"<small>version:</small> <span weight='bold' color='orange'>$version</span>, "\
"<small>type: <i><span color='#559'>$type</span></i> "\
"category: <i><span color='blue'>$category</span></i></small>\n"\
"<span size='smaller' color='#364'>$description</span>\n"\
"<span size='small' color='#532' weight='ultralight'>$extras, <a href='$file'>view src</a></span>"
self.add_(b, label=safe_format(text, **p), markup=1, align=10, label_size=375)
# Add placeholder fields
def update_p(self, p):
fields = ("status", "priority", "support", "author", "depends")
extras = ["{}: <b>{}</b>".format(n, html_escape(p[n])) for n in fields if p.get(n)]
p["extras"] = " ".join(["💁"] + extras)
p["file"] = p["$file"].replace("/cat/", "/doc/tip/")
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")
name = p["$name"]
with open("{}/{}.py".format(conf.plugin_dir, name), "w") as f:
f.write(src)
self.parent.status("Plugin '{}.py' installed.".format(name))
conf.add_plugin_defaults(plugin_meta(module=name), name)
# 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):
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)
|