diff options
Diffstat (limited to 'pyload/plugins/internal')
-rw-r--r-- | pyload/plugins/internal/AbstractExtractor.py | 101 | ||||
-rw-r--r-- | pyload/plugins/internal/DeadCrypter.py | 20 | ||||
-rw-r--r-- | pyload/plugins/internal/DeadHoster.py | 28 | ||||
-rw-r--r-- | pyload/plugins/internal/MultiHoster.py | 192 | ||||
-rw-r--r-- | pyload/plugins/internal/SimpleCrypter.py | 138 | ||||
-rw-r--r-- | pyload/plugins/internal/SimpleHoster.py | 301 | ||||
-rw-r--r-- | pyload/plugins/internal/UnRar.py | 221 | ||||
-rw-r--r-- | pyload/plugins/internal/UnZip.py | 38 | ||||
-rw-r--r-- | pyload/plugins/internal/UpdateManager.py | 300 | ||||
-rw-r--r-- | pyload/plugins/internal/XFSPAccount.py | 75 | ||||
-rw-r--r-- | pyload/plugins/internal/XFSPHoster.py | 360 | ||||
-rw-r--r-- | pyload/plugins/internal/__init__.py | 0 |
12 files changed, 1774 insertions, 0 deletions
diff --git a/pyload/plugins/internal/AbstractExtractor.py b/pyload/plugins/internal/AbstractExtractor.py new file mode 100644 index 000000000..39bc46b1e --- /dev/null +++ b/pyload/plugins/internal/AbstractExtractor.py @@ -0,0 +1,101 @@ +# -*- coding: utf-8 -*- + +class ArchiveError(Exception): + pass + + +class CRCError(Exception): + pass + + +class WrongPassword(Exception): + pass + + +class AbtractExtractor: + __name__ = "AbtractExtractor" + __version__ = "0.1" + + __description__ = """Abtract extractor plugin""" + __author_name__ = "pyLoad Team" + __author_mail__ = "admin@pyload.org" + + + @staticmethod + def checkDeps(): + """ Check if system statisfy dependencies + :return: boolean + """ + return True + + @staticmethod + def getTargets(files_ids): + """ Filter suited targets from list of filename id tuple list + :param files_ids: List of filepathes + :return: List of targets, id tuple list + """ + raise NotImplementedError + + def __init__(self, m, file, out, fullpath, overwrite, excludefiles, renice): + """Initialize extractor for specific file + + :param m: ExtractArchive Addon plugin + :param file: Absolute filepath + :param out: Absolute path to destination directory + :param fullpath: extract to fullpath + :param overwrite: Overwrite existing archives + :param renice: Renice value + """ + self.m = m + self.file = file + self.out = out + self.fullpath = fullpath + self.overwrite = overwrite + self.excludefiles = excludefiles + self.renice = renice + self.files = [] #: Store extracted files here + + def init(self): + """ Initialize additional data structures """ + pass + + def checkArchive(self): + """Check if password if needed. Raise ArchiveError if integrity is + questionable. + + :return: boolean + :raises ArchiveError + """ + return False + + def checkPassword(self, password): + """ Check if the given password is/might be correct. + If it can not be decided at this point return true. + + :param password: + :return: boolean + """ + return True + + def extract(self, progress, password=None): + """Extract the archive. Raise specific errors in case of failure. + + :param progress: Progress function, call this to update status + :param password password to use + :raises WrongPassword + :raises CRCError + :raises ArchiveError + :return: + """ + raise NotImplementedError + + def getDeleteFiles(self): + """Return list of files to delete, do *not* delete them here. + + :return: List with paths of files to delete + """ + raise NotImplementedError + + def getExtractedFiles(self): + """Populate self.files at some point while extracting""" + return self.files diff --git a/pyload/plugins/internal/DeadCrypter.py b/pyload/plugins/internal/DeadCrypter.py new file mode 100644 index 000000000..2d4dfc7e6 --- /dev/null +++ b/pyload/plugins/internal/DeadCrypter.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- + +from pyload.plugins.base.Crypter import Crypter as _Crypter + + +class DeadCrypter(_Crypter): + __name__ = "DeadCrypter" + __type__ = "crypter" + __version__ = "0.02" + + __pattern__ = None + + __description__ = """Crypter is no longer available""" + __author_name__ = "stickell" + __author_mail__ = "l.stickell@yahoo.it" + + + def setup(self): + self.pyfile.error = "Crypter is no longer available" + self.offline() #@TODO: self.offline("Crypter is no longer available") diff --git a/pyload/plugins/internal/DeadHoster.py b/pyload/plugins/internal/DeadHoster.py new file mode 100644 index 000000000..72fd356bd --- /dev/null +++ b/pyload/plugins/internal/DeadHoster.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- + +from pyload.plugins.base.Hoster import Hoster as _Hoster + + +def create_getInfo(plugin): + + def getInfo(urls): + yield map(lambda url: ('#N/A: ' + url, 0, 1, url), urls) + + return getInfo + + +class DeadHoster(_Hoster): + __name__ = "DeadHoster" + __type__ = "hoster" + __version__ = "0.12" + + __pattern__ = None + + __description__ = """Hoster is no longer available""" + __author_name__ = "zoidberg" + __author_mail__ = "zoidberg@mujmail.cz" + + + def setup(self): + self.pyfile.error = "Hoster is no longer available" + self.offline() #@TODO: self.offline("Hoster is no longer available") diff --git a/pyload/plugins/internal/MultiHoster.py b/pyload/plugins/internal/MultiHoster.py new file mode 100644 index 000000000..1781cb17f --- /dev/null +++ b/pyload/plugins/internal/MultiHoster.py @@ -0,0 +1,192 @@ +# -*- coding: utf-8 -*- + +import re + +from pyload.plugins.base.Addon import Addon +from pyload.utils import remove_chars + + +class MultiHoster(Addon): + __name__ = "MultiHoster" + __type__ = "addon" + __version__ = "0.20" + + __description__ = """Generic MultiHoster plugin""" + __author_name__ = "pyLoad Team" + __author_mail__ = "admin@pyload.org" + + replacements = [("2shared.com", "twoshared.com"), ("4shared.com", "fourshared.com"), ("cloudnator.com", "shragle.com"), + ("ifile.it", "filecloud.io"), ("easy-share.com", "crocko.com"), ("freakshare.net", "freakshare.com"), + ("hellshare.com", "hellshare.cz"), ("share-rapid.cz", "sharerapid.com"), ("sharerapid.cz", "sharerapid.com"), + ("ul.to", "uploaded.to"), ("uploaded.net", "uploaded.to"), ("1fichier.com", "onefichier.com")] + ignored = [] + interval = 24 * 60 * 60 #: reload hosters daily + + + def setup(self): + self.hosters = [] + self.supported = [] + self.new_supported = [] + + def getConfig(self, option, default=''): + """getConfig with default value - subclass may not implements all config options""" + try: + # Fixed loop due to getConf deprecation in 0.4.10 + return super(MultiHoster, self).getConfig(option) + except KeyError: + return default + + def getHosterCached(self): + if not self.hosters: + try: + hosterSet = self.toHosterSet(self.getHoster()) - set(self.ignored) + except Exception, e: + self.logError(e) + return [] + + try: + configMode = self.getConfig('hosterListMode', 'all') + if configMode in ("listed", "unlisted"): + configSet = self.toHosterSet(self.getConfig('hosterList', '').replace('|', ',').replace(';', ',').split(',')) + + if configMode == "listed": + hosterSet &= configSet + else: + hosterSet -= configSet + + except Exception, e: + self.logError(e) + + self.hosters = list(hosterSet) + + return self.hosters + + def toHosterSet(self, hosters): + hosters = set((str(x).strip().lower() for x in hosters)) + + for rep in self.replacements: + if rep[0] in hosters: + hosters.remove(rep[0]) + hosters.add(rep[1]) + + hosters.discard('') + return hosters + + def getHoster(self): + """Load list of supported hoster + + :return: List of domain names + """ + raise NotImplementedError + + def coreReady(self): + if self.cb: + self.core.scheduler.removeJob(self.cb) + + self.setConfig("activated", True) #: config not in sync after plugin reload + + cfg_interval = self.getConfig("interval", None) #: reload interval in hours + if cfg_interval is not None: + self.interval = cfg_interval * 60 * 60 + + if self.interval: + self._periodical() + else: + self.periodical() + + def initPeriodical(self): + pass + + def periodical(self): + """reload hoster list periodically""" + self.logInfo(_("Reloading supported hoster list")) + + old_supported = self.supported + self.supported, self.new_supported, self.hosters = [], [], [] + + self.overridePlugins() + + old_supported = [hoster for hoster in old_supported if hoster not in self.supported] + if old_supported: + self.logDebug("UNLOAD", ", ".join(old_supported)) + for hoster in old_supported: + self.unloadHoster(hoster) + + def overridePlugins(self): + pluginMap = {} + for name in self.core.pluginManager.hosterPlugins.keys(): + pluginMap[name.lower()] = name + + accountList = [name.lower() for name, data in self.core.accountManager.accounts.items() if data] + excludedList = [] + + for hoster in self.getHosterCached(): + name = remove_chars(hoster.lower(), "-.") + + if name in accountList: + excludedList.append(hoster) + else: + if name in pluginMap: + self.supported.append(pluginMap[name]) + else: + self.new_supported.append(hoster) + + if not self.supported and not self.new_supported: + self.logError(_("No Hoster loaded")) + return + + module = self.core.pluginManager.getPlugin(self.__name__) + klass = getattr(module, self.__name__) + + # inject plugin plugin + self.logDebug("Overwritten Hosters", ", ".join(sorted(self.supported))) + for hoster in self.supported: + dict = self.core.pluginManager.hosterPlugins[hoster] + dict['new_module'] = module + dict['new_name'] = self.__name__ + + if excludedList: + self.logInfo(_("The following hosters were not overwritten - account exists"), ", ".join(sorted(excludedList))) + + if self.new_supported: + self.logDebug("New Hosters", ", ".join(sorted(self.new_supported))) + + # create new regexp + regexp = r".*(%s).*" % "|".join([x.replace(".", "\\.") for x in self.new_supported]) + if hasattr(klass, "__pattern__") and isinstance(klass.__pattern__, basestring) and '://' in klass.__pattern__: + regexp = r"%s|%s" % (klass.__pattern__, regexp) + + self.logDebug("Regexp", regexp) + + dict = self.core.pluginManager.hosterPlugins[self.__name__] + dict['pattern'] = regexp + dict['re'] = re.compile(regexp) + + def unloadHoster(self, hoster): + dict = self.core.pluginManager.hosterPlugins[hoster] + if "module" in dict: + del dict['module'] + + if "new_module" in dict: + del dict['new_module'] + del dict['new_name'] + + def unload(self): + """Remove override for all hosters. Scheduler job is removed by AddonManager""" + for hoster in self.supported: + self.unloadHoster(hoster) + + # reset pattern + klass = getattr(self.core.pluginManager.getPlugin(self.__name__), self.__name__) + dict = self.core.pluginManager.hosterPlugins[self.__name__] + dict['pattern'] = getattr(klass, "__pattern__", r'^unmatchable$') + dict['re'] = re.compile(dict['pattern']) + + def downloadFailed(self, pyfile): + """remove plugin override if download fails but not if file is offline/temp.offline""" + if pyfile.hasStatus("failed") and self.getConfig("unloadFailing", True): + hdict = self.core.pluginManager.hosterPlugins[pyfile.pluginname] + if "new_name" in hdict and hdict['new_name'] == self.__name__: + self.logDebug("Unload MultiHoster", pyfile.pluginname, hdict) + self.unloadHoster(pyfile.pluginname) + pyfile.setStatus("queued") diff --git a/pyload/plugins/internal/SimpleCrypter.py b/pyload/plugins/internal/SimpleCrypter.py new file mode 100644 index 000000000..454a95e84 --- /dev/null +++ b/pyload/plugins/internal/SimpleCrypter.py @@ -0,0 +1,138 @@ +# -*- coding: utf-8 -*- + +import re + +from pyload.plugins.base.Crypter import Crypter +from pyload.plugins.internal.SimpleHoster import PluginParseError, replace_patterns, set_cookies +from pyload.utils import html_unescape + + +class SimpleCrypter(Crypter): + __name__ = "SimpleCrypter" + __type__ = "crypter" + __version__ = "0.12" + + __pattern__ = None + + __description__ = """Simple decrypter plugin""" + __author_name__ = ("stickell", "zoidberg", "Walter Purcaro") + __author_mail__ = ("l.stickell@yahoo.it", "zoidberg@mujmail.cz", "vuolter@gmail.com") + + """ + Following patterns should be defined by each crypter: + + LINK_PATTERN: group(1) must be a download link or a regex to catch more links + example: LINK_PATTERN = r'<div class="link"><a href="(http://speedload.org/\w+)' + + TITLE_PATTERN: (optional) The group defined by 'title' should be the folder name or the webpage title + example: TITLE_PATTERN = r'<title>Files of: (?P<title>[^<]+) folder</title>' + + OFFLINE_PATTERN: (optional) Checks if the file is yet available online + example: OFFLINE_PATTERN = r'File (deleted|not found)' + + TEMP_OFFLINE_PATTERN: (optional) Checks if the file is temporarily offline + example: TEMP_OFFLINE_PATTERN = r'Server maintainance' + + + You can override the getLinks method if you need a more sophisticated way to extract the links. + + + If the links are splitted on multiple pages you can define the PAGES_PATTERN regex: + + PAGES_PATTERN: (optional) The group defined by 'pages' should be the number of overall pages containing the links + example: PAGES_PATTERN = r'Pages: (?P<pages>\d+)' + + and its loadPage method: + + def loadPage(self, page_n): + return the html of the page number page_n + """ + + + URL_REPLACEMENTS = [] + + TEXT_ENCODING = False #: Set to True or encoding name if encoding in http header is not correct + COOKIES = True #: or False or list of tuples [(domain, name, value)] + + LOGIN_ACCOUNT = False + LOGIN_PREMIUM = False + + + def prepare(self): + if self.LOGIN_ACCOUNT and not self.account: + self.fail('Required account not found!') + + if self.LOGIN_PREMIUM and not self.premium: + self.fail('Required premium account not found!') + + if isinstance(self.COOKIES, list): + set_cookies(self.req.cj, self.COOKIES) + + + def decrypt(self, pyfile): + self.prepare() + + pyfile.url = replace_patterns(pyfile.url, self.URL_REPLACEMENTS) + + self.html = self.load(pyfile.url, decode=not self.TEXT_ENCODING) + + self.checkOnline() + + package_name, folder_name = self.getPackageNameAndFolder() + + self.package_links = self.getLinks() + + if hasattr(self, 'PAGES_PATTERN') and hasattr(self, 'loadPage'): + self.handleMultiPages() + + self.logDebug("Package has %d links" % len(self.package_links)) + + if self.package_links: + self.packages = [(package_name, self.package_links, folder_name)] + else: + self.fail('Could not extract any links') + + + def getLinks(self): + """ + Returns the links extracted from self.html + You should override this only if it's impossible to extract links using only the LINK_PATTERN. + """ + return re.findall(self.LINK_PATTERN, self.html) + + + def checkOnline(self): + if hasattr(self, "OFFLINE_PATTERN") and re.search(self.OFFLINE_PATTERN, self.html): + self.offline() + elif hasattr(self, "TEMP_OFFLINE_PATTERN") and re.search(self.TEMP_OFFLINE_PATTERN, self.html): + self.tempOffline() + + + def getPackageNameAndFolder(self): + if hasattr(self, 'TITLE_PATTERN'): + m = re.search(self.TITLE_PATTERN, self.html) + if m: + name = folder = html_unescape(m.group('title').strip()) + self.logDebug("Found name [%s] and folder [%s] in package info" % (name, folder)) + return name, folder + + name = self.pyfile.package().name + folder = self.pyfile.package().folder + self.logDebug("Package info not found, defaulting to pyfile name [%s] and folder [%s]" % (name, folder)) + return name, folder + + + def handleMultiPages(self): + pages = re.search(self.PAGES_PATTERN, self.html) + if pages: + pages = int(pages.group('pages')) + else: + pages = 1 + + for p in xrange(2, pages + 1): + self.html = self.loadPage(p) + self.package_links += self.getLinks() + + + def parseError(self, msg): + raise PluginParseError(msg) diff --git a/pyload/plugins/internal/SimpleHoster.py b/pyload/plugins/internal/SimpleHoster.py new file mode 100644 index 000000000..8fdff5dd5 --- /dev/null +++ b/pyload/plugins/internal/SimpleHoster.py @@ -0,0 +1,301 @@ +# -*- coding: utf-8 -*- + +import re + +from time import time +from urlparse import urlparse + +from pyload.network.CookieJar import CookieJar +from pyload.network.RequestFactory import getURL +from pyload.plugins.base.Hoster import Hoster +from pyload.utils import fixup, html_unescape, parseFileSize + + +def replace_patterns(string, ruleslist): + for r in ruleslist: + rf, rt = r + string = re.sub(rf, rt, string) + return string + + +def set_cookies(cj, cookies): + for cookie in cookies: + if isinstance(cookie, tuple) and len(cookie) == 3: + domain, name, value = cookie + cj.setCookie(domain, name, value) + + +def parseHtmlTagAttrValue(attr_name, tag): + m = re.search(r"%s\s*=\s*([\"']?)((?<=\")[^\"]+|(?<=')[^']+|[^>\s\"'][^>\s]*)\1" % attr_name, tag, re.I) + return m.group(2) if m else None + + +def parseHtmlForm(attr_str, html, input_names=None): + for form in re.finditer(r"(?P<tag><form[^>]*%s[^>]*>)(?P<content>.*?)</?(form|body|html)[^>]*>" % attr_str, + html, re.S | re.I): + inputs = {} + action = parseHtmlTagAttrValue("action", form.group('tag')) + for inputtag in re.finditer(r'(<(input|textarea)[^>]*>)([^<]*(?=</\2)|)', form.group('content'), re.S | re.I): + name = parseHtmlTagAttrValue("name", inputtag.group(1)) + if name: + value = parseHtmlTagAttrValue("value", inputtag.group(1)) + if not value: + inputs[name] = inputtag.group(3) or '' + else: + inputs[name] = value + + if isinstance(input_names, dict): + # check input attributes + for key, val in input_names.items(): + if key in inputs: + if isinstance(val, basestring) and inputs[key] == val: + continue + elif isinstance(val, tuple) and inputs[key] in val: + continue + elif hasattr(val, "search") and re.match(val, inputs[key]): + continue + break # attibute value does not match + else: + break # attibute name does not match + else: + return action, inputs # passed attribute check + else: + # no attribute check + return action, inputs + + return {}, None # no matching form found + + +def parseFileInfo(self, url='', html=''): + info = {"name": url, "size": 0, "status": 3} + + if hasattr(self, "pyfile"): + url = self.pyfile.url + + if hasattr(self, "req") and self.req.http.code == '404': + info['status'] = 1 + else: + if not html and hasattr(self, "html"): + html = self.html + if isinstance(self.TEXT_ENCODING, basestring): + html = unicode(html, self.TEXT_ENCODING) + if hasattr(self, "html"): + self.html = html + + if hasattr(self, "OFFLINE_PATTERN") and re.search(self.OFFLINE_PATTERN, html): + info['status'] = 1 + elif hasattr(self, "FILE_OFFLINE_PATTERN") and re.search(self.FILE_OFFLINE_PATTERN, html): #@TODO: Remove in 0.4.10 + info['status'] = 1 + elif hasattr(self, "TEMP_OFFLINE_PATTERN") and re.search(self.TEMP_OFFLINE_PATTERN, html): + info['status'] = 6 + else: + online = False + try: + info.update(re.match(self.__pattern__, url).groupdict()) + except: + pass + + for pattern in ("FILE_INFO_PATTERN", "FILE_NAME_PATTERN", "FILE_SIZE_PATTERN"): + try: + info.update(re.search(getattr(self, pattern), html).groupdict()) + online = True + except AttributeError: + continue + + if online: + # File online, return name and size + info['status'] = 2 + if 'N' in info: + info['name'] = replace_patterns(info['N'], self.FILE_NAME_REPLACEMENTS) + if 'S' in info: + size = replace_patterns(info['S'] + info['U'] if 'U' in info else info['S'], + self.FILE_SIZE_REPLACEMENTS) + info['size'] = parseFileSize(size) + elif isinstance(info['size'], basestring): + if 'units' in info: + info['size'] += info['units'] + info['size'] = parseFileSize(info['size']) + + if hasattr(self, "file_info"): + self.file_info = info + + return info['name'], info['size'], info['status'], url + + +def create_getInfo(plugin): + + def getInfo(urls): + for url in urls: + cj = CookieJar(plugin.__name__) + if isinstance(plugin.COOKIES, list): + set_cookies(cj, plugin.COOKIES) + file_info = parseFileInfo(plugin, url, getURL(replace_patterns(url, plugin.FILE_URL_REPLACEMENTS), + decode=not plugin.TEXT_ENCODING, cookies=cj)) + yield file_info + + return getInfo + + +def timestamp(): + return int(time() * 1000) + + +class PluginParseError(Exception): + + def __init__(self, msg): + Exception.__init__(self) + self.value = 'Parse error (%s) - plugin may be out of date' % msg + + def __str__(self): + return repr(self.value) + + +class SimpleHoster(Hoster): + __name__ = "SimpleHoster" + __type__ = "hoster" + __version__ = "0.36" + + __pattern__ = None + + __description__ = """Simple hoster plugin""" + __author_name__ = ("zoidberg", "stickell", "Walter Purcaro") + __author_mail__ = ("zoidberg@mujmail.cz", "l.stickell@yahoo.it", "vuolter@gmail.com") + + """ + Following patterns should be defined by each hoster: + + FILE_INFO_PATTERN: Name and Size of the file + example: FILE_INFO_PATTERN = r'(?P<N>file_name) (?P<S>file_size) (?P<U>size_unit)' + or + FILE_NAME_PATTERN: Name that will be set for the file + example: FILE_NAME_PATTERN = r'(?P<N>file_name)' + FILE_SIZE_PATTERN: Size that will be checked for the file + example: FILE_SIZE_PATTERN = r'(?P<S>file_size) (?P<U>size_unit)' + + OFFLINE_PATTERN: Checks if the file is yet available online + example: OFFLINE_PATTERN = r'File (deleted|not found)' + + TEMP_OFFLINE_PATTERN: Checks if the file is temporarily offline + example: TEMP_OFFLINE_PATTERN = r'Server (maintenance|maintainance)' + + PREMIUM_ONLY_PATTERN: (optional) Checks if the file can be downloaded only with a premium account + example: PREMIUM_ONLY_PATTERN = r'Premium account required' + """ + + FILE_NAME_REPLACEMENTS = [("&#?\w+;", fixup)] + FILE_SIZE_REPLACEMENTS = [] + FILE_URL_REPLACEMENTS = [] + + TEXT_ENCODING = False #: Set to True or encoding name if encoding in http header is not correct + COOKIES = True #: or False or list of tuples [(domain, name, value)] + FORCE_CHECK_TRAFFIC = False #: Set to True to force checking traffic left for premium account + + + def init(self): + self.file_info = {} + + + def setup(self): + self.resumeDownload = self.multiDL = self.premium + + + def prepare(self): + if isinstance(self.COOKIES, list): + set_cookies(self.req.cj, self.COOKIES) + self.req.setOption("timeout", 120) + + + def process(self, pyfile): + self.prepare() + + pyfile.url = replace_patterns(pyfile.url, self.FILE_URL_REPLACEMENTS) + + # Due to a 0.4.9 core bug self.load would keep previous cookies even if overridden by cookies parameter. + # Workaround using getURL. Can be reverted in 0.4.10 as the cookies bug has been fixed. + self.html = getURL(pyfile.url, decode=not self.TEXT_ENCODING, cookies=self.COOKIES) + premium_only = hasattr(self, 'PREMIUM_ONLY_PATTERN') and re.search(self.PREMIUM_ONLY_PATTERN, self.html) + if not premium_only: # Usually premium only pages doesn't show the file information + self.getFileInfo() + + if self.premium and (not self.FORCE_CHECK_TRAFFIC or self.checkTrafficLeft()): + self.handlePremium() + elif premium_only: + self.fail("This link require a premium account") + else: + # This line is required due to the getURL workaround. Can be removed in 0.4.10 + self.html = self.load(pyfile.url, decode=not self.TEXT_ENCODING) + self.handleFree() + + + def getFileInfo(self): + self.logDebug("URL", self.pyfile.url) + + name, size, status = parseFileInfo(self)[:3] + + if status == 1: + self.offline() + elif status == 6: + self.tempOffline() + elif status != 2: + self.logDebug(self.file_info) + self.parseError('File info') + + if name: + self.pyfile.name = name + else: + self.pyfile.name = html_unescape(urlparse(self.pyfile.url).path.split("/")[-1]) + + if size: + self.pyfile.size = size + else: + self.logError(_("File size not parsed")) + + self.logDebug("FILE NAME: %s FILE SIZE: %s" % (self.pyfile.name, self.pyfile.size)) + return self.file_info + + + def handleFree(self): + self.fail("Free download not implemented") + + + def handlePremium(self): + self.fail("Premium download not implemented") + + + def parseError(self, msg): + raise PluginParseError(msg) + + + def longWait(self, wait_time=None, max_tries=3): + if wait_time and isinstance(wait_time, (int, long, float)): + time_str = "%dh %dm" % divmod(wait_time / 60, 60) + else: + wait_time = 900 + time_str = "(unknown time)" + max_tries = 100 + + self.logInfo(_("Download limit reached, reconnect or wait %s") % time_str) + + self.setWait(wait_time, True) + self.wait() + self.retry(max_tries=max_tries, reason="Download limit reached") + + + def parseHtmlForm(self, attr_str='', input_names=None): + return parseHtmlForm(attr_str, self.html, input_names) + + + def checkTrafficLeft(self): + traffic = self.account.getAccountInfo(self.user, True)['trafficleft'] + if traffic == -1: + return True + size = self.pyfile.size / 1024 + self.logInfo(_("Filesize: %i KiB, Traffic left for user %s: %i KiB") % (size, self.user, traffic)) + return size <= traffic + + + #@TODO: Remove in 0.4.10 + def wait(self, seconds=False, reconnect=False): + if seconds: + self.setWait(seconds, reconnect) + super(SimpleHoster, self).wait() diff --git a/pyload/plugins/internal/UnRar.py b/pyload/plugins/internal/UnRar.py new file mode 100644 index 000000000..0f54e75b9 --- /dev/null +++ b/pyload/plugins/internal/UnRar.py @@ -0,0 +1,221 @@ +# -*- coding: utf-8 -*- + +import os +import re + +from glob import glob +from os.path import basename, join +from string import digits +from subprocess import Popen, PIPE + +from pyload.plugins.internal.AbstractExtractor import AbtractExtractor, WrongPassword, ArchiveError, CRCError +from pyload.utils import safe_join, decode + + +def renice(pid, value): + if os.name != "nt" and value: + try: + Popen(["renice", str(value), str(pid)], stdout=PIPE, stderr=PIPE, bufsize=-1) + except: + print "Renice failed" + + +class UnRar(AbtractExtractor): + __name__ = "UnRar" + __version__ = "0.18" + + __description__ = """Rar extractor plugin""" + __author_name__ = "RaNaN" + __author_mail__ = "RaNaN@pyload.org" + + + CMD = "unrar" + + # there are some more uncovered rar formats + re_version = re.compile(r"(UNRAR 5[\.\d]+(.*?)freeware)") + re_splitfile = re.compile(r"(.*)\.part(\d+)\.rar$", re.I) + re_partfiles = re.compile(r".*\.(rar|r[0-9]+)", re.I) + re_filelist = re.compile(r"(.+)\s+(\d+)\s+(\d+)\s+") + re_filelist5 = re.compile(r"(.+)\s+(\d+)\s+\d\d-\d\d-\d\d\s+\d\d:\d\d\s+(.+)") + re_wrongpwd = re.compile("(Corrupt file or wrong password|password incorrect)", re.I) + + + @staticmethod + def checkDeps(): + if os.name == "nt": + UnRar.CMD = join(pypath, "UnRAR.exe") + p = Popen([UnRar.CMD], stdout=PIPE, stderr=PIPE) + p.communicate() + else: + try: + p = Popen([UnRar.CMD], stdout=PIPE, stderr=PIPE) + p.communicate() + except OSError: + + # fallback to rar + UnRar.CMD = "rar" + p = Popen([UnRar.CMD], stdout=PIPE, stderr=PIPE) + p.communicate() + + return True + + + @staticmethod + def getTargets(files_ids): + result = [] + + for file, id in files_ids: + if not file.endswith(".rar"): + continue + + match = UnRar.re_splitfile.findall(file) + if match: + # only add first parts + if int(match[0][1]) == 1: + result.append((file, id)) + else: + result.append((file, id)) + + return result + + + def init(self): + self.passwordProtected = False + self.headerProtected = False #: list files will not work without password + self.smallestFile = None #: small file to test passwords + self.password = "" #: save the correct password + + + def checkArchive(self): + p = self.call_unrar("l", "-v", self.file) + out, err = p.communicate() + if self.re_wrongpwd.search(err): + self.passwordProtected = True + self.headerProtected = True + return True + + # output only used to check if passworded files are present + if self.re_version.search(out): + for attr, size, name in self.re_filelist5.findall(out): + if attr.startswith("*"): + self.passwordProtected = True + return True + else: + for name, size, packed in self.re_filelist.findall(out): + if name.startswith("*"): + self.passwordProtected = True + return True + + self.listContent() + if not self.files: + raise ArchiveError("Empty Archive") + + return False + + + def checkPassword(self, password): + # at this point we can only verify header protected files + if self.headerProtected: + p = self.call_unrar("l", "-v", self.file, password=password) + out, err = p.communicate() + if self.re_wrongpwd.search(err): + return False + + return True + + + def extract(self, progress, password=None): + command = "x" if self.fullpath else "e" + + p = self.call_unrar(command, self.file, self.out, password=password) + renice(p.pid, self.renice) + + progress(0) + progressstring = "" + while True: + c = p.stdout.read(1) + # quit loop on eof + if not c: + break + # reading a percentage sign -> set progress and restart + if c == '%': + progress(int(progressstring)) + progressstring = "" + # not reading a digit -> therefore restart + elif c not in digits: + progressstring = "" + # add digit to progressstring + else: + progressstring = progressstring + c + progress(100) + + # retrieve stderr + err = p.stderr.read() + + if "CRC failed" in err and not password and not self.passwordProtected: + raise CRCError + elif "CRC failed" in err: + raise WrongPassword + if err.strip(): #: raise error if anything is on stderr + raise ArchiveError(err.strip()) + if p.returncode: + raise ArchiveError("Process terminated") + + if not self.files: + self.password = password + self.listContent() + + + def getDeleteFiles(self): + if ".part" in basename(self.file): + return glob(re.sub("(?<=\.part)([01]+)", "*", self.file, re.IGNORECASE)) + # get files which matches .r* and filter unsuited files out + parts = glob(re.sub(r"(?<=\.r)ar$", "*", self.file, re.IGNORECASE)) + return filter(lambda x: self.re_partfiles.match(x), parts) + + + def listContent(self): + command = "vb" if self.fullpath else "lb" + p = self.call_unrar(command, "-v", self.file, password=self.password) + out, err = p.communicate() + + if "Cannot open" in err: + raise ArchiveError("Cannot open file") + + if err.strip(): #: only log error at this point + self.m.logError(err.strip()) + + result = set() + + for f in decode(out).splitlines(): + f = f.strip() + result.add(safe_join(self.out, f)) + + self.files = result + + + def call_unrar(self, command, *xargs, **kwargs): + args = [] + # overwrite flag + args.append("-o+") if self.overwrite else args.append("-o-") + + if self.excludefiles: + for word in self.excludefiles.split(';'): + args.append("-x%s" % word) + + # assume yes on all queries + args.append("-y") + + # set a password + if "password" in kwargs and kwargs['password']: + args.append("-p%s" % kwargs['password']) + else: + args.append("-p-") + + # NOTE: return codes are not reliable, some kind of threading, cleanup whatever issue + call = [self.CMD, command] + args + list(xargs) + self.m.logDebug(" ".join(call)) + + p = Popen(call, stdout=PIPE, stderr=PIPE) + + return p diff --git a/pyload/plugins/internal/UnZip.py b/pyload/plugins/internal/UnZip.py new file mode 100644 index 000000000..65a5a82bb --- /dev/null +++ b/pyload/plugins/internal/UnZip.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- + +import sys +import zipfile + +from pyload.plugins.internal.AbstractExtractor import AbtractExtractor + + +class UnZip(AbtractExtractor): + __name__ = "UnZip" + __version__ = "0.1" + + __description__ = """Zip extractor plugin""" + __author_name__ = "RaNaN" + __author_mail__ = "RaNaN@pyload.org" + + + @staticmethod + def checkDeps(): + return sys.version_info[:2] >= (2, 6) + + @staticmethod + def getTargets(files_ids): + result = [] + + for file, id in files_ids: + if file.endswith(".zip"): + result.append((file, id)) + + return result + + def extract(self, progress, password=None): + z = zipfile.ZipFile(self.file) + self.files = z.namelist() + z.extractall(self.out) + + def getDeleteFiles(self): + return [self.file] diff --git a/pyload/plugins/internal/UpdateManager.py b/pyload/plugins/internal/UpdateManager.py new file mode 100644 index 000000000..7848fa2de --- /dev/null +++ b/pyload/plugins/internal/UpdateManager.py @@ -0,0 +1,300 @@ +# -*- coding: utf-8 -*- + +import re +import sys + +from operator import itemgetter +from os import path, remove, stat + +from pyload.network.RequestFactory import getURL +from pyload.plugins.base.Addon import Expose, Addon, threaded +from pyload.utils import safe_join + + +class UpdateManager(Addon): + __name__ = "UpdateManager" + __type__ = "addon" + __version__ = "0.36" + + __config__ = [("activated", "bool", "Activated", True), + ("mode", "pyLoad + plugins;plugins only", "Check updates for", "pyLoad + plugins"), + ("interval", "int", "Check interval in hours", 8), + ("reloadplugins", "bool", "Monitor plugins for code changes (debug mode only)", True), + ("nodebugupdate", "bool", "Don't check for updates in debug mode", True)] + + __description__ = """Check for updates""" + __author_name__ = "Walter Purcaro" + __author_mail__ = "vuolter@gmail.com" + + + event_list = ["pluginConfigChanged"] + + SERVER_URL = "http://updatemanager.pyload.org" + MIRROR_URL = "" #: empty actually + + MIN_INTERVAL = 3 * 60 * 60 #: 3h minimum check interval (value is in seconds) + + + def pluginConfigChanged(self, plugin, name, value): + if name == "interval": + interval = value * 60 * 60 + if self.MIN_INTERVAL <= interval != self.interval: + self.core.scheduler.removeJob(self.cb) + self.interval = interval + self.initPeriodical() + else: + self.logDebug("Invalid interval value, kept current") + elif name == "reloadplugins": + if self.cb2: + self.core.scheduler.removeJob(self.cb2) + if value is True and self.core.debug: + self.periodical2() + + + def coreReady(self): + self.pluginConfigChanged(self.__name__, "interval", self.getConfig("interval")) + x = lambda: self.pluginConfigChanged(self.__name__, "reloadplugins", self.getConfig("reloadplugins")) + self.core.scheduler.addJob(10, x, threaded=False) + + + def unload(self): + self.pluginConfigChanged(self.__name__, "reloadplugins", False) + + + def setup(self): + self.cb2 = None + self.interval = self.MIN_INTERVAL + self.updating = False + self.info = {'pyload': False, 'version': None, 'plugins': False} + self.mtimes = {} #: store modification time for each plugin + + + def periodical2(self): + if not self.updating: + self.autoreloadPlugins() + self.cb2 = self.core.scheduler.addJob(4, self.periodical2, threaded=False) + + + @Expose + def autoreloadPlugins(self): + """ reload and reindex all modified plugins """ + modules = filter( + lambda m: m and (m.__name__.startswith("pyload.plugins.") or + m.__name__.startswith("userplugins.")) and + m.__name__.count(".") >= 2, sys.modules.itervalues() + ) + + reloads = [] + + for m in modules: + root, type, name = m.__name__.rsplit(".", 2) + id = (type, name) + if type in self.core.pluginManager.plugins: + f = m.__file__.replace(".pyc", ".py") + if not path.isfile(f): + continue + + mtime = stat(f).st_mtime + + if id not in self.mtimes: + self.mtimes[id] = mtime + elif self.mtimes[id] < mtime: + reloads.append(id) + self.mtimes[id] = mtime + + return True if self.core.pluginManager.reloadPlugins(reloads) else False + + + def periodical(self): + if not self.info['pyload'] and not (self.getConfig("nodebugupdate") and self.core.debug): + self.updateThread() + + + def server_request(self): + request = lambda x: getURL(x, get={'v': self.core.api.getServerVersion()}).splitlines() + try: + return request(self.SERVER_URL) + except: + try: + return request(self.MIRROR_URL) + except: + pass + self.logWarning(_("Unable to contact server to get updates")) + + + @threaded + def updateThread(self): + self.updating = True + status = self.update(onlyplugin=self.getConfig("mode") == "plugins only") + if status == 2: + self.core.api.restart() + else: + self.updating = False + + + @Expose + def updatePlugins(self): + """ simple wrapper for calling plugin update quickly """ + return self.update(onlyplugin=True) + + + @Expose + def update(self, onlyplugin=False): + """ check for updates """ + data = self.server_request() + if not data: + exitcode = 0 + elif data[0] == "None": + self.logInfo(_("No new pyLoad version available")) + updates = data[1:] + exitcode = self._updatePlugins(updates) + elif onlyplugin: + exitcode = 0 + else: + newversion = data[0] + self.logInfo(_("*** New pyLoad Version %s available ***") % newversion) + self.logInfo(_("*** Get it here: https://github.com/pyload/pyload/releases ***")) + exitcode = 3 + self.info['pyload'] = True + self.info['version'] = newversion + return exitcode #: 0 = No plugins updated; 1 = Plugins updated; 2 = Plugins updated, but restart required; 3 = No plugins updated, new pyLoad version available + + + def _updatePlugins(self, updates): + """ check for plugin updates """ + + if self.info['plugins']: + return False #: plugins were already updated + + updated = [] + + vre = re.compile(r'__version__.*=.*("|\')([0-9.]+)') + url = updates[0] + schema = updates[1].split('|') + if "BLACKLIST" in updates: + blacklist = updates[updates.index('BLACKLIST') + 1:] + updates = updates[2:updates.index('BLACKLIST')] + else: + blacklist = None + updates = updates[2:] + + upgradable = sorted(map(lambda x: dict(zip(schema, x.split('|'))), updates), key=itemgetter("type", "name")) + for plugin in upgradable: + filename = plugin['name'] + prefix = plugin['type'] + version = plugin['version'] + + if filename.endswith(".pyc"): + name = filename[:filename.find("_")] + else: + name = filename.replace(".py", "") + + #@TODO: obsolete after 0.4.10 + if prefix.endswith("s"): + type = prefix[:-1] + else: + type = prefix + + plugins = getattr(self.core.pluginManager, "%sPlugins" % type) + + oldver = float(plugins[name]['v']) if name in plugins else None + newver = float(version) + + if not oldver: + msg = "New [%(type)s] %(name)s (v%(newver)s)" + elif newver > oldver: + msg = "New version of [%(type)s] %(name)s (v%(oldver)s -> v%(newver)s)" + else: + continue + + self.logInfo(_(msg) % { + 'type': type, + 'name': name, + 'oldver': oldver, + 'newver': newver, + }) + + try: + content = getURL(url % plugin) + m = vre.search(content) + if m and m.group(2) == version: + f = open(safe_join("userplugins", prefix, filename), "wb") + f.write(content) + f.close() + updated.append((prefix, name)) + else: + raise Exception, _("Version mismatch") + except Exception, e: + self.logError(_("Error updating plugin: %s") % filename, e) + + if blacklist: + blacklisted = map(lambda x: (x.split('|')[0], x.split('|')[1].rsplit('.', 1)[0]), blacklist) + + # Always protect base and internal plugins from removing + for i, n, t in blacklisted.enumerate(): + if t in ("base", "internal"): + del blacklisted[i] + + blacklisted = sorted(blacklisted) + removed = self.removePlugins(blacklisted) + for t, n in removed: + self.logInfo(_("Removed blacklisted plugin [%(type)s] %(name)s") % { + 'type': t, + 'name': n, + }) + + if updated: + reloaded = self.core.pluginManager.reloadPlugins(updated) + if reloaded: + self.logInfo(_("Plugins updated and reloaded")) + exitcode = 1 + else: + self.logInfo(_("*** Plugins have been updated, but need a pyLoad restart to be reloaded ***")) + self.info['plugins'] = True + exitcode = 2 + else: + self.logInfo(_("No plugin updates available")) + exitcode = 0 + + return exitcode #: 0 = No plugins updated; 1 = Plugins updated; 2 = Plugins updated, but restart required + + + @Expose + def removePlugins(self, type_plugins): + """ delete plugins from disk """ + + if not type_plugins: + return + + self.logDebug("Requested deletion of plugins: %s" % type_plugins) + + removed = [] + + for type, name in type_plugins: + err = False + file = name + ".py" + + for root in ("userplugins", path.join(pypath, "pyload", "plugins")): + + filename = safe_join(root, type, file) + try: + remove(filename) + except Exception, e: + self.logDebug("Error deleting: %s" % path.basename(filename), e) + err = True + + filename += "c" + if path.isfile(filename): + try: + if type == "addon": + self.manager.deactivateAddon(name) + remove(filename) + except Exception, e: + self.logDebug("Error deleting: %s" % path.basename(filename), e) + err = True + + if not err: + id = (type, name) + removed.append(id) + + return removed #: return a list of the plugins successfully removed diff --git a/pyload/plugins/internal/XFSPAccount.py b/pyload/plugins/internal/XFSPAccount.py new file mode 100644 index 000000000..058fdf2b5 --- /dev/null +++ b/pyload/plugins/internal/XFSPAccount.py @@ -0,0 +1,75 @@ +# -*- coding: utf-8 -*- + +import re + +from time import mktime, strptime + +from pyload.plugins.base.Account import Account +from pyload.plugins.internal.SimpleHoster import parseHtmlForm, set_cookies +from pyload.utils import parseFileSize + + +class XFSPAccount(Account): + __name__ = "XFSPAccount" + __type__ = "account" + __version__ = "0.07" + + __description__ = """XFileSharingPro base account plugin""" + __author_name__ = "zoidberg" + __author_mail__ = "zoidberg@mujmail.cz" + + + HOSTER_URL = None + + COOKIES = None #: or list of tuples [(domain, name, value)] + + VALID_UNTIL_PATTERN = r'>Premium.[Aa]ccount expire:</TD><TD><b>([^<]+)</b>' + TRAFFIC_LEFT_PATTERN = r'>Traffic available today:</TD><TD><b>([^<]+)</b>' + LOGIN_FAIL_PATTERN = r'Incorrect Login or Password|>Error<' + PREMIUM_PATTERN = r'>Renew premium<' + + + def loadAccountInfo(self, user, req): + html = req.load(self.HOSTER_URL + "?op=my_account", decode=True) + + validuntil = trafficleft = None + premium = True if re.search(self.PREMIUM_PATTERN, html) else False + + m = re.search(self.VALID_UNTIL_PATTERN, html) + if m: + premium = True + trafficleft = -1 + try: + self.logDebug(m.group(1)) + validuntil = mktime(strptime(m.group(1), "%d %B %Y")) + except Exception, e: + self.logError(e) + else: + m = re.search(self.TRAFFIC_LEFT_PATTERN, html) + if m: + trafficleft = m.group(1) + if "Unlimited" in trafficleft: + premium = True + else: + trafficleft = parseFileSize(trafficleft) / 1024 + + return {"validuntil": validuntil, "trafficleft": trafficleft, "premium": premium} + + + def login(self, user, data, req): + set_cookies(req.cj, self.COOKIES) + + html = req.load('%slogin.html' % self.HOSTER_URL, decode=True) + + action, inputs = parseHtmlForm('name="FL"', html) + if not inputs: + inputs = {"op": "login", + "redirect": self.HOSTER_URL} + + inputs.update({"login": user, + "password": data['password']}) + + html = req.load(self.HOSTER_URL, post=inputs, decode=True) + + if re.search(self.LOGIN_FAIL_PATTERN, html): + self.wrongPassword() diff --git a/pyload/plugins/internal/XFSPHoster.py b/pyload/plugins/internal/XFSPHoster.py new file mode 100644 index 000000000..3adae43c5 --- /dev/null +++ b/pyload/plugins/internal/XFSPHoster.py @@ -0,0 +1,360 @@ +# -*- coding: utf-8 -*- + +import re + +from pycurl import FOLLOWLOCATION, LOW_SPEED_TIME +from random import random +from urllib import unquote +from urlparse import urlparse + +from pyload.network.RequestFactory import getURL +from pyload.plugins.internal.CaptchaService import ReCaptcha, SolveMedia +from pyload.plugins.internal.SimpleHoster import SimpleHoster, create_getInfo, PluginParseError, replace_patterns +from pyload.utils import html_unescape + + +class XFSPHoster(SimpleHoster): + """ + Common base for XFileSharingPro hosters like EasybytezCom, CramitIn, FiledinoCom... + Some hosters may work straight away when added to __pattern__ + However, most of them will NOT work because they are either down or running a customized version + """ + __name__ = "XFSPHoster" + __type__ = "hoster" + __version__ = "0.37" + + __pattern__ = r'^unmatchable$' + + __description__ = """XFileSharingPro base hoster plugin""" + __author_name__ = ("zoidberg", "stickell", "Walter Purcaro") + __author_mail__ = ("zoidberg@mujmail.cz", "l.stickell@yahoo.it", "vuolter@gmail.com") + + + HOSTER_NAME = None + + FILE_URL_REPLACEMENTS = [(r'/embed-(\w{12}).*', r'/\1')] #: support embedded files + + COOKIES = [(HOSTER_NAME, "lang", "english")] + + FILE_INFO_PATTERN = r'<tr><td align=right><b>Filename:</b></td><td nowrap>(?P<N>[^<]+)</td></tr>\s*.*?<small>\((?P<S>[^<]+)\)</small>' + FILE_NAME_PATTERN = r'<input type="hidden" name="fname" value="(?P<N>[^"]+)"' + FILE_SIZE_PATTERN = r'You have requested .*\((?P<S>[\d\.\,]+) ?(?P<U>\w+)?\)</font>' + + OFFLINE_PATTERN = r'>\s*\w+ (Not Found|file (was|has been) removed)' + TEMP_OFFLINE_PATTERN = r'>\s*\w+ server (is in )?(maintenance|maintainance)' + + WAIT_PATTERN = r'<span id="countdown_str">.*?>(\d+)</span>' + + OVR_LINK_PATTERN = r'<h2>Download Link</h2>\s*<textarea[^>]*>([^<]+)' + LINK_PATTERN = None #: final download url pattern + + CAPTCHA_URL_PATTERN = r'(http://[^"\']+?/captchas?/[^"\']+)' + CAPTCHA_DIV_PATTERN = r'>Enter code.*?<div.*?>(.+?)</div>' + RECAPTCHA_PATTERN = None + SOLVEMEDIA_PATTERN = None + + ERROR_PATTERN = r'class=["\']err["\'][^>]*>(.+?)</' + + + def setup(self): + self.chunkLimit = 1 + + if self.__name__ == "XFSPHoster": + self.multiDL = True + self.__pattern__ = self.core.pluginManager.hosterPlugins[self.__name__]['pattern'] + self.HOSTER_NAME = re.match(self.__pattern__, self.pyfile.url).group(1).lower() + self.COOKIES = [(self.HOSTER_NAME, "lang", "english")] + else: + self.resumeDownload = self.multiDL = self.premium + + + def prepare(self): + """ Initialize important variables """ + if not self.HOSTER_NAME: + self.fail("Missing HOSTER_NAME") + + if not self.LINK_PATTERN: + pattr = r'(http://([^/]*?%s|\d+\.\d+\.\d+\.\d+)(:\d+)?(/d/|(?:/files)?/\d+/\w+/)[^"\'<]+)' + self.LINK_PATTERN = pattr % self.HOSTER_NAME + + if isinstance(self.COOKIES, list): + set_cookies(self.req.cj, self.COOKIES) + + self.captcha = None + self.errmsg = None + self.passwords = self.getPassword().splitlines() + + + def process(self, pyfile): + self.prepare() + + pyfile.url = replace_patterns(pyfile.url, self.FILE_URL_REPLACEMENTS) + + if not re.match(self.__pattern__, pyfile.url): + if self.premium: + self.handleOverriden() + else: + self.fail("Only premium users can download from other hosters with %s" % self.HOSTER_NAME) + else: + try: + # Due to a 0.4.9 core bug self.load would use cookies even if + # cookies=False. Workaround using getURL to avoid cookies. + # Can be reverted in 0.4.10 as the cookies bug has been fixed. + self.html = getURL(pyfile.url, decode=True, cookies=self.COOKIES) + self.file_info = self.getFileInfo() + except PluginParseError: + self.file_info = None + + self.location = self.getDirectDownloadLink() + + if not self.file_info: + pyfile.name = html_unescape(unquote(urlparse( + self.location if self.location else pyfile.url).path.split("/")[-1])) + + if self.location: + self.startDownload(self.location) + elif self.premium: + self.handlePremium() + else: + self.handleFree() + + + def getDirectDownloadLink(self): + """ Get download link for premium users with direct download enabled """ + self.req.http.lastURL = self.pyfile.url + + self.req.http.c.setopt(FOLLOWLOCATION, 0) + self.html = self.load(self.pyfile.url, decode=True) + self.header = self.req.http.header + self.req.http.c.setopt(FOLLOWLOCATION, 1) + + location = None + m = re.search(r"Location\s*:\s*(.*)", self.header, re.I) + if m and re.match(self.LINK_PATTERN, m.group(1)): + location = m.group(1).strip() + + return location + + + def handleFree(self): + url = self.getDownloadLink() + self.logDebug("Download URL: %s" % url) + self.startDownload(url) + + + def getDownloadLink(self): + for i in xrange(5): + self.logDebug("Getting download link: #%d" % i) + data = self.getPostParameters() + + self.req.http.c.setopt(FOLLOWLOCATION, 0) + self.html = self.load(self.pyfile.url, post=data, ref=True, decode=True) + self.header = self.req.http.header + self.req.http.c.setopt(FOLLOWLOCATION, 1) + + m = re.search(r"Location\s*:\s*(.*)", self.header, re.I) + if m: + break + + m = re.search(self.LINK_PATTERN, self.html, re.S) + if m: + break + + else: + if self.errmsg and 'captcha' in self.errmsg: + self.fail("No valid captcha code entered") + else: + self.fail("Download link not found") + + return m.group(1) + + + def handlePremium(self): + self.html = self.load(self.pyfile.url, post=self.getPostParameters()) + m = re.search(self.LINK_PATTERN, self.html) + if m is None: + self.parseError('LINK_PATTERN not found') + self.startDownload(m.group(1)) + + + def handleOverriden(self): + #only tested with easybytez.com + self.html = self.load("http://www.%s/" % self.HOSTER_NAME) + action, inputs = self.parseHtmlForm('') + upload_id = "%012d" % int(random() * 10 ** 12) + action += upload_id + "&js_on=1&utype=prem&upload_type=url" + inputs['tos'] = '1' + inputs['url_mass'] = self.pyfile.url + inputs['up1oad_type'] = 'url' + + self.logDebug(self.HOSTER_NAME, action, inputs) + #wait for file to upload to easybytez.com + self.req.http.c.setopt(LOW_SPEED_TIME, 600) + self.html = self.load(action, post=inputs) + + action, inputs = self.parseHtmlForm('F1') + if not inputs: + self.parseError('TEXTAREA not found') + self.logDebug(self.HOSTER_NAME, inputs) + if inputs['st'] == 'OK': + self.html = self.load(action, post=inputs) + elif inputs['st'] == 'Can not leech file': + self.retry(max_tries=20, wait_time=3 * 60, reason=inputs['st']) + else: + self.fail(inputs['st']) + + #get easybytez.com link for uploaded file + m = re.search(self.OVR_LINK_PATTERN, self.html) + if m is None: + self.parseError('OVR_LINK_PATTERN not found') + self.pyfile.url = m.group(1) + header = self.load(self.pyfile.url, just_header=True) + if 'location' in header: # Direct link + self.startDownload(self.pyfile.url) + else: + self.retry() + + + def startDownload(self, link): + link = link.strip() + if self.captcha: + self.correctCaptcha() + self.logDebug("DIRECT LINK: %s" % link) + self.download(link, disposition=True) + + + def checkErrors(self): + m = re.search(self.ERROR_PATTERN, self.html) + if m: + self.errmsg = m.group(1) + self.logWarning(re.sub(r"<.*?>", " ", self.errmsg)) + + if 'wait' in self.errmsg: + wait_time = sum([int(v) * {"hour": 3600, "minute": 60, "second": 1}[u] for v, u in + re.findall(r'(\d+)\s*(hour|minute|second)', self.errmsg)]) + self.wait(wait_time, True) + elif 'captcha' in self.errmsg: + self.invalidCaptcha() + elif 'premium' in self.errmsg and 'require' in self.errmsg: + self.fail("File can be downloaded by premium users only") + elif 'limit' in self.errmsg: + self.wait(1 * 60 * 60, True) + self.retry(25) + elif 'countdown' in self.errmsg or 'Expired' in self.errmsg: + self.retry() + elif 'maintenance' in self.errmsg: + self.tempOffline() + elif 'download files up to' in self.errmsg: + self.fail("File too large for free download") + else: + self.fail(self.errmsg) + + else: + self.errmsg = None + + return self.errmsg + + + def getPostParameters(self): + for _ in xrange(3): + if not self.errmsg: + self.checkErrors() + + if hasattr(self, "FORM_PATTERN"): + action, inputs = self.parseHtmlForm(self.FORM_PATTERN) + else: + action, inputs = self.parseHtmlForm(input_names={"op": re.compile("^download")}) + + if not inputs: + action, inputs = self.parseHtmlForm('F1') + if not inputs: + if self.errmsg: + self.retry() + else: + self.parseError("Form not found") + + self.logDebug(self.HOSTER_NAME, inputs) + + if 'op' in inputs and inputs['op'] in ("download2", "download3"): + if "password" in inputs: + if self.passwords: + inputs['password'] = self.passwords.pop(0) + else: + self.fail("No or invalid passport") + + if not self.premium: + m = re.search(self.WAIT_PATTERN, self.html) + if m: + wait_time = int(m.group(1)) + 1 + self.setWait(wait_time, False) + else: + wait_time = 0 + + self.captcha = self.handleCaptcha(inputs) + + if wait_time: + self.wait() + + self.errmsg = None + return inputs + + else: + inputs['referer'] = self.pyfile.url + + if self.premium: + inputs['method_premium'] = "Premium Download" + if 'method_free' in inputs: + del inputs['method_free'] + else: + inputs['method_free'] = "Free Download" + if 'method_premium' in inputs: + del inputs['method_premium'] + + self.html = self.load(self.pyfile.url, post=inputs, ref=True) + self.errmsg = None + + else: + self.parseError('FORM: %s' % (inputs['op'] if 'op' in inputs else 'UNKNOWN')) + + + def handleCaptcha(self, inputs): + m = re.search(self.CAPTCHA_URL_PATTERN, self.html) + if m: + captcha_url = m.group(1) + inputs['code'] = self.decryptCaptcha(captcha_url) + return 1 + + m = re.search(self.CAPTCHA_DIV_PATTERN, self.html, re.DOTALL) + if m: + captcha_div = m.group(1) + self.logDebug(captcha_div) + numerals = re.findall(r'<span.*?padding-left\s*:\s*(\d+).*?>(\d)</span>', html_unescape(captcha_div)) + inputs['code'] = "".join([a[1] for a in sorted(numerals, key=lambda num: int(num[0]))]) + self.logDebug("CAPTCHA", inputs['code'], numerals) + return 2 + + recaptcha = ReCaptcha(self) + try: + captcha_key = re.search(self.RECAPTCHA_PATTERN, self.html).group(1) + except: + captcha_key = recaptcha.detect_key() + + if captcha_key: + self.logDebug("RECAPTCHA KEY: %s" % captcha_key) + inputs['recaptcha_challenge_field'], inputs['recaptcha_response_field'] = recaptcha.challenge(captcha_key) + return 3 + + solvemedia = SolveMedia(self) + try: + captcha_key = re.search(self.SOLVEMEDIA_PATTERN, self.html).group(1) + except: + captcha_key = solvemedia.detect_key() + + if captcha_key: + inputs['adcopy_challenge'], inputs['adcopy_response'] = solvemedia.challenge(captcha_key) + return 4 + + return 0 + + +getInfo = create_getInfo(XFSPHoster) diff --git a/pyload/plugins/internal/__init__.py b/pyload/plugins/internal/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/pyload/plugins/internal/__init__.py |