diff options
Diffstat (limited to 'module/plugins/Plugin.py')
-rw-r--r-- | module/plugins/Plugin.py | 422 |
1 files changed, 279 insertions, 143 deletions
diff --git a/module/plugins/Plugin.py b/module/plugins/Plugin.py index 15bf3971f..0a9c647fb 100644 --- a/module/plugins/Plugin.py +++ b/module/plugins/Plugin.py @@ -1,21 +1,6 @@ # -*- coding: utf-8 -*- -""" - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation; either version 3 of the License, - or (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program; if not, see <http://www.gnu.org/licenses/>. - - @author: RaNaN, spoob, mkaay -""" +from __future__ import with_statement from time import time, sleep from random import randint @@ -30,8 +15,11 @@ if os.name != "nt": from grp import getgrnam from itertools import islice +from traceback import print_exc +from urlparse import urlparse + +from pyload.utils import fs_decode, fs_encode, safe_filename, safe_join -from module.utils import save_join, save_path, fs_encode, fs_decode def chunks(iterable, size): it = iter(iterable) @@ -69,28 +57,40 @@ class Base(object): def __init__(self, core): #: Core instance self.core = core - #: logging instance - self.log = core.log - #: core config - self.config = core.config - #log functions + + def _log(self, type, args): + msg = " | ".join([encode(a).strip() for a in args if a]) + logger = getattr(self.core.log, type) + logger("%s: %s" % (self.__name__, msg or _("%s MARK" % type.upper()))) + + + def logDebug(self, *args): + if self.core.debug: + return self._log("debug", args) + + def logInfo(self, *args): - self.log.info("%s: %s" % (self.__name__, " | ".join([a if isinstance(a, basestring) else str(a) for a in args]))) + return self._log("info", args) + def logWarning(self, *args): - self.log.warning("%s: %s" % (self.__name__, " | ".join([a if isinstance(a, basestring) else str(a) for a in args]))) + return self._log("warning", args) + def logError(self, *args): - self.log.error("%s: %s" % (self.__name__, " | ".join([a if isinstance(a, basestring) else str(a) for a in args]))) + return self._log("error", args) - def logDebug(self, *args): - self.log.debug("%s: %s" % (self.__name__, " | ".join([a if isinstance(a, basestring) else str(a) for a in args]))) + + def logCritical(self, *args): + return self._log("critical", args) + #: Deprecated method def setConf(self, option, value): """ see `setConfig` """ - self.core.config.setPlugin(self.__name__, option, value) + self.setConfig(option, value) + def setConfig(self, option, value): """ Set config value for current plugin @@ -99,11 +99,14 @@ class Base(object): :param value: :return: """ - self.setConf(option, value) + self.core.config.setPlugin(self.__name__, option, value) + + #: Deprecated method def getConf(self, option): """ see `getConfig` """ - return self.core.config.getPlugin(self.__name__, option) + return self.getConfig(option) + def getConfig(self, option): """ Returns config value for current plugin @@ -111,26 +114,31 @@ class Base(object): :param option: :return: """ - return self.getConf(option) + return self.core.config.getPlugin(self.__name__, option) + def setStorage(self, key, value): """ Saves a value persistently to the database """ self.core.db.setStorage(self.__name__, key, value) + def store(self, key, value): """ same as `setStorage` """ self.core.db.setStorage(self.__name__, key, value) + def getStorage(self, key=None, default=None): """ Retrieves saved value or dict of all saved entries if key is None """ - if key is not None: + if key: return self.core.db.getStorage(self.__name__, key) or default return self.core.db.getStorage(self.__name__, key) + def retrieve(self, *args, **kwargs): """ same as `getStorage` """ return self.getStorage(*args, **kwargs) + def delStorage(self, key): """ Delete entry in db """ self.core.db.delStorage(self.__name__, key) @@ -141,22 +149,33 @@ class Plugin(Base): Base plugin for hoster/crypter. Overwrite `process` / `decrypt` in your subclassed plugin. """ - __name__ = "Plugin" - __version__ = "0.4" - __pattern__ = None - __type__ = "hoster" - __config__ = [("name", "type", "desc", "default")] - __description__ = """Base Plugin""" - __author_name__ = ("RaNaN", "spoob", "mkaay") - __author_mail__ = ("RaNaN@pyload.org", "spoob@pyload.org", "mkaay@mkaay.de") + __name__ = "Plugin" + __type__ = "hoster" + __version__ = "0.07" + + __pattern__ = r'^unmatchable$' + __config__ = [] #: [("name", "type", "desc", "default")] + + __description__ = """Base plugin""" + __license__ = "GPLv3" + __authors__ = [("RaNaN", "RaNaN@pyload.org"), + ("spoob", "spoob@pyload.org"), + ("mkaay", "mkaay@mkaay.de")] + + + info = {} #: file info dict + def __init__(self, pyfile): Base.__init__(self, pyfile.m.core) + #: engage wan reconnection self.wantReconnect = False - #: enables simultaneous processing of multiple downloads + + #: enable simultaneous processing of multiple downloads self.multiDL = True self.limitDL = 0 + #: chunk limit self.chunkLimit = 1 self.resumeDownload = False @@ -165,7 +184,9 @@ class Plugin(Base): self.waitUntil = 0 self.waiting = False - self.ocr = None #captcha reader instance + #: captcha reader instance + self.ocr = None + #: account handler instance, see :py:class:`Account` self.account = pyfile.m.core.accountManager.getAccountPlugin(self.__name__) @@ -174,7 +195,9 @@ class Plugin(Base): #: username/login self.user = None - if self.account and not self.account.canUse(): self.account = None + if self.account and not self.account.canUse(): + self.account = None + if self.account: self.user, data = self.account.selectAccount() #: Browser instance, see `network.Browser` @@ -190,37 +213,46 @@ class Plugin(Base): #: associated pyfile instance, see `PyFile` self.pyfile = pyfile + self.thread = None # holds thread in future #: location where the last call to download was saved self.lastDownload = "" #: re match of the last call to `checkDownload` self.lastCheck = None + #: js engine, see `JsEngine` self.js = self.core.js - self.cTask = None #captcha task - self.retries = 0 # amount of retries already made - self.html = None # some plugins store html code here + #: captcha task + self.cTask = None + + self.html = None #@TODO: Move to hoster class in 0.4.10 + self.retries = 0 self.init() + def getChunkCount(self): if self.chunkLimit <= 0: - return self.config["download"]["chunks"] - return min(self.config["download"]["chunks"], self.chunkLimit) + return self.core.config['download']['chunks'] + return min(self.core.config['download']['chunks'], self.chunkLimit) + def __call__(self): return self.__name__ + def init(self): """initialize the plugin (in addition to `__init__`)""" pass + def setup(self): """ setup for enviroment and other things, called before downloading (possibly more than one time)""" pass + def preprocessing(self, thread): """ handles important things to do before starting """ self.thread = thread @@ -241,12 +273,14 @@ class Plugin(Base): """the 'main' method of every plugin, you **have to** overwrite it""" raise NotImplementedError + def resetAccount(self): """ dont use account and retry download """ self.account = None self.req = self.core.requestFactory.getRequest(self.__name__) self.retry() + def checksum(self, local_file=None): """ return codes: @@ -256,51 +290,119 @@ class Plugin(Base): 10 - not implemented 20 - unknown error """ - #@TODO checksum check hook + #@TODO checksum check addon return True, 10 - def setWait(self, seconds, reconnect=False): + def setReconnect(self, reconnect): + reconnect = bool(reconnect) + self.logDebug("Set wantReconnect to: %s (previous: %s)" % (reconnect, self.wantReconnect)) + self.wantReconnect = reconnect + + + def setWait(self, seconds, reconnect=None): """Set a specific wait time later used with `wait` - + :param seconds: wait time in seconds :param reconnect: True if a reconnect would avoid wait time """ - if reconnect: - self.wantReconnect = True - self.pyfile.waitUntil = time() + int(seconds) + wait_time = int(seconds) + 1 + wait_until = time() + wait_time + + self.logDebug("Set waitUntil to: %f (previous: %f)" % (wait_until, self.pyfile.waitUntil), + "Wait: %d seconds" % wait_time) - def wait(self): + self.pyfile.waitUntil = wait_until + + if reconnect is not None: + self.setReconnect(reconnect) + + + def wait(self, seconds=None, reconnect=None): """ waits the time previously set """ + + pyfile = self.pyfile + + if seconds is not None: + self.setWait(seconds) + + if reconnect is not None: + self.setReconnect(reconnect) + self.waiting = True - self.pyfile.setStatus("waiting") - while self.pyfile.waitUntil > time(): - self.thread.m.reconnecting.wait(2) + status = pyfile.status + pyfile.setStatus("waiting") - if self.pyfile.abort: raise Abort - if self.thread.m.reconnecting.isSet(): - self.waiting = False - self.wantReconnect = False - raise Reconnect + self.logInfo(_("Wait: %d seconds") % (pyfile.waitUntil - time()), + _("Reconnect: %s") % self.wantReconnect) + + if self.account: + self.logDebug("Ignore reconnection due account logged") + + while pyfile.waitUntil > time(): + if pyfile.abort: + self.abort() + + sleep(1) + else: + while pyfile.waitUntil > time(): + self.thread.m.reconnecting.wait(2) + + if pyfile.abort: + self.abort() + + if self.thread.m.reconnecting.isSet(): + self.waiting = False + self.wantReconnect = False + raise Reconnect + + sleep(1) self.waiting = False - self.pyfile.setStatus("starting") + + pyfile.status = status + def fail(self, reason): """ fail and give reason """ raise Fail(reason) - def offline(self): + + def abort(self, reason=""): + """ abort and give reason """ + if reason: + self.pyfile.error = str(reason) + raise Abort + + + def error(self, reason="", type=""): + if not reason and not type: + type = "unknown" + + msg = _("%s error") % _(type.strip().capitalize()) if type else _("Error") + msg += ": " + reason.strip() if reason else "" + msg += _(" | Plugin may be out of date") + + raise Fail(msg) + + + def offline(self, reason=""): """ fail and indicate file is offline """ + if reason: + self.pyfile.error = str(reason) raise Fail("offline") - def tempOffline(self): + + def tempOffline(self, reason=""): """ fail and indicates file ist temporary offline, the core may take consequences """ + if reason: + self.pyfile.error = str(reason) raise Fail("temp. offline") - def retry(self, max_tries=3, wait_time=1, reason=""): + + def retry(self, max_tries=5, wait_time=1, reason=""): """Retries and begin again from the beginning :param max_tries: number of maximum retries @@ -308,26 +410,28 @@ class Plugin(Base): :param reason: reason for retrying, will be passed to fail if max_tries reached """ if 0 < max_tries <= self.retries: - if not reason: reason = "Max retries reached" - raise Fail(reason) + self.error(reason or _("Max retries reached"), "retry") - self.wantReconnect = False - self.setWait(wait_time) - self.wait() + self.wait(wait_time, False) self.retries += 1 raise Retry(reason) + def invalidCaptcha(self): + self.logError(_("Invalid captcha")) if self.cTask: self.cTask.invalid() + def correctCaptcha(self): + self.logInfo(_("Correct captcha")) if self.cTask: self.cTask.correct() + def decryptCaptcha(self, url, get={}, post={}, cookies=False, forceUser=False, imgtype='jpg', - result_type='textual'): + result_type='textual', timeout=290): """ Loads a captcha and decrypts it with ocr, plugin, user input :param url: url of captcha image @@ -339,40 +443,41 @@ class Plugin(Base): :param result_type: 'textual' if text is written on the captcha\ or 'positional' for captcha where the user have to click\ on a specific region on the captcha - + :return: result of decrypting """ img = self.load(url, get=get, post=post, cookies=cookies) id = ("%.2f" % time())[-6:].replace(".", "") - temp_file = open(join("tmp", "tmpCaptcha_%s_%s.%s" % (self.__name__, id, imgtype)), "wb") - temp_file.write(img) - temp_file.close() - has_plugin = self.__name__ in self.core.pluginManager.captchaPlugins + with open(join("tmp", "tmpCaptcha_%s_%s.%s" % (self.__name__, id, imgtype)), "wb") as tmpCaptcha: + tmpCaptcha.write(img) + + has_plugin = self.__name__ in self.core.pluginManager.ocrPlugins if self.core.captcha: - Ocr = self.core.pluginManager.loadClass("captcha", self.__name__) + Ocr = self.core.pluginManager.loadClass("ocr", self.__name__) else: Ocr = None if Ocr and not forceUser: sleep(randint(3000, 5000) / 1000.0) - if self.pyfile.abort: raise Abort + if self.pyfile.abort: + self.abort() ocr = Ocr() - result = ocr.get_captcha(temp_file.name) + result = ocr.get_captcha(tmpCaptcha.name) else: captchaManager = self.core.captchaManager - task = captchaManager.newTask(img, imgtype, temp_file.name, result_type) + task = captchaManager.newTask(img, imgtype, tmpCaptcha.name, result_type) self.cTask = task - captchaManager.handleCaptcha(task) + captchaManager.handleCaptcha(task, timeout) while task.isWaiting(): if self.pyfile.abort: captchaManager.removeTask(task) - raise Abort + self.abort() sleep(1) captchaManager.removeTask(task) @@ -382,21 +487,21 @@ class Plugin(Base): elif task.error: self.fail(task.error) elif not task.result: - self.fail(_("No captcha result obtained in appropiate time by any of the plugins.")) + self.fail(_("No captcha result obtained in appropiate time by any of the plugins")) result = task.result - self.log.debug("Received captcha result: %s" % str(result)) + self.logDebug("Received captcha result: %s" % result) if not self.core.debug: try: - remove(temp_file.name) - except: + remove(tmpCaptcha.name) + except Exception: pass return result - def load(self, url, get={}, post={}, ref=True, cookies=True, just_header=False, decode=False): + def load(self, url, get={}, post={}, ref=True, cookies=True, just_header=False, decode=False, follow_location=True, save_cookies=True): """Load content at url and returns it :param url: @@ -404,35 +509,42 @@ class Plugin(Base): :param post: :param ref: :param cookies: - :param just_header: if True only the header will be retrieved and returned as dict + :param just_header: If True only the header will be retrieved and returned as dict :param decode: Wether to decode the output according to http header, should be True in most cases + :param follow_location: If True follow location else not + :param save_cookies: If True saves received cookies else discard them :return: Loaded content """ - if self.pyfile.abort: raise Abort - #utf8 vs decode -> please use decode attribute in all future plugins - if type(url) == unicode: url = str(url) + if self.pyfile.abort: + self.abort() + + if not url: + self.fail(_("No url given")) - res = self.req.load(url, get, post, ref, cookies, just_header, decode=decode) + url = encode(url).strip() #@NOTE: utf8 vs decode -> please use decode attribute in all future plugins if self.core.debug: - from inspect import currentframe + self.logDebug("Load url: " + url, *["%s=%s" % (key, val) for key, val in locals().iteritems() if key not in ("self", "url")]) - frame = currentframe() - if not exists(join("tmp", self.__name__)): - makedirs(join("tmp", self.__name__)) + res = self.req.load(url, get, post, ref, cookies, just_header, decode=decode, follow_location=follow_location, save_cookies=save_cookies) - f = open( - join("tmp", self.__name__, "%s_line%s.dump.html" % (frame.f_back.f_code.co_name, frame.f_back.f_lineno)) - , "wb") - del frame # delete the frame or it wont be cleaned + if decode: + res = encode(res) + if self.core.debug: + from inspect import currentframe + + frame = currentframe() + framefile = safe_join("tmp", self.__name__, "%s_line%s.dump.html" % (frame.f_back.f_code.co_name, frame.f_back.f_lineno)) try: - tmp = res.encode("utf8") - except: - tmp = res + if not exists(join("tmp", self.__name__)): + makedirs(join("tmp", self.__name__)) - f.write(tmp) - f.close() + with open(framefile, "wb") as f: + del frame #: delete the frame or it wont be cleaned + f.write(res) + except IOError, e: + self.logError(e) if just_header: #parse header @@ -442,7 +554,7 @@ class Plugin(Base): if not line or ":" not in line: continue key, none, value = line.partition(":") - key = key.lower().strip() + key = key.strip().lower() value = value.strip() if key in header: @@ -456,6 +568,7 @@ class Plugin(Base): return res + def download(self, url, get={}, post={}, ref=True, cookies=True, disposition=False): """Downloads the content at url to download folder @@ -468,34 +581,44 @@ class Plugin(Base): the filename will be changed if needed :return: The location where the file was saved """ + if self.pyfile.abort: + self.abort() + + if not url: + self.fail(_("No url given")) + + url = encode(url).strip() + + if self.core.debug: + self.logDebug("Download url: " + url, *["%s=%s" % (key, val) for key, val in locals().iteritems() if key not in ("self", "url")]) self.checkForSameFiles() self.pyfile.setStatus("downloading") - download_folder = self.config['general']['download_folder'] + download_folder = self.core.config['general']['download_folder'] - location = save_join(download_folder, self.pyfile.package().folder) + location = safe_join(download_folder, self.pyfile.package().folder) if not exists(location): - makedirs(location, int(self.core.config["permission"]["folder"], 8)) - - if self.core.config["permission"]["change_dl"] and os.name != "nt": - try: - uid = getpwnam(self.config["permission"]["user"])[2] - gid = getgrnam(self.config["permission"]["group"])[2] + try: + makedirs(location, int(self.core.config['permission']['folder'], 8)) + if self.core.config['permission']['change_dl'] and os.name != "nt": + uid = getpwnam(self.core.config['permission']['user'])[2] + gid = getgrnam(self.core.config['permission']['group'])[2] chown(location, uid, gid) - except Exception, e: - self.log.warning(_("Setting User and Group failed: %s") % str(e)) + + except Exception, e: + self.fail(e) # convert back to unicode location = fs_decode(location) - name = save_path(self.pyfile.name) + name = safe_filename(self.pyfile.name) filename = join(location, name) - self.core.hookManager.dispatchEvent("downloadStarts", self.pyfile, url, filename) + self.core.addonManager.dispatchEvent("download-start", self.pyfile, url, filename) try: newname = self.req.httpDownload(url, filename, get=get, post=post, ref=ref, cookies=cookies, @@ -504,31 +627,38 @@ class Plugin(Base): finally: self.pyfile.size = self.req.size - if disposition and newname and newname != name: #triple check, just to be sure - self.log.info("%(name)s saved as %(newname)s" % {"name": name, "newname": newname}) - self.pyfile.name = newname - filename = join(location, newname) + if newname: + newname = urlparse(newname).path.split("/")[-1] - fs_filename = fs_encode(filename) + if disposition and newname != name: + self.logInfo(_("%(name)s saved as %(newname)s") % {"name": name, "newname": newname}) + self.pyfile.name = newname + filename = join(location, newname) - if self.core.config["permission"]["change_file"]: - chmod(fs_filename, int(self.core.config["permission"]["file"], 8)) + fs_filename = fs_encode(filename) - if self.core.config["permission"]["change_dl"] and os.name != "nt": + if self.core.config['permission']['change_file']: try: - uid = getpwnam(self.config["permission"]["user"])[2] - gid = getgrnam(self.config["permission"]["group"])[2] + chmod(fs_filename, int(self.core.config['permission']['file'], 8)) + except Exception, e: + self.logWarning(_("Setting file mode failed"), e) + if self.core.config['permission']['change_dl'] and os.name != "nt": + try: + uid = getpwnam(self.core.config['permission']['user'])[2] + gid = getgrnam(self.core.config['permission']['group'])[2] chown(fs_filename, uid, gid) + except Exception, e: - self.log.warning(_("Setting User and Group failed: %s") % str(e)) + self.logWarning(_("Setting User and Group failed"), e) self.lastDownload = filename return self.lastDownload + def checkDownload(self, rules, api_size=0, max_size=50000, delete=True, read_size=0): """ checks the content of the last downloaded file, re match is saved to `lastCheck` - + :param rules: dict with names and rules to match (compiled regexp or strings) :param api_size: expected file size :param max_size: if the file is larger then it wont be checked @@ -537,21 +667,23 @@ class Plugin(Base): :return: dictionary key of the first rule that matched """ lastDownload = fs_encode(self.lastDownload) - if not exists(lastDownload): return None + if not exists(lastDownload): + return None size = stat(lastDownload) size = size.st_size if api_size and api_size <= size: return None elif size > max_size and not read_size: return None - self.log.debug("Download Check triggered") - f = open(lastDownload, "rb") - content = f.read(read_size if read_size else -1) - f.close() + self.logDebug("Download Check triggered") + + with open(lastDownload, "rb") as f: + content = f.read(read_size if read_size else -1) + #produces encoding errors, better log to other file in the future? - #self.log.debug("Content: %s" % content) + #self.logDebug("Content: %s" % content) for name, rule in rules.iteritems(): - if type(rule) in (str, unicode): + if isinstance(rule, basestring): if rule in content: if delete: remove(lastDownload) @@ -589,29 +721,33 @@ class Plugin(Base): 5, 7) and starting: #a download is waiting/starting and was appenrently started before raise SkipDownload(pyfile.pluginname) - download_folder = self.config['general']['download_folder'] - location = save_join(download_folder, pack.folder, self.pyfile.name) + download_folder = self.core.config['general']['download_folder'] + location = safe_join(download_folder, pack.folder, self.pyfile.name) if starting and self.core.config['download']['skip_existing'] and exists(location): size = os.stat(location).st_size if size >= self.pyfile.size: - raise SkipDownload("File exists.") + raise SkipDownload("File exists") pyfile = self.core.db.findDuplicates(self.pyfile.id, self.pyfile.package().folder, self.pyfile.name) if pyfile: if exists(location): raise SkipDownload(pyfile[0]) - self.log.debug("File %s not skipped, because it does not exists." % self.pyfile.name) + self.logDebug("File %s not skipped, because it does not exists." % self.pyfile.name) + def clean(self): """ clean everything and remove references """ if hasattr(self, "pyfile"): del self.pyfile + if hasattr(self, "req"): self.req.close() del self.req + if hasattr(self, "thread"): del self.thread + if hasattr(self, "html"): del self.html |