summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--pyload/AddonManager.py98
-rw-r--r--pyload/api/AddonApi.py36
-rw-r--r--pyload/plugins/Addon.py65
-rw-r--r--pyload/plugins/Hoster.py4
-rw-r--r--pyload/plugins/addons/ExtractArchive.py47
-rw-r--r--pyload/remote/apitypes.py18
-rw-r--r--pyload/remote/apitypes_debug.py9
-rw-r--r--pyload/remote/pyload.thrift19
-rw-r--r--pyload/utils/PluginLoader.py2
-rw-r--r--pyload/utils/fs.py7
-rw-r--r--pyload/web/cnl_app.py4
11 files changed, 198 insertions, 111 deletions
diff --git a/pyload/AddonManager.py b/pyload/AddonManager.py
index 7935ff112..5c5524061 100644
--- a/pyload/AddonManager.py
+++ b/pyload/AddonManager.py
@@ -17,14 +17,23 @@
import __builtin__
+from gettext import gettext
+from copy import copy
from thread import start_new_thread
from threading import RLock
+from collections import defaultdict
+from new_collections import namedtuple
+
from types import MethodType
+from pyload.Api import AddonService, AddonInfo
from pyload.threads.AddonThread import AddonThread
from utils import lock, to_string
+AddonTuple = namedtuple('AddonTuple', 'instances events handler')
+
+
class AddonManager:
""" Manages addons, loading, unloading. """
@@ -35,10 +44,13 @@ class AddonManager:
__builtin__.addonManager = self #needed to let addons register themselves
self.log = self.core.log
- # TODO: multiuser, addons can store the user itself, probably not needed here
- self.plugins = {}
- self.methods = {} # dict of names and list of methods usable by rpc
- self.events = {} # Contains event that will be registered
+
+ # TODO: multiuser addons
+
+ # maps plugin names to info tuple
+ self.plugins = defaultdict(lambda: AddonTuple([], [], {}))
+ # Property hash mapped to meta data
+ self.info_props = {}
self.lock = RLock()
self.createIndex()
@@ -46,11 +58,16 @@ class AddonManager:
# manage addons on config change
self.listenTo("config:changed", self.manageAddon)
+ def iterAddons(self):
+ """ Yields (name, meta_data) of all addons """
+ return self.plugins.iteritems()
+
@lock
def callInHooks(self, event, eventName, *args):
""" Calls a method in all addons and catch / log errors"""
for plugin in self.plugins.itervalues():
- self.call(plugin, event, *args)
+ for inst in plugin.instances:
+ self.call(inst, event, *args)
self.dispatchEvent(eventName, *args)
def call(self, addon, f, *args):
@@ -78,7 +95,7 @@ class AddonManager:
if not pluginClass: continue
plugin = pluginClass(self.core, self)
- self.plugins[pluginClass.__name__] = plugin
+ self.plugins[pluginClass.__name__].instances.append(plugin)
# hide internals from printing
if not internal and plugin.isActivated():
@@ -96,7 +113,7 @@ class AddonManager:
self.log.info(_("Deactivated addons: %s") % ", ".join(sorted(deactive)))
def manageAddon(self, plugin, name, value):
- # TODO: user
+ # TODO: multi user
# check if section was a plugin
if plugin not in self.core.pluginManager.getPlugins("addons"):
@@ -120,18 +137,18 @@ class AddonManager:
self.log.debug("Plugin loaded: %s" % plugin)
plugin = pluginClass(self.core, self)
- self.plugins[pluginClass.__name__] = plugin
+ self.plugins[pluginClass.__name__].instances.append(plugin)
# active the addon in new thread
start_new_thread(plugin.activate, tuple())
- self.registerEvents() # TODO: BUG: events will be destroyed and not re-registered
+ self.registerEvents()
@lock
def deactivateAddon(self, plugin):
if plugin not in self.plugins:
return
- else:
- addon = self.plugins[plugin]
+ else: # todo: multiple instances
+ addon = self.plugins[plugin].instances[0]
if addon.__internal__: return
@@ -140,8 +157,11 @@ class AddonManager:
#remove periodic call
self.log.debug("Removed callback %s" % self.core.scheduler.removeJob(addon.cb))
+
+ # todo: only delete instances, meta data is lost otherwise
del self.plugins[addon.__name__]
+ # TODO: could be improved
#remove event listener
for f in dir(addon):
if f.startswith("__") or type(getattr(addon, f)) != MethodType:
@@ -151,8 +171,9 @@ class AddonManager:
def activateAddons(self):
self.log.info(_("Activating addons..."))
for plugin in self.plugins.itervalues():
- if plugin.isActivated():
- self.call(plugin, "activate")
+ for inst in plugin.instances:
+ if inst.isActivated():
+ self.call(inst, "activate")
self.registerEvents()
@@ -160,7 +181,8 @@ class AddonManager:
""" Called when core is shutting down """
self.log.info(_("Deactivating addons..."))
for plugin in self.plugins.itervalues():
- self.call(plugin, "deactivate")
+ for inst in plugin.instances:
+ self.call(inst, "deactivate")
def downloadPreparing(self, pyfile):
self.callInHooks("downloadPreparing", "download:preparing", pyfile)
@@ -180,40 +202,40 @@ class AddonManager:
def activePlugins(self):
""" returns all active plugins """
- return [x for x in self.plugins.itervalues() if x.isActivated()]
-
- def getAllInfo(self):
- """returns info stored by addon plugins"""
- info = {}
- for name, plugin in self.plugins.iteritems():
- if plugin.info:
- #copy and convert so str
- info[name] = dict(
- [(x, to_string(y)) for x, y in plugin.info.iteritems()])
- return info
+ return [p for x in self.plugins.values() for p in x.instances if p.isActivated()]
def getInfo(self, plugin):
- info = {}
- if plugin in self.plugins and self.plugins[plugin].info:
- info = dict([(x, to_string(y))
- for x, y in self.plugins[plugin].info.iteritems()])
+ """ Retrieves all info data for a plugin """
- return info
+ data = []
+ # TODO
+ if plugin in self.plugins:
+ if plugin.instances:
+ for attr in dir(plugin.instances[0]):
+ if attr.startswith("__Property"):
+ info = self.info_props[attr]
+ info.value = getattr(plugin.instances[0], attr)
+ data.append(info)
+ return data
def addEventListener(self, plugin, func, event):
""" add the event to the list """
- if plugin not in self.events:
- self.events[plugin] = []
- self.events[plugin].append((func, event))
+ self.plugins[plugin].events.append((func, event))
def registerEvents(self):
""" actually register all saved events """
for name, plugin in self.plugins.iteritems():
- if name in self.events:
- for func, event in self.events[name]:
- self.listenTo(event, getattr(plugin, func))
- # clean up
- del self.events[name]
+ for func, event in plugin.events:
+ for inst in plugin.instances:
+ self.listenTo(event, getattr(inst, func))
+
+ def addAddonHandler(self, plugin, func, label, desc, args, package, media):
+ """ Registers addon service description """
+ self.plugins[plugin].handler[func] = AddonService(func, gettext(label), gettext(desc), args, package, media)
+
+ def addInfoProperty(self, h, name, desc):
+ """ Register property as :class:`AddonInfo` """
+ self.info_props[h] = AddonInfo(name, desc)
def listenTo(self, *args):
self.core.eventManager.listenTo(*args)
diff --git a/pyload/api/AddonApi.py b/pyload/api/AddonApi.py
index 12d3170d7..ea1e3ce6e 100644
--- a/pyload/api/AddonApi.py
+++ b/pyload/api/AddonApi.py
@@ -5,25 +5,49 @@ from pyload.Api import Api, RequirePerm, Permission
from ApiComponent import ApiComponent
-
+# TODO: multi user
class AddonApi(ApiComponent):
""" Methods to interact with addons """
+ @RequirePerm(Permission.Interaction)
def getAllInfo(self):
"""Returns all information stored by addon plugins. Values are always strings
- :return: {"plugin": {"name": value } }
+ :return:
"""
- return self.core.addonManager.getAllInfo()
+ # TODO
+ @RequirePerm(Permission.Interaction)
def getInfoByPlugin(self, plugin):
- """Returns information stored by a specific plugin.
+ """Returns public information associated with specific plugin.
- :param plugin: pluginname
- :return: dict of attr names mapped to value {"name": value}
+ :param plugin: pluginName
+ :return: list of :class:`AddonInfo`
"""
return self.core.addonManager.getInfo(plugin)
+ @RequirePerm(Permission.Interaction)
+ def getAddonHandler(self):
+ """ Lists all available addon handler
+
+ :return: dict of plugin name to list of :class:`AddonService`
+ """
+ handler = {}
+ for name, data in self.core.addonManager.iterAddons():
+ if data.handler:
+ handler[name] = data.handler
+ return handler
+
+ @RequirePerm(Permission.Interaction)
+ def callAddon(self, plugin, func, arguments):
+ """ Calls any function exposed by an addon """
+ pass
+
+ @RequirePerm(Permission.Interaction)
+ def callAddonHandler(self, plugin, func, pid_or_fid):
+ """ Calls an addon handler registered to work with packages or files """
+ pass
+
if Api.extend(AddonApi):
del AddonApi \ No newline at end of file
diff --git a/pyload/plugins/Addon.py b/pyload/plugins/Addon.py
index c1a297d28..5c27fa983 100644
--- a/pyload/plugins/Addon.py
+++ b/pyload/plugins/Addon.py
@@ -1,7 +1,5 @@
# -*- coding: utf-8 -*-
-from traceback import print_exc
-
#from functools import wraps
from pyload.utils import has_method, to_list
@@ -27,23 +25,58 @@ def AddEventListener(event):
return _klass
-def AddonHandler(desc, media=None):
- """ Register Handler for files, packages, or arbitrary callable methods.
- To let the method work on packages/files, media must be set and the argument named pid or fid.
+def AddonHandler(label, desc, package=True, media=-1):
+ """ Register Handler for files, packages, or arbitrary callable methods. In case package is True (default)
+ The method should only accept a pid as argument. When media is set it will work on files
+ and should accept a fileid. Only when both is None the method can be arbitrary.
- :param desc: verbose description
- :param media: if True or bits of media type
+ :param label: verbose name
+ :param desc: short description
+ :param package: True if method works withs packages
+ :param media: media type of the file to work with.
"""
- pass
+ class _klass(object):
+ def __new__(cls, f, *args, **kwargs):
+ addonManager.addAddonHandler(class_name(f.__module__), f.func_name, label, desc,
+ f.func_code.co_varnames[1:], package, media)
+ return f
+
+ return _klass
-def AddonInfo(desc):
- """ Called to retrieve information about the current state.
- Decorated method must return anything convertable into string.
+def AddonProperty(name, desc, default=None, fire_event=True):
+ """ Use this function to declare class variables, that will be exposed as :class:`AddonInfo`.
+ It works similar to the @property function. You declare the variable like `state = AddonProperty(...)`
+ and use it as any other variable.
+
+ :param name: display name
:param desc: verbose description
+ :param default: the default value
+ :param fire_event: Fire `addon:property:change` event, when modified
"""
- pass
+
+ # generated name for the attribute
+ h = "__Property" + str(hash(name) ^ hash(desc))
+
+ addonManager.addInfoProperty(h, name, desc)
+
+ def _get(self):
+ if not hasattr(self, h):
+ return default
+
+ return getattr(self, h)
+
+ def _set(self, value):
+ if fire_event:
+ self.manager.dispatchEvent("addon:property:change", value)
+
+ return setattr(self, h, value)
+
+ def _del(self):
+ return delattr(self, h)
+
+ return property(_get, _set, _del)
def threaded(f):
@@ -73,9 +106,6 @@ class Addon(Base):
def __init__(self, core, manager, user=None):
Base.__init__(self, core, user)
- #: Provide information in dict here, usable by API `getInfo`
- self.info = None
-
#: Callback of periodical job task, used by addonManager
self.cb = None
@@ -130,9 +160,8 @@ class Addon(Base):
try:
if self.isActivated(): self.periodical()
except Exception, e:
- self.core.log.error(_("Error executing addons: %s") % str(e))
- if self.core.debug:
- print_exc()
+ self.core.log.error(_("Error executing addon: %s") % str(e))
+ self.core.print_exc()
if self.cb:
self.cb = self.core.scheduler.addJob(self.interval, self._periodical, threaded=False)
diff --git a/pyload/plugins/Hoster.py b/pyload/plugins/Hoster.py
index 976918c0d..6bfe47e1f 100644
--- a/pyload/plugins/Hoster.py
+++ b/pyload/plugins/Hoster.py
@@ -9,7 +9,7 @@ if os.name != "nt":
from grp import getgrnam
from pyload.utils import chunks as _chunks
-from pyload.utils.fs import save_join, save_filename, fs_encode, fs_decode, \
+from pyload.utils.fs import save_join, safe_filename, fs_encode, fs_decode, \
remove, makedirs, chmod, stat, exists, join
from Base import Base, Fail, Retry
@@ -268,7 +268,7 @@ class Hoster(Base):
# convert back to unicode
location = fs_decode(location)
- name = save_filename(self.pyfile.name)
+ name = safe_filename(self.pyfile.name)
filename = join(location, name)
diff --git a/pyload/plugins/addons/ExtractArchive.py b/pyload/plugins/addons/ExtractArchive.py
index be023301c..67fa5c820 100644
--- a/pyload/plugins/addons/ExtractArchive.py
+++ b/pyload/plugins/addons/ExtractArchive.py
@@ -49,12 +49,13 @@ if os.name != "nt":
from pwd import getpwnam
from grp import getgrnam
-from module.utils import save_join, fs_encode
-from module.plugins.Hook import Hook, threaded, Expose
-from module.plugins.internal.AbstractExtractor import ArchiveError, CRCError, WrongPassword
+from pyload.utils.fs import safe_join as save_join, fs_encode
+from pyload.plugins.Addon import Addon, threaded, AddonHandler, AddonProperty
+from pyload.plugins.internal.AbstractExtractor import ArchiveError, CRCError, WrongPassword
-class ExtractArchive(Hook):
+
+class ExtractArchive(Addon):
"""
Provides: unrarFinished (folder, filename)
"""
@@ -77,7 +78,7 @@ class ExtractArchive(Hook):
event_list = ["allDownloadsProcessed"]
- def setup(self):
+ def init(self):
self.plugins = []
self.passwords = []
names = []
@@ -111,10 +112,10 @@ class ExtractArchive(Hook):
# queue with package ids
self.queue = []
- @Expose
- def extractPackage(self, id):
+ @AddonHandler(_("Extract package"), _("Scans package for archives and extract them"))
+ def extractPackage(self, pid):
""" Extract package with given id"""
- self.manager.startThread(self.extract, [id])
+ self.manager.startThread(self.extract, [pid])
def packageFinished(self, pypack):
if self.getConfig("queue"):
@@ -267,25 +268,12 @@ class ExtractArchive(Hook):
return []
- @Expose
+ # TODO: config handler for passwords?
+
def getPasswords(self):
""" List of saved passwords """
return self.passwords
- def reloadPasswords(self):
- pwfile = self.getConfig("passwordfile")
- if not exists(pwfile):
- open(pwfile, "wb").close()
-
- passwords = []
- f = open(pwfile, "rb")
- for pw in f.read().splitlines():
- passwords.append(pw)
- f.close()
-
- self.passwords = passwords
-
- @Expose
def addPassword(self, pw):
""" Adds a password to saved list"""
pwfile = self.getConfig("passwordfile")
@@ -299,6 +287,19 @@ class ExtractArchive(Hook):
f.write(pw + "\n")
f.close()
+ def reloadPasswords(self):
+ pwfile = self.getConfig("passwordfile")
+ if not exists(pwfile):
+ open(pwfile, "wb").close()
+
+ passwords = []
+ f = open(pwfile, "rb")
+ for pw in f.read().splitlines():
+ passwords.append(pw)
+ f.close()
+
+ self.passwords = passwords
+
def setPermissions(self, files):
for f in files:
if not exists(f):
diff --git a/pyload/remote/apitypes.py b/pyload/remote/apitypes.py
index 287a5f096..6a7d2f063 100644
--- a/pyload/remote/apitypes.py
+++ b/pyload/remote/apitypes.py
@@ -114,20 +114,22 @@ class AccountInfo(BaseObject):
self.config = config
class AddonInfo(BaseObject):
- __slots__ = ['func_name', 'description', 'value']
+ __slots__ = ['name', 'description', 'value']
- def __init__(self, func_name=None, description=None, value=None):
- self.func_name = func_name
+ def __init__(self, name=None, description=None, value=None):
+ self.name = name
self.description = description
self.value = value
class AddonService(BaseObject):
- __slots__ = ['func_name', 'description', 'arguments', 'media']
+ __slots__ = ['func_name', 'label', 'description', 'arguments', 'pack', 'media']
- def __init__(self, func_name=None, description=None, arguments=None, media=None):
+ def __init__(self, func_name=None, label=None, description=None, arguments=None, pack=None, media=None):
self.func_name = func_name
+ self.label = label
self.description = description
self.arguments = arguments
+ self.pack = pack
self.media = media
class ConfigHolder(BaseObject):
@@ -419,6 +421,8 @@ class Iface(object):
pass
def getAllFiles(self):
pass
+ def getAllInfo(self):
+ pass
def getAllUserData(self):
pass
def getAvailablePlugins(self):
@@ -437,6 +441,8 @@ class Iface(object):
pass
def getFilteredFiles(self, state):
pass
+ def getInfoByPlugin(self, plugin):
+ pass
def getInteractionTasks(self, mode):
pass
def getLog(self, offset):
@@ -457,8 +463,6 @@ class Iface(object):
pass
def getWSAddress(self):
pass
- def hasAddonHandler(self, plugin, func):
- pass
def isInteractionWaiting(self, mode):
pass
def loadConfig(self, name):
diff --git a/pyload/remote/apitypes_debug.py b/pyload/remote/apitypes_debug.py
index 74ea8a6a8..14b0cc98e 100644
--- a/pyload/remote/apitypes_debug.py
+++ b/pyload/remote/apitypes_debug.py
@@ -20,7 +20,7 @@ enums = [
classes = {
'AccountInfo' : [basestring, basestring, int, bool, int, int, int, bool, bool, bool, (list, ConfigItem)],
'AddonInfo' : [basestring, basestring, basestring],
- 'AddonService' : [basestring, basestring, (list, basestring), (None, int)],
+ 'AddonService' : [basestring, basestring, basestring, (list, basestring), bool, int],
'ConfigHolder' : [basestring, basestring, basestring, basestring, (list, ConfigItem), (None, (list, AddonInfo))],
'ConfigInfo' : [basestring, basestring, basestring, basestring, bool, (None, bool)],
'ConfigItem' : [basestring, basestring, basestring, Input, basestring],
@@ -53,8 +53,8 @@ methods = {
'addPackageChild': int,
'addPackageP': int,
'addUser': UserData,
- 'callAddon': None,
- 'callAddonHandler': None,
+ 'callAddon': basestring,
+ 'callAddonHandler': basestring,
'checkContainer': OnlineCheck,
'checkHTML': OnlineCheck,
'checkLinks': OnlineCheck,
@@ -72,6 +72,7 @@ methods = {
'getAccounts': (list, AccountInfo),
'getAddonHandler': (dict, basestring, list),
'getAllFiles': TreeCollection,
+ 'getAllInfo': (dict, basestring, list),
'getAllUserData': (dict, int, UserData),
'getAvailablePlugins': (list, ConfigInfo),
'getConfig': (dict, basestring, ConfigHolder),
@@ -81,6 +82,7 @@ methods = {
'getFileTree': TreeCollection,
'getFilteredFileTree': TreeCollection,
'getFilteredFiles': TreeCollection,
+ 'getInfoByPlugin': (list, AddonInfo),
'getInteractionTasks': (list, InteractionTask),
'getLog': (list, basestring),
'getPackageContent': TreeCollection,
@@ -91,7 +93,6 @@ methods = {
'getServerVersion': basestring,
'getUserData': UserData,
'getWSAddress': basestring,
- 'hasAddonHandler': bool,
'isInteractionWaiting': bool,
'loadConfig': ConfigHolder,
'login': bool,
diff --git a/pyload/remote/pyload.thrift b/pyload/remote/pyload.thrift
index 9bcc2ce89..07782ef42 100644
--- a/pyload/remote/pyload.thrift
+++ b/pyload/remote/pyload.thrift
@@ -226,13 +226,15 @@ struct InteractionTask {
struct AddonService {
1: string func_name,
- 2: string description,
- 3: list<string> arguments,
- 4: optional i16 media,
+ 2: string label,
+ 3: string description,
+ 4: list<string> arguments,
+ 5: bool pack,
+ 6: i16 media,
}
struct AddonInfo {
- 1: string func_name,
+ 1: string name,
2: string description,
3: JSONString value,
}
@@ -511,17 +513,16 @@ service Pyload {
// Addon Methods
///////////////////////
- //map<PluginName, list<AddonInfo>> getAllInfo(),
- //list<AddonInfo> getInfoByPlugin(1: PluginName plugin),
+ map<PluginName, list<AddonInfo>> getAllInfo(),
+ list<AddonInfo> getInfoByPlugin(1: PluginName plugin),
map<PluginName, list<AddonService>> getAddonHandler(),
- bool hasAddonHandler(1: PluginName plugin, 2: string func),
- void callAddon(1: PluginName plugin, 2: string func, 3: list<JSONString> arguments)
+ JSONString callAddon(1: PluginName plugin, 2: string func, 3: list<JSONString> arguments)
throws (1: ServiceDoesNotExists e, 2: ServiceException ex),
// special variant of callAddon that works on the media types, acccepting integer
- void callAddonHandler(1: PluginName plugin, 2: string func, 3: PackageID pid_or_fid)
+ JSONString callAddonHandler(1: PluginName plugin, 2: string func, 3: PackageID pid_or_fid)
throws (1: ServiceDoesNotExists e, 2: ServiceException ex),
diff --git a/pyload/utils/PluginLoader.py b/pyload/utils/PluginLoader.py
index cb1039443..57a899e39 100644
--- a/pyload/utils/PluginLoader.py
+++ b/pyload/utils/PluginLoader.py
@@ -182,6 +182,8 @@ class PluginLoader:
# save number of of occurred
stack = 0
endpos = m.start(2) - size
+
+ #TODO: strings must be parsed too, otherwise breaks very easily
for i in xrange(m.end(2), len(content) - size + 1):
if content[i:i+size] == endchar:
# closing char seen and match now complete
diff --git a/pyload/utils/fs.py b/pyload/utils/fs.py
index 05e098e2a..939adb87c 100644
--- a/pyload/utils/fs.py
+++ b/pyload/utils/fs.py
@@ -48,7 +48,7 @@ def makedirs(path, mode=0755):
def listdir(path):
return [fs_decode(x) for x in os.listdir(fs_encode(path))]
-def save_filename(name):
+def safe_filename(name):
#remove some chars
if os.name == 'nt':
return remove_chars(name, '/\\?%*:|"<>,')
@@ -58,10 +58,13 @@ def save_filename(name):
def stat(name):
return os.stat(fs_encode(name))
-def save_join(*args):
+def safe_join(*args):
""" joins a path, encoding aware """
return fs_encode(join(*[x if type(x) == unicode else decode(x) for x in args]))
+def save_join(*args):
+ return safe_join(*args)
+
def free_space(folder):
folder = fs_encode(folder)
diff --git a/pyload/web/cnl_app.py b/pyload/web/cnl_app.py
index 90aa76d72..d8311d90f 100644
--- a/pyload/web/cnl_app.py
+++ b/pyload/web/cnl_app.py
@@ -6,7 +6,7 @@ from urllib import unquote
from base64 import standard_b64decode
from binascii import unhexlify
-from pyload.utils.fs import save_filename
+from pyload.utils.fs import safe_filename
from bottle import route, request, HTTPError
from webinterface import PYLOAD, DL_ROOT, JS
@@ -55,7 +55,7 @@ def addcrypted():
package = request.forms.get('referer', 'ClickAndLoad Package')
dlc = request.forms['crypted'].replace(" ", "+")
- dlc_path = join(DL_ROOT, save_filename(package) + ".dlc")
+ dlc_path = join(DL_ROOT, safe_filename(package) + ".dlc")
dlc_file = open(dlc_path, "wb")
dlc_file.write(dlc)
dlc_file.close()