diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000..df5f5e852f --- /dev/null +++ b/.gitignore @@ -0,0 +1,211 @@ +# https://raw.githubusercontent.com/github/gitignore/master/Global/Archives.gitignore + +# It's better to unpack these files and commit the raw source because +# git has its own built in compression methods. +*.7z +*.jar +*.rar +*.zip +*.gz +*.tgz +*.bzip +*.bz2 +*.xz +*.lzma +*.cab + +# Packing-only formats +*.iso +*.tar + +# Package management formats +*.dmg +*.xpi +*.gem +*.egg +*.deb +*.rpm +*.msi +*.msm +*.msp + + +# https://raw.githubusercontent.com/github/gitignore/master/Global/Linux.gitignore + +*~ + +# Temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + + +# https://raw.githubusercontent.com/github/gitignore/master/Global/Windows.gitignore + +# Windows thumbnail cache files +Thumbs.db +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +Desktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msm +*.msp + +# Windows shortcuts +*.lnk + + +# https://raw.githubusercontent.com/github/gitignore/master/Global/macOS.gitignore + +*.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + + +# https://raw.githubusercontent.com/github/gitignore/master/Python.gitignore + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +# TODO: Activate this rule when dependencies are not handled in this repo anymore +#lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ + +# TODO: Activate these rules when translations are not handled in this repo anymore +# Translations +#*.mo +#*.po +#*.pot + +# Django stuff: +*.log +local_settings.py + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ + +# PyCharm +.idea/ diff --git a/.hgignore b/.hgignore deleted file mode 100644 index 1215b241de..0000000000 --- a/.hgignore +++ /dev/null @@ -1,36 +0,0 @@ -# ignoreing unneeded files, using glob syntax -syntax: glob -*.pyc -*~ -*.pidaproject -.svn -*.DS_Store -*.egg-info -*.project -*.pydevproject -Downloads/* -container/* -Logs/* -docs/module/ -docs/_build -module/plugins/container/DLC_*.py -failed_links.txt -module/config/gui.xml -module/config/core.xml -module/config/plugin.xml -links.txt -ssl.crt -ssl.key -cert.pem -module/web/pyload.db -*.svg -*.prefs -*.po -*.orig -*.rej -pyload/* -dist/* -build/* -setup.py -paver-minilib.zip -env/* diff --git a/LICENSE b/LICENSE.MD similarity index 100% rename from LICENSE rename to LICENSE.MD diff --git a/README b/README deleted file mode 100644 index 7f3c4f4c83..0000000000 --- a/README +++ /dev/null @@ -1,89 +0,0 @@ - -Description -=========== - -pyLoad is a free and open source downloader for 1-click-hosting sites -like rapidshare.com or uploaded.to. -It supports link decryption as well as all important container formats. - -pyLoad is written entirely in Python and is currently under heavy development. - -For news, downloads, wiki, forum and further information visit http://pyload.org/ - -To report bugs, suggest features, ask a question, get the developer version -or help us out, visit http://bitbucket.org/spoob/pyload/ - -Documentation about extending pyLoad can be found at http://docs.pyload.org or join us at #pyload on irc.freenode.net - -Dependencies -============ - -You need at least python 2.5 to run pyLoad and all of these required libaries. -They should be automatically installed when using pip install. -The prebuilt pyload packages also install these dependencies or have them included, so manuall install -is only needed when installing pyLoad from source. - -Required --------- - -- pycurl a.k.a python-curl -- jinja2 -- beaker -- thrift -- simplejson (for python 2.5) - -Some plugins require additional packages, only install these when needed. - -Optional --------- - -- pycrypto: RSDF/CCF/DLC support -- tesseract, python-pil a.k.a python-imaging: Automatic captcha recognition for a small amount of plugins -- jsengine (spidermonkey, ossp-js, pyv8, rhino): Used for several hoster, ClickNLoad -- feedparser -- BeautifulSoup -- pyOpenSSL: For SSL connection - -First start -=========== - -Note: If you installed pyload via package-manager `python pyLoadCore.py` is probably equivalent to `pyLoadCore` - -Run:: - - python pyLoadCore.py - -and follow the instructions of the setup assistent. - -For a list of options use:: - - python pyLoadCore.py -h - -Configuration -============= - -After finishing the setup assistent pyLoad is ready to use and more configuration can be done via webinterface. -Additionally you could simply edit the config files located in your pyLoad home dir (defaults to: ~/.pyload) -with your favorite editor and edit the appropriate options. For a short description of -the options take a look at http://pyload.org/configuration. - -To restart the configure assistent run:: - - python pyLoadCore.py -s - -Adding downloads ----------------- - -To start the CLI and connect to a local server, run:: - - python pyLoadCli.py -l - -for more options refer to:: - - python pyLoadCli.py -h - -The webinterface can be accessed when pointing your webbrowser to the ip and configured port, defaults to http://localhost:8000 - -Notes -===== -For more information, see http://pyload.org/ diff --git a/README.MD b/README.MD new file mode 100644 index 0000000000..2e588f2e6d --- /dev/null +++ b/README.MD @@ -0,0 +1,87 @@ +# ⚠️ Important: This Branch is Deprecated + +pyLoad v0.4.x has reached **end-of-life**. +There will be **no further updates, bug fixes, or new features** here. +Python 2.x has been deprecated for over 6 years, yet pyLoad continued to support it until now. It's time to move on. + +# **please migrate to [pyLoad-ng](https://github.com/pyload/pyload/tree/main) (v0.5.x).** +
+# pyLoad + +[![pyload.net](https://img.shields.io/badge/.net-pyload-orange.svg)](https://pyload.net) +[![wiki](https://img.shields.io/badge/docs-wiki-blue.svg)](https://github.com/pyload/pyload/wiki) +[![support](https://img.shields.io/badge/support-issues-red.svg)](https://github.com/pyload/pyload/issues) +[![Join the chat](https://badges.gitter.im/pyload/pyload.svg)](https://gitter.im/pyload/pyload?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) +[![IRC Freenode](https://img.shields.io/badge/irc-freenode-lightgray.svg)](https://kiwiirc.com/client/irc.freenode.com/#pyload) +[![Twitter](https://img.shields.io/badge/-twitter-429cd6.svg?logo=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPD94bWwgdmVyc2lvbj0iMS4wIiA%2FPjwhRE9DVFlQRSBzdmcgIFBVQkxJQyAnLS8vVzNDLy9EVEQgU1ZHIDEuMC8vRU4nICAnaHR0cDovL3d3dy53My5vcmcvVFIvMjAwMS9SRUMtU1ZHLTIwMDEwOTA0L0RURC9zdmcxMC5kdGQnPjxzdmcgZW5hYmxlLWJhY2tncm91bmQ9Im5ldyAwIDAgMzIgMzIiIGhlaWdodD0iMzJweCIgaWQ9IkxheWVyXzEiIHZlcnNpb249IjEuMCIgdmlld0JveD0iMCAwIDMyIDMyIiB3aWR0aD0iMzJweCIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayI%2BPHBhdGggZD0iTTMxLjk5Myw2LjA3N0MzMC44MTYsNi42LDI5LjU1Miw2Ljk1MywyOC4yMjMsNy4xMWMxLjM1NS0wLjgxMiwyLjM5Ni0yLjA5OCwyLjg4Ny0zLjYzICBjLTEuMjY5LDAuNzUxLTIuNjczLDEuMjk5LTQuMTY4LDEuNTkyQzI1Ljc0NCwzLjc5NywyNC4wMzgsMywyMi4xNDksM2MtMy42MjUsMC02LjU2MiwyLjkzOC02LjU2Miw2LjU2MyAgYzAsMC41MTQsMC4wNTcsMS4wMTYsMC4xNjksMS40OTZDMTAuMzAxLDEwLjc4NSw1LjQ2NSw4LjE3MiwyLjIyNyw0LjIwMWMtMC41NjQsMC45Ny0wLjg4OCwyLjA5Ny0wLjg4OCwzLjMgIGMwLDIuMjc4LDEuMTU5LDQuMjg2LDIuOTE5LDUuNDY0Yy0xLjA3NS0wLjAzNS0yLjA4Ny0wLjMyOS0yLjk3Mi0wLjgyMWMtMC4wMDEsMC4wMjctMC4wMDEsMC4wNTYtMC4wMDEsMC4wODIgIGMwLDMuMTgxLDIuMjYyLDUuODM0LDUuMjY1LDYuNDM3Yy0wLjU1LDAuMTQ5LTEuMTMsMC4yMy0xLjcyOSwwLjIzYy0wLjQyNCwwLTAuODM0LTAuMDQxLTEuMjM0LTAuMTE3ICBjMC44MzQsMi42MDYsMy4yNTksNC41MDQsNi4xMyw0LjU1OGMtMi4yNDUsMS43Ni01LjA3NSwyLjgxMS04LjE1LDIuODExYy0wLjUzLDAtMS4wNTMtMC4wMzEtMS41NjYtMC4wOTIgIEMyLjkwNSwyNy45MTMsNi4zNTUsMjksMTAuMDYyLDI5YzEyLjA3MiwwLDE4LjY3NS0xMC4wMDEsMTguNjc1LTE4LjY3NWMwLTAuMjg0LTAuMDA4LTAuNTY4LTAuMDItMC44NSAgQzMwLDguNTUsMzEuMTEyLDcuMzk1LDMxLjk5Myw2LjA3N3oiIGZpbGw9IiM1NUFDRUUiLz48Zy8%2BPGcvPjxnLz48Zy8%2BPGcvPjxnLz48L3N2Zz4%3D)](https://twitter.com/pyload) +[![Facebook](https://img.shields.io/badge/-facebook-3a589e.svg?logo=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPD94bWwgdmVyc2lvbj0iMS4wIiA%2FPjwhRE9DVFlQRSBzdmcgIFBVQkxJQyAnLS8vVzNDLy9EVEQgU1ZHIDEuMC8vRU4nICAnaHR0cDovL3d3dy53My5vcmcvVFIvMjAwMS9SRUMtU1ZHLTIwMDEwOTA0L0RURC9zdmcxMC5kdGQnPjxzdmcgZW5hYmxlLWJhY2tncm91bmQ9Im5ldyAwIDAgMzIgMzIiIGhlaWdodD0iMzJweCIgaWQ9IkxheWVyXzEiIHZlcnNpb249IjEuMCIgdmlld0JveD0iMCAwIDMyIDMyIiB3aWR0aD0iMzJweCIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayI%2BPGc%2BPHBhdGggZD0iTTMyLDMwYzAsMS4xMDQtMC44OTYsMi0yLDJIMmMtMS4xMDQsMC0yLTAuODk2LTItMlYyYzAtMS4xMDQsMC44OTYtMiwyLTJoMjhjMS4xMDQsMCwyLDAuODk2LDIsMlYzMHoiIGZpbGw9IiMzQjU5OTgiLz48cGF0aCBkPSJNMjIsMzJWMjBoNGwxLTVoLTV2LTJjMC0yLDEuMDAyLTMsMy0zaDJWNWMtMSwwLTIuMjQsMC00LDBjLTMuNjc1LDAtNiwyLjg4MS02LDd2M2gtNHY1aDR2MTJIMjJ6IiBmaWxsPSIjRkZGRkZGIiBpZD0iZiIvPjwvZz48Zy8%2BPGcvPjxnLz48Zy8%2BPGcvPjxnLz48L3N2Zz4%3D)](https://www.facebook.com/pyload) + +*pyLoad* is a free and open source downloader for 1-click-hosting sites. +It supports link decryption as well as all important container formats. +Targeted for headless NASs it is designed to be extremely lightweight, fully customizable and remotely manageable via web interface. + +# Dependencies + +You need at least Python 2.5 to run pyLoad and all of these required libraries, unless shipped with pyLoad. +They should be automatically installed when using pip install. +The prebuilt pyLoad packages also install these dependencies or have them included, so manual install +is only needed when installing pyLoad from source. + +## Required + +- pycurl a.k.a python-curl +- jinja2 (shipped with pyLoad) +- beaker (shipped with pyLoad) +- thrift (shipped with pyLoad) + +Some plugins require additional packages, only install these when needed. + +## Optional + +- pycrypto: RSDF/CCF/DLC support +- tesseract, pillow (or python-pil a.k.a python-imaging): Automatic captcha recognition for a small amount of plugins +- jsengine (js2py, node, spidermonkey, ossp-js, pyv8, rhino): Used for several hoster, ClickNLoad +- feedparser +- BeautifulSoup (shipped with pyLoad) +- pyOpenSSL: For SSL connection + +# First start + +Note: If you installed pyLoad via package-manager `python pyLoadCore.py` is probably equivalent to `pyLoadCore` + +Run: + + python pyLoadCore.py + +and follow the instructions of the setup assistant. + +For a list of options use: + + python pyLoadCore.py -h + +# Configuration + +After finishing the setup assistant pyLoad is ready to use and more configuration can be done via webinterface. +Additionally you could simply edit the config files located in your pyLoad home dir (defaults to: ~/.pyload) +with your favorite editor and edit the appropriate options. For a short description of +the options take a look at https://github.com/pyload/pyload/wiki/Configuration. + +To restart the configure assistant run: + + python pyLoadCore.py -s + +## Adding downloads + +To start the CLI and connect to a local server, run: + + python pyLoadCli.py -l + +for more options refer to: + + python pyLoadCli.py -h + +The webinterface can be accessed when pointing your web browser to the ip and configured port, defaults to http://localhost:8000 + +# Notes + +For more information, see https://pyload.net diff --git a/docs/conf.py b/docs/conf.py index 9d2cf98f9f..39e92cdb88 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -66,7 +66,7 @@ cog.outl("release = '%s'" % ".".join(v)) ]]]""" version = '0.4' -release = '0.4.9' +release = '0.4.20' # [[[end]]] diff --git a/locale/cli.pot b/locale/cli.pot index 646c6c70ef..84b0c4ef61 100644 --- a/locale/cli.pot +++ b/locale/cli.pot @@ -6,8 +6,8 @@ #, fuzzy msgid "" msgstr "" -"Project-Id-Version: pyLoad 0.4.9\n" -"Report-Msgid-Bugs-To: 'bugs@pyload.org'\n" +"Project-Id-Version: pyLoad 0.4.20\n" +"Report-Msgid-Bugs-To: 'bugs@pyload.net'\n" "POT-Creation-Date: 2011-12-07 19:21+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" diff --git a/locale/core.pot b/locale/core.pot index 546f0e4d3b..84730c3d57 100644 --- a/locale/core.pot +++ b/locale/core.pot @@ -6,8 +6,8 @@ #, fuzzy msgid "" msgstr "" -"Project-Id-Version: pyLoad 0.4.9\n" -"Report-Msgid-Bugs-To: 'bugs@pyload.org'\n" +"Project-Id-Version: pyLoad 0.4.20\n" +"Report-Msgid-Bugs-To: 'bugs@pyload.net'\n" "POT-Creation-Date: 2011-12-07 19:21+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" @@ -484,7 +484,7 @@ msgid "*** New pyLoad Version %s available ***" msgstr "" #: module/plugins/hooks/UpdateManager.py:94 -msgid "*** Get it here: http://pyload.org/download ***" +msgid "*** Get it here: http://pyload.net/download ***" msgstr "" #: module/plugins/hooks/UpdateManager.py:97 diff --git a/locale/de/LC_MESSAGES/django.mo b/locale/de/LC_MESSAGES/django.mo index 017f373dc8..8897b3ff44 100644 Binary files a/locale/de/LC_MESSAGES/django.mo and b/locale/de/LC_MESSAGES/django.mo differ diff --git a/locale/django.pot b/locale/django.pot index 81c9c7b6bb..9ea1c98fea 100644 --- a/locale/django.pot +++ b/locale/django.pot @@ -6,8 +6,8 @@ #, fuzzy msgid "" msgstr "" -"Project-Id-Version: pyLoad 0.4.9\n" -"Report-Msgid-Bugs-To: 'bugs@pyload.org'\n" +"Project-Id-Version: pyLoad 0.4.20\n" +"Report-Msgid-Bugs-To: 'bugs@pyload.net'\n" "POT-Creation-Date: 2011-12-07 19:21+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" @@ -17,670 +17,1272 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -#: module/web/translations.js:1 module/web/templates/default/base.html:123 -#: module/web/templates/default/base.html:124 -#: module/web/templates/default/settings_item.html:14 -msgid "on" +#: module\web\templates\classic\window.html:22 +msgid "Password for RAR-Archive" msgstr "" -#: module/web/translations.js:2 module/web/templates/default/captcha.html:7 -msgid "Please read the text on the captcha." +#: module\web\templates\modern\pathchooser.html:87 module\web\templates\pyplex\pathchooser.html:87 +msgid "Parent Directory" msgstr "" -#: module/web/translations.js:3 -msgid "Settings saved." +#: module\web\templates\modern\admin.html:136 module\web\templates\classic\admin.html:75 +#: module\web\templates\pyplex\admin.html:140 +msgid "Current password" msgstr "" -#: module/web/translations.js:4 module/web/templates/default/base.html:123 -#: module/web/templates/default/base.html:124 -#: module/web/templates/default/settings_item.html:16 -msgid "off" +#: module\web\templates\classic\pathchooser.html:39 module\web\templates\classic\pathchooser.html:41 +#: module\web\templates\pyplex\settings.html:210 module\web\templates\modern\settings.html:210 +msgid "absolute" msgstr "" -#: module/web/translations.js:5 -msgid "Success" +#: module\web\templates\classic\window.html:13 +msgid "Paste your links here or any text and press the filter button." msgstr "" -#: module/web/translations.js:6 -msgid "Passwords did not match." +#: module\web\templates\pyplex\settings.html:63 module\web\templates\pyplex\settings.html:94 +#: module\web\templates\classic\settings.html:45 module\web\templates\classic\settings.html:74 +#: module\web\templates\modern\settings.html:63 module\web\templates\modern\settings.html:94 +msgid "Choose a section from the menu" msgstr "" -#: module/web/translations.js:7 -msgid "Delete Link" +#: module\web\templates\classic\window.html:8 +msgid "The name of the new package." msgstr "" -#: module/web/translations.js:8 -msgid "pyLoad restarted" +#: module\web\templates\pyplex\settings.html:112 module\web\templates\classic\settings.html:97 +#: module\web\templates\modern\settings.html:112 +msgid "Time" msgstr "" -#: module/web/translations.js:9 -msgid "You are really sure you want to quit pyLoad?" +#: module\config\default.conf:68 +msgid "Use Proxy" msgstr "" -#: module/web/translations.js:10 -msgid "Please Enter a packagename." +#: module\web\templates\classic\base.html:278 module\web\templates\pyplex\base.html:220 +#: module\web\templates\modern\base.html:220 +msgid "Back to top" msgstr "" -#: module/web/translations.js:11 -msgid "Please click on the right captcha position." +#: module\config\default.conf:10 +msgid "SSL Certificate" msgstr "" -#: module/web/translations.js:12 -msgid "Error occured." +#: module\web\templates\pyplex\info.html:74 module\web\templates\modern\info.html:74 +msgid "Wiki" msgstr "" -#: module/web/translations.js:13 -msgid "New Captcha Request" +#: module\web\templates\modern\info.html:93 module\web\templates\pyplex\info.html:93 +#: module\web\templates\classic\info.html:52 +msgid "Installation Folder:" msgstr "" -#: module/web/translations.js:14 -msgid "Failed" +#: module\web\templates\modern\admin.html:31 module\web\templates\classic\admin.html:14 +#: module\web\templates\pyplex\admin.html:35 +msgid "Restart pyLoad" msgstr "" -#: module/web/translations.js:15 -msgid "No Captchas to read." +#: module\web\templates\modern\info.html:97 module\web\templates\pyplex\info.html:97 +#: module\web\templates\classic\info.html:60 +msgid "Download Folder:" msgstr "" -#: module/web/translations.js:16 -#: module/web/templates/default/filemanager.html:65 -#: module/web/templates/default/folder.html:14 -msgid "Folder is empty" +#: module\config\default.conf:20 +msgid "Template" msgstr "" -#: module/web/translations.js:17 -msgid "Restart Link" +#: module\web\templates\classic\base.html:238 module\web\templates\pyplex\base.html:187 +#: module\web\templates\modern\base.html:187 +msgid "Speed:" msgstr "" -#: module/web/translations.js:18 -msgid "New folder" +#: module\web\templates\pyplex\info.html:82 module\web\templates\modern\info.html:82 +msgid "Issue Tracker" msgstr "" -#: module/web/translations.js:19 -msgid "Are you sure you want to restart pyLoad?" +#: module\web\templates\classic\home.html:241 module\web\templates\pyplex\home.html:23 +#: module\web\templates\modern\home.html:23 +msgid "Progress" msgstr "" -#: module/web/pyload_app.py:125 -msgid "You dont have permission to access this page." +#: module\web\templates\modern\queue.html:44 module\web\templates\classic\queue.html:46 +#: module\web\templates\pyplex\queue.html:44 +msgid "Restart Package" msgstr "" -#: module/web/pyload_app.py:193 -msgid "Download directory not found." +#: module\web\templates\modern\window.html:17 module\web\templates\classic\window.html:14 +#: module\web\templates\pyplex\window.html:17 +msgid "Filter urls" msgstr "" -#: module/web/pyload_app.py:260 module/web/pyload_app.py:267 -msgid "unlimited" +#: module\config\default.conf:11 +msgid "SSL Key" msgstr "" -#: module/web/pyload_app.py:262 module/web/pyload_app.py:269 -msgid "not available" +#: module\web\templates\modern\admin.html:143 module\web\templates\classic\admin.html:81 +#: module\web\templates\pyplex\admin.html:147 +msgid "The new password." +msgstr "" + +#: module\web\templates\classic\base.html:269 module\web\templates\pyplex\base.html:82 +#: module\web\templates\pyplex\base.html:208 module\web\templates\modern\base.html:82 +#: module\web\templates\modern\base.html:208 +msgid "loading" msgstr "" -#: module/web/pyload_app.py:509 -msgid "Run pyLoadCore.py -s to access the setup." +#: module\web\templates\pyplex\settings.html:109 module\web\templates\classic\settings.html:94 +#: module\web\templates\modern\settings.html:109 +msgid "Premium" msgstr "" -#: module/web/json_app.py:60 -#, python-format -msgid "waiting %s" +#: module\web\templates\pyplex\settings.html:74 module\web\templates\modern\settings.html:74 +msgid " Loading data..." msgstr "" -#: module/web/templates/default/info.html:14 -#: module/web/templates/default/info.html:15 -#: module/web/templates/default/home.html:239 -msgid "Information" +#: module\web\templates\pyplex\settings.html:132 module\web\templates\classic\settings.html:121 +#: module\web\templates\modern\settings.html:132 +msgid "valid" msgstr "" -#: module/web/templates/default/info.html:18 +#: module\web\templates\modern\info.html:110 module\web\templates\pyplex\info.html:110 +#: module\web\templates\classic\info.html:18 msgid "News" msgstr "" -#: module/web/templates/default/info.html:21 -msgid "Support" +#: module\web\templates\modern\info.html:91 module\web\templates\pyplex\info.html:91 +#: module\web\templates\classic\info.html:48 +msgid "pyLoad version:" msgstr "" -#: module/web/templates/default/info.html:37 -msgid "System" +#: module\config\default.conf:52 +msgid "Groupname" msgstr "" -#: module/web/templates/default/info.html:40 -msgid "Python:" +#: module\web\templates\classic\base.html:166 module\web\templates\pyplex\base.html:77 +#: module\web\templates\pyplex\base.html:179 module\web\templates\modern\base.html:77 +#: module\web\templates\modern\base.html:179 +msgid "Captcha waiting" msgstr "" -#: module/web/templates/default/info.html:44 -msgid "OS:" +#: module\web\templates\modern\logs.html:110 module\web\templates\classic\logs.html:36 +#: module\web\templates\pyplex\logs.html:110 +msgid "Jump to time:" msgstr "" -#: module/web/templates/default/info.html:48 -msgid "pyLoad version:" +#: module\web\templates\modern\window.html:38 module\web\templates\classic\window.html:31 +#: module\web\templates\pyplex\window.html:38 +msgid "Destination" msgstr "" -#: module/web/templates/default/info.html:52 -msgid "Installation Folder:" +#: module\web\templates\pyplex\admin.html:106 module\web\templates\pyplex\admin.html:119 +#: module\web\templates\pyplex\admin.html:154 module\web\templates\modern\admin.html:102 +#: module\web\templates\modern\admin.html:115 module\web\templates\modern\admin.html:150 +msgid "Ok" msgstr "" -#: module/web/templates/default/info.html:56 -msgid "Config Folder:" +#: module\config\default.conf:51 +msgid "Change group of running process" msgstr "" -#: module/web/templates/default/info.html:60 -msgid "Download Folder:" +#: module\web\templates\modern\base.html:65 module\web\templates\modern\base.html:140 +#: module\web\templates\pyplex\base.html:65 module\web\templates\pyplex\base.html:140 +msgid "Administration" msgstr "" -#: module/web/templates/default/info.html:64 -msgid "Free Space:" +#: module\web\templates\pyplex\settings.html:105 module\web\templates\classic\settings.html:90 +#: module\web\templates\modern\settings.html:105 +msgid "Plugin" msgstr "" -#: module/web/templates/default/info.html:68 -msgid "Language:" +#: module\web\templates\pyplex\settings.html:110 module\web\templates\classic\settings.html:95 +#: module\web\templates\modern\settings.html:110 +msgid "Valid until" msgstr "" -#: module/web/templates/default/info.html:72 -msgid "Webinterface Port:" +#: module\web\templates\classic\queue.html:25 +msgid "Delete Finished" msgstr "" -#: module/web/templates/default/info.html:76 -msgid "Remote Interface Port:" +#: module\config\default.conf:32 +msgid "Debug Mode" msgstr "" -#: module/web/templates/default/downloads.html:6 -#: module/web/templates/default/base.html:93 -#: module/web/templates/default/home.html:220 -msgid "Downloads" +#: module/web/media/js/pyplex/base.js:330 module/web/media/js/pyplex/base.js:341 module/web/media/js/classic/base.js:2 +#: module/web/media/js/modern/base.js:330 module/web/media/js/modern/base.js:341 +msgid "New Captcha Request" msgstr "" -#: module/web/templates/default/filemanager.html:19 -msgid "FileManager" +#: module\web\templates\modern\login.html:64 module\web\templates\pyplex\login.html:64 +msgid "SIGN IN" msgstr "" -#: module/web/templates/default/admin.html:8 -#: module/web/templates/default/admin.html:9 -#: module/web/templates/default/base.html:59 -msgid "Administrate" +#: module\web\templates\modern\captcha.html:7 module\web\templates\classic\captcha.html:4 +#: module\web\templates\pyplex\captcha.html:7 +msgid "Captcha reading" msgstr "" -#: module/web/templates/default/admin.html:13 -msgid "Quit pyLoad" +#: module\config\default.conf:17 +msgid "Listen on both IPv4 and IPv6 (IP must be set to 0.0.0.0)" msgstr "" -#: module/web/templates/default/admin.html:14 -msgid "Restart pyLoad" +#: module\config\default.conf:65 +msgid "Protocol" msgstr "" -#: module/web/templates/default/admin.html:18 -msgid "To add user or change passwords use:" +#: module\web\templates\classic\pathchooser.html:46 module\web\templates\pyplex\pathchooser.html:78 +#: module\web\templates\modern\pathchooser.html:78 +msgid "name" msgstr "" -#: module/web/templates/default/admin.html:19 -msgid "Important: Admin user have always all permissions!" +#: module\web\templates\modern\window.html:15 module\web\templates\classic\window.html:12 +#: module\web\templates\pyplex\window.html:15 +msgid "Links" msgstr "" -#: module/web/templates/default/admin.html:25 -#: module/web/templates/default/settings.html:91 -#: module/web/templates/default/queue.html:82 -#: module/web/templates/default/window.html:7 -#: module/web/templates/default/home.html:237 -msgid "Name" +#: module\web\templates\pyplex\queue.html:47 module\web\templates\modern\queue.html:47 +msgid "Reverse Entries" msgstr "" -#: module/web/templates/default/admin.html:28 -#: module/web/templates/default/admin.html:67 -msgid "Change Password" +#: module\web\templates\classic\base.html:209 module\web\templates\classic\home.html:223 +#: module\web\templates\classic\filemanager.html:19 module\web\templates\pyplex\base.html:133 +#: module\web\templates\modern\base.html:133 +msgid "FileManager" msgstr "" -#: module/web/templates/default/admin.html:31 -msgid "Admin" +#: module\config\default.conf:16 +msgid "Use HTTPS" msgstr "" -#: module/web/templates/default/admin.html:34 -msgid "Permissions" +#: module/web/templates/classic/filemanager_ui.js:180 module\web\templates\classic\filemanager.html:65 +#: module\web\templates\classic\folder.html:14 +msgid "Folder is empty" msgstr "" -#: module/web/templates/default/admin.html:41 -msgid "change" +#: module\web\templates\modern\captcha.html:14 module\web\templates\classic\captcha.html:9 +#: module\web\templates\pyplex\captcha.html:14 +msgid "Captcha" msgstr "" -#: module/web/templates/default/admin.html:61 -#: module/web/templates/default/admin.html:91 -#: module/web/templates/default/settings.html:167 -#: module/web/templates/default/queue.html:97 -#: module/web/templates/default/captcha.html:33 -msgid "Submit" +#: module\web\templates\pyplex\settings.html:175 module\web\templates\modern\settings.html:175 +msgid "Your username" msgstr "" -#: module/web/templates/default/admin.html:69 -msgid "Enter your current and desired Password." +#: module\web\templates\classic\base.html:137 module\web\templates\classic\base.html:252 module\config\default.conf:13 +#: module\web\templates\pyplex\base.html:34 module\web\templates\modern\base.html:34 +msgid "Webinterface" msgstr "" -#: module/web/templates/default/admin.html:70 -msgid "User" +#: module\web\templates\pyplex\settings.html:44 module\web\templates\classic\settings.html:18 +#: module\web\templates\modern\settings.html:44 +msgid "Accounts" msgstr "" -#: module/web/templates/default/admin.html:71 -#: module/web/templates/default/settings.html:179 -msgid "Your username." +#: module\web\templates\modern\base.html:73 module\web\templates\modern\base.html:171 +#: module\web\templates\pyplex\base.html:73 module\web\templates\pyplex\base.html:171 +#: module\web\templates\modern\window.html:5 module\web\templates\modern\window.html:46 +#: module\web\templates\classic\window.html:5 module\web\templates\classic\window.html:40 +#: module\web\templates\pyplex\window.html:5 module\web\templates\pyplex\window.html:46 +msgid "Add Package" msgstr "" -#: module/web/templates/default/admin.html:75 -msgid "Current password" +#: module\web\templates\pyplex\window.html:28 module\web\templates\modern\window.html:28 +msgid "Upload a container" msgstr "" -#: module/web/templates/default/admin.html:76 -#: module/web/templates/default/settings.html:184 -msgid "The password for this account." +#: module\web\templates\modern\logs.html:112 module\web\templates\pyplex\logs.html:112 +msgid "Go" msgstr "" -#: module/web/templates/default/admin.html:80 -msgid "New password" +#: module\web\templates\classic\pathchooser.html:70 +msgid "no content" msgstr "" -#: module/web/templates/default/admin.html:81 -msgid "The new password." +#: module\config\default.conf:5 +msgid "Adress" msgstr "" -#: module/web/templates/default/admin.html:85 -msgid "New password (repeat)" +#: module\web\templates\modern\queue.html:43 module\web\templates\classic\queue.html:44 +#: module\web\templates\pyplex\queue.html:43 +msgid "Delete Package" msgstr "" -#: module/web/templates/default/admin.html:86 -msgid "Please repeat the new password." +#: module\web\templates\modern\base.html:55 module\web\templates\modern\base.html:127 +#: module\web\templates\modern\window.html:43 module\web\templates\pyplex\queue.html:46 +#: module\web\templates\classic\home.html:217 module\web\templates\pyplex\base.html:55 +#: module\web\templates\pyplex\base.html:127 module\web\templates\classic\base.html:203 +#: module\web\templates\modern\queue.html:46 module\web\templates\pyplex\window.html:43 +#: module\web\templates\classic\window.html:36 +msgid "Collector" msgstr "" -#: module/web/templates/default/admin.html:92 -#: module/web/templates/default/settings.html:198 -#: module/web/templates/default/queue.html:98 -#: module/web/templates/default/window.html:41 -msgid "Reset" +#: module/web/media/js/modern/package.js:213 module/web/media/js/pyplex/package.js:218 +msgid "queued" msgstr "" -#: module/web/templates/default/settings.html:3 -#: module/web/templates/default/settings.html:4 -#: module/web/templates/default/base.html:102 -#: module/web/templates/default/home.html:229 -msgid "Config" +#: module\web\templates\modern\queue.html:23 module\web\templates\classic\queue.html:26 +#: module\web\templates\pyplex\queue.html:23 +msgid "Restart Failed" msgstr "" -#: module/web/templates/default/settings.html:16 -msgid "General" +#: module\web\templates\modern\base.html:169 module\web\templates\pyplex\base.html:169 +msgid "Pause Queue" msgstr "" -#: module/web/templates/default/settings.html:17 -msgid "Plugins" +#: module\config\default.conf:53 +msgid "Change Group and User of Downloads" msgstr "" -#: module/web/templates/default/settings.html:18 -msgid "Accounts" +#: module/web/media/js/classic/base.js:2 module\web\templates\classic\captcha.html:5 +msgid "Please read the text on the captcha." msgstr "" -#: module/web/templates/default/settings.html:45 -#: module/web/templates/default/settings.html:74 -msgid "Choose a section from the menu" +#: module/web/media/js/classic/settings.js:2 +msgid "Settings saved." msgstr "" -#: module/web/templates/default/settings.html:90 -msgid "Plugin" +#: module\web\templates\classic\logs.html:12 module\config\default.conf:58 module\config\default.conf:61 +msgid "End" msgstr "" -#: module/web/templates/default/settings.html:92 -#: module/web/templates/default/settings.html:183 -#: module/web/templates/default/login.html:19 -#: module/web/templates/default/queue.html:92 -#: module/web/templates/default/window.html:21 -msgid "Password" +#: module\web\templates\modern\settings_item.html:27 module\web\templates\modern\settings_item.html:36 +#: module\web\templates\pyplex\settings_item.html:27 module\web\templates\pyplex\settings_item.html:36 +#: module\web\templates\classic\settings_item.html:30 module\web\templates\classic\settings_item.html:36 +msgid "Browse" msgstr "" -#: module/web/templates/default/settings.html:93 -#: module/web/templates/default/home.html:238 -msgid "Status" +#: module\web\templates\pyplex\queue.html:46 module\web\templates\modern\queue.html:46 +msgid "Move Package To" msgstr "" -#: module/web/templates/default/settings.html:94 -msgid "Premium" +#: module\web\templates\modern\admin.html:30 module\web\templates\classic\admin.html:13 +#: module\web\templates\pyplex\admin.html:34 +msgid "Quit pyLoad" msgstr "" -#: module/web/templates/default/settings.html:95 -msgid "Valid until" +#: module\config\default.conf:6 +msgid "No authentication on local connections" msgstr "" -#: module/web/templates/default/settings.html:96 -msgid "Traffic left" +#: module\web\templates\classic\pathchooser.html:39 module\web\templates\classic\pathchooser.html:41 +#: module\web\templates\pyplex\settings.html:213 module\web\templates\modern\settings.html:213 +msgid "relative" msgstr "" -#: module/web/templates/default/settings.html:97 -msgid "Time" +#: module/web/media/js/pyplex/base.js:31 module/web/templates/classic/filemanager_ui.js:43 +#: module/web/media/js/classic/package_ui.js:2 module/web/media/js/modern/base.js:31 +msgid "Failed" msgstr "" -#: module/web/templates/default/settings.html:98 -msgid "Max Parallel" +#: module\web\templates\modern\info.html:89 module\web\templates\pyplex\info.html:89 +#: module\web\templates\classic\info.html:44 +msgid "OS:" msgstr "" -#: module/web/templates/default/settings.html:99 -msgid "Delete?" +#: module\config\default.conf:49 +msgid "Change file mode of downloads" msgstr "" -#: module/web/templates/default/settings.html:121 -msgid "valid" +#: module\web\templates\pyplex\window.html:20 module\web\templates\modern\window.html:20 +msgid "Add a list of links" msgstr "" -#: module/web/templates/default/settings.html:124 +#: module\web\templates\pyplex\settings.html:132 module\web\templates\classic\settings.html:124 +#: module\web\templates\modern\settings.html:132 msgid "not valid" msgstr "" -#: module/web/templates/default/settings.html:131 +#: module\web\templates\pyplex\settings.html:136 module\web\templates\classic\settings.html:131 +#: module\web\templates\modern\settings.html:136 msgid "yes" msgstr "" -#: module/web/templates/default/settings.html:134 -msgid "no" +#: module\config\default.conf:47 module\config\default.conf:66 module\web\templates\classic\login.html:14 +#: module\web\templates\modern\login.html:56 module\web\templates\pyplex\login.html:56 +msgid "Username" msgstr "" -#: module/web/templates/default/settings.html:168 -#: module/web/templates/default/settings.html:197 -#: module/web/templates/default/base.html:117 -msgid "Add" +#: module\web\templates\pyplex\info.html:115 module\web\templates\modern\info.html:115 +msgid "Donate" +msgstr "" + +#: module\web\templates\pyplex\settings.html:171 module\web\templates\modern\settings.html:171 +msgid "Enter your account data to use premium features" +msgstr "" + +#: module\web\templates\modern\base.html:58 module\web\templates\modern\base.html:130 +#: module\web\templates\classic\downloads.html:6 module\web\templates\classic\home.html:220 +#: module\web\templates\modern\downloads.html:6 module\web\templates\pyplex\base.html:58 +#: module\web\templates\pyplex\base.html:130 module\web\templates\classic\base.html:206 +#: module\web\templates\pyplex\downloads.html:6 +msgid "Downloads" msgstr "" -#: module/web/templates/default/settings.html:176 +#: module\config\default.conf:55 +msgid "Use Reconnect" +msgstr "" + +#: module\config\default.conf:39 +msgid "Max Parallel Downloads" +msgstr "" + +#: module\config\default.conf:56 +msgid "Method" +msgstr "" + +#: module/web/media/js/modern/settings.js:178 module/web/media/js/pyplex/settings.js:178 +msgid "Select File" +msgstr "" + +#: module\web\templates\pyplex\settings.html:168 module\web\templates\classic\settings.html:176 +#: module\web\templates\modern\settings.html:168 msgid "Add Account" msgstr "" -#: module/web/templates/default/settings.html:177 -msgid "Enter your account data to use premium features." +#: module\web\templates\modern\captcha.html:36 module\web\templates\classic\captcha.html:34 +#: module\web\templates\pyplex\captcha.html:36 +msgid "Please install the Tampermonkey add-on in your browser and add the " msgstr "" -#: module/web/templates/default/settings.html:178 -#: module/web/templates/default/login.html:3 -msgid "Login" +#: module\web\templates\modern\info.html:87 module\web\templates\pyplex\info.html:87 +#: module\web\templates\classic\info.html:40 +msgid "Python:" msgstr "" -#: module/web/templates/default/settings.html:188 -msgid "Type" +#: module/web/media/js/pyplex/base.js:248 module/web/media/js/modern/settings.js:120 +#: module/web/media/js/modern/settings.js:138 module/web/media/js/modern/settings.js:155 +#: module/web/media/js/modern/base.js:248 module/web/media/js/pyplex/settings.js:120 +#: module/web/media/js/pyplex/settings.js:138 module/web/media/js/pyplex/settings.js:155 +#: module/web/media/js/modern/admin.js:21 module/web/media/js/pyplex/admin.js:21 +msgid "Error occurred" msgstr "" -#: module/web/templates/default/settings.html:189 -msgid "Choose the hoster for your account." +#: module\config\default.conf:36 +msgid "CPU Priority" msgstr "" -#: module/web/templates/default/pathchooser.html:39 -#: module/web/templates/default/pathchooser.html:41 -msgid "Path" +#: module\config\default.conf:44 +msgid "Skip already existing files" msgstr "" -#: module/web/templates/default/pathchooser.html:39 -#: module/web/templates/default/pathchooser.html:41 -msgid "absolute" +#: module\web\templates\classic\base.html:175 module\web\templates\pyplex\base.html:154 +#: module\web\templates\modern\base.html:154 +msgid "Info" msgstr "" -#: module/web/templates/default/pathchooser.html:39 -#: module/web/templates/default/pathchooser.html:41 -msgid "relative" +#: module/web/media/js/modern/admin.js:25 module/web/media/js/pyplex/admin.js:25 module/web/media/js/classic/admin.js:2 +msgid "Passwords did not match." msgstr "" -#: module/web/templates/default/pathchooser.html:46 -msgid "name" +#: module\web\templates\classic\queue.html:83 +msgid "The name of the package." msgstr "" -#: module/web/templates/default/pathchooser.html:47 -msgid "size" +#: module/web/media/js/modern/settings.js:83 module/web/media/js/pyplex/settings.js:83 +msgid "Name of plugin" msgstr "" -#: module/web/templates/default/pathchooser.html:48 -msgid "type" +#: module\web\templates\pyplex\info.html:76 module\web\templates\modern\info.html:76 +msgid "Forum" msgstr "" -#: module/web/templates/default/pathchooser.html:49 -msgid "last modified" +#: module\web\templates\modern\base.html:197 module\web\templates\pyplex\base.html:197 +msgid " " msgstr "" -#: module/web/templates/default/pathchooser.html:54 -msgid "parent directory" +#: module\web\templates\modern\info.html:105 module\web\templates\pyplex\info.html:105 +#: module\web\templates\classic\info.html:72 +msgid "Webinterface Port:" msgstr "" -#: module/web/templates/default/pathchooser.html:70 -msgid "no content" +#: module/web/media/js/modern/settings.js:182 module/web/media/js/pyplex/settings.js:182 +msgid "Select Folder" msgstr "" -#: module/web/templates/default/setup.html:3 -#: module/web/templates/default/setup.html:4 -msgid "Setup" +#: module\web\templates\pyplex\settings.html:113 module\web\templates\classic\settings.html:98 +#: module\web\templates\modern\settings.html:113 +msgid "Max Parallel" msgstr "" -#: module/web/templates/default/login.html:14 -msgid "Username" +#: module/web/media/js/classic/base.js:2 +msgid "Please Enter a packagename." msgstr "" -#: module/web/templates/default/login.html:29 -msgid "Your username and password didn't match. Please try again." +#: module\config\default.conf:15 +msgid "Server" msgstr "" -#: module/web/templates/default/login.html:30 -msgid "To reset your login data or add an user run:" +#: module\web\templates\modern\base.html:52 module\web\templates\modern\base.html:124 +#: module\web\templates\modern\window.html:40 module\web\templates\pyplex\queue.html:46 +#: module\web\templates\classic\home.html:214 module\web\templates\pyplex\base.html:52 +#: module\web\templates\pyplex\base.html:124 module\web\templates\classic\base.html:200 +#: module\web\templates\modern\queue.html:46 module\web\templates\pyplex\window.html:40 +#: module\web\templates\classic\window.html:34 +msgid "Queue" msgstr "" -#: module/web/templates/default/base.html:20 -#: module/web/templates/default/base.html:139 -msgid "Webinterface" +#: module\web\templates\modern\admin.html:141 module\web\templates\classic\admin.html:80 +#: module\web\templates\pyplex\admin.html:145 +msgid "New password" msgstr "" -#: module/web/templates/default/base.html:39 -msgid "pyLoad Update available!" +#: module\web\templates\modern\base.html:170 module\web\templates\pyplex\base.html:170 +msgid "Abort Downloads" msgstr "" -#: module/web/templates/default/base.html:46 -msgid "Plugins updated, please restart!" +#: module\web\templates\pyplex\settings.html:180 module\web\templates\modern\settings.html:180 +msgid "The password for this account" msgstr "" -#: module/web/templates/default/base.html:52 -msgid "Captcha waiting" +#: module\config\default.conf:48 +msgid "Folder Permission mode" msgstr "" -#: module/web/templates/default/base.html:57 -msgid "Logout" +#: module\web\templates\pyplex\admin.html:90 module\web\templates\modern\admin.html:86 +msgid "pyLoad is restarting..." msgstr "" -#: module/web/templates/default/base.html:61 -msgid "Info" +#: module\config\default.conf:38 +msgid "Max connections for one download" msgstr "" -#: module/web/templates/default/base.html:65 -msgid "Please Login!" +#: module\web\templates\modern\admin.html:60 module\web\templates\classic\admin.html:41 +#: module\web\templates\pyplex\admin.html:64 +msgid "change" msgstr "" -#: module/web/templates/default/base.html:84 -#: module/web/templates/default/home.html:211 -msgid "Home" +#: module\web\templates\modern\admin.html:100 module/web/media/js/classic/admin.js:2 +#: module\web\templates\pyplex\admin.html:104 +msgid "Are you sure you want to restart pyLoad?" msgstr "" -#: module/web/templates/default/base.html:87 -#: module/web/templates/default/queue.html:15 -#: module/web/templates/default/window.html:34 -#: module/web/templates/default/home.html:214 -msgid "Queue" +#: module\web\templates\modern\base.html:168 module\web\templates\pyplex\base.html:168 +msgid "Resume Queue" msgstr "" -#: module/web/templates/default/base.html:90 -#: module/web/templates/default/queue.html:17 -#: module/web/templates/default/window.html:36 -#: module/web/templates/default/home.html:217 -msgid "Collector" +#: module/web/media/js/classic/base.js:2 module\web\templates\modern\base.html:185 +#: module\web\templates\modern\base.html:186 module/web/media/js/pyplex/base.js:358 +#: module/web/media/js/pyplex/base.js:363 module\web\templates\modern\settings_item.html:14 +#: module\web\templates\classic\settings_item.html:14 module/web/media/js/modern/base.js:358 +#: module/web/media/js/modern/base.js:363 module\web\templates\classic\base.html:236 +#: module\web\templates\classic\base.html:237 module\web\templates\pyplex\settings_item.html:14 +#: module\web\templates\pyplex\base.html:185 module\web\templates\pyplex\base.html:186 +msgid "on" msgstr "" -#: module/web/templates/default/base.html:99 -#: module/web/templates/default/logs.html:3 -#: module/web/templates/default/logs.html:4 -#: module/web/templates/default/home.html:226 +#: module\web\templates\classic\base.html:236 module\web\templates\pyplex\base.html:185 +#: module\web\templates\modern\base.html:185 +msgid "Download:" +msgstr "" + +#: module\web\templates\modern\admin.html:51 module\web\templates\modern\admin.html:126 +#: module\web\templates\classic\admin.html:28 module\web\templates\classic\admin.html:67 +#: module\web\templates\pyplex\admin.html:55 module\web\templates\pyplex\admin.html:130 +msgid "Change Password" +msgstr "" + +#: module\web\templates\modern\base.html:61 module\web\templates\modern\base.html:136 +#: module\web\templates\classic\home.html:226 module\web\templates\pyplex\logs.html:3 +#: module\web\templates\pyplex\logs.html:4 module\web\templates\classic\logs.html:3 +#: module\web\templates\classic\logs.html:4 module\web\templates\pyplex\base.html:61 +#: module\web\templates\pyplex\base.html:136 module\web\templates\classic\base.html:212 +#: module\web\templates\modern\logs.html:3 module\web\templates\modern\logs.html:4 msgid "Logs" msgstr "" -#: module/web/templates/default/base.html:114 -#: module/web/templates/default/logs.html:12 -msgid "Start" +#: module\web\templates\modern\login.html:49 module\web\templates\pyplex\login.html:49 +msgid "Incorrect username/email or password." msgstr "" -#: module/web/templates/default/base.html:115 -msgid "Stop" +#: module\web\templates\pyplex\info.html:111 module\web\templates\modern\info.html:111 +msgid "Loading..." msgstr "" -#: module/web/templates/default/base.html:116 -msgid "Cancel" +#: module\config\default.conf:43 +msgid "Allow IPv6" msgstr "" -#: module/web/templates/default/base.html:123 -msgid "Download:" +#: module\web\templates\pyplex\queue.html:90 module\web\templates\modern\queue.html:90 +msgid "Name of subfolder for these downloads" msgstr "" -#: module/web/templates/default/base.html:124 -msgid "Reconnect:" +#: module\config\default.conf:28 +msgid "Log Rotate" msgstr "" -#: module/web/templates/default/base.html:125 -msgid "Speed:" +#: module\config\default.conf:8 +msgid "SSL" +msgstr "" + +#: module\web\templates\classic\captcha.html:10 +msgid "The captcha." msgstr "" -#: module/web/templates/default/base.html:126 +#: module\web\templates\modern\info.html:99 module\web\templates\pyplex\info.html:99 +#: module\web\templates\classic\info.html:64 +msgid "Free Space:" +msgstr "" + +#: module\web\templates\pyplex\info.html:78 module\web\templates\modern\info.html:78 +msgid "Chat" +msgstr "" + +#: module\web\templates\pyplex\admin.html:44 module\web\templates\modern\admin.html:40 +msgid "Admin user have always all permissions!" +msgstr "" + +#: module\web\templates\classic\base.html:197 module\web\templates\classic\home.html:211 +#: module\web\templates\pyplex\base.html:49 module\web\templates\pyplex\base.html:121 +#: module\web\templates\modern\base.html:49 module\web\templates\modern\base.html:121 +msgid "Home" +msgstr "" + +#: module\web\templates\modern\admin.html:146 module\web\templates\classic\admin.html:85 +#: module\web\templates\pyplex\admin.html:150 +msgid "New password (repeat)" +msgstr "" + +#: module\config\default.conf:12 +msgid "CA's intermediate certificate bundle (optional)" +msgstr "" + +#: module\web\templates\pyplex\home.html:21 module\web\templates\classic\home.html:239 +#: module\web\templates\classic\info.html:14 module\web\templates\classic\info.html:15 +#: module\web\templates\modern\home.html:21 +msgid "Information" +msgstr "" + +#: module\config\default.conf:24 +msgid "File Log" +msgstr "" + +#: module\web\templates\pyplex\settings.html:205 module\web\templates\pyplex\settings.html:219 +#: module\web\templates\modern\settings.html:205 module\web\templates\modern\settings.html:219 +msgid "Select" +msgstr "" + +#: module\config\default.conf:23 +msgid "Log" +msgstr "" + +#: module\web\templates\classic\base.html:239 module\web\templates\pyplex\base.html:188 +#: module\web\templates\modern\base.html:188 msgid "Active:" msgstr "" -#: module/web/templates/default/base.html:127 -msgid "Reload page" +#: module\web\templates\classic\queue.html:93 +msgid "List of passwords used for unrar." msgstr "" -#: module/web/templates/default/base.html:157 -msgid "loading" +#: module\web\templates\classic\admin.html:19 +msgid "Important: Admin user have always all permissions!" msgstr "" -#: module/web/templates/default/base.html:166 -msgid "Back to top" +#: module\web\templates\modern\info.html:3 module\web\templates\modern\info.html:4 +#: module\web\templates\pyplex\info.html:3 module\web\templates\pyplex\info.html:4 +#: module\web\templates\classic\info.html:21 +msgid "Support" msgstr "" -#: module/web/templates/default/logs.html:12 -msgid "prev" +#: module/web/media/js/modern/package.js:174 module/web/media/js/classic/package_ui.js:2 +#: module/web/media/js/pyplex/package.js:179 +msgid "Restart Link" +msgstr "" + +#: module\web\templates\classic\window.html:27 +msgid "Upload a container." +msgstr "" + +#: module\web\templates\classic\base.html:227 module\web\templates\classic\logs.html:12 module\config\default.conf:57 +#: module\config\default.conf:60 +msgid "Start" +msgstr "" + +#: module\web\templates\classic\base.html:230 module\web\templates\pyplex\settings.html:191 +#: module\web\templates\classic\settings.html:168 module\web\templates\classic\settings.html:197 +#: module\web\templates\modern\settings.html:191 +msgid "Add" +msgstr "" + +#: module\config\default.conf:34 +msgid "Min Free Space (MB)" msgstr "" -#: module/web/templates/default/logs.html:12 +#: module\web\templates\modern\info.html:95 module\web\templates\pyplex\info.html:95 +#: module\web\templates\classic\info.html:56 +msgid "Config Folder:" +msgstr "" + +#: module\web\templates\classic\settings.html:189 +msgid "Choose the hoster for your account." +msgstr "" + +#: module\web\templates\classic\logs.html:12 msgid "next" msgstr "" -#: module/web/templates/default/logs.html:12 -msgid "End" +#: module\web\templates\modern\base.html:69 module\web\templates\modern\base.html:144 +#: module\web\templates\modern\settings.html:3 module\web\templates\modern\settings.html:4 +#: module\web\templates\classic\home.html:229 module\web\templates\pyplex\base.html:69 +#: module\web\templates\pyplex\base.html:144 module\web\templates\classic\base.html:215 +#: module\web\templates\pyplex\settings.html:3 module\web\templates\pyplex\settings.html:4 +#: module\web\templates\classic\settings.html:3 module\web\templates\classic\settings.html:4 +msgid "Config" msgstr "" -#: module/web/templates/default/logout.html:8 -msgid "You were successfully logged out." +#: module\web\templates\modern\admin.html:133 module\web\templates\classic\admin.html:71 +#: module\web\templates\classic\settings.html:179 module\web\templates\pyplex\admin.html:137 +msgid "Your username." msgstr "" -#: module/web/templates/default/queue.html:25 -msgid "Delete Finished" +#: module\web\templates\classic\pathchooser.html:47 module\web\templates\pyplex\pathchooser.html:79 +#: module\web\templates\modern\pathchooser.html:79 +msgid "size" msgstr "" -#: module/web/templates/default/queue.html:26 -msgid "Restart Failed" +#: module\web\templates\pyplex\home.html:18 module\web\templates\modern\settings.html:108 +#: module\web\templates\classic\home.html:238 module\web\templates\modern\home.html:18 +#: module\web\templates\pyplex\settings.html:108 module\web\templates\classic\settings.html:93 +msgid "Status" msgstr "" -#: module/web/templates/default/queue.html:65 -msgid "Folder:" +#: module\web\templates\classic\folder.html:9 module\web\templates\classic\filemanager.html:31 +#: module\web\templates\classic\filemanager.html:46 +msgid "Delete Directory" msgstr "" -#: module/web/templates/default/queue.html:65 -msgid "Password:" +#: module\config\default.conf:27 +msgid "Size in kb" msgstr "" -#: module/web/templates/default/queue.html:79 +#: module\web\templates\modern\captcha.html:30 module\web\templates\classic\captcha.html:28 +#: module\web\templates\pyplex\captcha.html:30 +msgid "Your browser does not support iframes." +msgstr "" + +#: module\web\templates\classic\base.html:171 module\web\templates\pyplex\base.html:153 +#: module\web\templates\modern\base.html:153 +msgid "Logout" +msgstr "" + +#: module\web\templates\modern\admin.html:131 module\web\templates\classic\admin.html:70 +#: module\web\templates\pyplex\admin.html:135 +msgid "User" +msgstr "" + +#: module\web\templates\classic\base.html:239 module\web\templates\pyplex\base.html:188 +#: module\web\templates\modern\base.html:188 +msgid "Active" +msgstr "" + +#: module\config\default.conf:46 +msgid "Change user of running process" +msgstr "" + +#: module\web\templates\classic\logs.html:12 +msgid "prev" +msgstr "" + +#: module\web\templates\classic\pathchooser.html:48 module\web\templates\pyplex\pathchooser.html:80 +#: module\web\templates\modern\pathchooser.html:80 +msgid "type" +msgstr "" + +#: module\web\templates\modern\captcha.html:36 module\web\templates\classic\captcha.html:34 +#: module\web\templates\pyplex\captcha.html:36 +msgid "pyload userscript" +msgstr "" + +#: module\web\templates\modern\admin.html:53 module\web\templates\classic\admin.html:34 module\config\default.conf:45 +#: module\web\templates\pyplex\admin.html:57 +msgid "Permissions" +msgstr "" + +#: module\config\default.conf:26 +msgid "Count" +msgstr "" + +#: module\web\templates\modern\admin.html:39 module\web\templates\classic\admin.html:18 +#: module\web\templates\pyplex\admin.html:43 +msgid "To add user or change passwords use:" +msgstr "" + +#: module\web\templates\pyplex\queue.html:95 module\web\templates\modern\queue.html:95 +msgid "The package password" +msgstr "" + +#: module/web/media/js/pyplex/base.js:16 module/web/templates/classic/filemanager_ui.js:36 +#: module/web/media/js/classic/package_ui.js:2 module/web/media/js/modern/base.js:16 +msgid "Success" +msgstr "" + +#: module\web\templates\pyplex\home.html:20 module\web\templates\modern\home.html:20 +msgid "Hoster" +msgstr "" + +#: module\config\default.conf:22 +msgid "Use basic auth" +msgstr "" + +#: module\web\templates\modern\info.html:85 module\web\templates\pyplex\info.html:85 +#: module\web\templates\classic\info.html:37 +msgid "System" +msgstr "" + +#: module\web\templates\modern\queue.html:45 module\web\templates\modern\queue.html:77 +#: module\web\templates\classic\queue.html:48 module\web\templates\classic\queue.html:79 +#: module\web\templates\pyplex\queue.html:45 module\web\templates\pyplex\queue.html:77 msgid "Edit Package" msgstr "" -#: module/web/templates/default/queue.html:80 -msgid "Edit the package detais below." +#: module\web\templates\pyplex\window.html:34 module\web\templates\modern\window.html:34 +msgid "not available" msgstr "" -#: module/web/templates/default/queue.html:83 -msgid "The name of the package." +#: module\web\templates\classic\base.html:179 +msgid "Please Login!" msgstr "" -#: module/web/templates/default/queue.html:87 -msgid "Folder" +#: module\web\templates\classic\pathchooser.html:39 module\web\templates\classic\pathchooser.html:41 +#: module\web\templates\pyplex\settings.html:208 module\web\templates\modern\settings.html:208 +msgid "Path" msgstr "" -#: module/web/templates/default/queue.html:88 -msgid "Name of subfolder for these downloads." +#: module\web\templates\classic\base.html:173 module\web\templates\classic\admin.html:8 +#: module\web\templates\classic\admin.html:9 module\web\templates\modern\admin.html:25 +#: module\web\templates\modern\admin.html:26 module\web\templates\pyplex\admin.html:29 +#: module\web\templates\pyplex\admin.html:30 +msgid "Administrate" msgstr "" -#: module/web/templates/default/queue.html:93 -msgid "List of passwords used for unrar." +#: module\web\templates\pyplex\settings.html:111 module\web\templates\classic\settings.html:96 +#: module\web\templates\modern\settings.html:111 +msgid "Traffic left" msgstr "" -#: module/web/templates/default/window.html:5 -#: module/web/templates/default/window.html:40 -msgid "Add Package" +#: module\config\default.conf:50 +msgid "Filemode for Downloads" +msgstr "" + +#: module/web/templates/classic/filemanager_ui.js:237 +msgid "New folder" +msgstr "" + +#: module\web\templates\pyplex\queue.html:22 module\web\templates\modern\queue.html:22 +msgid "Clear Finished" +msgstr "" + +#: module/web/media/js/classic/admin.js:2 +msgid "pyLoad restarted" +msgstr "" + +#: module\web\templates\classic\login.html:29 +msgid "Your username and password didn't match. Please try again." +msgstr "" + +#: module\web\templates\classic\pathchooser.html:49 module\web\templates\pyplex\pathchooser.html:81 +#: module\web\templates\modern\pathchooser.html:81 +msgid "last modified" msgstr "" -#: module/web/templates/default/window.html:6 +#: module\web\templates\modern\captcha.html:18 module\web\templates\classic\captcha.html:15 +#: module\web\templates\pyplex\captcha.html:18 +msgid "Text" +msgstr "" + +#: module\config\default.conf:41 +msgid "Limit Download Speed" +msgstr "" + +#: module\config\default.conf:40 +msgid "Max Download Speed in kb/s" +msgstr "" + +#: module\config\default.conf:59 +msgid "Download Time" +msgstr "" + +#: module\config\default.conf:29 module\web\templates\pyplex\settings.html:42 +#: module\web\templates\classic\settings.html:16 module\web\templates\modern\settings.html:42 +msgid "General" +msgstr "" + +#: module\web\templates\modern\window.html:7 module\web\templates\classic\window.html:6 +#: module\web\templates\pyplex\window.html:7 msgid "Paste your links or upload a container." msgstr "" -#: module/web/templates/default/window.html:8 -msgid "The name of the new package." +#: module\config\default.conf:21 +msgid "Path Prefix" msgstr "" -#: module/web/templates/default/window.html:12 -msgid "Links" +#: module\web\templates\classic\queue.html:88 +msgid "Name of subfolder for these downloads." msgstr "" -#: module/web/templates/default/window.html:13 -msgid "Paste your links here or any text and press the filter button." +#: module\web\templates\modern\captcha.html:35 module\web\templates\classic\captcha.html:33 +#: module\web\templates\pyplex\captcha.html:35 +msgid "Note: to solve this interactive captchas" msgstr "" -#: module/web/templates/default/window.html:14 -msgid "Filter urls" +#: module\web\templates\pyplex\settings.html:43 module\web\templates\classic\settings.html:17 +#: module\web\templates\modern\settings.html:43 +msgid "Plugins" msgstr "" -#: module/web/templates/default/window.html:22 -msgid "Password for RAR-Archive" +#: module\web\templates\modern\settings.html:192 module\web\templates\modern\settings.html:220 +#: module\web\templates\modern\window.html:47 module\web\templates\pyplex\admin.html:107 +#: module\web\templates\pyplex\admin.html:120 module\web\templates\pyplex\admin.html:155 +#: module\web\templates\pyplex\queue.html:98 module\web\templates\modern\admin.html:103 +#: module\web\templates\modern\admin.html:116 module\web\templates\modern\admin.html:151 +#: module\web\templates\classic\base.html:229 module\web\templates\pyplex\settings.html:192 +#: module\web\templates\pyplex\settings.html:220 module\web\templates\modern\queue.html:98 +#: module\web\templates\pyplex\window.html:47 +msgid "Cancel" msgstr "" -#: module/web/templates/default/window.html:26 -msgid "File" +#: module\web\templates\modern\captcha.html:39 module\web\templates\classic\captcha.html:40 +#: module\web\templates\pyplex\captcha.html:39 +msgid "Close" msgstr "" -#: module/web/templates/default/window.html:27 -msgid "Upload a container." +#: module\web\templates\pyplex\admin.html:96 module\web\templates\modern\admin.html:92 +msgid "pyLoad was shut down, goodbye!" msgstr "" -#: module/web/templates/default/window.html:31 -msgid "Destination" +#: module\web\templates\pyplex\settings.html:184 module\web\templates\modern\settings.html:184 +msgid "Choose the hoster for your account" +msgstr "" + +#: module\web\templates\classic\home.html:240 module\web\templates\pyplex\home.html:22 +#: module\web\templates\modern\home.html:22 +msgid "Size" +msgstr "" + +#: module\web\templates\classic\queue.html:98 module\web\templates\classic\admin.html:92 +#: module\web\templates\classic\settings.html:198 module\web\templates\classic\window.html:41 +msgid "Reset" +msgstr "" + +#: module\web\templates\pyplex\queue.html:85 module\web\templates\modern\queue.html:85 +#: module\web\templates\pyplex\window.html:12 module\web\templates\modern\window.html:12 +msgid "The name of the package" +msgstr "" + +#: module\web\templates\classic\settings.html:177 +msgid "Enter your account data to use premium features." +msgstr "" + +#: module\config\default.conf:7 module\config\default.conf:9 module\config\default.conf:14 +msgid "Activated" +msgstr "" + +#: module\web\templates\modern\logs.html:91 module\web\templates\pyplex\logs.html:91 +msgid "Lines per page" msgstr "" -#: module/web/templates/default/home.html:206 +#: module\web\templates\modern\admin.html:113 module/web/media/js/classic/admin.js:2 +#: module\web\templates\pyplex\admin.html:117 +msgid "You are really sure you want to quit pyLoad?" +msgstr "" + +#: module\web\templates\classic\base.html:237 module\web\templates\pyplex\base.html:186 +#: module\web\templates\modern\base.html:186 +msgid "Reconnect:" +msgstr "" + +#: module\web\templates\modern\captcha.html:38 module\web\templates\modern\settings.html:66 +#: module\web\templates\modern\settings.html:97 module\web\templates\modern\settings.html:157 +#: module\web\templates\classic\admin.html:61 module\web\templates\classic\admin.html:91 +#: module\web\templates\modern\admin.html:79 module\web\templates\pyplex\admin.html:83 +#: module\web\templates\pyplex\settings.html:66 module\web\templates\pyplex\settings.html:97 +#: module\web\templates\pyplex\settings.html:157 module\web\templates\modern\queue.html:97 +#: module\web\templates\pyplex\captcha.html:38 module\web\templates\pyplex\queue.html:97 +#: module\web\templates\classic\captcha.html:39 module\web\templates\classic\queue.html:97 +#: module\web\templates\classic\settings.html:49 module\web\templates\classic\settings.html:77 +#: module\web\templates\classic\settings.html:167 +msgid "Submit" +msgstr "" + +#: module\web\templates\modern\admin.html:148 module\web\templates\classic\admin.html:86 +#: module\web\templates\pyplex\admin.html:152 +msgid "Please repeat the new password." +msgstr "" + +#: module\web\templates\classic\home.html:206 module\web\templates\pyplex\home.html:13 +#: module\web\templates\modern\home.html:13 msgid "Active Downloads" msgstr "" -#: module/web/templates/default/home.html:240 -msgid "Size" +#: module\config\default.conf:35 +msgid "Create folder for each package" msgstr "" -#: module/web/templates/default/home.html:241 -msgid "Progress" +#: module\web\templates\classic\base.html:240 +msgid "Reload page" msgstr "" -#: module/web/templates/default/captcha.html:6 -msgid "Captcha reading" +#: module\config\default.conf:3 +msgid "Remote" msgstr "" -#: module/web/templates/default/captcha.html:13 -msgid "Captcha" +#: module\web\templates\modern\queue.html:88 module\web\templates\classic\queue.html:87 module\config\default.conf:25 +#: module\web\templates\pyplex\queue.html:88 +msgid "Folder" msgstr "" -#: module/web/templates/default/captcha.html:14 -msgid "The captcha." +#: module\web\templates\classic\queue.html:80 +msgid "Edit the package detais below." msgstr "" -#: module/web/templates/default/captcha.html:20 -msgid "Text" +#: module\web\templates\pyplex\admin.html:44 module\web\templates\modern\admin.html:40 +msgid "Important:" +msgstr "" + +#: module\web\templates\classic\folder.html:7 module\web\templates\classic\filemanager.html:29 +#: module\web\templates\classic\filemanager.html:44 +msgid "Rename Directory" msgstr "" -#: module/web/templates/default/captcha.html:21 +#: module\web\templates\pyplex\info.html:81 module\web\templates\modern\info.html:81 +msgid "Development" +msgstr "" + +#: module\web\templates\pyplex\info.html:103 module\web\templates\modern\info.html:103 +msgid "Webinterface Theme:" +msgstr "" + +#: module\web\templates\classic\base.html:154 module\web\templates\pyplex\base.html:92 +#: module\web\templates\modern\base.html:92 +msgid "pyLoad Update available!" +msgstr "" + +#: module\config\default.conf:30 +msgid "Language" +msgstr "" + +#: module\web\templates\classic\base.html:160 module\web\templates\pyplex\base.html:97 +#: module\web\templates\modern\base.html:97 +msgid "Plugins updated, please restart!" +msgstr "" + +#: module\web\templates\pyplex\settings.html:183 module\web\templates\classic\settings.html:188 +#: module\web\templates\modern\settings.html:183 +msgid "Type" +msgstr "" + +#: module\web\templates\modern\logs.html:89 module\web\templates\pyplex\logs.html:89 +msgid "Reversed" +msgstr "" + +#: module\web\templates\classic\base.html:228 +msgid "Stop" +msgstr "" + +#: module/web/media/js/pyplex/base.js:382 module/web/media/js/classic/base.js:2 +#: module\web\templates\modern\captcha.html:25 module\web\templates\pyplex\captcha.html:25 +#: module/web/media/js/modern/base.js:382 +msgid "Please click on the right captcha position." +msgstr "" + +#: module\web\templates\modern\settings.html:173 module\web\templates\pyplex\login.html:2 +#: module\web\templates\classic\login.html:3 module\web\templates\pyplex\settings.html:173 +#: module\web\templates\modern\login.html:2 module\web\templates\classic\settings.html:178 +msgid "Login" +msgstr "" + +#: module\config\default.conf:31 +msgid "Download Folder" +msgstr "" + +#: module\web\templates\pyplex\settings.html:114 module\web\templates\classic\settings.html:99 +#: module\web\templates\modern\settings.html:114 +msgid "Delete?" +msgstr "" + +#: module\config\default.conf:62 +msgid "Proxy" +msgstr "" + +#: module\config\default.conf:63 +msgid "Address" +msgstr "" + +#: module/web/media/js/modern/admin.js:17 module/web/media/js/pyplex/admin.js:17 +#: module/web/media/js/pyplex/settings.js:116 module/web/media/js/modern/settings.js:116 +msgid "Settings saved" +msgstr "" + +#: module\web\templates\modern\settings.html:107 module\web\templates\modern\settings.html:178 +#: module\web\templates\modern\window.html:23 module\web\templates\pyplex\login.html:60 +#: module\web\templates\pyplex\queue.html:93 module\web\templates\classic\login.html:19 +#: module\web\templates\pyplex\window.html:23 module\web\templates\pyplex\settings.html:107 +#: module\web\templates\pyplex\settings.html:178 module\web\templates\modern\queue.html:93 module\config\default.conf:67 +#: module\web\templates\modern\login.html:60 module\web\templates\classic\queue.html:92 +#: module\web\templates\classic\settings.html:92 module\web\templates\classic\settings.html:183 +#: module\web\templates\classic\window.html:21 +msgid "Password" +msgstr "" + +#: module\web\templates\classic\setup.html:3 module\web\templates\classic\setup.html:4 +msgid "Setup" +msgstr "" + +#: module\web\templates\modern\info.html:101 module\web\templates\pyplex\info.html:101 +#: module\web\templates\classic\info.html:68 +msgid "Language:" +msgstr "" + +#: module\web\templates\classic\pathchooser.html:54 +msgid "parent directory" +msgstr "" + +#: module\web\templates\modern\captcha.html:20 module\web\templates\classic\captcha.html:16 +#: module\web\templates\pyplex\captcha.html:20 msgid "Input the text on the captcha." msgstr "" -#: module/web/templates/default/captcha.html:34 -msgid "Close" +#: module\web\templates\classic\login.html:30 +msgid "To reset your login data or add an user run:" +msgstr "" + +#: module\web\templates\pyplex\home.html:19 module\web\templates\modern\settings.html:106 +#: module\web\templates\modern\window.html:10 module\web\templates\classic\admin.html:25 +#: module\web\templates\classic\home.html:237 module\web\templates\modern\admin.html:50 +#: module\web\templates\modern\home.html:19 module\web\templates\pyplex\admin.html:54 +#: module\web\templates\pyplex\settings.html:106 module\web\templates\modern\queue.html:83 +#: module\web\templates\pyplex\window.html:10 module\web\templates\pyplex\queue.html:83 +#: module\web\templates\classic\queue.html:82 module\web\templates\classic\settings.html:91 +#: module\web\templates\classic\window.html:7 +msgid "Name" +msgstr "" + +#: module\web\templates\modern\admin.html:52 module\web\templates\classic\admin.html:31 +#: module\web\templates\pyplex\admin.html:56 +msgid "Admin" +msgstr "" + +#: module\config\default.conf:18 +msgid "IP" +msgstr "" + +#: module\web\templates\pyplex\info.html:80 module\web\templates\modern\info.html:80 +msgid "Homepage" +msgstr "" + +#: module\web\templates\modern\logout.html:16 module\web\templates\classic\logout.html:8 +#: module\web\templates\pyplex\logout.html:16 +msgid "You were successfully logged out." +msgstr "" + +#: module\config\default.conf:37 +msgid "Download" +msgstr "" + +#: module/web/media/js/pyplex/base.js:416 module/web/media/js/modern/base.js:416 +msgid "No Captchas to read." +msgstr "" + +#: module\web\templates\modern\captcha.html:34 module\web\templates\classic\captcha.html:32 +#: module\web\templates\pyplex\captcha.html:34 +msgid "The captcha may take a few seconds to load." +msgstr "" + +#: module\web\templates\pyplex\queue.html:80 module\web\templates\modern\queue.html:80 +msgid "Edit the package detais below" +msgstr "" + +#: module\web\templates\modern\admin.html:138 module\web\templates\classic\admin.html:76 +#: module\web\templates\classic\settings.html:184 module\web\templates\pyplex\admin.html:142 +msgid "The password for this account." +msgstr "" + +#: module\web\templates\classic\base.html:239 module\web\templates\pyplex\base.html:188 +#: module\web\templates\modern\base.html:188 +msgid "Queued" +msgstr "" + +#: module\web\templates\modern\admin.html:129 module\web\templates\classic\admin.html:69 +#: module\web\templates\pyplex\admin.html:133 +msgid "Enter your current and desired Password." +msgstr "" + +#: module/web/media/js/pyplex/base.js:231 module/web/media/js/modern/base.js:231 +msgid "Please Enter a package name." +msgstr "" + +#: module\config\default.conf:42 +msgid "Download interface to bind (ip or Name)" +msgstr "" + +#: module/web/media/js/classic/base.js:2 module\web\templates\modern\base.html:185 +#: module\web\templates\modern\base.html:186 module/web/media/js/pyplex/base.js:360 +#: module/web/media/js/pyplex/base.js:365 module\web\templates\modern\settings_item.html:15 +#: module\web\templates\classic\settings_item.html:16 module/web/media/js/modern/base.js:360 +#: module/web/media/js/modern/base.js:365 module\web\templates\classic\base.html:236 +#: module\web\templates\classic\base.html:237 module\web\templates\pyplex\settings_item.html:15 +#: module\web\templates\pyplex\base.html:185 module\web\templates\pyplex\base.html:186 +msgid "off" +msgstr "" + +#: module\config\default.conf:33 +msgid "Use Checksum" +msgstr "" + +#: module\web\templates\modern\info.html:107 module\web\templates\pyplex\info.html:107 +#: module\web\templates\classic\info.html:76 +msgid "Remote Interface Port:" msgstr "" + +#: module\web\templates\modern\queue.html:60 module\web\templates\classic\queue.html:65 +#: module\web\templates\pyplex\queue.html:60 +msgid "Folder:" +msgstr "" + +#: module\web\templates\modern\queue.html:61 module\web\templates\classic\queue.html:65 +#: module\web\templates\pyplex\queue.html:61 +msgid "Password:" +msgstr "" + +#: module\web\templates\pyplex\settings.html:136 module\web\templates\classic\settings.html:134 +#: module\web\templates\modern\settings.html:136 +msgid "no" +msgstr "" + +#: module/web/media/js/modern/package.js:173 module/web/media/js/classic/package_ui.js:2 +#: module/web/media/js/pyplex/package.js:178 +msgid "Delete Link" +msgstr "" + +#: module/web/media/js/classic/settings.js:2 +msgid "Error occured." +msgstr "" + +#: module\web\templates\pyplex\window.html:25 module\web\templates\modern\window.html:25 +msgid "Type the package password" +msgstr "" + +#: module\web\templates\classic\queue.html:50 +msgid "Move Package" +msgstr "" + +#: module\web\templates\modern\window.html:32 module\web\templates\classic\window.html:26 +#: module\web\templates\pyplex\window.html:32 +msgid "File" +msgstr "" + +#: module\config\default.conf:54 +msgid "Reconnect" +msgstr "" + +#: module\web\templates\classic\base.html:239 module\web\templates\pyplex\base.html:188 +#: module\web\templates\modern\base.html:188 +msgid "Total" +msgstr "" + +#: module\web\templates\classic\folder.html:11 module\web\templates\classic\filemanager.html:48 +msgid "Add subdirectory" +msgstr "" + +#: module\config\default.conf:4 module\config\default.conf:19 module\config\default.conf:64 +msgid "Port" +msgstr "" + diff --git a/locale/el_GR/LC_MESSAGES/django.mo b/locale/el_GR/LC_MESSAGES/django.mo new file mode 100644 index 0000000000..135530b4a6 Binary files /dev/null and b/locale/el_GR/LC_MESSAGES/django.mo differ diff --git a/locale/el_GR/LC_MESSAGES/pyLoad.mo b/locale/el_GR/LC_MESSAGES/pyLoad.mo new file mode 100644 index 0000000000..a1662ae0aa Binary files /dev/null and b/locale/el_GR/LC_MESSAGES/pyLoad.mo differ diff --git a/locale/el_GR/LC_MESSAGES/pyLoadCli.mo b/locale/el_GR/LC_MESSAGES/pyLoadCli.mo new file mode 100644 index 0000000000..33b83abbb4 Binary files /dev/null and b/locale/el_GR/LC_MESSAGES/pyLoadCli.mo differ diff --git a/locale/el_GR/LC_MESSAGES/setup.mo b/locale/el_GR/LC_MESSAGES/setup.mo new file mode 100644 index 0000000000..3be5e303b9 Binary files /dev/null and b/locale/el_GR/LC_MESSAGES/setup.mo differ diff --git a/locale/gui.pot b/locale/gui.pot index dc74397a01..3678a79e33 100644 --- a/locale/gui.pot +++ b/locale/gui.pot @@ -6,8 +6,8 @@ #, fuzzy msgid "" msgstr "" -"Project-Id-Version: pyLoad 0.4.9\n" -"Report-Msgid-Bugs-To: 'bugs@pyload.org'\n" +"Project-Id-Version: pyLoad 0.4.20\n" +"Report-Msgid-Bugs-To: 'bugs@pyload.net'\n" "POT-Creation-Date: 2011-12-07 19:21+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" diff --git a/locale/setup.pot b/locale/setup.pot index cf4dd8cfcd..48964a5c82 100644 --- a/locale/setup.pot +++ b/locale/setup.pot @@ -6,8 +6,8 @@ #, fuzzy msgid "" msgstr "" -"Project-Id-Version: pyLoad 0.4.9\n" -"Report-Msgid-Bugs-To: 'bugs@pyload.org'\n" +"Project-Id-Version: pyLoad 0.4.20\n" +"Report-Msgid-Bugs-To: 'bugs@pyload.net'\n" "POT-Creation-Date: 2011-12-07 19:21+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" diff --git a/module/Api.py b/module/Api.py index f0bf5e2648..f738ecbc26 100644 --- a/module/Api.py +++ b/module/Api.py @@ -17,7 +17,6 @@ @author: RaNaN """ -from base64 import standard_b64encode from os.path import join from time import time import re @@ -25,6 +24,7 @@ from PyFile import PyFile from utils import freeSpace, compare_time from common.packagetools import parseNames +from common.json_layer import json from network.RequestFactory import getURL from remote import activated @@ -53,7 +53,7 @@ def __new__(cls, func, *args, **kwargs): return _Dec -urlmatcher = re.compile(r"((https?|ftps?|xdcc|sftp):((//)|(\\\\))+[\w\d:#@%/;$()~_?\+\-=\\\.&]*)", re.IGNORECASE) +urlmatcher = re.compile(r"(?:(?:https?|ftps?|xdcc|sftp):(?://|\\\\)+[\w\-._~:/?#\[\]@!$&'()*+,;=]*)|magnet:\?.+", re.IGNORECASE) class PERMS: ALL = 0 # requires no permission, but login @@ -321,7 +321,8 @@ def addPackage(self, name, links, dest=Destination.Queue): else: folder = "" - folder = folder.replace("http://", "").replace(":", "").replace("/", "_").replace("\\", "_") + folder = folder.replace("http://", "").replace("https://", "").rstrip("/") \ + .replace(":", "").replace("/", "_").replace("\\", "_") pid = self.core.files.addPackage(name, folder, dest) @@ -343,11 +344,11 @@ def parseURLs(self, html=None, url=None): urls = [] if html: - urls += [x[0] for x in urlmatcher.findall(html)] + urls += urlmatcher.findall(html) if url: page = getURL(url) - urls += [x[0] for x in urlmatcher.findall(page)] + urls += urlmatcher.findall(page) # remove duplicates return self.checkURLs(set(urls)) @@ -805,7 +806,7 @@ def getCaptchaTask(self, exclusive=False): if task: task.setWatingForUser(exclusive=exclusive) data, type, result = task.getCaptcha() - t = CaptchaTask(int(task.id), standard_b64encode(data), type, result) + t = CaptchaTask(int(task.id), json.dumps(data), type, result) return t else: return CaptchaTask(-1) diff --git a/module/CaptchaManager.py b/module/CaptchaManager.py index 02cd10a118..66bcbd4fca 100644 --- a/module/CaptchaManager.py +++ b/module/CaptchaManager.py @@ -18,19 +18,20 @@ """ from time import time -from traceback import print_exc from threading import Lock +from common.json_layer import json + class CaptchaManager(): def __init__(self, core): self.lock = Lock() self.core = core - self.tasks = [] #task store, for outgoing tasks only + self.tasks = [] # Task store, for outgoing tasks only - self.ids = 0 #only for internal purpose + self.ids = 0 # Only for internal purpose - def newTask(self, img, format, file, result_type): - task = CaptchaTask(self.ids, img, format, file, result_type) + def newTask(self, format, params, result_type): + task = CaptchaTask(self.ids, format, params, result_type) self.ids += 1 return task @@ -52,26 +53,27 @@ def getTask(self): def getTaskByID(self, tid): self.lock.acquire() for task in self.tasks: - if task.id == str(tid): #task ids are strings + if task.id == str(tid): # Task ids are strings self.lock.release() return task self.lock.release() return None - def handleCaptcha(self, task): + def handleCaptcha(self, task, timeout): cli = self.core.isClientConnected() - if cli: #client connected -> should solve the captcha - task.setWaiting(50) #wait 50 sec for response + task.setWaiting(timeout) + + # if cli: # Client connected -> should solve the captcha + # task.setWaiting(50) # Wait minimum 50 sec for response for plugin in self.core.hookManager.activePlugins(): try: plugin.newCaptchaTask(task) except: - if self.core.debug: - print_exc() - - if task.handler or cli: #the captcha was handled + self.core.log.debug(_("Exception in captcha task has occurred" ), exc_info=True) + + if task.handler or cli: # The captcha was handled self.tasks.append(task) return True @@ -81,11 +83,10 @@ def handleCaptcha(self, task): class CaptchaTask(): - def __init__(self, id, img, format, file, result_type='textual'): + def __init__(self, id, format, params={}, result_type='textual'): self.id = str(id) - self.captchaImg = img + self.captchaParams = params self.captchaFormat = format - self.captchaFile = file self.captchaResultType = result_type self.handler = [] #the hook plugins that will take care of the solution self.result = None @@ -96,14 +97,15 @@ def __init__(self, id, img, format, file, result_type='textual'): self.data = {} #handler can store data here def getCaptcha(self): - return self.captchaImg, self.captchaFormat, self.captchaResultType + return self.captchaParams, self.captchaFormat, self.captchaResultType - def setResult(self, text): - if self.isTextual(): - self.result = text - if self.isPositional(): + def setResult(self, result): + if self.isTextual() or self.isInteractive() or self.isInvisible(): + self.result = result + + elif self.isPositional(): try: - parts = text.split(',') + parts = result.split(',') self.result = (int(parts[0]), int(parts[1])) except: self.result = None @@ -137,6 +139,16 @@ def isTextual(self): def isPositional(self): """ returns if user have to click a specific region on the captcha """ return self.captchaResultType == 'positional' + + def isInteractive(self): + """ returns if user has to solve the captcha in an interactive iframe """ + return self.captchaResultType == 'interactive' + + def isInvisible(self): + """ + returns if invisible (browser only, no user interaction) captcha. + """ + return self.captchaResultType == "invisible" def setWatingForUser(self, exclusive): if exclusive: diff --git a/module/ConfigParser.py b/module/ConfigParser.py index 78b612f139..34774250ca 100644 --- a/module/ConfigParser.py +++ b/module/ConfigParser.py @@ -1,6 +1,9 @@ # -*- coding: utf-8 -*- from __future__ import with_statement + +import re + from time import sleep from os.path import exists, join from shutil import copy @@ -37,6 +40,7 @@ class ConfigParser: """ + CONFLINE = re.compile(r'^\s*(?P.+?)\s+(?P[^ ]+?)\s*:\s*"(?P.+?)"\s*=\s?(?P.*)') def __init__(self): """Constructor""" @@ -58,11 +62,13 @@ def checkVersion(self, n=0): try: if not exists("pyload.conf"): copy(join(pypath, "module", "config", "default.conf"), "pyload.conf") + chmod("pyload.conf", 0600) if not exists("plugin.conf"): f = open("plugin.conf", "wb") f.write("version: " + str(CONF_VERSION)) f.close() + chmod("plugin.conf", 0600) f = open("pyload.conf", "rb") v = f.readline() @@ -160,15 +166,12 @@ def parseConfig(self, config): else: - content, none, value = line.partition("=") - - content, none, desc = content.partition(":") - - desc = desc.replace('"', "").strip() + m = self.CONFLINE.search(line) - typ, none, option = content.strip().rpartition(" ") - - value = value.strip() + typ = m.group('T') + option = m.group('N') + desc = m.group('D').strip() + value = m.group('V').strip() if value.startswith("["): if value.endswith("]"): @@ -187,7 +190,8 @@ def parseConfig(self, config): "value": value} except Exception, e: - print "Config Warning" + print "Config Warning:" + print line print_exc() f.close() @@ -217,11 +221,12 @@ def saveConfig(self, config, filename): with open(filename, "wb") as f: chmod(filename, 0600) f.write("version: %i \n" % CONF_VERSION) - for section in config.iterkeys(): + for section in sorted(config.iterkeys()): f.write('\n%s - "%s":\n' % (section, config[section]["desc"])) - for option, data in config[section].iteritems(): - if option in ("desc", "outline"): continue + for option, data in sorted(config[section].items(), key=lambda _x: _x[0]): + if option in ("desc", "outline"): + continue if isinstance(data["value"], list): value = "[ \n" @@ -253,7 +258,7 @@ def cast(self, typ, value): return value elif typ in ("str", "file", "folder"): try: - return value.encode("utf8") + return value.decode("utf8") except: return value else: @@ -307,7 +312,8 @@ def setPlugin(self, plugin, option, value): value = self.cast(self.plugin[plugin][option]["type"], value) - if self.pluginCB: self.pluginCB(plugin, option, value) + if self.pluginCB: + self.pluginCB(plugin, option, value) self.plugin[plugin][option]["value"] = value self.save() @@ -327,8 +333,7 @@ def addPluginConfig(self, name, config, outline=""): conf["outline"] = outline for item in config: - if item[0] in conf: - conf[item[0]]["type"] = item[1] + if item[0] in conf and item[1] == conf[item[0]]["type"]: conf[item[0]]["desc"] = item[2] else: conf[item[0]] = { @@ -351,12 +356,12 @@ def deleteConfig(self, name): def deleteOldPlugins(self): """ remove old plugins from config """ - for name in IGNORE: if name in self.plugin: del self.plugin[name] + class Section: """provides dictionary like access for configparser""" diff --git a/module/HookManager.py b/module/HookManager.py index 16f692d76f..41053f833b 100644 --- a/module/HookManager.py +++ b/module/HookManager.py @@ -19,7 +19,6 @@ """ import __builtin__ -import traceback from thread import start_new_thread from threading import RLock @@ -87,9 +86,7 @@ def new(*args): try: return func(*args) except Exception, e: - args[0].log.error(_("Error executing hooks: %s") % str(e)) - if args[0].core.debug: - traceback.print_exc() + args[0].log.error(_("Error executing hooks: %s") % str(e), exc_info=args[0].core.debug) return new @@ -137,9 +134,7 @@ def createIndex(self): except: - self.log.warning(_("Failed activating %(name)s") % {"name": pluginname}) - if self.core.debug: - traceback.print_exc() + self.log.warning(_("Failed activating %(name)s") % {"name": pluginname}, exc_info=self.core.debug) self.log.info(_("Activated plugins: %s") % ", ".join(sorted(active))) self.log.info(_("Deactivate plugins: %s") % ", ".join(sorted(deactive))) @@ -292,14 +287,16 @@ def getInfo(self, plugin): def addEvent(self, event, func): """Adds an event listener for event name""" if event in self.events: - self.events[event].append(func) + if func not in self.events[event]: + self.events[event].append(func) else: self.events[event] = [func] def removeEvent(self, event, func): """removes previously added event listener""" if event in self.events: - self.events[event].remove(func) + if func in self.events[event]: + self.events[event].remove(func) def dispatchEvent(self, event, *args): """dispatches event with args""" @@ -309,7 +306,6 @@ def dispatchEvent(self, event, *args): f(*args) except Exception, e: self.log.warning("Error calling event handler %s: %s, %s, %s" - % (event, f, args, str(e))) - if self.core.debug: - traceback.print_exc() - + % (event, f, args, str(e)), + exc_info=self.core.debug) + diff --git a/module/InitHomeDir.py b/module/InitHomeDir.py index 156c9f9325..9d38b8719e 100644 --- a/module/InitHomeDir.py +++ b/module/InitHomeDir.py @@ -52,27 +52,22 @@ __builtin__.homedir = homedir -args = " ".join(argv[1:]) - # dirty method to set configdir from commandline arguments -if "--configdir=" in args: - pos = args.find("--configdir=") - end = args.find("-", pos + 12) - - if end == -1: - configdir = args[pos + 12:].strip() - else: - configdir = args[pos + 12:end].strip() -elif path.exists(path.join(pypath, "module", "config", "configdir")): - f = open(path.join(pypath, "module", "config", "configdir"), "rb") - c = f.read().strip() - f.close() - configdir = path.join(pypath, c) +for arg in argv[1:]: + if arg.startswith("--configdir="): + configdir=arg[12:].strip() + break else: - if platform in ("posix", "linux2"): - configdir = path.join(homedir, ".pyload") + if path.exists(path.join(pypath, "module", "config", "configdir")): + f = open(path.join(pypath, "module", "config", "configdir"), "rb") + c = f.read().strip() + f.close() + configdir = path.join(pypath, c) else: - configdir = path.join(homedir, "pyload") + if platform in ("posix", "linux2"): + configdir = path.join(homedir, ".pyload") + else: + configdir = path.join(homedir, "pyload") if not path.exists(configdir): makedirs(configdir, 0700) diff --git a/module/PluginThread.py b/module/PluginThread.py index 56c36c778a..beaf136e33 100644 --- a/module/PluginThread.py +++ b/module/PluginThread.py @@ -21,9 +21,9 @@ from Queue import Queue from threading import Thread from os import listdir, stat -from os.path import join +from os.path import join, isdir from time import sleep, time, strftime, gmtime -from traceback import print_exc, format_exc +from traceback import format_exc from pprint import pformat from sys import exc_info, exc_clear from copy import copy @@ -61,12 +61,14 @@ def writeDebugReport(self, pyfile): zip = zipfile.ZipFile(dump_name, "w") - for f in listdir(join("tmp", pyfile.pluginname)): - try: - # avoid encoding errors - zip.write(join("tmp", pyfile.pluginname, f), save_join(pyfile.pluginname, f)) - except: - pass + dumps_dir = join("tmp", pyfile.pluginname) + if isdir(dumps_dir): + for f in listdir(dumps_dir): + try: + # avoid encoding errors + zip.write(join("tmp", pyfile.pluginname, f), save_join(pyfile.pluginname, f)) + except: + pass info = zipfile.ZipInfo(save_join(pyfile.pluginname, "debug_Report.txt"), gmtime()) info.external_attr = 0644 << 16L # change permissions @@ -273,9 +275,8 @@ def run(self): else: pyfile.setStatus("failed") - self.m.log.error("pycurl error %s: %s" % (code, msg)) + self.m.log.error("pycurl error %s: %s" % (code, msg), exc_info=self.m.core.debug) if self.m.core.debug: - print_exc() self.writeDebugReport(pyfile) self.m.core.hookManager.downloadFailed(pyfile) @@ -287,7 +288,7 @@ def run(self): pyfile.setStatus("skipped") self.m.log.info( - _("Download skipped: %(name)s due to %(plugin)s") % {"name": pyfile.name, "plugin": e.message}) + _("Download skipped: %(name)s due to %(plugin)s") % {"name": pyfile.name, "plugin": e.args[0]}) self.clean(pyfile) @@ -301,11 +302,10 @@ def run(self): except Exception, e: pyfile.setStatus("failed") - self.m.log.warning(_("Download failed: %(name)s | %(msg)s") % {"name": pyfile.name, "msg": str(e)}) + self.m.log.warning(_("Download failed: %(name)s | %(msg)s") % {"name": pyfile.name, "msg": str(e)}, exc_info=self.m.core.debug) pyfile.error = str(e) if self.m.core.debug: - print_exc() self.writeDebugReport(pyfile) self.m.core.hookManager.downloadFailed(pyfile) @@ -392,11 +392,10 @@ def run(self): except Exception, e: self.active.setStatus("failed") - self.m.log.error(_("Decrypting failed: %(name)s | %(msg)s") % {"name": self.active.name, "msg": str(e)}) + self.m.log.error(_("Decrypting failed: %(name)s | %(msg)s") % {"name": self.active.name, "msg": str(e)}, exc_info=self.m.core.debug) self.active.error = str(e) if self.m.core.debug: - print_exc() self.writeDebugReport(pyfile) return @@ -546,8 +545,7 @@ def run(self): try: data = self.decryptContainer(name, url) except: - print_exc() - self.m.log.error("Could not decrypt container.") + self.m.log.error("Could not decrypt container.", exc_info=self.m.core.debug) data = [] for url, plugin in data: @@ -634,9 +632,7 @@ def fetchForPlugin(self, pluginname, plugin, urls, cb, err=None): self.m.log.debug("Finished Info Fetching for %s" % pluginname) except Exception, e: self.m.log.warning(_("Info Fetching for %(name)s failed | %(err)s") % - {"name": pluginname, "err": str(e)}) - if self.m.core.debug: - print_exc() + {"name": pluginname, "err": str(e)}, exc_info=self.m.core.debug) # generate default results if err: diff --git a/module/PyFile.py b/module/PyFile.py index 3dede93600..6c989e415e 100644 --- a/module/PyFile.py +++ b/module/PyFile.py @@ -283,3 +283,8 @@ def setProgress(self, value): if not value == self.progress: self.progress = value self.notifyChange() + + def setName(self, value): + if not value == self.name: + self.name = value + self.notifyChange() diff --git a/module/ThreadManager.py b/module/ThreadManager.py index 8937f4a293..1691c73d5f 100644 --- a/module/ThreadManager.py +++ b/module/ThreadManager.py @@ -23,7 +23,6 @@ from subprocess import Popen from threading import Event, Lock from time import sleep, time -from traceback import print_exc from random import choice import pycurl @@ -69,12 +68,11 @@ def __init__(self, core): pycurl.global_init(pycurl.GLOBAL_DEFAULT) for i in range(0, self.core.config.get("download", "max_downloads")): - self.createThread() + self.createDownloadThread() - def createThread(self): + def createDownloadThread(self): """create a download thread""" - thread = PluginThread.DownloadThread(self) self.threads.append(thread) @@ -134,19 +132,15 @@ def work(self): try: self.tryReconnect() except Exception, e: - self.log.error(_("Reconnect Failed: %s") % str(e) ) + self.log.error(_("Reconnect Failed: %s") % str(e), exc_info=self.core.debug) self.reconnecting.clear() - if self.core.debug: - print_exc() self.checkThreadCount() try: self.assignJob() except Exception, e: - self.log.warning("Assign job error", e) - if self.core.debug: - print_exc() - + self.log.warning(_("Assign job error: %s") % str(e), exc_info=self.core.debug) + sleep(0.5) self.assignJob() #it may be failed non critical so we try it again @@ -193,11 +187,9 @@ def tryReconnect(self): try: reconn = Popen(self.core.config['reconnect']['method'], bufsize=-1, shell=True)#, stdout=subprocess.PIPE) except: - self.log.warning(_("Failed executing reconnect script!")) + self.log.warning(_("Failed executing reconnect script!"), exc_info=self.core.debug) self.core.config["reconnect"]["activated"] = False self.reconnecting.clear() - if self.core.debug: - print_exc() return reconn.wait() @@ -234,7 +226,7 @@ def checkThreadCount(self): if len(self.threads) == self.core.config.get("download", "max_downloads"): return True elif len(self.threads) < self.core.config.get("download", "max_downloads"): - self.createThread() + self.createDownloadThread() else: free = [x for x in self.threads if not x.active] if free: @@ -262,7 +254,7 @@ def assignJob(self): free = [x for x in self.threads if not x.active] - inuse = set([(x.active.pluginname,self.getLimit(x)) for x in self.threads if x.active and x.active.hasPlugin() and x.active.plugin.account]) + inuse = set([(x.active.pluginname,self.getLimit(x)) for x in self.threads if x.active and x.active.hasPlugin()]) inuse = map(lambda x : (x[0], x[1], len([y for y in self.threads if y.active and y.active.pluginname == x[0]])) ,inuse) onlimit = [x[0] for x in inuse if x[1] > 0 and x[2] >= x[1]] @@ -275,8 +267,7 @@ def assignJob(self): try: job.initPlugin() except Exception, e: - self.log.critical(str(e)) - print_exc() + self.log.critical(str(e), exc_info=True) job.setStatus("failed") job.error = str(e) job.release() @@ -292,6 +283,7 @@ def assignJob(self): thread = free[0] #self.downloaded += 1 + job.setStatus("starting") thread.put(job) else: #put job back @@ -299,7 +291,7 @@ def assignJob(self): self.core.files.jobCache[occ] = [] self.core.files.jobCache[occ].append(job.id) - #check for decrypt jobs + # check for decrypt jobs job = self.core.files.getDecryptJob() if job: job.initPlugin() @@ -310,8 +302,29 @@ def assignJob(self): thread = PluginThread.DecrypterThread(self, job) def getLimit(self, thread): - limit = thread.active.plugin.account.getAccountData(thread.active.plugin.user)["options"].get("limitDL",["0"])[0] - return int(limit) + if thread.active.plugin.account: + account_limit = max( + int( + thread.active.plugin.account.getAccountData( + thread.active.plugin.account.user + )["options"].get("limitDL", ["0"])[0] + ), + 0, + ) + else: + account_limit = 0 + + plugin_limit = ( + max(thread.active.plugin.limitDL, 0) + if hasattr(thread.active.plugin, "limitDL") + else 0 + ) + if account_limit > 0 and plugin_limit > 0: + limit = min(account_limit, plugin_limit) + else: + limit = account_limit or plugin_limit + + return limit def cleanup(self): """do global cleanup, should be called when finished with pycurl""" diff --git a/module/common/JsEngine.py b/module/common/JsEngine.py index 576be2a1bf..061460eb88 100644 --- a/module/common/JsEngine.py +++ b/module/common/JsEngine.py @@ -17,20 +17,49 @@ @author: RaNaN """ -from imp import find_module -from os.path import join, exists -from urllib import quote +from __future__ import with_statement +import os +import tempfile +import urllib +from imp import find_module ENGINE = "" DEBUG = False JS = False PYV8 = False +NODE = False RHINO = False - +JS2PY = False if not ENGINE: + try: + import js2py + out = js2py.eval_js("(23+19).toString()") + + # integrity check + if out.strip() == "42": + ENGINE = "js2py" + + def monkey_patch(): + """Patching js2py for CVE-2024-28397""" + from js2py.constructors.jsobject import Object + fn = Object.own["getOwnPropertyNames"]["value"].code + + def wraps(*args, **kwargs): + result = fn(*args, **kwargs) + return list(result) + Object.own["getOwnPropertyNames"]["value"].code = wraps + + monkey_patch() + js2py.disable_pyimport() + + JS2PY = True + except: + pass + +if not ENGINE or DEBUG: try: import subprocess @@ -52,16 +81,29 @@ except: pass +if not ENGINE or DEBUG: + try: + import subprocess + subprocess.Popen(["node", "-v"], bufsize=-1, stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate() + p = subprocess.Popen(["node", "-e", "console.log(23+19)"], stdout=subprocess.PIPE, stderr=subprocess.PIPE) + out, err = p.communicate() + #integrity check + if out.strip() == "42": + ENGINE = "node" + NODE = True + except: + pass + if not ENGINE or DEBUG: try: path = "" #path where to find rhino - if exists("/usr/share/java/js.jar"): + if os.path.exists("/usr/share/java/js.jar"): path = "/usr/share/java/js.jar" - elif exists("js.jar"): + elif os.path.exists("js.jar"): path = "js.jar" - elif exists(join(pypath, "js.jar")): #may raises an exception, but js.jar wasnt found anyway - path = join(pypath, "js.jar") + elif os.path.exists(os.path.join(pypath, "js.jar")): #may raises an exception, but js.jar wasnt found anyway + path = os.path.join(pypath, "js.jar") if not path: raise Exception @@ -103,8 +145,12 @@ def eval(self, script): if not DEBUG: if ENGINE == "pyv8": return self.eval_pyv8(script) + elif ENGINE == "js2py": + return self.eval_js2py(script) elif ENGINE == "js": return self.eval_js(script) + elif ENGINE == "node": + return self.eval_node(script) elif ENGINE == "rhino": return self.eval_rhino(script) else: @@ -113,10 +159,18 @@ def eval(self, script): res = self.eval_pyv8(script) print "PyV8:", res results.append(res) + if JS2PY: + res = self.eval_js2py(script) + print "js2py:", res + results.append(res) if JS: res = self.eval_js(script) print "JS:", res results.append(res) + if NODE: + res = self.eval_node(script) + print "NODE:", res + results.append(res) if RHINO: res = self.eval_rhino(script) print "Rhino:", res @@ -133,30 +187,70 @@ def eval(self, script): return results[0] def eval_pyv8(self, script): - rt = PyV8.JSContext() - rt.enter() - return rt.eval(script) + with PyV8.JSLocker(): + with PyV8.JSContext() as rt: + return rt.eval(script) def eval_js(self, script): - script = "print(eval(unescape('%s')))" % quote(script) - p = subprocess.Popen(["js", "-e", script], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, bufsize=-1) + script = "print(eval(unescape('%s')))" % urllib.quote(script) + if len(script) <= 2000: + script_file = None + p = subprocess.Popen(["js", "-e", script], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, bufsize=-1) + else: + fd, script_file = tempfile.mkstemp(prefix='script_file_', suffix='.js', dir="tmp") + os.write(fd, script) + os.close(fd) + p = subprocess.Popen(["js", "-f", script_file], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, bufsize=-1) out, err = p.communicate() + if script_file and os.path.exists(script_file): + os.unlink(script_file) + res = out.strip() + return res + + def eval_js2py(self, script): + script = "(eval(unescape('%s'))).toString()" % urllib.quote(script) + res = js2py.eval_js(script).strip() + return res + + def eval_node(self, script): + script = "console.log(eval(unescape('%s')))" % urllib.quote(script) + if len(script) <= 2000: + script_file = None + p = subprocess.Popen(["node", "-e", script], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, bufsize=-1) + else: + fd, script_file = tempfile.mkstemp(prefix='script_file_', suffix='.js', dir="tmp") + os.write(fd, script) + os.close(fd) + p = subprocess.Popen(["node",script_file], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, bufsize=-1) + out, err = p.communicate() + if script_file and os.path.exists(script_file): + os.unlink(script_file) res = out.strip() return res def eval_rhino(self, script): - script = "print(eval(unescape('%s')))" % quote(script) - p = subprocess.Popen(["java", "-cp", path, "org.mozilla.javascript.tools.shell.Main", "-e", script], - stdout=subprocess.PIPE, stderr=subprocess.STDOUT, bufsize=-1) + script = "print(eval(unescape('%s')))" % urllib.quote(script) + if len(script) <= 1800: + script_file = None + p = subprocess.Popen(["java", "-cp", path, "org.mozilla.javascript.tools.shell.Main", "-e", script], + stdout=subprocess.PIPE, stderr=subprocess.STDOUT, bufsize=-1) + else: + fd, script_file = tempfile.mkstemp(prefix='script_file_', suffix='.js', dir="tmp") + os.write(fd, script) + os.close(fd) + p = subprocess.Popen(["java", "-cp", path, "org.mozilla.javascript.tools.shell.Main", "-f", script_file], + stdout=subprocess.PIPE, stderr=subprocess.STDOUT, bufsize=-1) out, err = p.communicate() + if script_file and os.path.exists(script_file): + os.unlink(script_file) res = out.strip() return res.decode("utf8").encode("ISO-8859-1") def error(self): - return _("No js engine detected, please install either Spidermonkey, ossp-js, pyv8 or rhino") + return _("No js engine detected, please install either js2py, Spidermonkey, ossp-js, pyv8, nodejs or rhino") if __name__ == "__main__": js = JsEngine() test = u'"ü"+"ä"' - js.eval(test) \ No newline at end of file + js.eval(test) diff --git a/module/common/pylgettext.py b/module/common/pylgettext.py index fb36feceee..4b6ebc1887 100644 --- a/module/common/pylgettext.py +++ b/module/common/pylgettext.py @@ -59,3 +59,4 @@ def find(domain, localedir=None, languages=None, all=False): #Is there a smarter/cleaner pythonic way for this? translation.func_globals['find'] = find +origfind.func_globals['find'] = origfind diff --git a/module/config/default.conf b/module/config/default.conf index 2e9152ba2c..a4b661655f 100644 --- a/module/config/default.conf +++ b/module/config/default.conf @@ -1,65 +1,69 @@ version: 1 remote - "Remote": - int port : "Port" = 7227 - ip listenaddr : "Adress" = 0.0.0.0 - bool nolocalauth : "No authentication on local connections" = True - bool activated : "Activated" = True + int port : "Port" = 7227 + ip listenaddr : "Adress" = 0.0.0.0 + bool nolocalauth : "No authentication on local connections" = True + bool activated : "Activated" = True ssl - "SSL": - bool activated : "Activated"= False - file cert : "SSL Certificate" = ssl.crt - file key : "SSL Key" = ssl.key + bool activated : "Activated"= False + file cert : "SSL certificate" = ssl.crt + file key : "SSL key" = ssl.key + file cert_chain : "CA's intermediate certificate bundle (optional)" = webinterface - "Webinterface": - bool activated : "Activated" = True - builtin;threaded;fastcgi;lightweight server : "Server" = builtin - bool https : "Use HTTPS" = False - ip host : "IP" = 0.0.0.0 - int port : "Port" = 8001 - str template : "Template" = default - str prefix: "Path Prefix" = + bool activated : "Activated" = True + builtin;threaded;fastcgi;lightweight server : "Server" = builtin + bool https : "Use HTTPS" = False + bool dualstack : "Listen on both IPv4 and IPv6 (IP must be set to 0.0.0.0)" = True + ip host : "IP" = 0.0.0.0 + int port : "Port" = 8000 + classic;modern;pyplex template : "Template" = modern + str prefix: "Path prefix" = + bool basicauth : "Use basic auth" = False log - "Log": - bool file_log : "File Log" = True - folder log_folder : "Folder" = Logs - int log_count : "Count" = 5 - int log_size : "Size in kb" = 100 - bool log_rotate : "Log Rotate" = True + bool file_log : "File log" = True + folder log_folder : "Folder" = Logs + int log_count : "Count" = 5 + int log_size : "Size in kb" = 100 + bool log_rotate : "Log rotate" = True general - "General": - en;de;fr;it;es;nl;sv;ru;pl;cs;sr;pt_BR language : "Language" = en - folder download_folder : "Download Folder" = Downloads - bool debug_mode : "Debug Mode" = False - bool checksum : "Use Checksum" = False - int min_free_space : "Min Free Space (MB)" = 200 - bool folder_per_package : "Create folder for each package" = True - int renice : "CPU Priority" = 0 + en;de;fr;it;es;el_GR;nl;sv;ru;pl;cs;sr;pt_BR language : "Language" = en + folder download_folder : "Download folder" = Downloads + bool debug_mode : "Debug mode" = False + bool checksum : "Use checksum" = False + int min_free_space : "Minimum free Space (MB)" = 200 + bool folder_per_package : "Create folder for each package" = True + int renice : "CPU Priority" = 0 download - "Download": - int chunks : "Max connections for one download" = 3 - int max_downloads : "Max Parallel Downloads" = 3 - int max_speed : "Max Download Speed in kb/s" = -1 - bool limit_speed : "Limit Download Speed" = False + int chunks : "Maximum connections for one download" = 3 + int max_downloads : "Maximum parallel downloads" = 3 + int max_speed : "Maximum download speed in KiB/s" = -1 + bool limit_speed : "Limit download speed" = False str interface : "Download interface to bind (ip or Name)" = None bool ipv6 : "Allow IPv6" = False bool skip_existing : "Skip already existing files" = False permission - "Permissions": bool change_user : "Change user of running process" = False - str user : "Username" = user - str folder : "Folder Permission mode" = 0755 - bool change_file : "Change file mode of downloads" = False - str file : "Filemode for Downloads" = 0644 + str user : "Username for ownership" = user + str folder : "Permission mode for created folders" = 0755 + bool change_file : "Change permissions of downloads" = False + str file : "Permission mode for downloaded files" = 0644 bool change_group : "Change group of running process" = False - str group : "Groupname" = users - bool change_dl : "Change Group and User of Downloads" = False + str group : "Groupname for ownership" = users + bool change_dl : "Change ownership of downloads" = False reconnect - "Reconnect": - bool activated : "Use Reconnect" = False - str method : "Method" = None - time startTime : "Start" = 0:00 - time endTime : "End" = 0:00 -downloadTime - "Download Time": - time start : "Start" = 0:00 - time end : "End" = 0:00 + bool activated : "Use reconnect" = False + str method : "Method" = None + time startTime : "Start" = 0:00 + time endTime : "End" = 0:00 +downloadTime - "Download time": + time start : "Start" = 0:00 + time end : "End" = 0:00 proxy - "Proxy": - str address : "Address" = "localhost" - int port : "Port" = 7070 - http;socks4;socks5 type : "Protocol" = http - str username : "Username" = None - password password : "Password" = None - bool proxy : "Use Proxy" = False + str address : "Address" = "localhost" + int port : "Port" = 7070 + http;https;socks4;socks5 type : "Protocol" = http + bool socksResolveDns : "Enable DNS resolution through SOCKS proxy" = False + str username : "Username" = + password password : "Password" = + bool proxy : "Use proxy" = False diff --git a/module/database/DatabaseBackend.py b/module/database/DatabaseBackend.py index 9530390c30..5c42da0054 100644 --- a/module/database/DatabaseBackend.py +++ b/module/database/DatabaseBackend.py @@ -190,7 +190,7 @@ def _convertDB(self, v): except: print "Filedatabase could NOT be converted." - #--convert scripts start + # --convert scripts start def _convertV2(self): self.c.execute('CREATE TABLE IF NOT EXISTS "storage" ("id" INTEGER PRIMARY KEY AUTOINCREMENT, "identifier" TEXT NOT NULL, "key" TEXT NOT NULL, "value" TEXT DEFAULT "")') @@ -207,7 +207,7 @@ def _convertV3(self): except: print "Database was converted from v3 to v4." - #--convert scripts end + # --convert scripts end def _createTables(self): """create tables for database""" @@ -225,7 +225,7 @@ def _createTables(self): FROM packages p JOIN links l ON p.id = l.package AND l.status in (0,4,13) GROUP BY p.id) s ON s.id = p.id \ GROUP BY p.id') - #try to lower ids + # try to lower ids self.c.execute('SELECT max(id) FROM LINKS') fid = self.c.fetchone()[0] if fid: @@ -243,6 +243,9 @@ def _createTables(self): pid = 0 self.c.execute('UPDATE SQLITE_SEQUENCE SET seq=? WHERE name=?', (pid, "packages")) + # set unfinished links as aborted + self.c.execute("UPDATE links SET status=9 WHERE status NOT IN (0, 1, 4, 6, 8, 9)") + self.c.execute('VACUUM') diff --git a/module/gui/connector.py b/module/gui/Connector.py similarity index 99% rename from module/gui/connector.py rename to module/gui/Connector.py index c16ccd08e9..adc3f345eb 100644 --- a/module/gui/connector.py +++ b/module/gui/Connector.py @@ -16,7 +16,7 @@ @author: mkaay """ -SERVER_VERSION = "0.4.9" +SERVER_VERSION = "0.4.20" from time import sleep from uuid import uuid4 as uuid diff --git a/module/lib/BeautifulSoup.py b/module/lib/BeautifulSoup.py index 55567f588b..4b17b853d0 100644 --- a/module/lib/BeautifulSoup.py +++ b/module/lib/BeautifulSoup.py @@ -79,7 +79,7 @@ from __future__ import generators __author__ = "Leonard Richardson (leonardr@segfault.org)" -__version__ = "3.0.8.1" +__version__ = "3.2.0" __copyright__ = "Copyright (c) 2004-2010 Leonard Richardson" __license__ = "New-style BSD" @@ -531,6 +531,8 @@ def __init__(self, parser, name, attrs=None, parent=None, self.name = name if attrs is None: attrs = [] + elif isinstance(attrs, dict): + attrs = attrs.items() self.attrs = attrs self.contents = [] self.setup(parent, previous) @@ -1295,7 +1297,7 @@ def _smartPop(self, name): """ nestingResetTriggers = self.NESTABLE_TAGS.get(name) - isNestable = nestingResetTriggers is not None + isNestable = nestingResetTriggers != None isResetNesting = self.RESET_NESTING_TAGS.has_key(name) popTo = None inclusive = True diff --git a/module/lib/SafeEval.py b/module/lib/SafeEval.py index 8fc57f261f..d2a3206d9a 100644 --- a/module/lib/SafeEval.py +++ b/module/lib/SafeEval.py @@ -30,7 +30,7 @@ def test_expr(expr, allowed_codes): try: c = compile(expr, "", "eval") except: - raise ValueError, "%s is not a valid expression" % expr + raise ValueError, "'%s' is not a valid expression" % expr codes, names = _get_opcodes(c) for code in codes: if code not in allowed_codes: diff --git a/module/lib/beaker/session.py b/module/lib/beaker/session.py index 7d465530bd..f782748883 100644 --- a/module/lib/beaker/session.py +++ b/module/lib/beaker/session.py @@ -183,7 +183,7 @@ def _delete_cookie(self): if self.secure: self.cookie[self.key]['secure'] = True self.cookie[self.key]['path'] = '/' - expires = datetime.today().replace(year=2003) + expires = datetime.today().replace(day=1, year=2003) self.cookie[self.key]['expires'] = \ expires.strftime("%a, %d-%b-%Y %H:%M:%S GMT" ) self.request['cookie_out'] = self.cookie[self.key].output(header='') diff --git a/module/lib/bottle.py b/module/lib/bottle.py index 2c243278e2..96ca3e4a6b 100644 --- a/module/lib/bottle.py +++ b/module/lib/bottle.py @@ -9,14 +9,14 @@ Homepage and documentation: http://bottlepy.org/ -Copyright (c) 2011, Marcel Hellkamp. -License: MIT (see LICENSE.txt for details) +Copyright (c) 2016, Marcel Hellkamp. +License: MIT (see LICENSE for details) """ from __future__ import with_statement __author__ = 'Marcel Hellkamp' -__version__ = '0.10.2' +__version__ = '0.12.18' __license__ = 'MIT' # The gevent server adapter needs to patch some modules before they are imported @@ -35,102 +35,122 @@ if _cmd_options.server and _cmd_options.server.startswith('gevent'): import gevent.monkey; gevent.monkey.patch_all() -import sys -import base64 -import cgi -import email.utils -import functools -import hmac -import httplib -import imp -import itertools -import mimetypes -import os -import re -import subprocess -import tempfile -import thread -import threading -import time -import warnings - -from Cookie import SimpleCookie +import base64, cgi, email.utils, functools, hmac, itertools, mimetypes,\ + os, re, subprocess, sys, tempfile, threading, time, warnings, hashlib + from datetime import date as datedate, datetime, timedelta from tempfile import TemporaryFile from traceback import format_exc, print_exc -from urlparse import urljoin, SplitResult as UrlSplitResult - -# Workaround for a bug in some versions of lib2to3 (fixed on CPython 2.7 and 3.2) -import urllib -urlencode = urllib.urlencode -urlquote = urllib.quote -urlunquote = urllib.unquote - -try: from collections import MutableMapping as DictMixin -except ImportError: # pragma: no cover - from UserDict import DictMixin +from inspect import getargspec +from unicodedata import normalize -try: from urlparse import parse_qs -except ImportError: # pragma: no cover - from cgi import parse_qs -try: import cPickle as pickle +try: from simplejson import dumps as json_dumps, loads as json_lds except ImportError: # pragma: no cover - import pickle - -try: from json import dumps as json_dumps, loads as json_lds -except ImportError: # pragma: no cover - try: from simplejson import dumps as json_dumps, loads as json_lds - except ImportError: # pragma: no cover + try: from json import dumps as json_dumps, loads as json_lds + except ImportError: try: from django.utils.simplejson import dumps as json_dumps, loads as json_lds - except ImportError: # pragma: no cover + except ImportError: def json_dumps(data): raise ImportError("JSON support requires Python 2.6 or simplejson.") json_lds = json_dumps -py3k = sys.version_info >= (3,0,0) -NCTextIOWrapper = None -if py3k: # pragma: no cover - json_loads = lambda s: json_lds(touni(s)) - # See Request.POST + +# We now try to fix 2.5/2.6/3.1/3.2 incompatibilities. +# It ain't pretty but it works... Sorry for the mess. + +py = sys.version_info +py3k = py >= (3, 0, 0) +py25 = py < (2, 6, 0) +py31 = (3, 1, 0) <= py < (3, 2, 0) + +# Workaround for the missing "as" keyword in py3k. +def _e(): return sys.exc_info()[1] + +# Workaround for the "print is a keyword/function" Python 2/3 dilemma +# and a fallback for mod_wsgi (resticts stdout/err attribute access) +try: + _stdout, _stderr = sys.stdout.write, sys.stderr.write +except IOError: + _stdout = lambda x: sys.stdout.write(x) + _stderr = lambda x: sys.stderr.write(x) + +# Lots of stdlib and builtin differences. +if py3k: + import http.client as httplib + import _thread as thread + from urllib.parse import urljoin, SplitResult as UrlSplitResult + from urllib.parse import urlencode, quote as urlquote, unquote as urlunquote + urlunquote = functools.partial(urlunquote, encoding='latin1') + from http.cookies import SimpleCookie + if py >= (3, 3, 0): + from collections.abc import MutableMapping as DictMixin + from types import ModuleType as new_module + else: + from collections import MutableMapping as DictMixin + from imp import new_module + import pickle from io import BytesIO - def touni(x, enc='utf8', err='strict'): - """ Convert anything to unicode """ - return str(x, enc, err) if isinstance(x, bytes) else str(x) - if sys.version_info < (3,2,0): - from io import TextIOWrapper - class NCTextIOWrapper(TextIOWrapper): - ''' Garbage collecting an io.TextIOWrapper(buffer) instance closes - the wrapped buffer. This subclass keeps it open. ''' - def close(self): pass -else: - json_loads = json_lds + from configparser import ConfigParser + basestring = str + unicode = str + json_loads = lambda s: json_lds(touni(s)) + callable = lambda x: hasattr(x, '__call__') + imap = map + def _raise(*a): raise a[0](a[1]).with_traceback(a[2]) +else: # 2.x + import httplib + import thread + from urlparse import urljoin, SplitResult as UrlSplitResult + from urllib import urlencode, quote as urlquote, unquote as urlunquote + from Cookie import SimpleCookie + from itertools import imap + import cPickle as pickle + from imp import new_module from StringIO import StringIO as BytesIO - bytes = str - def touni(x, enc='utf8', err='strict'): - """ Convert anything to unicode """ - return x if isinstance(x, unicode) else unicode(str(x), enc, err) - -def tob(data, enc='utf8'): - """ Convert anything to bytes """ - return data.encode(enc) if isinstance(data, unicode) else bytes(data) + from ConfigParser import SafeConfigParser as ConfigParser + if py25: + msg = "Python 2.5 support may be dropped in future versions of Bottle." + warnings.warn(msg, DeprecationWarning) + from UserDict import DictMixin + def next(it): return it.next() + bytes = str + else: # 2.6, 2.7 + from collections import MutableMapping as DictMixin + unicode = unicode + json_loads = json_lds + eval(compile('def _raise(*a): raise a[0], a[1], a[2]', '', 'exec')) +# Some helpers for string/byte handling +def tob(s, enc='utf8'): + return s.encode(enc) if isinstance(s, unicode) else bytes(s) +def touni(s, enc='utf8', err='strict'): + return s.decode(enc, err) if isinstance(s, bytes) else unicode(s) tonat = touni if py3k else tob -tonat.__doc__ = """ Convert anything to native strings """ -def try_update_wrapper(wrapper, wrapped, *a, **ka): - try: # Bug: functools breaks if wrapper is an instane method - functools.update_wrapper(wrapper, wrapped, *a, **ka) +# 3.2 fixes cgi.FieldStorage to accept bytes (which makes a lot of sense). +# 3.1 needs a workaround. +if py31: + from io import TextIOWrapper + class NCTextIOWrapper(TextIOWrapper): + def close(self): pass # Keep wrapped buffer open. + + +# A bug in functools causes it to break if the wrapper is an instance method +def update_wrapper(wrapper, wrapped, *a, **ka): + try: functools.update_wrapper(wrapper, wrapped, *a, **ka) except AttributeError: pass -# Backward compatibility -def depr(message): - warnings.warn(message, DeprecationWarning, stacklevel=3) -# Small helpers -def makelist(data): +# These helpers are used at module level and need to be defined first. +# And yes, I know PEP-8, but sometimes a lower-case classname makes more sense. + +def depr(message, hard=False): + warnings.warn(message, DeprecationWarning, stacklevel=3) + +def makelist(data): # This is just to handy if isinstance(data, (tuple, list, set, dict)): return list(data) elif data: return [data] else: return [] @@ -161,12 +181,13 @@ def __delete__(self, obj): del getattr(obj, self.attr)[self.key] -class CachedProperty(object): +class cached_property(object): ''' A property that is only computed once per instance and then replaces itself with an ordinary attribute. Deleting the attribute resets the property. ''' def __init__(self, func): + self.__doc__ = getattr(func, '__doc__') self.func = func def __get__(self, obj, cls): @@ -174,10 +195,8 @@ def __get__(self, obj, cls): value = obj.__dict__[self.func.__name__] = self.func(obj) return value -cached_property = CachedProperty - -class lazy_attribute(object): # Does not need configuration -> lower-case name +class lazy_attribute(object): ''' A property that caches itself to the class object. ''' def __init__(self, func): functools.update_wrapper(self, func, updated=[]) @@ -203,35 +222,6 @@ class BottleException(Exception): pass -#TODO: These should subclass BaseRequest - -class HTTPResponse(BottleException): - """ Used to break execution and immediately finish the response """ - def __init__(self, output='', status=200, header=None): - super(BottleException, self).__init__("HTTP Response %d" % status) - self.status = int(status) - self.output = output - self.headers = HeaderDict(header) if header else None - - def apply(self, response): - if self.headers: - for key, value in self.headers.iterallitems(): - response.headers[key] = value - response.status = self.status - - -class HTTPError(HTTPResponse): - """ Used to generate an error page """ - def __init__(self, code=500, output='Unknown Error', exception=None, - traceback=None, header=None): - super(HTTPError, self).__init__(output, code, header) - self.exception = exception - self.traceback = traceback - - def __repr__(self): - return template(ERROR_PAGE_TEMPLATE, e=self) - - @@ -251,11 +241,22 @@ class RouteReset(BottleException): class RouterUnknownModeError(RouteError): pass + class RouteSyntaxError(RouteError): - """ The route parser found something not supported by this router """ + """ The route parser found something not supported by this router. """ + class RouteBuildError(RouteError): - """ The route could not been built """ + """ The route could not be built. """ + + +def _re_flatten(p): + ''' Turn all capturing groups in a regular expression pattern into + non-capturing groups. ''' + if '(' not in p: return p + return re.sub(r'(\\*)(\(\?P<[^>]+>|\((?!\?))', + lambda m: m.group(0) if len(m.group(1)) % 2 else m.group(1) + '(?:', p) + class Router(object): ''' A Router is an ordered collection of route->target pairs. It is used to @@ -270,44 +271,40 @@ class Router(object): ''' default_pattern = '[^/]+' - default_filter = 're' - #: Sorry for the mess. It works. Trust me. - rule_syntax = re.compile('(\\\\*)'\ - '(?:(?::([a-zA-Z_][a-zA-Z_0-9]*)?()(?:#(.*?)#)?)'\ - '|(?:<([a-zA-Z_][a-zA-Z_0-9]*)?(?::([a-zA-Z_]*)'\ - '(?::((?:\\\\.|[^\\\\>]+)+)?)?)?>))') + default_filter = 're' + + #: The current CPython regexp implementation does not allow more + #: than 99 matching groups per regular expression. + _MAX_GROUPS_PER_PATTERN = 99 def __init__(self, strict=False): - self.rules = {} # A {rule: Rule} mapping - self.builder = {} # A rule/name->build_info mapping - self.static = {} # Cache for static routes: {path: {method: target}} - self.dynamic = [] # Cache for dynamic routes. See _compile() + self.rules = [] # All rules in order + self._groups = {} # index of regexes to find them in dyna_routes + self.builder = {} # Data structure for the url builder + self.static = {} # Search structure for static routes + self.dyna_routes = {} + self.dyna_regexes = {} # Search structure for dynamic routes #: If true, static routes are no longer checked first. self.strict_order = strict - self.filters = {'re': self.re_filter, 'int': self.int_filter, - 'float': self.re_filter, 'path': self.path_filter} - - def re_filter(self, conf): - return conf or self.default_pattern, None, None - - def int_filter(self, conf): - return r'-?\d+', int, lambda x: str(int(x)) + self.filters = { + 're': lambda conf: + (_re_flatten(conf or self.default_pattern), None, None), + 'int': lambda conf: (r'-?\d+', int, lambda x: str(int(x))), + 'float': lambda conf: (r'-?[\d.]+', float, lambda x: str(float(x))), + 'path': lambda conf: (r'.+?', None, None)} - def float_filter(self, conf): - return r'-?\d*\.\d+', float, lambda x: str(float(x)) - - def path_filter(self, conf): - return r'.*?', None, None - def add_filter(self, name, func): ''' Add a filter. The provided function is called with the configuration string as parameter and must return a (regexp, to_python, to_url) tuple. The first element is a string, the last two are callables or None. ''' self.filters[name] = func - - def parse_rule(self, rule): - ''' Parses a rule into a (name, filter, conf) token stream. If mode is - None, name contains a static rule part. ''' + + rule_syntax = re.compile('(\\\\*)'\ + '(?:(?::([a-zA-Z_][a-zA-Z_0-9]*)?()(?:#(.*?)#)?)'\ + '|(?:<([a-zA-Z_][a-zA-Z_0-9]*)?(?::([a-zA-Z_]*)'\ + '(?::((?:\\\\.|[^\\\\>]+)+)?)?)?>))') + + def _itertokens(self, rule): offset, prefix = 0, '' for match in self.rule_syntax.finditer(rule): prefix += rule[offset:match.start()] @@ -316,77 +313,95 @@ def parse_rule(self, rule): prefix += match.group(0)[len(g[0]):] offset = match.end() continue - if prefix: yield prefix, None, None - name, filtr, conf = g[1:4] if not g[2] is None else g[4:7] - if not filtr: filtr = self.default_filter - yield name, filtr, conf or None + if prefix: + yield prefix, None, None + name, filtr, conf = g[4:7] if g[2] is None else g[1:4] + yield name, filtr or 'default', conf or None offset, prefix = match.end(), '' if offset <= len(rule) or prefix: yield prefix+rule[offset:], None, None def add(self, rule, method, target, name=None): - ''' Add a new route or replace the target for an existing route. ''' - if rule in self.rules: - self.rules[rule][method] = target - if name: self.builder[name] = self.builder[rule] - return - - target = self.rules[rule] = {method: target} - - # Build pattern and other structures for dynamic routes - anons = 0 # Number of anonymous wildcards - pattern = '' # Regular expression pattern - filters = [] # Lists of wildcard input filters - builder = [] # Data structure for the URL builder + ''' Add a new rule or replace the target for an existing rule. ''' + anons = 0 # Number of anonymous wildcards found + keys = [] # Names of keys + pattern = '' # Regular expression pattern with named groups + filters = [] # Lists of wildcard input filters + builder = [] # Data structure for the URL builder is_static = True - for key, mode, conf in self.parse_rule(rule): + + for key, mode, conf in self._itertokens(rule): if mode: is_static = False + if mode == 'default': mode = self.default_filter mask, in_filter, out_filter = self.filters[mode](conf) - if key: - pattern += '(?P<%s>%s)' % (key, mask) - else: + if not key: pattern += '(?:%s)' % mask - key = 'anon%d' % anons; anons += 1 + key = 'anon%d' % anons + anons += 1 + else: + pattern += '(?P<%s>%s)' % (key, mask) + keys.append(key) if in_filter: filters.append((key, in_filter)) builder.append((key, out_filter or str)) elif key: pattern += re.escape(key) builder.append((None, key)) + self.builder[rule] = builder if name: self.builder[name] = builder if is_static and not self.strict_order: - self.static[self.build(rule)] = target + self.static.setdefault(method, {}) + self.static[method][self.build(rule)] = (target, None) return - def fpat_sub(m): - return m.group(0) if len(m.group(1)) % 2 else m.group(1) + '(?:' - flat_pattern = re.sub(r'(\\*)(\(\?P<[^>]*>|\((?!\?))', fpat_sub, pattern) - try: - re_match = re.compile('^(%s)$' % pattern).match - except re.error, e: - raise RouteSyntaxError("Could not add Route: %s (%s)" % (rule, e)) - - def match(path): - """ Return an url-argument dictionary. """ - url_args = re_match(path).groupdict() - for name, wildcard_filter in filters: - try: - url_args[name] = wildcard_filter(url_args[name]) - except ValueError: - raise HTTPError(400, 'Path has wrong format.') - return url_args + re_pattern = re.compile('^(%s)$' % pattern) + re_match = re_pattern.match + except re.error: + raise RouteSyntaxError("Could not add Route: %s (%s)" % (rule, _e())) + + if filters: + def getargs(path): + url_args = re_match(path).groupdict() + for name, wildcard_filter in filters: + try: + url_args[name] = wildcard_filter(url_args[name]) + except ValueError: + raise HTTPError(400, 'Path has wrong format.') + return url_args + elif re_pattern.groupindex: + def getargs(path): + return re_match(path).groupdict() + else: + getargs = None - try: - combined = '%s|(^%s$)' % (self.dynamic[-1][0].pattern, flat_pattern) - self.dynamic[-1] = (re.compile(combined), self.dynamic[-1][1]) - self.dynamic[-1][1].append((match, target)) - except (AssertionError, IndexError), e: # AssertionError: Too many groups - self.dynamic.append((re.compile('(^%s$)' % flat_pattern), - [(match, target)])) - return match + flatpat = _re_flatten(pattern) + whole_rule = (rule, flatpat, target, getargs) + + if (flatpat, method) in self._groups: + if DEBUG: + msg = 'Route <%s %s> overwrites a previously defined route' + warnings.warn(msg % (method, rule), RuntimeWarning) + self.dyna_routes[method][self._groups[flatpat, method]] = whole_rule + else: + self.dyna_routes.setdefault(method, []).append(whole_rule) + self._groups[flatpat, method] = len(self.dyna_routes[method]) - 1 + + self._compile(method) + + def _compile(self, method): + all_rules = self.dyna_routes[method] + comborules = self.dyna_regexes[method] = [] + maxgroups = self._MAX_GROUPS_PER_PATTERN + for x in range(0, len(all_rules), maxgroups): + some = all_rules[x:x+maxgroups] + combined = (flatpat for (_, flatpat, _, _) in some) + combined = '|'.join('(^%s$)' % flatpat for flatpat in combined) + combined = re.compile(combined).match + rules = [(target, getargs) for (_, _, target, getargs) in some] + comborules.append((combined, rules)) def build(self, _name, *anons, **query): ''' Build an URL by filling the wildcards in a rule. ''' @@ -396,36 +411,50 @@ def build(self, _name, *anons, **query): for i, value in enumerate(anons): query['anon%d'%i] = value url = ''.join([f(query.pop(n)) if n else f for (n,f) in builder]) return url if not query else url+'?'+urlencode(query) - except KeyError, e: - raise RouteBuildError('Missing URL argument: %r' % e.args[0]) + except KeyError: + raise RouteBuildError('Missing URL argument: %r' % _e().args[0]) def match(self, environ): ''' Return a (target, url_agrs) tuple or raise HTTPError(400/404/405). ''' - path, targets, urlargs = environ['PATH_INFO'] or '/', None, {} - if path in self.static: - targets = self.static[path] + verb = environ['REQUEST_METHOD'].upper() + path = environ['PATH_INFO'] or '/' + target = None + if verb == 'HEAD': + methods = ['PROXY', verb, 'GET', 'ANY'] else: - for combined, rules in self.dynamic: - match = combined.match(path) - if not match: continue - getargs, targets = rules[match.lastindex - 1] - urlargs = getargs(path) if getargs else {} - break - - if not targets: - raise HTTPError(404, "Not found: " + repr(environ['PATH_INFO'])) - method = environ['REQUEST_METHOD'].upper() - if method in targets: - return targets[method], urlargs - if method == 'HEAD' and 'GET' in targets: - return targets['GET'], urlargs - if 'ANY' in targets: - return targets['ANY'], urlargs - allowed = [verb for verb in targets if verb != 'ANY'] - if 'GET' in allowed and 'HEAD' not in allowed: - allowed.append('HEAD') - raise HTTPError(405, "Method not allowed.", - header=[('Allow',",".join(allowed))]) + methods = ['PROXY', verb, 'ANY'] + + for method in methods: + if method in self.static and path in self.static[method]: + target, getargs = self.static[method][path] + return target, getargs(path) if getargs else {} + elif method in self.dyna_regexes: + for combined, rules in self.dyna_regexes[method]: + match = combined(path) + if match: + target, getargs = rules[match.lastindex - 1] + return target, getargs(path) if getargs else {} + + # No matching route found. Collect alternative methods for 405 response + allowed = set([]) + nocheck = set(methods) + for method in set(self.static) - nocheck: + if path in self.static[method]: + allowed.add(verb) + for method in set(self.dyna_regexes) - allowed - nocheck: + for combined, rules in self.dyna_regexes[method]: + match = combined(path) + if match: + allowed.add(method) + if allowed: + allow_header = ",".join(sorted(allowed)) + raise HTTPError(405, "Method not allowed.", Allow=allow_header) + + # No matching route and no alternative method found. We give up + raise HTTPError(404, "Not found: " + repr(path)) + + + @@ -435,7 +464,6 @@ class Route(object): turing an URL path rule into a regular expression usable by the Router. ''' - def __init__(self, app, rule, method, callback, name=None, plugins=None, skiplist=None, **config): #: The application this route is installed to. @@ -455,12 +483,12 @@ def __init__(self, app, rule, method, callback, name=None, #: Additional keyword arguments passed to the :meth:`Bottle.route` #: decorator are stored in this dictionary. Used for route-specific #: plugin configuration and meta-data. - self.config = ConfigDict(config) + self.config = ConfigDict().load_dict(config, make_namespaces=True) def __call__(self, *a, **ka): depr("Some APIs changed to return Route() instances instead of"\ " callables. Make sure to use the Route.call method and not to"\ - " call Route instances directly.") + " call Route instances directly.") #0.12 return self.call(*a, **ka) @cached_property @@ -480,7 +508,7 @@ def prepare(self): @property def _context(self): - depr('Switch to Plugin API v2 and access the Route object directly.') + depr('Switch to Plugin API v2 and access the Route object directly.') #0.12 return dict(rule=self.rule, method=self.method, callback=self.callback, name=self.name, app=self.app, config=self.config, apply=self.plugins, skip=self.skiplist) @@ -509,9 +537,36 @@ def _make_callback(self): except RouteReset: # Try again with changed configuration. return self._make_callback() if not callback is self.callback: - try_update_wrapper(callback, self.callback) + update_wrapper(callback, self.callback) return callback + def get_undecorated_callback(self): + ''' Return the callback. If the callback is a decorated function, try to + recover the original function. ''' + func = self.callback + func = getattr(func, '__func__' if py3k else 'im_func', func) + closure_attr = '__closure__' if py3k else 'func_closure' + while hasattr(func, closure_attr) and getattr(func, closure_attr): + func = getattr(func, closure_attr)[0].cell_contents + return func + + def get_callback_args(self): + ''' Return a list of argument names the callback (most likely) accepts + as keyword arguments. If the callback is a decorated function, try + to recover the original function before inspection. ''' + return getargspec(self.get_undecorated_callback())[0] + + def get_config(self, key, default=None): + ''' Lookup a config field and return its value, first checking the + route.config, then route.app.config.''' + for conf in (self.config, self.app.conifg): + if key in conf: return conf[key] + return default + + def __repr__(self): + cb = self.get_undecorated_callback() + return '<%s %r %r>' % (self.method, self.rule, cb) + @@ -523,27 +578,81 @@ def _make_callback(self): class Bottle(object): - """ WSGI application """ + """ Each Bottle object represents a single, distinct web application and + consists of routes, callbacks, plugins, resources and configuration. + Instances are callable WSGI applications. + + :param catchall: If true (default), handle all exceptions. Turn off to + let debugging middleware handle exceptions. + """ + + def __init__(self, catchall=True, autojson=True): + + #: A :class:`ConfigDict` for app specific configuration. + self.config = ConfigDict() + self.config._on_change = functools.partial(self.trigger_hook, 'config') + self.config.meta_set('autojson', 'validate', bool) + self.config.meta_set('catchall', 'validate', bool) + self.config['catchall'] = catchall + self.config['autojson'] = autojson + + #: A :class:`ResourceManager` for application files + self.resources = ResourceManager() - def __init__(self, catchall=True, autojson=True, config=None): - """ Create a new bottle instance. - You usually don't do that. Use `bottle.app.push()` instead. - """ self.routes = [] # List of installed :class:`Route` instances. self.router = Router() # Maps requests to :class:`Route` instances. - self.plugins = [] # List of installed plugins. - self.error_handler = {} - #: If true, most exceptions are catched and returned as :exc:`HTTPError` - self.config = ConfigDict(config or {}) - self.catchall = catchall - #: An instance of :class:`HooksPlugin`. Empty by default. - self.hooks = HooksPlugin() - self.install(self.hooks) - if autojson: + + # Core plugins + self.plugins = [] # List of installed plugins. + if self.config['autojson']: self.install(JSONPlugin()) self.install(TemplatePlugin()) + #: If true, most exceptions are caught and returned as :exc:`HTTPError` + catchall = DictProperty('config', 'catchall') + + __hook_names = 'before_request', 'after_request', 'app_reset', 'config' + __hook_reversed = 'after_request' + + @cached_property + def _hooks(self): + return dict((name, []) for name in self.__hook_names) + + def add_hook(self, name, func): + ''' Attach a callback to a hook. Three hooks are currently implemented: + + before_request + Executed once before each request. The request context is + available, but no routing has happened yet. + after_request + Executed once after each request regardless of its outcome. + app_reset + Called whenever :meth:`Bottle.reset` is called. + ''' + if name in self.__hook_reversed: + self._hooks[name].insert(0, func) + else: + self._hooks[name].append(func) + + def remove_hook(self, name, func): + ''' Remove a callback from a hook. ''' + if name in self._hooks and func in self._hooks[name]: + self._hooks[name].remove(func) + return True + + def trigger_hook(self, __name, *args, **kwargs): + ''' Trigger a hook and return a list of results. ''' + return [hook(*args, **kwargs) for hook in self._hooks[__name][:]] + + def hook(self, name): + """ Return a decorator that attaches a callback to a hook. See + :meth:`add_hook` for details.""" + def decorator(func): + self.add_hook(name, func) + return func + return decorator + def mount(self, prefix, app, **options): ''' Mount an application (:class:`Bottle` or plain WSGI) to a specific URL prefix. Example:: @@ -557,31 +666,50 @@ def mount(self, prefix, app, **options): All other parameters are passed to the underlying :meth:`route` call. ''' if isinstance(app, basestring): - prefix, app = app, prefix - depr('Parameter order of Bottle.mount() changed.') # 0.10 + depr('Parameter order of Bottle.mount() changed.', True) # 0.10 - parts = filter(None, prefix.split('/')) - if not parts: raise ValueError('Empty path prefix.') - path_depth = len(parts) - options.setdefault('skip', True) - options.setdefault('method', 'ANY') + segments = [p for p in prefix.split('/') if p] + if not segments: raise ValueError('Empty path prefix.') + path_depth = len(segments) - @self.route('/%s/:#.*#' % '/'.join(parts), **options) - def mountpoint(): + def mountpoint_wrapper(): try: request.path_shift(path_depth) - rs = BaseResponse([], 200) - def start_response(status, header): + rs = HTTPResponse([]) + def start_response(status, headerlist, exc_info=None): + if exc_info: + try: + _raise(*exc_info) + finally: + exc_info = None rs.status = status - for name, value in header: rs.add_header(name, value) + for name, value in headerlist: rs.add_header(name, value) return rs.body.append - rs.body = itertools.chain(rs.body, app(request.environ, start_response)) - return HTTPResponse(rs.body, rs.status, rs.headers) + body = app(request.environ, start_response) + if body and rs.body: body = itertools.chain(rs.body, body) + rs.body = body or rs.body + return rs finally: request.path_shift(-path_depth) + options.setdefault('skip', True) + options.setdefault('method', 'PROXY') + options.setdefault('mountpoint', {'prefix': prefix, 'target': app}) + options['callback'] = mountpoint_wrapper + + self.route('/%s/<:re:.*>' % '/'.join(segments), **options) if not prefix.endswith('/'): - self.route('/' + '/'.join(parts), callback=mountpoint, **options) + self.route('/' + '/'.join(segments), **options) + + def merge(self, routes): + ''' Merge the routes of another :class:`Bottle` application or a list of + :class:`Route` objects into this application. The routes keep their + 'owner', meaning that the :data:`Route.app` attribute is not + changed. ''' + if isinstance(routes, Bottle): + routes = routes.routes + for route in routes: + self.add_route(route) def install(self, plugin): ''' Add a plugin to the list of plugins and prepare it for being @@ -620,7 +748,7 @@ def reset(self, route=None): for route in routes: route.reset() if DEBUG: for route in routes: route.prepare() - self.hooks.trigger('app_reset') + self.trigger_hook('app_reset') def close(self): ''' Close the application and all installed plugins. ''' @@ -628,6 +756,10 @@ def close(self): if hasattr(plugin, 'close'): plugin.close() self.stopped = True + def run(self, **kwargs): + ''' Calls :func:`run` with the same parameters. ''' + run(self, **kwargs) + def match(self, environ): """ Search for a matching route and return a (:class:`Route` , urlargs) tuple. The second value is a dictionary with parameters extracted @@ -640,6 +772,13 @@ def get_url(self, routename, **kargs): location = self.router.build(routename, **kargs).lstrip('/') return urljoin(urljoin('/', scriptname), location) + def add_route(self, route): + ''' Add a route object, but do not change the :data:`Route.app` + attribute.''' + self.routes.append(route) + self.router.add(route.rule, route.method, route, name=route.name) + if DEBUG: route.prepare() + def route(self, path=None, method='GET', callback=None, name=None, apply=None, skip=None, **config): """ A decorator to bind a function to a request URL. Example:: @@ -678,9 +817,7 @@ def decorator(callback): verb = verb.upper() route = Route(self, rule, verb, callback, name=name, plugins=plugins, skiplist=skiplist, **config) - self.routes.append(route) - self.router.add(rule, verb, route, name=name) - if DEBUG: route.prepare() + self.add_route(route) return callback return decorator(callback) if callback else decorator @@ -707,44 +844,45 @@ def wrapper(handler): return handler return wrapper - def hook(self, name): - """ Return a decorator that attaches a callback to a hook. """ - def wrapper(func): - self.hooks.add(name, func) - return func - return wrapper - - def handle(self, path, method='GET'): - """ (deprecated) Execute the first matching route callback and return - the result. :exc:`HTTPResponse` exceptions are catched and returned. - If :attr:`Bottle.catchall` is true, other exceptions are catched as - well and returned as :exc:`HTTPError` instances (500). - """ - depr("This method will change semantics in 0.10. Try to avoid it.") - if isinstance(path, dict): - return self._handle(path) - return self._handle({'PATH_INFO': path, 'REQUEST_METHOD': method.upper()}) + def default_error_handler(self, res): + return tob(template(ERROR_PAGE_TEMPLATE, e=res)) def _handle(self, environ): + path = environ['bottle.raw_path'] = environ['PATH_INFO'] + if py3k: + try: + environ['PATH_INFO'] = path.encode('latin1').decode('utf8') + except UnicodeError: + return HTTPError(400, 'Invalid path string. Expected UTF-8') + try: - route, args = self.router.match(environ) - environ['route.handle'] = environ['bottle.route'] = route - environ['route.url_args'] = args - return route.call(**args) - except HTTPResponse, r: - return r + environ['bottle.app'] = self + request.bind(environ) + response.bind() + try: + self.trigger_hook('before_request') + route, args = self.router.match(environ) + environ['route.handle'] = route + environ['bottle.route'] = route + environ['route.url_args'] = args + return route.call(**args) + finally: + self.trigger_hook('after_request') + + except HTTPResponse: + return _e() except RouteReset: route.reset() return self._handle(environ) except (KeyboardInterrupt, SystemExit, MemoryError): raise - except Exception, e: + except Exception: if not self.catchall: raise - stacktrace = format_exc(10) + stacktrace = format_exc() environ['wsgi.errors'].write(stacktrace) - return HTTPError(500, "Internal Server Error", e, stacktrace) + return HTTPError(500, "Internal Server Error", _e(), stacktrace) - def _cast(self, out, request, response, peek=None): + def _cast(self, out, peek=None): """ Try to convert the parameter into something WSGI compatible and set correct HTTP headers when possible. Support: False, str, unicode, dict, HTTPResponse, HTTPError, file-like, @@ -753,7 +891,8 @@ def _cast(self, out, request, response, peek=None): # Empty output is done here if not out: - response['Content-Length'] = 0 + if 'Content-Length' not in response: + response['Content-Length'] = 0 return [] # Join lists of byte or unicode strings. Mixed lists are NOT supported if isinstance(out, (tuple, list))\ @@ -764,19 +903,18 @@ def _cast(self, out, request, response, peek=None): out = out.encode(response.charset) # Byte Strings are just returned if isinstance(out, bytes): - response['Content-Length'] = len(out) + if 'Content-Length' not in response: + response['Content-Length'] = len(out) return [out] # HTTPError or HTTPException (recursive, because they may wrap anything) # TODO: Handle these explicitly in handle() or make them iterable. if isinstance(out, HTTPError): out.apply(response) - out = self.error_handler.get(out.status, repr)(out) - if isinstance(out, HTTPResponse): - depr('Error handlers must not return :exc:`HTTPResponse`.') #0.9 - return self._cast(out, request, response) + out = self.error_handler.get(out.status_code, self.default_error_handler)(out) + return self._cast(out) if isinstance(out, HTTPResponse): out.apply(response) - return self._cast(out.output, request, response) + return self._cast(out.body) # File-like objects. if hasattr(out, 'read'): @@ -787,55 +925,59 @@ def _cast(self, out, request, response, peek=None): # Handle Iterables. We peek into them to detect their inner type. try: - out = iter(out) - first = out.next() + iout = iter(out) + first = next(iout) while not first: - first = out.next() + first = next(iout) except StopIteration: - return self._cast('', request, response) - except HTTPResponse, e: - first = e - except Exception, e: - first = HTTPError(500, 'Unhandled exception', e, format_exc(10)) - if isinstance(e, (KeyboardInterrupt, SystemExit, MemoryError))\ - or not self.catchall: - raise + return self._cast('') + except HTTPResponse: + first = _e() + except (KeyboardInterrupt, SystemExit, MemoryError): + raise + except Exception: + if not self.catchall: raise + first = HTTPError(500, 'Unhandled exception', _e(), format_exc()) + # These are the inner types allowed in iterator or generator objects. if isinstance(first, HTTPResponse): - return self._cast(first, request, response) - if isinstance(first, bytes): - return itertools.chain([first], out) - if isinstance(first, unicode): - return itertools.imap(lambda x: x.encode(response.charset), - itertools.chain([first], out)) - return self._cast(HTTPError(500, 'Unsupported response type: %s'\ - % type(first)), request, response) + return self._cast(first) + elif isinstance(first, bytes): + new_iter = itertools.chain([first], iout) + elif isinstance(first, unicode): + encoder = lambda x: x.encode(response.charset) + new_iter = imap(encoder, itertools.chain([first], iout)) + else: + msg = 'Unsupported response type: %s' % type(first) + return self._cast(HTTPError(500, msg)) + if hasattr(out, 'close'): + new_iter = _closeiter(new_iter, out.close) + return new_iter def wsgi(self, environ, start_response): """ The bottle WSGI-interface. """ try: - environ['bottle.app'] = self - request.bind(environ) - response.bind() - out = self._cast(self._handle(environ), request, response) + out = self._cast(self._handle(environ)) # rfc2616 section 4.3 if response._status_code in (100, 101, 204, 304)\ - or request.method == 'HEAD': + or environ['REQUEST_METHOD'] == 'HEAD': if hasattr(out, 'close'): out.close() out = [] - start_response(response._status_line, list(response.iter_headers())) + start_response(response._status_line, response.headerlist) return out except (KeyboardInterrupt, SystemExit, MemoryError): raise - except Exception, e: + except Exception: if not self.catchall: raise err = '

Critical error while processing request: %s

' \ - % environ.get('PATH_INFO', '/') + % html_escape(environ.get('PATH_INFO', '/')) if DEBUG: - err += '

Error:

\n
%s
\n' % repr(e) - err += '

Traceback:

\n
%s
\n' % format_exc(10) - environ['wsgi.errors'].write(err) #TODO: wsgi.error should not get html - start_response('500 INTERNAL SERVER ERROR', [('Content-Type', 'text/html')]) + err += '

Error:

\n
\n%s\n
\n' \ + '

Traceback:

\n
\n%s\n
\n' \ + % (html_escape(repr(_e())), html_escape(format_exc())) + environ['wsgi.errors'].write(err) + headers = [('Content-Type', 'text/html; charset=UTF-8')] + start_response('500 INTERNAL SERVER ERROR', headers, sys.exc_info()) return [tob(err)] def __call__(self, environ, start_response): @@ -851,20 +993,41 @@ def __call__(self, environ, start_response): # HTTP and WSGI Tools ########################################################## ############################################################################### - -class BaseRequest(DictMixin): +class BaseRequest(object): """ A wrapper for WSGI environment dictionaries that adds a lot of - convenient access methods and properties. Most of them are read-only.""" + convenient access methods and properties. Most of them are read-only. + + Adding new attributes to a request actually adds them to the environ + dictionary (as 'bottle.request.ext.'). This is the recommended + way to store and access request-specific data. + """ + + __slots__ = ('environ') #: Maximum size of memory buffer for :attr:`body` in bytes. MEMFILE_MAX = 102400 - def __init__(self, environ): + def __init__(self, environ=None): """ Wrap a WSGI environ dictionary. """ #: The wrapped WSGI environ dictionary. This is the only real attribute. #: All other attributes actually are read-only properties. - self.environ = environ - environ['bottle.request'] = self + self.environ = {} if environ is None else environ + self.environ['bottle.request'] = self + + @DictProperty('environ', 'bottle.app', read_only=True) + def app(self): + ''' Bottle application handling this request. ''' + raise RuntimeError('This request is not connected to an application.') + + @DictProperty('environ', 'bottle.route', read_only=True) + def route(self): + """ The bottle :class:`Route` object that matches this request. """ + raise RuntimeError('This request is not connected to a route.') + + @DictProperty('environ', 'route.url_args', read_only=True) + def url_args(self): + """ The arguments extracted from the URL. """ + raise RuntimeError('This request is not connected to a route.') @property def path(self): @@ -891,8 +1054,8 @@ def get_header(self, name, default=None): def cookies(self): """ Cookies parsed into a :class:`FormsDict`. Signed cookies are NOT decoded. Use :meth:`get_cookie` if you expect signed cookies. """ - cookies = SimpleCookie(self.environ.get('HTTP_COOKIE','')) - return FormsDict((c.key, c.value) for c in cookies.itervalues()) + cookies = SimpleCookie(self.environ.get('HTTP_COOKIE','')).values() + return FormsDict((c.key, c.value) for c in cookies) def get_cookie(self, key, default=None, secret=None): """ Return the content of a cookie. To read a `Signed Cookie`, the @@ -911,22 +1074,21 @@ def query(self): values are sometimes called "URL arguments" or "GET parameters", but not to be confused with "URL wildcards" as they are provided by the :class:`Router`. ''' - data = parse_qs(self.query_string, keep_blank_values=True) get = self.environ['bottle.get'] = FormsDict() - for key, values in data.iteritems(): - for value in values: - get[key] = value + pairs = _parse_qsl(self.environ.get('QUERY_STRING', '')) + for key, value in pairs: + get[key] = value return get @DictProperty('environ', 'bottle.request.forms', read_only=True) def forms(self): """ Form values parsed from an `url-encoded` or `multipart/form-data` - encoded POST or PUT request body. The result is retuned as a + encoded POST or PUT request body. The result is returned as a :class:`FormsDict`. All keys and values are strings. File uploads are stored separately in :attr:`files`. """ forms = FormsDict() - for name, item in self.POST.iterallitems(): - if not hasattr(item, 'filename'): + for name, item in self.POST.allitems(): + if not isinstance(item, FileUpload): forms[name] = item return forms @@ -935,32 +1097,21 @@ def params(self): """ A :class:`FormsDict` with the combined values of :attr:`query` and :attr:`forms`. File uploads are stored in :attr:`files`. """ params = FormsDict() - for key, value in self.query.iterallitems(): + for key, value in self.query.allitems(): params[key] = value - for key, value in self.forms.iterallitems(): + for key, value in self.forms.allitems(): params[key] = value return params @DictProperty('environ', 'bottle.request.files', read_only=True) def files(self): - """ File uploads parsed from an `url-encoded` or `multipart/form-data` - encoded POST or PUT request body. The values are instances of - :class:`cgi.FieldStorage`. The most important attributes are: - - filename - The filename, if specified; otherwise None; this is the client - side filename, *not* the file name on which it is stored (that's - a temporary file you don't deal with) - file - The file(-like) object from which you can read the data. - value - The value as a *string*; for file uploads, this transparently - reads the file every time you request the value. Do not do this - on big files. + """ File uploads parsed from `multipart/form-data` encoded POST or PUT + request body. The values are instances of :class:`FileUpload`. + """ files = FormsDict() - for name, item in self.POST.iterallitems(): - if hasattr(item, 'filename'): + for name, item in self.POST.allitems(): + if isinstance(item, FileUpload): files[name] = item return files @@ -970,25 +1121,78 @@ def json(self): property holds the parsed content of the request body. Only requests smaller than :attr:`MEMFILE_MAX` are processed to avoid memory exhaustion. ''' - if 'application/json' in self.environ.get('CONTENT_TYPE', '') \ - and 0 < self.content_length < self.MEMFILE_MAX: - return json_loads(self.body.read(self.MEMFILE_MAX)) + ctype = self.environ.get('CONTENT_TYPE', '').lower().split(';')[0] + if ctype == 'application/json': + b = self._get_body_string() + if not b: + return None + return json_loads(b) return None - @DictProperty('environ', 'bottle.request.body', read_only=True) - def _body(self): + def _iter_body(self, read, bufsize): maxread = max(0, self.content_length) - stream = self.environ['wsgi.input'] - body = BytesIO() if maxread < self.MEMFILE_MAX else TemporaryFile(mode='w+b') - while maxread > 0: - part = stream.read(min(maxread, self.MEMFILE_MAX)) + while maxread: + part = read(min(maxread, bufsize)) if not part: break - body.write(part) + yield part maxread -= len(part) + + def _iter_chunked(self, read, bufsize): + err = HTTPError(400, 'Error while parsing chunked transfer body.') + rn, sem, bs = tob('\r\n'), tob(';'), tob('') + while True: + header = read(1) + while header[-2:] != rn: + c = read(1) + header += c + if not c: raise err + if len(header) > bufsize: raise err + size, _, _ = header.partition(sem) + try: + maxread = int(tonat(size.strip()), 16) + except ValueError: + raise err + if maxread == 0: break + buff = bs + while maxread > 0: + if not buff: + buff = read(min(maxread, bufsize)) + part, buff = buff[:maxread], buff[maxread:] + if not part: raise err + yield part + maxread -= len(part) + if read(2) != rn: + raise err + + @DictProperty('environ', 'bottle.request.body', read_only=True) + def _body(self): + body_iter = self._iter_chunked if self.chunked else self._iter_body + read_func = self.environ['wsgi.input'].read + body, body_size, is_temp_file = BytesIO(), 0, False + for part in body_iter(read_func, self.MEMFILE_MAX): + body.write(part) + body_size += len(part) + if not is_temp_file and body_size > self.MEMFILE_MAX: + body, tmp = TemporaryFile(mode='w+b'), body + body.write(tmp.getvalue()) + del tmp + is_temp_file = True self.environ['wsgi.input'] = body body.seek(0) return body + def _get_body_string(self): + ''' read body until content-length or MEMFILE_MAX into a string. Raise + HTTPError(413) on requests that are to large. ''' + clen = self.content_length + if clen > self.MEMFILE_MAX: + raise HTTPError(413, 'Request to large') + if clen < 0: clen = self.MEMFILE_MAX + 1 + data = self.body.read(clen) + if len(data) > self.MEMFILE_MAX: # Fail fast + raise HTTPError(413, 'Request to large') + return data + @property def body(self): """ The HTTP request body as a seek-able file-like object. Depending on @@ -999,6 +1203,11 @@ def body(self): self._body.seek(0) return self._body + @property + def chunked(self): + ''' True if Chunked transfer encoding was. ''' + return 'chunked' in self.environ.get('HTTP_TRANSFER_ENCODING', '').lower() + #: An alias for :attr:`query`. GET = query @@ -1009,24 +1218,34 @@ def POST(self): instances of :class:`cgi.FieldStorage` (file uploads). """ post = FormsDict() + # We default to application/x-www-form-urlencoded for everything that + # is not multipart and take the fast path (also: 3.1 workaround) + if not self.content_type.startswith('multipart/'): + pairs = _parse_qsl(tonat(self._get_body_string(), 'latin1')) + for key, value in pairs: + post[key] = value + return post + safe_env = {'QUERY_STRING':''} # Build a safe environment for cgi for key in ('REQUEST_METHOD', 'CONTENT_TYPE', 'CONTENT_LENGTH'): if key in self.environ: safe_env[key] = self.environ[key] - if NCTextIOWrapper: - fb = NCTextIOWrapper(self.body, encoding='ISO-8859-1', newline='\n') - else: - fb = self.body - data = cgi.FieldStorage(fp=fb, environ=safe_env, keep_blank_values=True) - for item in data.list or []: - post[item.name] = item if item.filename else item.value + args = dict(fp=self.body, environ=safe_env, keep_blank_values=True) + if py31: + args['fp'] = NCTextIOWrapper(args['fp'], encoding='utf8', + newline='\n') + elif py3k: + args['encoding'] = 'utf8' + data = cgi.FieldStorage(**args) + self['_cgi.FieldStorage'] = data #http://bugs.python.org/issue18394#msg207958 + data = data.list or [] + for item in data: + if item.filename: + post[item.name] = FileUpload(item.file, item.name, + item.filename, item.headers) + else: + post[item.name] = item.value return post - @property - def COOKIES(self): - ''' Alias for :attr:`cookies` (deprecated). ''' - depr('BaseRequest.COOKIES was renamed to BaseRequest.cookies (lowercase).') - return self.cookies - @property def url(self): """ The full request URI including hostname and scheme. If your app @@ -1042,7 +1261,7 @@ def urlparts(self): but the fragment is always empty because it is not visible to the server. ''' env = self.environ - http = env.get('wsgi.url_scheme', 'http') + http = env.get('HTTP_X_FORWARDED_PROTO') or env.get('wsgi.url_scheme', 'http') host = env.get('HTTP_X_FORWARDED_HOST') or env.get('HTTP_HOST') if not host: # HTTP 1.1 requires a Host-header. This is for HTTP/1.0 clients. @@ -1090,6 +1309,11 @@ def content_length(self): and -1 is returned. In this case, :attr:`body` will be empty. ''' return int(self.environ.get('CONTENT_LENGTH') or -1) + @property + def content_type(self): + ''' The Content-Type header as a lowercase-string (default: empty). ''' + return self.environ.get('CONTENT_TYPE', '').lower() + @property def is_xhr(self): ''' True if the request was triggered by a XMLHttpRequest. This only @@ -1139,6 +1363,7 @@ def copy(self): """ Return a new :class:`Request` with a shallow :attr:`environ` copy. """ return Request(self.environ.copy()) + def get(self, value, default=None): return self.environ.get(value, default) def __getitem__(self, key): return self.environ[key] def __delitem__(self, key): self[key] = ""; del(self.environ[key]) def __iter__(self): return iter(self.environ) @@ -1166,27 +1391,49 @@ def __setitem__(self, key, value): def __repr__(self): return '<%s: %s %s>' % (self.__class__.__name__, self.method, self.url) -def _hkey(s): - return s.title().replace('_','-') + def __getattr__(self, name): + ''' Search in self.environ for additional user defined attributes. ''' + try: + var = self.environ['bottle.request.ext.%s'%name] + return var.__get__(self) if hasattr(var, '__get__') else var + except KeyError: + raise AttributeError('Attribute %r not defined.' % name) + + def __setattr__(self, name, value): + if name == 'environ': return object.__setattr__(self, name, value) + self.environ['bottle.request.ext.%s'%name] = value + + +def _hkey(key): + if '\n' in key or '\r' in key or '\0' in key: + raise ValueError("Header names must not contain control characters: %r" % key) + return key.title().replace('_', '-') + + +def _hval(value): + value = tonat(value) + if '\n' in value or '\r' in value or '\0' in value: + raise ValueError("Header value must not contain control characters: %r" % value) + return value + class HeaderProperty(object): - def __init__(self, name, reader=None, writer=str, default=''): - self.name, self.reader, self.writer, self.default = name, reader, writer, default + def __init__(self, name, reader=None, writer=None, default=''): + self.name, self.default = name, default + self.reader, self.writer = reader, writer self.__doc__ = 'Current value of the %r header.' % name.title() def __get__(self, obj, cls): if obj is None: return self - value = obj.headers.get(self.name) - return self.reader(value) if (value and self.reader) else (value or self.default) + value = obj.get_header(self.name, self.default) + return self.reader(value) if self.reader else value def __set__(self, obj, value): - if self.writer: value = self.writer(value) - obj.headers[self.name] = value + obj[self.name] = self.writer(value) if self.writer else value def __delete__(self, obj): - if self.name in obj.headers: - del obj.headers[self.name] + del obj[self.name] class BaseResponse(object): @@ -1195,6 +1442,14 @@ class BaseResponse(object): This class does support dict-like case-insensitive item-access to headers, but is NOT a dict. Most notably, iterating over a response yields parts of the body and not the headers. + + :param body: The response body as one of the supported types. + :param status: Either an HTTP status code (e.g. 200) or a status line + including the reason phrase (e.g. '200 OK'). + :param headers: A dictionary or a list of name-value pairs. + + Additional keyword arguments are added to the list of headers. + Underscores in the header name are replaced with dashes. """ default_status = 200 @@ -1208,22 +1463,30 @@ class BaseResponse(object): 'Content-Length', 'Content-Range', 'Content-Type', 'Content-Md5', 'Last-Modified'))} - def __init__(self, body='', status=None, **headers): - self._status_line = None - self._status_code = None - self.body = body + def __init__(self, body='', status=None, headers=None, **more_headers): self._cookies = None - self._headers = {'Content-Type': [self.default_content_type]} + self._headers = {} + self.body = body self.status = status or self.default_status if headers: - for name, value in headers.items(): - self[name] = value - - def copy(self): + if isinstance(headers, dict): + headers = headers.items() + for name, value in headers: + self.add_header(name, value) + if more_headers: + for name, value in more_headers.items(): + self.add_header(name, value) + + def copy(self, cls=None): ''' Returns a copy of self. ''' - copy = Response() + cls = cls or BaseResponse + assert issubclass(cls, BaseResponse) + copy = cls() copy.status = self.status copy._headers = dict((k, v[:]) for (k, v) in self._headers.items()) + if self._cookies: + copy._cookies = SimpleCookie() + copy._cookies.load(self._cookies.output(header='')) return copy def __iter__(self): @@ -1253,92 +1516,81 @@ def _set_status(self, status): raise ValueError('String status line without a reason phrase.') if not 100 <= code <= 999: raise ValueError('Status code out of range.') self._status_code = code - self._status_line = status or ('%d Unknown' % code) + self._status_line = str(status or ('%d Unknown' % code)) def _get_status(self): - depr('BaseReuqest.status will change to return a string in 0.11. Use'\ - ' status_line and status_code to make sure.') #0.10 - return self._status_code + return self._status_line status = property(_get_status, _set_status, None, ''' A writeable property to change the HTTP response status. It accepts either a numeric code (100-999) or a string with a custom reason phrase (e.g. "404 Brain not found"). Both :data:`status_line` and - :data:`status_code` are updates accordingly. The return value is - always a numeric code. ''') + :data:`status_code` are updated accordingly. The return value is + always a status string. ''') del _get_status, _set_status @property def headers(self): ''' An instance of :class:`HeaderDict`, a case-insensitive dict-like view on the response headers. ''' - self.__dict__['headers'] = hdict = HeaderDict() + hdict = HeaderDict() hdict.dict = self._headers return hdict def __contains__(self, name): return _hkey(name) in self._headers def __delitem__(self, name): del self._headers[_hkey(name)] def __getitem__(self, name): return self._headers[_hkey(name)][-1] - def __setitem__(self, name, value): self._headers[_hkey(name)] = [str(value)] + def __setitem__(self, name, value): self._headers[_hkey(name)] = [_hval(value)] def get_header(self, name, default=None): ''' Return the value of a previously defined header. If there is no header with that name, return a default value. ''' return self._headers.get(_hkey(name), [default])[-1] - def set_header(self, name, value, append=False): + def set_header(self, name, value): ''' Create a new response header, replacing any previously defined headers with the same name. ''' - if append: - self.add_header(name, value) - else: - self._headers[_hkey(name)] = [str(value)] + self._headers[_hkey(name)] = [_hval(value)] def add_header(self, name, value): ''' Add an additional response header, not removing duplicates. ''' - self._headers.setdefault(_hkey(name), []).append(str(value)) + self._headers.setdefault(_hkey(name), []).append(_hval(value)) def iter_headers(self): ''' Yield (header, value) tuples, skipping headers that are not allowed with the current response status code. ''' - headers = self._headers.iteritems() - bad_headers = self.bad_headers.get(self.status_code) - if bad_headers: - headers = [h for h in headers if h[0] not in bad_headers] - for name, values in headers: - for value in values: - yield name, value - if self._cookies: - for c in self._cookies.values(): - yield 'Set-Cookie', c.OutputString() - - def wsgiheader(self): - depr('The wsgiheader method is deprecated. See headerlist.') #0.10 return self.headerlist @property def headerlist(self): - ''' WSGI conform list of (header, value) tuples. ''' - return list(self.iter_headers()) + """ WSGI conform list of (header, value) tuples. """ + out = [] + headers = list(self._headers.items()) + if 'Content-Type' not in self._headers: + headers.append(('Content-Type', [self.default_content_type])) + if self._status_code in self.bad_headers: + bad_headers = self.bad_headers[self._status_code] + headers = [h for h in headers if h[0] not in bad_headers] + out += [(name, val) for (name, vals) in headers for val in vals] + if self._cookies: + for c in self._cookies.values(): + out.append(('Set-Cookie', _hval(c.OutputString()))) + if py3k: + out = [(k, v.encode('utf8').decode('latin1')) for (k, v) in out] + return out content_type = HeaderProperty('Content-Type') content_length = HeaderProperty('Content-Length', reader=int) + expires = HeaderProperty('Expires', + reader=lambda x: datetime.utcfromtimestamp(parse_date(x)), + writer=lambda x: http_date(x)) @property - def charset(self): + def charset(self, default='UTF-8'): """ Return the charset specified in the content-type header (default: utf8). """ if 'charset=' in self.content_type: return self.content_type.split('charset=')[-1].split(';')[0].strip() - return 'UTF-8' - - @property - def COOKIES(self): - """ A dict-like SimpleCookie instance. This should not be used directly. - See :meth:`set_cookie`. """ - depr('The COOKIES dict is deprecated. Use `set_cookie()` instead.') # 0.10 - if not self._cookies: - self._cookies = SimpleCookie() - return self._cookies + return default def set_cookie(self, name, value, secret=None, **options): ''' Create a new cookie or replace an old one. If the `secret` parameter is @@ -1384,7 +1636,7 @@ def set_cookie(self, name, value, secret=None, **options): if len(value) > 4096: raise ValueError('Cookie value to long.') self._cookies[name] = value - for key, value in options.iteritems(): + for key, value in options.items(): if key == 'max_age': if isinstance(value, timedelta): value = value.seconds + value.days * 24 * 3600 @@ -1410,20 +1662,66 @@ def __repr__(self): return out -class LocalRequest(BaseRequest, threading.local): - ''' A thread-local subclass of :class:`BaseRequest`. ''' - def __init__(self): pass +def local_property(name=None): + if name: depr('local_property() is deprecated and will be removed.') #0.12 + ls = threading.local() + def fget(self): + try: return ls.var + except AttributeError: + raise RuntimeError("Request context not initialized.") + def fset(self, value): ls.var = value + def fdel(self): del ls.var + return property(fget, fset, fdel, 'Thread-local property') + + +class LocalRequest(BaseRequest): + ''' A thread-local subclass of :class:`BaseRequest` with a different + set of attributes for each thread. There is usually only one global + instance of this class (:data:`request`). If accessed during a + request/response cycle, this instance always refers to the *current* + request (even on a multithreaded server). ''' bind = BaseRequest.__init__ + environ = local_property() -class LocalResponse(BaseResponse, threading.local): - ''' A thread-local subclass of :class:`BaseResponse`. ''' +class LocalResponse(BaseResponse): + ''' A thread-local subclass of :class:`BaseResponse` with a different + set of attributes for each thread. There is usually only one global + instance of this class (:data:`response`). Its attributes are used + to build the HTTP response at the end of the request/response cycle. + ''' bind = BaseResponse.__init__ + _status_line = local_property() + _status_code = local_property() + _cookies = local_property() + _headers = local_property() + body = local_property() + -Response = LocalResponse # BC 0.9 -Request = LocalRequest # BC 0.9 +Request = BaseRequest +Response = BaseResponse +class HTTPResponse(Response, BottleException): + def __init__(self, body='', status=None, headers=None, **more_headers): + super(HTTPResponse, self).__init__(body, status, headers, **more_headers) + + def apply(self, response): + response._status_code = self._status_code + response._status_line = self._status_line + response._headers = self._headers + response._cookies = self._cookies + response.body = self.body + + +class HTTPError(HTTPResponse): + default_status = 500 + def __init__(self, status=None, body=None, exception=None, traceback=None, + **options): + self.exception = exception + self.traceback = traceback + super(HTTPError, self).__init__(body, status, **options) + @@ -1434,6 +1732,7 @@ class LocalResponse(BaseResponse, threading.local): class PluginError(BottleException): pass + class JSONPlugin(object): name = 'json' api = 2 @@ -1441,63 +1740,26 @@ class JSONPlugin(object): def __init__(self, json_dumps=json_dumps): self.json_dumps = json_dumps - def apply(self, callback, context): + def apply(self, callback, route): dumps = self.json_dumps if not dumps: return callback def wrapper(*a, **ka): - rv = callback(*a, **ka) + try: + rv = callback(*a, **ka) + except HTTPError: + rv = _e() + if isinstance(rv, dict): #Attempt to serialize, raises exception on failure json_response = dumps(rv) #Set content type only if serialization succesful response.content_type = 'application/json' return json_response + elif isinstance(rv, HTTPResponse) and isinstance(rv.body, dict): + rv.body = dumps(rv.body) + rv.content_type = 'application/json' return rv - return wrapper - - -class HooksPlugin(object): - name = 'hooks' - api = 2 - - _names = 'before_request', 'after_request', 'app_reset' - - def __init__(self): - self.hooks = dict((name, []) for name in self._names) - self.app = None - - def _empty(self): - return not (self.hooks['before_request'] or self.hooks['after_request']) - def setup(self, app): - self.app = app - - def add(self, name, func): - ''' Attach a callback to a hook. ''' - was_empty = self._empty() - self.hooks.setdefault(name, []).append(func) - if self.app and was_empty and not self._empty(): self.app.reset() - - def remove(self, name, func): - ''' Remove a callback from a hook. ''' - was_empty = self._empty() - if name in self.hooks and func in self.hooks[name]: - self.hooks[name].remove(func) - if self.app and not was_empty and self._empty(): self.app.reset() - - def trigger(self, name, *a, **ka): - ''' Trigger a hook and return a list of results. ''' - hooks = self.hooks[name] - if ka.pop('reversed', False): hooks = hooks[::-1] - return [hook(*a, **ka) for hook in hooks] - - def apply(self, callback, context): - if self._empty(): return callback - def wrapper(*a, **ka): - self.trigger('before_request') - rv = callback(*a, **ka) - self.trigger('after_request', reversed=True) - return rv return wrapper @@ -1513,9 +1775,6 @@ def apply(self, callback, route): conf = route.config.get('template') if isinstance(conf, (tuple, list)) and len(conf) == 2: return view(conf[0], **conf[1])(callback) - elif isinstance(conf, str) and 'template_opts' in route.config: - depr('The `template_opts` parameter is deprecated.') #0.9 - return view(conf, **route.config['template_opts'])(callback) elif isinstance(conf, str): return view(conf)(callback) else: @@ -1528,20 +1787,20 @@ def __init__(self, name, impmask): ''' Create a virtual package that redirects imports (see PEP 302). ''' self.name = name self.impmask = impmask - self.module = sys.modules.setdefault(name, imp.new_module(name)) + self.module = sys.modules.setdefault(name, new_module(name)) self.module.__dict__.update({'__file__': __file__, '__path__': [], '__all__': [], '__loader__': self}) sys.meta_path.append(self) def find_module(self, fullname, path=None): if '.' not in fullname: return - packname, modname = fullname.rsplit('.', 1) + packname = fullname.rsplit('.', 1)[0] if packname != self.name: return return self def load_module(self, fullname): if fullname in sys.modules: return sys.modules[fullname] - packname, modname = fullname.rsplit('.', 1) + modname = fullname.rsplit('.', 1)[1] realname = self.impmask % modname __import__(realname) module = sys.modules[fullname] = sys.modules[realname] @@ -1566,26 +1825,37 @@ class MultiDict(DictMixin): """ def __init__(self, *a, **k): - self.dict = dict((k, [v]) for k, v in dict(*a, **k).iteritems()) + self.dict = dict((k, [v]) for (k, v) in dict(*a, **k).items()) + def __len__(self): return len(self.dict) def __iter__(self): return iter(self.dict) def __contains__(self, key): return key in self.dict def __delitem__(self, key): del self.dict[key] def __getitem__(self, key): return self.dict[key][-1] def __setitem__(self, key, value): self.append(key, value) - def iterkeys(self): return self.dict.iterkeys() - def itervalues(self): return (v[-1] for v in self.dict.itervalues()) - def iteritems(self): return ((k, v[-1]) for (k, v) in self.dict.iteritems()) - def iterallitems(self): - for key, values in self.dict.iteritems(): - for value in values: - yield key, value - - # 2to3 is not able to fix these automatically. - keys = iterkeys if py3k else lambda self: list(self.iterkeys()) - values = itervalues if py3k else lambda self: list(self.itervalues()) - items = iteritems if py3k else lambda self: list(self.iteritems()) - allitems = iterallitems if py3k else lambda self: list(self.iterallitems()) + def keys(self): return self.dict.keys() + + if py3k: + def values(self): return (v[-1] for v in self.dict.values()) + def items(self): return ((k, v[-1]) for k, v in self.dict.items()) + def allitems(self): + return ((k, v) for k, vl in self.dict.items() for v in vl) + iterkeys = keys + itervalues = values + iteritems = items + iterallitems = allitems + + else: + def values(self): return [v[-1] for v in self.dict.values()] + def items(self): return [(k, v[-1]) for k, v in self.dict.items()] + def iterkeys(self): return self.dict.iterkeys() + def itervalues(self): return (v[-1] for v in self.dict.itervalues()) + def iteritems(self): + return ((k, v[-1]) for k, v in self.dict.iteritems()) + def iterallitems(self): + return ((k, v) for k, vl in self.dict.iteritems() for v in vl) + def allitems(self): + return [(k, v) for k, vl in self.dict.iteritems() for v in vl] def get(self, key, default=None, index=-1, type=None): ''' Return the most recent value for a key. @@ -1600,7 +1870,7 @@ def get(self, key, default=None, index=-1, type=None): try: val = self.dict[key][index] return type(val) if type else val - except Exception, e: + except Exception: pass return default @@ -1621,31 +1891,51 @@ def getall(self, key): getlist = getall - class FormsDict(MultiDict): ''' This :class:`MultiDict` subclass is used to store request form data. Additionally to the normal dict-like item access methods (which return unmodified data as native strings), this container also supports - attribute-like access to its values. Attribues are automatiically de- or - recoded to match :attr:`input_encoding` (default: 'utf8'). Missing + attribute-like access to its values. Attributes are automatically de- + or recoded to match :attr:`input_encoding` (default: 'utf8'). Missing attributes default to an empty string. ''' #: Encoding used for attribute values. input_encoding = 'utf8' + #: If true (default), unicode strings are first encoded with `latin1` + #: and then decoded to match :attr:`input_encoding`. + recode_unicode = True + + def _fix(self, s, encoding=None): + if isinstance(s, unicode) and self.recode_unicode: # Python 3 WSGI + return s.encode('latin1').decode(encoding or self.input_encoding) + elif isinstance(s, bytes): # Python 2 WSGI + return s.decode(encoding or self.input_encoding) + else: + return s + + def decode(self, encoding=None): + ''' Returns a copy with all keys and values de- or recoded to match + :attr:`input_encoding`. Some libraries (e.g. WTForms) want a + unicode dictionary. ''' + copy = FormsDict() + enc = copy.input_encoding = encoding or self.input_encoding + copy.recode_unicode = False + for key, value in self.allitems(): + copy.append(self._fix(key, enc), self._fix(value, enc)) + return copy def getunicode(self, name, default=None, encoding=None): - value, enc = self.get(name, default), encoding or self.input_encoding + ''' Return the value as a unicode string, or the default. ''' try: - if isinstance(value, bytes): # Python 2 WSGI - return value.decode(enc) - elif isinstance(value, unicode): # Python 3 WSGI - return value.encode('latin1').decode(enc) - return value - except UnicodeError, e: + return self._fix(self[name], encoding) + except (UnicodeError, KeyError): return default - def __getattr__(self, name): return self.getunicode(name, default=u'') - + def __getattr__(self, name, default=unicode()): + # Without this guard, pickle generates a cryptic TypeError: + if name.startswith('__') and name.endswith('__'): + return super(FormsDict, self).__getattr__(name) + return self.getunicode(name, default=default) class HeaderDict(MultiDict): """ A case-insensitive version of :class:`MultiDict` that defaults to @@ -1658,15 +1948,14 @@ def __init__(self, *a, **ka): def __contains__(self, key): return _hkey(key) in self.dict def __delitem__(self, key): del self.dict[_hkey(key)] def __getitem__(self, key): return self.dict[_hkey(key)][-1] - def __setitem__(self, key, value): self.dict[_hkey(key)] = [str(value)] - def append(self, key, value): - self.dict.setdefault(_hkey(key), []).append(str(value)) - def replace(self, key, value): self.dict[_hkey(key)] = [str(value)] + def __setitem__(self, key, value): self.dict[_hkey(key)] = [_hval(value)] + def append(self, key, value): self.dict.setdefault(_hkey(key), []).append(_hval(value)) + def replace(self, key, value): self.dict[_hkey(key)] = [_hval(value)] def getall(self, key): return self.dict.get(_hkey(key)) or [] def get(self, key, default=None, index=-1): return MultiDict.get(self, _hkey(key), default, index) def filter(self, names): - for name in map(_hkey, names): + for name in (_hkey(n) for n in names): if name in self.dict: del self.dict[name] @@ -1682,7 +1971,7 @@ class WSGIHeaderDict(DictMixin): Currently PEP 333, 444 and 3333 are supported. (PEP 444 is the only one that uses non-native strings.) ''' - #: List of keys that do not have a 'HTTP_' prefix. + #: List of keys that do not have a ``HTTP_`` prefix. cgikeys = ('CONTENT_TYPE', 'CONTENT_LENGTH') def __init__(self, environ): @@ -1720,39 +2009,212 @@ def __len__(self): return len(self.keys()) def __contains__(self, key): return self._ekey(key) in self.environ + class ConfigDict(dict): - ''' A dict-subclass with some extras: You can access keys like attributes. - Uppercase attributes create new ConfigDicts and act as name-spaces. - Other missing attributes return None. Calling a ConfigDict updates its - values and returns itself. - - >>> cfg = ConfigDict() - >>> cfg.Namespace.value = 5 - >>> cfg.OtherNamespace(a=1, b=2) - >>> cfg - {'Namespace': {'value': 5}, 'OtherNamespace': {'a': 1, 'b': 2}} + ''' A dict-like configuration storage with additional support for + namespaces, validators, meta-data, on_change listeners and more. + + This storage is optimized for fast read access. Retrieving a key + or using non-altering dict methods (e.g. `dict.get()`) has no overhead + compared to a native dict. ''' + __slots__ = ('_meta', '_on_change') + + class Namespace(DictMixin): + + def __init__(self, config, namespace): + self._config = config + self._prefix = namespace + + def __getitem__(self, key): + depr('Accessing namespaces as dicts is discouraged. ' + 'Only use flat item access: ' + 'cfg["names"]["pace"]["key"] -> cfg["name.space.key"]') #0.12 + return self._config[self._prefix + '.' + key] + + def __setitem__(self, key, value): + self._config[self._prefix + '.' + key] = value + + def __delitem__(self, key): + del self._config[self._prefix + '.' + key] + + def __iter__(self): + ns_prefix = self._prefix + '.' + for key in self._config: + ns, dot, name = key.rpartition('.') + if ns == self._prefix and name: + yield name + + def keys(self): return [x for x in self] + def __len__(self): return len(self.keys()) + def __contains__(self, key): return self._prefix + '.' + key in self._config + def __repr__(self): return '' % self._prefix + def __str__(self): return '' % self._prefix + + # Deprecated ConfigDict features + def __getattr__(self, key): + depr('Attribute access is deprecated.') #0.12 + if key not in self and key[0].isupper(): + self[key] = ConfigDict.Namespace(self._config, self._prefix + '.' + key) + if key not in self and key.startswith('__'): + raise AttributeError(key) + return self.get(key) + + def __setattr__(self, key, value): + if key in ('_config', '_prefix'): + self.__dict__[key] = value + return + depr('Attribute assignment is deprecated.') #0.12 + if hasattr(DictMixin, key): + raise AttributeError('Read-only attribute.') + if key in self and self[key] and isinstance(self[key], self.__class__): + raise AttributeError('Non-empty namespace attribute.') + self[key] = value + + def __delattr__(self, key): + if key in self: + val = self.pop(key) + if isinstance(val, self.__class__): + prefix = key + '.' + for key in self: + if key.startswith(prefix): + del self[prefix+key] + + def __call__(self, *a, **ka): + depr('Calling ConfDict is deprecated. Use the update() method.') #0.12 + self.update(*a, **ka) + return self + + def __init__(self, *a, **ka): + self._meta = {} + self._on_change = lambda name, value: None + if a or ka: + depr('Constructor does no longer accept parameters.') #0.12 + self.update(*a, **ka) + + def load_config(self, filename): + ''' Load values from an *.ini style config file. + + If the config file contains sections, their names are used as + namespaces for the values within. The two special sections + ``DEFAULT`` and ``bottle`` refer to the root namespace (no prefix). + ''' + conf = ConfigParser() + conf.read(filename) + for section in conf.sections(): + for key, value in conf.items(section): + if section not in ('DEFAULT', 'bottle'): + key = section + '.' + key + self[key] = value + return self + + def load_dict(self, source, namespace='', make_namespaces=False): + ''' Import values from a dictionary structure. Nesting can be used to + represent namespaces. + + >>> ConfigDict().load_dict({'name': {'space': {'key': 'value'}}}) + {'name.space.key': 'value'} + ''' + stack = [(namespace, source)] + while stack: + prefix, source = stack.pop() + if not isinstance(source, dict): + raise TypeError('Source is not a dict (r)' % type(key)) + for key, value in source.items(): + if not isinstance(key, basestring): + raise TypeError('Key is not a string (%r)' % type(key)) + full_key = prefix + '.' + key if prefix else key + if isinstance(value, dict): + stack.append((full_key, value)) + if make_namespaces: + self[full_key] = self.Namespace(self, full_key) + else: + self[full_key] = value + return self + + def update(self, *a, **ka): + ''' If the first parameter is a string, all keys are prefixed with this + namespace. Apart from that it works just as the usual dict.update(). + Example: ``update('some.namespace', key='value')`` ''' + prefix = '' + if a and isinstance(a[0], basestring): + prefix = a[0].strip('.') + '.' + a = a[1:] + for key, value in dict(*a, **ka).items(): + self[prefix+key] = value + + def setdefault(self, key, value): + if key not in self: + self[key] = value + return self[key] + + def __setitem__(self, key, value): + if not isinstance(key, basestring): + raise TypeError('Key has type %r (not a string)' % type(key)) + + value = self.meta_get(key, 'filter', lambda x: x)(value) + if key in self and self[key] is value: + return + self._on_change(key, value) + dict.__setitem__(self, key, value) + def __delitem__(self, key): + dict.__delitem__(self, key) + + def clear(self): + for key in self: + del self[key] + + def meta_get(self, key, metafield, default=None): + ''' Return the value of a meta field for a key. ''' + return self._meta.get(key, {}).get(metafield, default) + + def meta_set(self, key, metafield, value): + ''' Set the meta field for a key to a new value. This triggers the + on-change handler for existing keys. ''' + self._meta.setdefault(key, {})[metafield] = value + if key in self: + self[key] = self[key] + + def meta_list(self, key): + ''' Return an iterable of meta field names defined for a key. ''' + return self._meta.get(key, {}).keys() + + # Deprecated ConfigDict features def __getattr__(self, key): + depr('Attribute access is deprecated.') #0.12 if key not in self and key[0].isupper(): - self[key] = ConfigDict() + self[key] = self.Namespace(self, key) + if key not in self and key.startswith('__'): + raise AttributeError(key) return self.get(key) def __setattr__(self, key, value): + if key in self.__slots__: + return dict.__setattr__(self, key, value) + depr('Attribute assignment is deprecated.') #0.12 if hasattr(dict, key): raise AttributeError('Read-only attribute.') - if key in self and self[key] and isinstance(self[key], ConfigDict): + if key in self and self[key] and isinstance(self[key], self.Namespace): raise AttributeError('Non-empty namespace attribute.') self[key] = value def __delattr__(self, key): - if key in self: del self[key] + if key in self: + val = self.pop(key) + if isinstance(val, self.Namespace): + prefix = key + '.' + for key in self: + if key.startswith(prefix): + del self[prefix+key] def __call__(self, *a, **ka): - for key, value in dict(*a, **ka).iteritems(): setattr(self, key, value) + depr('Calling ConfDict is deprecated. Use the update() method.') #0.12 + self.update(*a, **ka) return self + class AppStack(list): """ A stack-like list. Calling it returns the head of the stack. """ @@ -1770,17 +2232,186 @@ def push(self, value=None): class WSGIFileWrapper(object): - def __init__(self, fp, buffer_size=1024*64): - self.fp, self.buffer_size = fp, buffer_size - for attr in ('fileno', 'close', 'read', 'readlines'): - if hasattr(fp, attr): setattr(self, attr, getattr(fp, attr)) + def __init__(self, fp, buffer_size=1024*64): + self.fp, self.buffer_size = fp, buffer_size + for attr in ('fileno', 'close', 'read', 'readlines', 'tell', 'seek'): + if hasattr(fp, attr): setattr(self, attr, getattr(fp, attr)) + + def __iter__(self): + buff, read = self.buffer_size, self.read + while True: + part = read(buff) + if not part: return + yield part + + +class _closeiter(object): + ''' This only exists to be able to attach a .close method to iterators that + do not support attribute assignment (most of itertools). ''' - def __iter__(self): - read, buff = self.fp.read, self.buffer_size - while True: - part = read(buff) - if not part: break - yield part + def __init__(self, iterator, close=None): + self.iterator = iterator + self.close_callbacks = makelist(close) + + def __iter__(self): + return iter(self.iterator) + + def close(self): + for func in self.close_callbacks: + func() + + +class ResourceManager(object): + ''' This class manages a list of search paths and helps to find and open + application-bound resources (files). + + :param base: default value for :meth:`add_path` calls. + :param opener: callable used to open resources. + :param cachemode: controls which lookups are cached. One of 'all', + 'found' or 'none'. + ''' + + def __init__(self, base='./', opener=open, cachemode='all'): + self.opener = open + self.base = base + self.cachemode = cachemode + + #: A list of search paths. See :meth:`add_path` for details. + self.path = [] + #: A cache for resolved paths. ``res.cache.clear()`` clears the cache. + self.cache = {} + + def add_path(self, path, base=None, index=None, create=False): + ''' Add a new path to the list of search paths. Return False if the + path does not exist. + + :param path: The new search path. Relative paths are turned into + an absolute and normalized form. If the path looks like a file + (not ending in `/`), the filename is stripped off. + :param base: Path used to absolutize relative search paths. + Defaults to :attr:`base` which defaults to ``os.getcwd()``. + :param index: Position within the list of search paths. Defaults + to last index (appends to the list). + + The `base` parameter makes it easy to reference files installed + along with a python module or package:: + + res.add_path('./resources/', __file__) + ''' + base = os.path.abspath(os.path.dirname(base or self.base)) + path = os.path.abspath(os.path.join(base, os.path.dirname(path))) + path += os.sep + if path in self.path: + self.path.remove(path) + if create and not os.path.isdir(path): + os.makedirs(path) + if index is None: + self.path.append(path) + else: + self.path.insert(index, path) + self.cache.clear() + return os.path.exists(path) + + def __iter__(self): + ''' Iterate over all existing files in all registered paths. ''' + search = self.path[:] + while search: + path = search.pop() + if not os.path.isdir(path): continue + for name in os.listdir(path): + full = os.path.join(path, name) + if os.path.isdir(full): search.append(full) + else: yield full + + def lookup(self, name): + ''' Search for a resource and return an absolute file path, or `None`. + + The :attr:`path` list is searched in order. The first match is + returend. Symlinks are followed. The result is cached to speed up + future lookups. ''' + if name not in self.cache or DEBUG: + for path in self.path: + fpath = os.path.join(path, name) + if os.path.isfile(fpath): + if self.cachemode in ('all', 'found'): + self.cache[name] = fpath + return fpath + if self.cachemode == 'all': + self.cache[name] = None + return self.cache[name] + + def open(self, name, mode='r', *args, **kwargs): + ''' Find a resource and return a file object, or raise IOError. ''' + fname = self.lookup(name) + if not fname: raise IOError("Resource %r not found." % name) + return self.opener(fname, mode=mode, *args, **kwargs) + + +class FileUpload(object): + + def __init__(self, fileobj, name, filename, headers=None): + ''' Wrapper for file uploads. ''' + #: Open file(-like) object (BytesIO buffer or temporary file) + self.file = fileobj + #: Name of the upload form field + self.name = name + #: Raw filename as sent by the client (may contain unsafe characters) + self.raw_filename = filename + #: A :class:`HeaderDict` with additional headers (e.g. content-type) + self.headers = HeaderDict(headers) if headers else HeaderDict() + + content_type = HeaderProperty('Content-Type') + content_length = HeaderProperty('Content-Length', reader=int, default=-1) + + def get_header(self, name, default=None): + """ Return the value of a header within the mulripart part. """ + return self.headers.get(name, default) + + @cached_property + def filename(self): + ''' Name of the file on the client file system, but normalized to ensure + file system compatibility. An empty filename is returned as 'empty'. + + Only ASCII letters, digits, dashes, underscores and dots are + allowed in the final filename. Accents are removed, if possible. + Whitespace is replaced by a single dash. Leading or tailing dots + or dashes are removed. The filename is limited to 255 characters. + ''' + fname = self.raw_filename + if not isinstance(fname, unicode): + fname = fname.decode('utf8', 'ignore') + fname = normalize('NFKD', fname).encode('ASCII', 'ignore').decode('ASCII') + fname = os.path.basename(fname.replace('\\', os.path.sep)) + fname = re.sub(r'[^a-zA-Z0-9-_.\s]', '', fname).strip() + fname = re.sub(r'[-\s]+', '-', fname).strip('.-') + return fname[:255] or 'empty' + + def _copy_file(self, fp, chunk_size=2**16): + read, write, offset = self.file.read, fp.write, self.file.tell() + while 1: + buf = read(chunk_size) + if not buf: break + write(buf) + self.file.seek(offset) + + def save(self, destination, overwrite=False, chunk_size=2**16): + ''' Save file to disk or copy its content to an open file(-like) object. + If *destination* is a directory, :attr:`filename` is added to the + path. Existing files are not overwritten by default (IOError). + + :param destination: File path, directory or file(-like) object. + :param overwrite: If True, replace existing files. (default: False) + :param chunk_size: Bytes to read at a time. (default: 64kb) + ''' + if isinstance(destination, basestring): # Except file-likes here + if os.path.isdir(destination): + destination = os.path.join(destination, self.filename) + if not overwrite and os.path.exists(destination): + raise IOError('File exists.') + with open(destination, 'wb') as fp: + self._copy_file(fp, chunk_size) + else: + self._copy_file(destination, chunk_size) @@ -1792,7 +2423,7 @@ def __iter__(self): ############################################################################### -def abort(code=500, text='Unknown Error: Application stopped.'): +def abort(code=500, text='Unknown Error.'): """ Aborts execution and causes a HTTP error. """ raise HTTPError(code, text) @@ -1800,21 +2431,48 @@ def abort(code=500, text='Unknown Error: Application stopped.'): def redirect(url, code=None): """ Aborts execution and causes a 303 or 302 redirect, depending on the HTTP protocol version. """ - if code is None: + if not code: code = 303 if request.get('SERVER_PROTOCOL') == "HTTP/1.1" else 302 - location = urljoin(request.url, url) - raise HTTPResponse("", status=code, header=dict(Location=location)) + res = response.copy(cls=HTTPResponse) + res.status = code + res.body = "" + res.set_header('Location', urljoin(request.url, url)) + raise res + +def _file_iter_range(fp, offset, bytes, maxread=1024*1024): + ''' Yield chunks from a range in a file. No chunk is bigger than maxread.''' + fp.seek(offset) + while bytes > 0: + part = fp.read(min(bytes, maxread)) + if not part: break + bytes -= len(part) + yield part -def static_file(filename, root, mimetype='auto', download=False): + +def static_file(filename, root, mimetype='auto', download=False, charset='UTF-8'): """ Open a file in a safe way and return :exc:`HTTPResponse` with status - code 200, 305, 401 or 404. Set Content-Type, Content-Encoding, - Content-Length and Last-Modified header. Obey If-Modified-Since header - and HEAD requests. + code 200, 305, 403 or 404. The ``Content-Type``, ``Content-Encoding``, + ``Content-Length`` and ``Last-Modified`` headers are set if possible. + Special support for ``If-Modified-Since``, ``Range`` and ``HEAD`` + requests. + + :param filename: Name or path of the file to send. + :param root: Root path for file lookups. Should be an absolute directory + path. + :param mimetype: Defines the content-type header (default: guess from + file extension) + :param download: If True, ask the browser to open a `Save as...` dialog + instead of opening the file with the associated program. You can + specify a custom filename as a string. If not specified, the + original filename is used (default: False). + :param charset: The charset to use for files with a ``text/*`` + mime-type. (default: UTF-8) """ + root = os.path.abspath(root) + os.sep filename = os.path.abspath(os.path.join(root, filename.strip('/\\'))) - header = dict() + headers = dict() if not filename.startswith(root): return HTTPError(403, "Access denied.") @@ -1825,29 +2483,43 @@ def static_file(filename, root, mimetype='auto', download=False): if mimetype == 'auto': mimetype, encoding = mimetypes.guess_type(filename) - if mimetype: header['Content-Type'] = mimetype - if encoding: header['Content-Encoding'] = encoding - elif mimetype: - header['Content-Type'] = mimetype + if encoding: headers['Content-Encoding'] = encoding + + if mimetype: + if mimetype[:5] == 'text/' and charset and 'charset' not in mimetype: + mimetype += '; charset=%s' % charset + headers['Content-Type'] = mimetype if download: download = os.path.basename(filename if download == True else download) - header['Content-Disposition'] = 'attachment; filename="%s"' % download + headers['Content-Disposition'] = 'attachment; filename="%s"' % download stats = os.stat(filename) - header['Content-Length'] = stats.st_size + headers['Content-Length'] = clen = stats.st_size lm = time.strftime("%a, %d %b %Y %H:%M:%S GMT", time.gmtime(stats.st_mtime)) - header['Last-Modified'] = lm + headers['Last-Modified'] = lm ims = request.environ.get('HTTP_IF_MODIFIED_SINCE') if ims: ims = parse_date(ims.split(";")[0].strip()) if ims is not None and ims >= int(stats.st_mtime): - header['Date'] = time.strftime("%a, %d %b %Y %H:%M:%S GMT", time.gmtime()) - return HTTPResponse(status=304, header=header) + headers['Date'] = time.strftime("%a, %d %b %Y %H:%M:%S GMT", time.gmtime()) + return HTTPResponse(status=304, **headers) body = '' if request.method == 'HEAD' else open(filename, 'rb') - return HTTPResponse(body, header=header) + + headers["Accept-Ranges"] = "bytes" + ranges = request.environ.get('HTTP_RANGE') + if 'HTTP_RANGE' in request.environ: + ranges = list(parse_range_header(request.environ['HTTP_RANGE'], clen)) + if not ranges: + return HTTPError(416, "Requested Range Not Satisfiable") + offset, end = ranges[0] + headers["Content-Range"] = "bytes %d-%d/%d" % (offset, end-1, clen) + headers["Content-Length"] = str(end-offset) + if body: body = _file_iter_range(body, offset, end-offset) + return HTTPResponse(body, status=206, **headers) + return HTTPResponse(body, **headers) @@ -1863,8 +2535,17 @@ def debug(mode=True): """ Change the debug level. There is only one debug level supported at the moment.""" global DEBUG + if mode: warnings.simplefilter('default') DEBUG = bool(mode) +def http_date(value): + if isinstance(value, (datedate, datetime)): + value = value.utctimetuple() + elif isinstance(value, (int, float)): + value = time.gmtime(value) + if not isinstance(value, basestring): + value = time.strftime("%a, %d %b %Y %H:%M:%S GMT", value) + return value def parse_date(ims): """ Parse rfc1123, rfc850 and asctime timestamps and return UTC epoch. """ @@ -1874,21 +2555,47 @@ def parse_date(ims): except (TypeError, ValueError, IndexError, OverflowError): return None - def parse_auth(header): """ Parse rfc2617 HTTP authentication header string (basic) and return (user,pass) tuple or None""" try: method, data = header.split(None, 1) if method.lower() == 'basic': - #TODO: Add 2to3 save base64[encode/decode] functions. user, pwd = touni(base64.b64decode(tob(data))).split(':',1) return user, pwd except (KeyError, ValueError): return None +def parse_range_header(header, maxlen=0): + ''' Yield (start, end) ranges parsed from a HTTP Range header. Skip + unsatisfiable ranges. The end index is non-inclusive.''' + if not header or header[:6] != 'bytes=': return + ranges = [r.split('-', 1) for r in header[6:].split(',') if '-' in r] + for start, end in ranges: + try: + if not start: # bytes=-100 -> last 100 bytes + start, end = max(0, maxlen-int(end)), maxlen + elif not end: # bytes=100- -> all but the first 99 bytes + start, end = int(start), maxlen + else: # bytes=100-200 -> bytes 100-200 (inclusive) + start, end = int(start), min(int(end)+1, maxlen) + if 0 <= start < end <= maxlen: + yield start, end + except ValueError: + pass + +def _parse_qsl(qs): + r = [] + for pair in qs.replace(';','&').split('&'): + if not pair: continue + nv = pair.split('=', 1) + if len(nv) != 2: nv.append('') + key = urlunquote(nv[0].replace('+', ' ')) + value = urlunquote(nv[1].replace('+', ' ')) + r.append((key, value)) + return r def _lscmp(a, b): - ''' Compares two strings in a cryptographically save way: + ''' Compares two strings in a cryptographically safe way: Runtime is not affected by length of common prefix. ''' return not sum(0 if x==y else 1 for x, y in zip(a, b)) and len(a) == len(b) @@ -1896,7 +2603,7 @@ def _lscmp(a, b): def cookie_encode(data, key): ''' Encode and sign a pickle-able object. Return a (byte) string ''' msg = base64.b64encode(pickle.dumps(data, -1)) - sig = base64.b64encode(hmac.new(tob(key), msg).digest()) + sig = base64.b64encode(hmac.new(tob(key), msg, digestmod=hashlib.md5).digest()) return tob('!') + sig + tob('?') + msg @@ -1905,7 +2612,7 @@ def cookie_decode(data, key): data = tob(data) if cookie_is_encoded(data): sig, msg = data.split(tob('?'), 1) - if _lscmp(sig[1:], base64.b64encode(hmac.new(tob(key), msg).digest())): + if _lscmp(sig[1:], base64.b64encode(hmac.new(tob(key), msg, digestmod=hashlib.md5).digest())): return pickle.loads(base64.b64decode(msg)) return None @@ -1923,7 +2630,7 @@ def html_escape(string): def html_quote(string): ''' Escape and quote a string to be used as an HTTP attribute.''' - return '"%s"' % html_escape(string).replace('\n','%#10;')\ + return '"%s"' % html_escape(string).replace('\n',' ')\ .replace('\r',' ').replace('\t',' ') @@ -1933,18 +2640,17 @@ def yieldroutes(func): takes optional keyword arguments. The output is best described by example:: a() -> '/a' - b(x, y) -> '/b/:x/:y' - c(x, y=5) -> '/c/:x' and '/c/:x/:y' - d(x=5, y=6) -> '/d' and '/d/:x' and '/d/:x/:y' + b(x, y) -> '/b//' + c(x, y=5) -> '/c/' and '/c//' + d(x=5, y=6) -> '/d' and '/d/' and '/d//' """ - import inspect # Expensive module. Only import if necessary. path = '/' + func.__name__.replace('__','/').lstrip('/') - spec = inspect.getargspec(func) + spec = getargspec(func) argc = len(spec[0]) - len(spec[3] or []) - path += ('/:%s' * argc) % tuple(spec[0][:argc]) + path += ('/<%s>' * argc) % tuple(spec[0][:argc]) yield path for arg in spec[0][argc:]: - path += '/:%s' % arg + path += '/<%s>' % arg yield path @@ -1979,41 +2685,24 @@ def path_shift(script_name, path_info, shift=1): return new_script_name, new_path_info -def validate(**vkargs): - """ - Validates and manipulates keyword arguments by user defined callables. - Handles ValueError and missing arguments by raising HTTPError(403). - """ - depr('Use route wildcard filters instead.') - def decorator(func): - @functools.wraps(func) - def wrapper(*args, **kargs): - for key, value in vkargs.iteritems(): - if key not in kargs: - abort(403, 'Missing parameter: %s' % key) - try: - kargs[key] = value(kargs[key]) - except ValueError: - abort(403, 'Wrong parameter format for: %s' % key) - return func(*args, **kargs) - return wrapper - return decorator - - def auth_basic(check, realm="private", text="Access denied"): ''' Callback decorator to require HTTP auth (basic). TODO: Add route(check_auth=...) parameter. ''' def decorator(func): - def wrapper(*a, **ka): - user, password = request.auth or (None, None) - if user is None or not check(user, password): - response.headers['WWW-Authenticate'] = 'Basic realm="%s"' % realm - return HTTPError(401, text) - return func(*a, **ka) - return wrapper + def wrapper(*a, **ka): + user, password = request.auth or (None, None) + if user is None or not check(user, password): + err = HTTPError(401, text) + err.add_header('WWW-Authenticate', 'Basic realm="%s"' % realm) + return err + return func(*a, **ka) + return wrapper return decorator +# Shortcuts for common Bottle methods. +# They all refer to the current default application. + def make_default_app_wrapper(name): ''' Return a callable that relays calls to the current default app. ''' @functools.wraps(getattr(Bottle, name)) @@ -2021,12 +2710,18 @@ def wrapper(*a, **ka): return getattr(app(), name)(*a, **ka) return wrapper +route = make_default_app_wrapper('route') +get = make_default_app_wrapper('get') +post = make_default_app_wrapper('post') +put = make_default_app_wrapper('put') +delete = make_default_app_wrapper('delete') +error = make_default_app_wrapper('error') +mount = make_default_app_wrapper('mount') +hook = make_default_app_wrapper('hook') +install = make_default_app_wrapper('install') +uninstall = make_default_app_wrapper('uninstall') +url = make_default_app_wrapper('get_url') -for name in '''route get post put delete error mount - hook install uninstall'''.split(): - globals()[name] = make_default_app_wrapper(name) -url = make_default_app_wrapper('get_url') -del name @@ -2040,8 +2735,8 @@ def wrapper(*a, **ka): class ServerAdapter(object): quiet = False - def __init__(self, host='127.0.0.1', port=8080, **config): - self.options = config + def __init__(self, host='127.0.0.1', port=8080, **options): + self.options = options self.host = host self.port = int(port) @@ -2071,32 +2766,66 @@ def run(self, handler): # pragma: no cover class WSGIRefServer(ServerAdapter): - def run(self, handler): # pragma: no cover - from wsgiref.simple_server import make_server, WSGIRequestHandler - if self.quiet: - class QuietHandler(WSGIRequestHandler): - def log_request(*args, **kw): pass - self.options['handler_class'] = QuietHandler - srv = make_server(self.host, self.port, handler, **self.options) + def run(self, app): # pragma: no cover + from wsgiref.simple_server import WSGIRequestHandler, WSGIServer + from wsgiref.simple_server import make_server + import socket + + class FixedHandler(WSGIRequestHandler): + def address_string(self): # Prevent reverse DNS lookups please. + return self.client_address[0] + def log_request(*args, **kw): + if not self.quiet: + return WSGIRequestHandler.log_request(*args, **kw) + + handler_cls = self.options.get('handler_class', FixedHandler) + server_cls = self.options.get('server_class', WSGIServer) + + if ':' in self.host: # Fix wsgiref for IPv6 addresses. + if getattr(server_cls, 'address_family') == socket.AF_INET: + class server_cls(server_cls): + address_family = socket.AF_INET6 + + srv = make_server(self.host, self.port, app, server_cls, handler_cls) srv.serve_forever() class CherryPyServer(ServerAdapter): def run(self, handler): # pragma: no cover from cherrypy import wsgiserver - server = wsgiserver.CherryPyWSGIServer((self.host, self.port), handler) + self.options['bind_addr'] = (self.host, self.port) + self.options['wsgi_app'] = handler + + certfile = self.options.get('certfile') + if certfile: + del self.options['certfile'] + keyfile = self.options.get('keyfile') + if keyfile: + del self.options['keyfile'] + + server = wsgiserver.CherryPyWSGIServer(**self.options) + if certfile: + server.ssl_certificate = certfile + if keyfile: + server.ssl_private_key = keyfile + try: server.start() finally: server.stop() +class WaitressServer(ServerAdapter): + def run(self, handler): + from waitress import serve + serve(handler, host=self.host, port=self.port) + + class PasteServer(ServerAdapter): def run(self, handler): # pragma: no cover from paste import httpserver - if not self.quiet: - from paste.translogger import TransLogger - handler = TransLogger(handler) + from paste.translogger import TransLogger + handler = TransLogger(handler, setup_console_handler=(not self.quiet)) httpserver.serve(handler, host=self.host, port=str(self.port), **self.options) @@ -2120,8 +2849,8 @@ def run(self, handler): # pragma: no cover evwsgi.start(self.host, port) # fapws3 never releases the GIL. Complain upstream. I tried. No luck. if 'BOTTLE_CHILD' in os.environ and not self.quiet: - print "WARNING: Auto-reloading does not work with Fapws3." - print " (Fapws3 breaks python thread support)" + _stderr("WARNING: Auto-reloading does not work with Fapws3.\n") + _stderr(" (Fapws3 breaks python thread support)\n") evwsgi.set_base_module(base) def app(environ, start_response): environ['wsgi.multiprocess'] = False @@ -2136,7 +2865,7 @@ def run(self, handler): # pragma: no cover import tornado.wsgi, tornado.httpserver, tornado.ioloop container = tornado.wsgi.WSGIContainer(handler) server = tornado.httpserver.HTTPServer(container) - server.listen(port=self.port) + server.listen(port=self.port,address=self.host) tornado.ioloop.IOLoop.instance().start() @@ -2178,16 +2907,32 @@ def run(self, handler): class GeventServer(ServerAdapter): """ Untested. Options: - * `monkey` (default: True) fixes the stdlib to use greenthreads. * `fast` (default: False) uses libevent's http server, but has some issues: No streaming, no pipelining, no SSL. + * See gevent.wsgi.WSGIServer() documentation for more options. """ def run(self, handler): - from gevent import wsgi as wsgi_fast, pywsgi, monkey, local - if self.options.get('monkey', True): - if not threading.local is local.local: monkey.patch_all() - wsgi = wsgi_fast if self.options.get('fast') else pywsgi - wsgi.WSGIServer((self.host, self.port), handler).serve_forever() + from gevent import pywsgi, local + if not isinstance(threading.local(), local.local): + msg = "Bottle requires gevent.monkey.patch_all() (before import)" + raise RuntimeError(msg) + if self.options.pop('fast', None): + depr('The "fast" option has been deprecated and removed by Gevent.') + if self.quiet: + self.options['log'] = None + address = (self.host, self.port) + server = pywsgi.WSGIServer(address, handler, **self.options) + if 'BOTTLE_CHILD' in os.environ: + import signal + signal.signal(signal.SIGINT, lambda s, f: server.stop()) + server.serve_forever() + + +class GeventSocketIOServer(ServerAdapter): + def run(self,handler): + from socketio import server + address = (self.host, self.port) + server.SocketIOServer(address, handler, **self.options).serve_forever() class GunicornServer(ServerAdapter): @@ -2212,7 +2957,12 @@ class EventletServer(ServerAdapter): """ Untested """ def run(self, handler): from eventlet import wsgi, listen - wsgi.server(listen((self.host, self.port)), handler) + try: + wsgi.server(listen((self.host, self.port)), handler, + log_output=(not self.quiet)) + except TypeError: + # Fallback, if we have old version of eventlet + wsgi.server(listen((self.host, self.port)), handler) class RocketServer(ServerAdapter): @@ -2232,7 +2982,7 @@ def run(self, handler): class AutoServer(ServerAdapter): """ Untested. """ - adapters = [PasteServer, CherryPyServer, TwistedServer, WSGIRefServer] + adapters = [WaitressServer, PasteServer, TwistedServer, CherryPyServer, WSGIRefServer] def run(self, handler): for sa in self.adapters: try: @@ -2244,6 +2994,7 @@ def run(self, handler): 'cgi': CGIServer, 'flup': FlupFCGIServer, 'wsgiref': WSGIRefServer, + 'waitress': WaitressServer, 'cherrypy': CherryPyServer, 'paste': PasteServer, 'fapws3': FapwsServer, @@ -2255,6 +3006,7 @@ def run(self, handler): 'gunicorn': GunicornServer, 'eventlet': EventletServer, 'gevent': GeventServer, + 'geventSocketIO':GeventSocketIOServer, 'rocket': RocketServer, 'bjoern' : BjoernServer, 'auto': AutoServer, @@ -2303,8 +3055,10 @@ def load_app(target): default_app.remove(tmp) # Remove the temporary added default application NORUN = nr_old +_debug = debug def run(app=None, server='wsgiref', host='127.0.0.1', port=8080, - interval=1, reloader=False, quiet=False, plugins=None, **kargs): + interval=1, reloader=False, quiet=False, plugins=None, + debug=None, **kargs): """ Start a server instance. This method blocks until the server terminates. :param app: WSGI application or target string supported by @@ -2324,6 +3078,7 @@ def run(app=None, server='wsgiref', host='127.0.0.1', port=8080, if NORUN: return if reloader and not os.environ.get('BOTTLE_CHILD'): try: + lockfile = None fd, lockfile = tempfile.mkstemp(prefix='bottle.', suffix='.lock') os.close(fd) # We only need this file to exist. We never write to it while os.path.exists(lockfile): @@ -2345,9 +3100,8 @@ def run(app=None, server='wsgiref', host='127.0.0.1', port=8080, os.unlink(lockfile) return - stderr = sys.stderr.write - try: + if debug is not None: _debug(debug) app = app or default_app() if isinstance(app, basestring): app = load_app(app) @@ -2368,9 +3122,9 @@ def run(app=None, server='wsgiref', host='127.0.0.1', port=8080, server.quiet = server.quiet or quiet if not server.quiet: - stderr("Bottle server starting up (using %s)...\n" % repr(server)) - stderr("Listening on http://%s:%d/\n" % (server.host, server.port)) - stderr("Hit Ctrl-C to quit.\n\n") + _stderr("Bottle v%s server starting up (using %s)...\n" % (__version__, repr(server))) + _stderr("Listening on http://%s:%d/\n" % (server.host, server.port)) + _stderr("Hit Ctrl-C to quit.\n\n") if reloader: lockfile = os.environ.get('BOTTLE_LOCKFILE') @@ -2383,12 +3137,15 @@ def run(app=None, server='wsgiref', host='127.0.0.1', port=8080, server.run(app) except KeyboardInterrupt: pass - except (SyntaxError, ImportError): + except (SystemExit, MemoryError): + raise + except: if not reloader: raise - if not getattr(server, 'quiet', False): print_exc() + if not getattr(server, 'quiet', quiet): + print_exc() + time.sleep(interval) sys.exit(3) - finally: - if not getattr(server, 'quiet', False): stderr('Shutdown...\n') + class FileCheckerThread(threading.Thread): @@ -2406,8 +3163,8 @@ def run(self): mtime = lambda path: os.stat(path).st_mtime files = dict() - for module in sys.modules.values(): - path = getattr(module, '__file__', '') + for module in list(sys.modules.values()): + path = getattr(module, '__file__', '') or '' if path[-4:] in ('.pyo', '.pyc'): path = path[:-1] if path and exists(path): files[path] = mtime(path) @@ -2416,20 +3173,20 @@ def run(self): or mtime(self.lockfile) < time.time() - self.interval - 5: self.status = 'error' thread.interrupt_main() - for path, lmtime in files.iteritems(): + for path, lmtime in list(files.items()): if not exists(path) or mtime(path) > lmtime: self.status = 'reload' thread.interrupt_main() break time.sleep(self.interval) - + def __enter__(self): self.start() - + def __exit__(self, exc_type, exc_val, exc_tb): if not self.status: self.status = 'exit' # silent exit self.join() - return issubclass(exc_type, KeyboardInterrupt) + return exc_type is not None and issubclass(exc_type, KeyboardInterrupt) @@ -2465,7 +3222,7 @@ def __init__(self, source=None, name=None, lookup=[], encoding='utf8', **setting self.name = name self.source = source.read() if hasattr(source, 'read') else source self.filename = source.filename if hasattr(source, 'filename') else None - self.lookup = map(os.path.abspath, lookup) + self.lookup = [os.path.abspath(x) for x in lookup] self.encoding = encoding self.settings = self.settings.copy() # Copy from class variable self.settings.update(settings) # Apply @@ -2481,11 +3238,19 @@ def __init__(self, source=None, name=None, lookup=[], encoding='utf8', **setting def search(cls, name, lookup=[]): """ Search name in all directories specified in lookup. First without, then with common extensions. Return first hit. """ - if os.path.isfile(name): return name + if not lookup: + depr('The template lookup path list should not be empty.') #0.12 + lookup = ['.'] + + if os.path.isabs(name) and os.path.isfile(name): + depr('Absolute template path names are deprecated.') #0.12 + return os.path.abspath(name) + for spath in lookup: - fname = os.path.join(spath, name) - if os.path.isfile(fname): - return fname + spath = os.path.abspath(spath) + os.sep + fname = os.path.abspath(os.path.join(spath, name)) + if not fname.startswith(spath): continue + if os.path.isfile(fname): return fname for ext in cls.extensions: if os.path.isfile('%s.%s' % (fname, ext)): return '%s.%s' % (fname, ext) @@ -2510,8 +3275,8 @@ def render(self, *args, **kwargs): """ Render the template with the specified local variables and return a single byte or unicode string. If it is a byte string, the encoding must match self.encoding. This method must be thread-safe! - Local variables may be provided in dictionaries (*args) - or directly, as keywords (**kwargs). + Local variables may be provided in dictionaries (args) + or directly, as keywords (kwargs). """ raise NotImplementedError @@ -2556,7 +3321,7 @@ def render(self, *args, **kwargs): class Jinja2Template(BaseTemplate): - def prepare(self, filters=None, tests=None, **kwargs): + def prepare(self, filters=None, tests=None, globals={}, **kwargs): from jinja2 import Environment, FunctionLoader if 'prefix' in kwargs: # TODO: to be removed after a while raise RuntimeError('The keyword argument `prefix` has been removed. ' @@ -2564,6 +3329,7 @@ def prepare(self, filters=None, tests=None, **kwargs): self.env = Environment(loader=FunctionLoader(self.loader), **kwargs) if filters: self.env.filters.update(filters) if tests: self.env.tests.update(tests) + if globals: self.env.globals.update(globals) if self.source: self.tpl = self.env.from_string(self.source) else: @@ -2577,189 +3343,267 @@ def render(self, *args, **kwargs): def loader(self, name): fname = self.search(name, self.lookup) - if fname: - with open(fname, "rb") as f: - return f.read().decode(self.encoding) - - -class SimpleTALTemplate(BaseTemplate): - ''' Untested! ''' - def prepare(self, **options): - from simpletal import simpleTAL - # TODO: add option to load METAL files during render - if self.source: - self.tpl = simpleTAL.compileHTMLTemplate(self.source) - else: - with open(self.filename, 'rb') as fp: - self.tpl = simpleTAL.compileHTMLTemplate(tonat(fp.read())) - - def render(self, *args, **kwargs): - from simpletal import simpleTALES - for dictarg in args: kwargs.update(dictarg) - # TODO: maybe reuse a context instead of always creating one - context = simpleTALES.Context() - for k,v in self.defaults.items(): - context.addGlobal(k, v) - for k,v in kwargs.items(): - context.addGlobal(k, v) - output = StringIO() - self.tpl.expand(context, output) - return output.getvalue() + if not fname: return + with open(fname, "rb") as f: + return f.read().decode(self.encoding) class SimpleTemplate(BaseTemplate): - blocks = ('if', 'elif', 'else', 'try', 'except', 'finally', 'for', 'while', - 'with', 'def', 'class') - dedent_blocks = ('elif', 'else', 'except', 'finally') - - @lazy_attribute - def re_pytokens(cls): - ''' This matches comments and all kinds of quoted strings but does - NOT match comments (#...) within quoted strings. (trust me) ''' - return re.compile(r''' - (''(?!')|""(?!")|'{6}|"{6} # Empty strings (all 4 types) - |'(?:[^\\']|\\.)+?' # Single quotes (') - |"(?:[^\\"]|\\.)+?" # Double quotes (") - |'{3}(?:[^\\]|\\.|\n)+?'{3} # Triple-quoted strings (') - |"{3}(?:[^\\]|\\.|\n)+?"{3} # Triple-quoted strings (") - |\#.* # Comments - )''', re.VERBOSE) - - def prepare(self, escape_func=html_escape, noescape=False, **kwargs): + + def prepare(self, escape_func=html_escape, noescape=False, syntax=None, **ka): self.cache = {} enc = self.encoding self._str = lambda x: touni(x, enc) self._escape = lambda x: escape_func(touni(x, enc)) + self.syntax = syntax if noescape: self._str, self._escape = self._escape, self._str - @classmethod - def split_comment(cls, code): - """ Removes comments (#...) from python code. """ - if '#' not in code: return code - #: Remove comments only (leave quoted strings as they are) - subf = lambda m: '' if m.group(0)[0]=='#' else m.group(0) - return re.sub(cls.re_pytokens, subf, code) - @cached_property def co(self): return compile(self.code, self.filename or '', 'exec') @cached_property def code(self): - stack = [] # Current Code indentation - lineno = 0 # Current line of code - ptrbuffer = [] # Buffer for printable strings and token tuple instances - codebuffer = [] # Buffer for generated python code - multiline = dedent = oneline = False - template = self.source or open(self.filename, 'rb').read() - - def yield_tokens(line): - for i, part in enumerate(re.split(r'\{\{(.*?)\}\}', line)): - if i % 2: - if part.startswith('!'): yield 'RAW', part[1:] - else: yield 'CMD', part - else: yield 'TXT', part - - def flush(): # Flush the ptrbuffer - if not ptrbuffer: return - cline = '' - for line in ptrbuffer: - for token, value in line: - if token == 'TXT': cline += repr(value) - elif token == 'RAW': cline += '_str(%s)' % value - elif token == 'CMD': cline += '_escape(%s)' % value - cline += ', ' - cline = cline[:-2] + '\\\n' - cline = cline[:-2] - if cline[:-1].endswith('\\\\\\\\\\n'): - cline = cline[:-7] + cline[-1] # 'nobr\\\\\n' --> 'nobr' - cline = '_printlist([' + cline + '])' - del ptrbuffer[:] # Do this before calling code() again - code(cline) - - def code(stmt): - for line in stmt.splitlines(): - codebuffer.append(' ' * len(stack) + line.strip()) - - for line in template.splitlines(True): - lineno += 1 - line = line if isinstance(line, unicode)\ - else unicode(line, encoding=self.encoding) - if lineno <= 2: - m = re.search(r"%.*coding[:=]\s*([-\w\.]+)", line) - if m: self.encoding = m.group(1) - if m: line = line.replace('coding','coding (removed)') - if line.strip()[:2].count('%') == 1: - line = line.split('%',1)[1].lstrip() # Full line following the % - cline = self.split_comment(line).strip() - cmd = re.split(r'[^a-zA-Z0-9_]', cline)[0] - flush() # You are actually reading this? Good luck, it's a mess :) - if cmd in self.blocks or multiline: - cmd = multiline or cmd - dedent = cmd in self.dedent_blocks # "else:" - if dedent and not oneline and not multiline: - cmd = stack.pop() - code(line) - oneline = not cline.endswith(':') # "if 1: pass" - multiline = cmd if cline.endswith('\\') else False - if not oneline and not multiline: - stack.append(cmd) - elif cmd == 'end' and stack: - code('#end(%s) %s' % (stack.pop(), line.strip()[3:])) - elif cmd == 'include': - p = cline.split(None, 2)[1:] - if len(p) == 2: - code("_=_include(%s, _stdout, %s)" % (repr(p[0]), p[1])) - elif p: - code("_=_include(%s, _stdout)" % repr(p[0])) - else: # Empty %include -> reverse of %rebase - code("_printlist(_base)") - elif cmd == 'rebase': - p = cline.split(None, 2)[1:] - if len(p) == 2: - code("globals()['_rebase']=(%s, dict(%s))" % (repr(p[0]), p[1])) - elif p: - code("globals()['_rebase']=(%s, {})" % repr(p[0])) - else: - code(line) - else: # Line starting with text (not '%') or '%%' (escaped) - if line.strip().startswith('%%'): - line = line.replace('%%', '%', 1) - ptrbuffer.append(yield_tokens(line)) - flush() - return '\n'.join(codebuffer) + '\n' - - def subtemplate(self, _name, _stdout, *args, **kwargs): - for dictarg in args: kwargs.update(dictarg) + source = self.source + if not source: + with open(self.filename, 'rb') as f: + source = f.read() + try: + source, encoding = touni(source), 'utf8' + except UnicodeError: + depr('Template encodings other than utf8 are no longer supported.') #0.11 + source, encoding = touni(source, 'latin1'), 'latin1' + parser = StplParser(source, encoding=encoding, syntax=self.syntax) + code = parser.translate() + self.encoding = parser.encoding + return code + + def _rebase(self, _env, _name=None, **kwargs): + if _name is None: + depr('Rebase function called without arguments.' + ' You were probably looking for {{base}}?', True) #0.12 + _env['_rebase'] = (_name, kwargs) + + def _include(self, _env, _name=None, **kwargs): + if _name is None: + depr('Rebase function called without arguments.' + ' You were probably looking for {{base}}?', True) #0.12 + env = _env.copy() + env.update(kwargs) if _name not in self.cache: self.cache[_name] = self.__class__(name=_name, lookup=self.lookup) - return self.cache[_name].execute(_stdout, kwargs) + return self.cache[_name].execute(env['_stdout'], env) - def execute(self, _stdout, *args, **kwargs): - for dictarg in args: kwargs.update(dictarg) + def execute(self, _stdout, kwargs): env = self.defaults.copy() - env.update({'_stdout': _stdout, '_printlist': _stdout.extend, - '_include': self.subtemplate, '_str': self._str, - '_escape': self._escape, 'get': env.get, - 'setdefault': env.setdefault, 'defined': env.__contains__}) env.update(kwargs) + env.update({'_stdout': _stdout, '_printlist': _stdout.extend, + 'include': functools.partial(self._include, env), + 'rebase': functools.partial(self._rebase, env), '_rebase': None, + '_str': self._str, '_escape': self._escape, 'get': env.get, + 'setdefault': env.setdefault, 'defined': env.__contains__ }) eval(self.co, env) - if '_rebase' in env: - subtpl, rargs = env['_rebase'] - rargs['_base'] = _stdout[:] #copy stdout + if env.get('_rebase'): + subtpl, rargs = env.pop('_rebase') + rargs['base'] = ''.join(_stdout) #copy stdout del _stdout[:] # clear stdout - return self.subtemplate(subtpl,_stdout,rargs) + return self._include(env, subtpl, **rargs) return env def render(self, *args, **kwargs): """ Render the template using keyword arguments as local variables. """ - for dictarg in args: kwargs.update(dictarg) - stdout = [] - self.execute(stdout, kwargs) + env = {}; stdout = [] + for dictarg in args: env.update(dictarg) + env.update(kwargs) + self.execute(stdout, env) return ''.join(stdout) +class StplSyntaxError(TemplateError): pass + + +class StplParser(object): + ''' Parser for stpl templates. ''' + _re_cache = {} #: Cache for compiled re patterns + # This huge pile of voodoo magic splits python code into 8 different tokens. + # 1: All kinds of python strings (trust me, it works) + _re_tok = '([urbURB]?(?:\'\'(?!\')|""(?!")|\'{6}|"{6}' \ + '|\'(?:[^\\\\\']|\\\\.)+?\'|"(?:[^\\\\"]|\\\\.)+?"' \ + '|\'{3}(?:[^\\\\]|\\\\.|\\n)+?\'{3}' \ + '|"{3}(?:[^\\\\]|\\\\.|\\n)+?"{3}))' + _re_inl = _re_tok.replace('|\\n','') # We re-use this string pattern later + # 2: Comments (until end of line, but not the newline itself) + _re_tok += '|(#.*)' + # 3,4: Open and close grouping tokens + _re_tok += '|([\\[\\{\\(])' + _re_tok += '|([\\]\\}\\)])' + # 5,6: Keywords that start or continue a python block (only start of line) + _re_tok += '|^([ \\t]*(?:if|for|while|with|try|def|class)\\b)' \ + '|^([ \\t]*(?:elif|else|except|finally)\\b)' + # 7: Our special 'end' keyword (but only if it stands alone) + _re_tok += '|((?:^|;)[ \\t]*end[ \\t]*(?=(?:%(block_close)s[ \\t]*)?\\r?$|;|#))' + # 8: A customizable end-of-code-block template token (only end of line) + _re_tok += '|(%(block_close)s[ \\t]*(?=\\r?$))' + # 9: And finally, a single newline. The 10th token is 'everything else' + _re_tok += '|(\\r?\\n)' + + # Match the start tokens of code areas in a template + _re_split = '(?m)^[ \t]*(\\\\?)((%(line_start)s)|(%(block_start)s))(%%?)' + # Match inline statements (may contain python strings) + _re_inl = '(?m)%%(inline_start)s((?:%s|[^\'"\n]*?)+)%%(inline_end)s' % _re_inl + _re_tok = '(?m)' + _re_tok + + default_syntax = '<% %> % {{ }}' + + def __init__(self, source, syntax=None, encoding='utf8'): + self.source, self.encoding = touni(source, encoding), encoding + self.set_syntax(syntax or self.default_syntax) + self.code_buffer, self.text_buffer = [], [] + self.lineno, self.offset = 1, 0 + self.indent, self.indent_mod = 0, 0 + self.paren_depth = 0 + + def get_syntax(self): + ''' Tokens as a space separated string (default: <% %> % {{ }}) ''' + return self._syntax + + def set_syntax(self, syntax): + self._syntax = syntax + self._tokens = syntax.split() + if not syntax in self._re_cache: + names = 'block_start block_close line_start inline_start inline_end' + etokens = map(re.escape, self._tokens) + pattern_vars = dict(zip(names.split(), etokens)) + patterns = (self._re_split, self._re_tok, self._re_inl) + patterns = [re.compile(p%pattern_vars) for p in patterns] + self._re_cache[syntax] = patterns + self.re_split, self.re_tok, self.re_inl = self._re_cache[syntax] + + syntax = property(get_syntax, set_syntax) + + def translate(self): + if self.offset: raise RuntimeError('Parser is a one time instance.') + while True: + m = self.re_split.search(self.source[self.offset:]) + if m: + text = self.source[self.offset:self.offset+m.start()] + self.text_buffer.append(text) + self.offset += m.end() + if m.group(1): # New escape syntax + line, sep, _ = self.source[self.offset:].partition('\n') + self.text_buffer.append(m.group(2)+m.group(5)+line+sep) + self.offset += len(line+sep)+1 + continue + elif m.group(5): # Old escape syntax + depr('Escape code lines with a backslash.') #0.12 + line, sep, _ = self.source[self.offset:].partition('\n') + self.text_buffer.append(m.group(2)+line+sep) + self.offset += len(line+sep)+1 + continue + self.flush_text() + self.read_code(multiline=bool(m.group(4))) + else: break + self.text_buffer.append(self.source[self.offset:]) + self.flush_text() + return ''.join(self.code_buffer) + + def read_code(self, multiline): + code_line, comment = '', '' + while True: + m = self.re_tok.search(self.source[self.offset:]) + if not m: + code_line += self.source[self.offset:] + self.offset = len(self.source) + self.write_code(code_line.strip(), comment) + return + code_line += self.source[self.offset:self.offset+m.start()] + self.offset += m.end() + _str, _com, _po, _pc, _blk1, _blk2, _end, _cend, _nl = m.groups() + if (code_line or self.paren_depth > 0) and (_blk1 or _blk2): # a if b else c + code_line += _blk1 or _blk2 + continue + if _str: # Python string + code_line += _str + elif _com: # Python comment (up to EOL) + comment = _com + if multiline and _com.strip().endswith(self._tokens[1]): + multiline = False # Allow end-of-block in comments + elif _po: # open parenthesis + self.paren_depth += 1 + code_line += _po + elif _pc: # close parenthesis + if self.paren_depth > 0: + # we could check for matching parentheses here, but it's + # easier to leave that to python - just check counts + self.paren_depth -= 1 + code_line += _pc + elif _blk1: # Start-block keyword (if/for/while/def/try/...) + code_line, self.indent_mod = _blk1, -1 + self.indent += 1 + elif _blk2: # Continue-block keyword (else/elif/except/...) + code_line, self.indent_mod = _blk2, -1 + elif _end: # The non-standard 'end'-keyword (ends a block) + self.indent -= 1 + elif _cend: # The end-code-block template token (usually '%>') + if multiline: multiline = False + else: code_line += _cend + else: # \n + self.write_code(code_line.strip(), comment) + self.lineno += 1 + code_line, comment, self.indent_mod = '', '', 0 + if not multiline: + break + + def flush_text(self): + text = ''.join(self.text_buffer) + del self.text_buffer[:] + if not text: return + parts, pos, nl = [], 0, '\\\n'+' '*self.indent + for m in self.re_inl.finditer(text): + prefix, pos = text[pos:m.start()], m.end() + if prefix: + parts.append(nl.join(map(repr, prefix.splitlines(True)))) + if prefix.endswith('\n'): parts[-1] += nl + parts.append(self.process_inline(m.group(1).strip())) + if pos < len(text): + prefix = text[pos:] + lines = prefix.splitlines(True) + if lines[-1].endswith('\\\\\n'): lines[-1] = lines[-1][:-3] + elif lines[-1].endswith('\\\\\r\n'): lines[-1] = lines[-1][:-4] + parts.append(nl.join(map(repr, lines))) + code = '_printlist((%s,))' % ', '.join(parts) + self.lineno += code.count('\n')+1 + self.write_code(code) + + def process_inline(self, chunk): + if chunk[0] == '!': return '_str(%s)' % chunk[1:] + return '_escape(%s)' % chunk + + def write_code(self, line, comment=''): + line, comment = self.fix_backward_compatibility(line, comment) + code = ' ' * (self.indent+self.indent_mod) + code += line.lstrip() + comment + '\n' + self.code_buffer.append(code) + + def fix_backward_compatibility(self, line, comment): + parts = line.strip().split(None, 2) + if parts and parts[0] in ('include', 'rebase'): + depr('The include and rebase keywords are functions now.') #0.12 + if len(parts) == 1: return "_printlist([base])", comment + elif len(parts) == 2: return "_=%s(%r)" % tuple(parts), comment + else: return "_=%s(%r, %s)" % tuple(parts), comment + if self.lineno <= 2 and not line.strip() and 'coding' in comment: + m = re.match(r"#.*coding[:=]\s*([-\w.]+)", comment) + if m: + depr('PEP263 encoding strings in templates are deprecated.') #0.12 + enc = m.group(1) + self.source = self.source.encode(self.encoding).decode(enc) + self.encoding = enc + return line, comment.replace('coding','coding*') + return line, comment + + def template(*args, **kwargs): ''' Get a rendered template as a string iterator. @@ -2768,26 +3612,26 @@ def template(*args, **kwargs): or directly (as keyword arguments). ''' tpl = args[0] if args else None - template_adapter = kwargs.pop('template_adapter', SimpleTemplate) - if tpl not in TEMPLATES or DEBUG: + adapter = kwargs.pop('template_adapter', SimpleTemplate) + lookup = kwargs.pop('template_lookup', TEMPLATE_PATH) + tplid = (id(lookup), tpl) + if tplid not in TEMPLATES or DEBUG: settings = kwargs.pop('template_settings', {}) - lookup = kwargs.pop('template_lookup', TEMPLATE_PATH) - if isinstance(tpl, template_adapter): - TEMPLATES[tpl] = tpl - if settings: TEMPLATES[tpl].prepare(**settings) + if isinstance(tpl, adapter): + TEMPLATES[tplid] = tpl + if settings: TEMPLATES[tplid].prepare(**settings) elif "\n" in tpl or "{" in tpl or "%" in tpl or '$' in tpl: - TEMPLATES[tpl] = template_adapter(source=tpl, lookup=lookup, **settings) + TEMPLATES[tplid] = adapter(source=tpl, lookup=lookup, **settings) else: - TEMPLATES[tpl] = template_adapter(name=tpl, lookup=lookup, **settings) - if not TEMPLATES[tpl]: + TEMPLATES[tplid] = adapter(name=tpl, lookup=lookup, **settings) + if not TEMPLATES[tplid]: abort(500, 'Template (%s) not found' % tpl) for dictarg in args[1:]: kwargs.update(dictarg) - return TEMPLATES[tpl].render(kwargs) + return TEMPLATES[tplid].render(kwargs) mako_template = functools.partial(template, template_adapter=MakoTemplate) cheetah_template = functools.partial(template, template_adapter=CheetahTemplate) jinja2_template = functools.partial(template, template_adapter=Jinja2Template) -simpletal_template = functools.partial(template, template_adapter=SimpleTALTemplate) def view(tpl_name, **defaults): @@ -2808,6 +3652,8 @@ def wrapper(*args, **kwargs): tplvars = defaults.copy() tplvars.update(result) return template(tpl_name, **tplvars) + elif result is None: + return template(tpl_name, defaults) return result return wrapper return decorator @@ -2815,7 +3661,6 @@ def wrapper(*args, **kwargs): mako_view = functools.partial(view, template_adapter=MakoTemplate) cheetah_view = functools.partial(view, template_adapter=CheetahTemplate) jinja2_view = functools.partial(view, template_adapter=Jinja2Template) -simpletal_view = functools.partial(view, template_adapter=SimpleTALTemplate) @@ -2835,21 +3680,21 @@ def wrapper(*args, **kwargs): #: A dict to map HTTP status codes (e.g. 404) to phrases (e.g. 'Not Found') HTTP_CODES = httplib.responses HTTP_CODES[418] = "I'm a teapot" # RFC 2324 +HTTP_CODES[422] = "Unprocessable Entity" # RFC 4918 HTTP_CODES[428] = "Precondition Required" HTTP_CODES[429] = "Too Many Requests" HTTP_CODES[431] = "Request Header Fields Too Large" HTTP_CODES[511] = "Network Authentication Required" -_HTTP_STATUS_LINES = dict((k, '%d %s'%(k,v)) for (k,v) in HTTP_CODES.iteritems()) +_HTTP_STATUS_LINES = dict((k, '%d %s'%(k,v)) for (k,v) in HTTP_CODES.items()) #: The default template used for error pages. Override with @error() ERROR_PAGE_TEMPLATE = """ -%try: - %from bottle import DEBUG, HTTP_CODES, request, touni - %status_name = HTTP_CODES.get(e.status, 'Unknown').title() +%%try: + %%from %s import DEBUG, HTTP_CODES, request, touni - Error {{e.status}}: {{status_name}} + Error: {{e.status}} -

Error {{e.status}}: {{status_name}}

+

Error: {{e.status}}

Sorry, the requested URL {{repr(request.url)}} caused an error:

-
{{e.output}}
- %if DEBUG and e.exception: +
{{e.body}}
+ %%if DEBUG and e.exception:

Exception:

{{repr(e.exception)}}
- %end - %if DEBUG and e.traceback: + %%end + %%if DEBUG and e.traceback:

Traceback:

{{e.traceback}}
- %end + %%end -%except ImportError: +%%except ImportError: ImportError: Could not generate the error page. Please add bottle to the import path. -%end -""" +%%end +""" % __name__ -#: A thread-safe instance of :class:`Request` representing the `current` request. -request = Request() +#: A thread-safe instance of :class:`LocalRequest`. If accessed from within a +#: request callback, this instance always refers to the *current* request +#: (even on a multithreaded server). +request = LocalRequest() -#: A thread-safe instance of :class:`Response` used to build the HTTP response. -response = Response() +#: A thread-safe instance of :class:`LocalResponse`. It is used to change the +#: HTTP response for the *current* request. +response = LocalResponse() #: A thread-safe namespace. Not used by Bottle. local = threading.local() @@ -2894,29 +3742,30 @@ def wrapper(*args, **kwargs): #: A virtual package that redirects import statements. #: Example: ``import bottle.ext.sqlite`` actually imports `bottle_sqlite`. -ext = _ImportRedirect(__name__+'.ext', 'bottle_%s').module +ext = _ImportRedirect('bottle.ext' if __name__ == '__main__' else __name__+".ext", 'bottle_%s').module if __name__ == '__main__': opt, args, parser = _cmd_options, _cmd_args, _cmd_parser if opt.version: - print 'Bottle', __version__; sys.exit(0) + _stdout('Bottle %s\n'%__version__) + sys.exit(0) if not args: parser.print_help() - print '\nError: No application specified.\n' + _stderr('\nError: No application specified.\n') sys.exit(1) - try: - sys.path.insert(0, '.') - sys.modules.setdefault('bottle', sys.modules['__main__']) - except (AttributeError, ImportError), e: - parser.error(e.args[0]) + sys.path.insert(0, '.') + sys.modules.setdefault('bottle', sys.modules['__main__']) + + host, port = (opt.bind or 'localhost'), 8080 + if ':' in host and host.rfind(']') < host.rfind(':'): + host, port = host.rsplit(':', 1) + host = host.strip('[]') + + run(args[0], host=host, port=int(port), server=opt.server, + reloader=opt.reload, plugins=opt.plugin, debug=opt.debug) + - if opt.bind and ':' in opt.bind: - host, port = opt.bind.rsplit(':', 1) - else: - host, port = (opt.bind or 'localhost'), 8080 - debug(opt.debug) - run(args[0], host=host, port=port, server=opt.server, reloader=opt.reload, plugins=opt.plugin) # THE END diff --git a/module/lib/wsgiserver/LICENSE.txt b/module/lib/wsgiserver/LICENSE.txt index a15165ee26..23c7d09635 100644 --- a/module/lib/wsgiserver/LICENSE.txt +++ b/module/lib/wsgiserver/LICENSE.txt @@ -1,22 +1,27 @@ -Copyright (c) 2004-2007, CherryPy Team (team@cherrypy.org) -All rights reserved. +**Copyright © 2004-2017, CherryPy Team (team@cherrypy.org)** -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: +**All rights reserved.** - * Redistributions of source code must retain the above copyright notice, - this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - * Neither the name of the CherryPy Team nor the names of its contributors - may be used to endorse or promote products derived from this software - without specific prior written permission. +* * * -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of CherryPy nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER diff --git a/module/lib/wsgiserver/__init__.py b/module/lib/wsgiserver/__init__.py index c380e18b05..e6e48a6c1f 100644 --- a/module/lib/wsgiserver/__init__.py +++ b/module/lib/wsgiserver/__init__.py @@ -1,1794 +1,13 @@ -"""A high-speed, production ready, thread pooled, generic WSGI server. - -Simplest example on how to use this module directly -(without using CherryPy's application machinery): - - from cherrypy import wsgiserver - - def my_crazy_app(environ, start_response): - status = '200 OK' - response_headers = [('Content-type','text/plain')] - start_response(status, response_headers) - return ['Hello world!\n'] - - server = wsgiserver.CherryPyWSGIServer( - ('0.0.0.0', 8070), my_crazy_app, - server_name='www.cherrypy.example') - -The CherryPy WSGI server can serve as many WSGI applications -as you want in one instance by using a WSGIPathInfoDispatcher: - - d = WSGIPathInfoDispatcher({'/': my_crazy_app, '/blog': my_blog_app}) - server = wsgiserver.CherryPyWSGIServer(('0.0.0.0', 80), d) - -Want SSL support? Just set these attributes: - - server.ssl_certificate = - server.ssl_private_key = - - if __name__ == '__main__': - try: - server.start() - except KeyboardInterrupt: - server.stop() - -This won't call the CherryPy engine (application side) at all, only the -WSGI server, which is independant from the rest of CherryPy. Don't -let the name "CherryPyWSGIServer" throw you; the name merely reflects -its origin, not its coupling. - -For those of you wanting to understand internals of this module, here's the -basic call flow. The server's listening thread runs a very tight loop, -sticking incoming connections onto a Queue: - - server = CherryPyWSGIServer(...) - server.start() - while True: - tick() - # This blocks until a request comes in: - child = socket.accept() - conn = HTTPConnection(child, ...) - server.requests.put(conn) - -Worker threads are kept in a pool and poll the Queue, popping off and then -handling each connection in turn. Each connection can consist of an arbitrary -number of requests and their responses, so we run a nested loop: - - while True: - conn = server.requests.get() - conn.communicate() - -> while True: - req = HTTPRequest(...) - req.parse_request() - -> # Read the Request-Line, e.g. "GET /page HTTP/1.1" - req.rfile.readline() - req.read_headers() - req.respond() - -> response = wsgi_app(...) - try: - for chunk in response: - if chunk: - req.write(chunk) - finally: - if hasattr(response, "close"): - response.close() - if req.close_connection: - return -""" - - -import base64 -import os -import Queue -import re -quoted_slash = re.compile("(?i)%2F") -import rfc822 -import socket -try: - import cStringIO as StringIO -except ImportError: - import StringIO - -_fileobject_uses_str_type = isinstance(socket._fileobject(None)._rbuf, basestring) - -import sys -import threading -import time -import traceback -from urllib import unquote -from urlparse import urlparse -import warnings +"""High-performance, pure-Python HTTP server used by CherryPy.""" try: - from OpenSSL import SSL - from OpenSSL import crypto + import pkg_resources except ImportError: - SSL = None - -import errno - -def plat_specific_errors(*errnames): - """Return error numbers for all errors in errnames on this platform. - - The 'errno' module contains different global constants depending on - the specific platform (OS). This function will return the list of - numeric values for a given list of potential names. - """ - errno_names = dir(errno) - nums = [getattr(errno, k) for k in errnames if k in errno_names] - # de-dupe the list - return dict.fromkeys(nums).keys() - -socket_error_eintr = plat_specific_errors("EINTR", "WSAEINTR") - -socket_errors_to_ignore = plat_specific_errors( - "EPIPE", - "EBADF", "WSAEBADF", - "ENOTSOCK", "WSAENOTSOCK", - "ETIMEDOUT", "WSAETIMEDOUT", - "ECONNREFUSED", "WSAECONNREFUSED", - "ECONNRESET", "WSAECONNRESET", - "ECONNABORTED", "WSAECONNABORTED", - "ENETRESET", "WSAENETRESET", - "EHOSTDOWN", "EHOSTUNREACH", - ) -socket_errors_to_ignore.append("timed out") - -socket_errors_nonblocking = plat_specific_errors( - 'EAGAIN', 'EWOULDBLOCK', 'WSAEWOULDBLOCK') - -comma_separated_headers = ['ACCEPT', 'ACCEPT-CHARSET', 'ACCEPT-ENCODING', - 'ACCEPT-LANGUAGE', 'ACCEPT-RANGES', 'ALLOW', 'CACHE-CONTROL', - 'CONNECTION', 'CONTENT-ENCODING', 'CONTENT-LANGUAGE', 'EXPECT', - 'IF-MATCH', 'IF-NONE-MATCH', 'PRAGMA', 'PROXY-AUTHENTICATE', 'TE', - 'TRAILER', 'TRANSFER-ENCODING', 'UPGRADE', 'VARY', 'VIA', 'WARNING', - 'WWW-AUTHENTICATE'] - - -class WSGIPathInfoDispatcher(object): - """A WSGI dispatcher for dispatch based on the PATH_INFO. - - apps: a dict or list of (path_prefix, app) pairs. - """ - - def __init__(self, apps): - try: - apps = apps.items() - except AttributeError: - pass - - # Sort the apps by len(path), descending - apps.sort() - apps.reverse() - - # The path_prefix strings must start, but not end, with a slash. - # Use "" instead of "/". - self.apps = [(p.rstrip("/"), a) for p, a in apps] - - def __call__(self, environ, start_response): - path = environ["PATH_INFO"] or "/" - for p, app in self.apps: - # The apps list should be sorted by length, descending. - if path.startswith(p + "/") or path == p: - environ = environ.copy() - environ["SCRIPT_NAME"] = environ["SCRIPT_NAME"] + p - environ["PATH_INFO"] = path[len(p):] - return app(environ, start_response) - - start_response('404 Not Found', [('Content-Type', 'text/plain'), - ('Content-Length', '0')]) - return [''] - - -class MaxSizeExceeded(Exception): pass -class SizeCheckWrapper(object): - """Wraps a file-like object, raising MaxSizeExceeded if too large.""" - - def __init__(self, rfile, maxlen): - self.rfile = rfile - self.maxlen = maxlen - self.bytes_read = 0 - - def _check_length(self): - if self.maxlen and self.bytes_read > self.maxlen: - raise MaxSizeExceeded() - - def read(self, size=None): - data = self.rfile.read(size) - self.bytes_read += len(data) - self._check_length() - return data - - def readline(self, size=None): - if size is not None: - data = self.rfile.readline(size) - self.bytes_read += len(data) - self._check_length() - return data - - # User didn't specify a size ... - # We read the line in chunks to make sure it's not a 100MB line ! - res = [] - while True: - data = self.rfile.readline(256) - self.bytes_read += len(data) - self._check_length() - res.append(data) - # See http://www.cherrypy.org/ticket/421 - if len(data) < 256 or data[-1:] == "\n": - return ''.join(res) - - def readlines(self, sizehint=0): - # Shamelessly stolen from StringIO - total = 0 - lines = [] - line = self.readline() - while line: - lines.append(line) - total += len(line) - if 0 < sizehint <= total: - break - line = self.readline() - return lines - - def close(self): - self.rfile.close() - - def __iter__(self): - return self - - def next(self): - data = self.rfile.next() - self.bytes_read += len(data) - self._check_length() - return data - - -class HTTPRequest(object): - """An HTTP Request (and response). - - A single HTTP connection may consist of multiple request/response pairs. - - send: the 'send' method from the connection's socket object. - wsgi_app: the WSGI application to call. - environ: a partial WSGI environ (server and connection entries). - The caller MUST set the following entries: - * All wsgi.* entries, including .input - * SERVER_NAME and SERVER_PORT - * Any SSL_* entries - * Any custom entries like REMOTE_ADDR and REMOTE_PORT - * SERVER_SOFTWARE: the value to write in the "Server" response header. - * ACTUAL_SERVER_PROTOCOL: the value to write in the Status-Line of - the response. From RFC 2145: "An HTTP server SHOULD send a - response version equal to the highest version for which the - server is at least conditionally compliant, and whose major - version is less than or equal to the one received in the - request. An HTTP server MUST NOT send a version for which - it is not at least conditionally compliant." - - outheaders: a list of header tuples to write in the response. - ready: when True, the request has been parsed and is ready to begin - generating the response. When False, signals the calling Connection - that the response should not be generated and the connection should - close. - close_connection: signals the calling Connection that the request - should close. This does not imply an error! The client and/or - server may each request that the connection be closed. - chunked_write: if True, output will be encoded with the "chunked" - transfer-coding. This value is set automatically inside - send_headers. - """ - - max_request_header_size = 0 - max_request_body_size = 0 - - def __init__(self, wfile, environ, wsgi_app): - self.rfile = environ['wsgi.input'] - self.wfile = wfile - self.environ = environ.copy() - self.wsgi_app = wsgi_app - - self.ready = False - self.started_response = False - self.status = "" - self.outheaders = [] - self.sent_headers = False - self.close_connection = False - self.chunked_write = False - - def parse_request(self): - """Parse the next HTTP request start-line and message-headers.""" - self.rfile.maxlen = self.max_request_header_size - self.rfile.bytes_read = 0 - - try: - self._parse_request() - except MaxSizeExceeded: - self.simple_response("413 Request Entity Too Large") - return - - def _parse_request(self): - # HTTP/1.1 connections are persistent by default. If a client - # requests a page, then idles (leaves the connection open), - # then rfile.readline() will raise socket.error("timed out"). - # Note that it does this based on the value given to settimeout(), - # and doesn't need the client to request or acknowledge the close - # (although your TCP stack might suffer for it: cf Apache's history - # with FIN_WAIT_2). - request_line = self.rfile.readline() - if not request_line: - # Force self.ready = False so the connection will close. - self.ready = False - return - - if request_line == "\r\n": - # RFC 2616 sec 4.1: "...if the server is reading the protocol - # stream at the beginning of a message and receives a CRLF - # first, it should ignore the CRLF." - # But only ignore one leading line! else we enable a DoS. - request_line = self.rfile.readline() - if not request_line: - self.ready = False - return - - environ = self.environ - - try: - method, path, req_protocol = request_line.strip().split(" ", 2) - except ValueError: - self.simple_response(400, "Malformed Request-Line") - return - - environ["REQUEST_METHOD"] = method - - # path may be an abs_path (including "http://host.domain.tld"); - scheme, location, path, params, qs, frag = urlparse(path) - - if frag: - self.simple_response("400 Bad Request", - "Illegal #fragment in Request-URI.") - return - - if scheme: - environ["wsgi.url_scheme"] = scheme - if params: - path = path + ";" + params - - environ["SCRIPT_NAME"] = "" - - # Unquote the path+params (e.g. "/this%20path" -> "this path"). - # http://www.w3.org/Protocols/rfc2616/rfc2616-sec5.html#sec5.1.2 - # - # But note that "...a URI must be separated into its components - # before the escaped characters within those components can be - # safely decoded." http://www.ietf.org/rfc/rfc2396.txt, sec 2.4.2 - atoms = [unquote(x) for x in quoted_slash.split(path)] - path = "%2F".join(atoms) - environ["PATH_INFO"] = path - - # Note that, like wsgiref and most other WSGI servers, - # we unquote the path but not the query string. - environ["QUERY_STRING"] = qs - - # Compare request and server HTTP protocol versions, in case our - # server does not support the requested protocol. Limit our output - # to min(req, server). We want the following output: - # request server actual written supported response - # protocol protocol response protocol feature set - # a 1.0 1.0 1.0 1.0 - # b 1.0 1.1 1.1 1.0 - # c 1.1 1.0 1.0 1.0 - # d 1.1 1.1 1.1 1.1 - # Notice that, in (b), the response will be "HTTP/1.1" even though - # the client only understands 1.0. RFC 2616 10.5.6 says we should - # only return 505 if the _major_ version is different. - rp = int(req_protocol[5]), int(req_protocol[7]) - server_protocol = environ["ACTUAL_SERVER_PROTOCOL"] - sp = int(server_protocol[5]), int(server_protocol[7]) - if sp[0] != rp[0]: - self.simple_response("505 HTTP Version Not Supported") - return - # Bah. "SERVER_PROTOCOL" is actually the REQUEST protocol. - environ["SERVER_PROTOCOL"] = req_protocol - self.response_protocol = "HTTP/%s.%s" % min(rp, sp) - - # If the Request-URI was an absoluteURI, use its location atom. - if location: - environ["SERVER_NAME"] = location - - # then all the http headers - try: - self.read_headers() - except ValueError, ex: - self.simple_response("400 Bad Request", repr(ex.args)) - return - - mrbs = self.max_request_body_size - if mrbs and int(environ.get("CONTENT_LENGTH", 0)) > mrbs: - self.simple_response("413 Request Entity Too Large") - return - - # Persistent connection support - if self.response_protocol == "HTTP/1.1": - # Both server and client are HTTP/1.1 - if environ.get("HTTP_CONNECTION", "") == "close": - self.close_connection = True - else: - # Either the server or client (or both) are HTTP/1.0 - if environ.get("HTTP_CONNECTION", "") != "Keep-Alive": - self.close_connection = True - - # Transfer-Encoding support - te = None - if self.response_protocol == "HTTP/1.1": - te = environ.get("HTTP_TRANSFER_ENCODING") - if te: - te = [x.strip().lower() for x in te.split(",") if x.strip()] - - self.chunked_read = False - - if te: - for enc in te: - if enc == "chunked": - self.chunked_read = True - else: - # Note that, even if we see "chunked", we must reject - # if there is an extension we don't recognize. - self.simple_response("501 Unimplemented") - self.close_connection = True - return - - # From PEP 333: - # "Servers and gateways that implement HTTP 1.1 must provide - # transparent support for HTTP 1.1's "expect/continue" mechanism. - # This may be done in any of several ways: - # 1. Respond to requests containing an Expect: 100-continue request - # with an immediate "100 Continue" response, and proceed normally. - # 2. Proceed with the request normally, but provide the application - # with a wsgi.input stream that will send the "100 Continue" - # response if/when the application first attempts to read from - # the input stream. The read request must then remain blocked - # until the client responds. - # 3. Wait until the client decides that the server does not support - # expect/continue, and sends the request body on its own. - # (This is suboptimal, and is not recommended.) - # - # We used to do 3, but are now doing 1. Maybe we'll do 2 someday, - # but it seems like it would be a big slowdown for such a rare case. - if environ.get("HTTP_EXPECT", "") == "100-continue": - self.simple_response(100) - - self.ready = True - - def read_headers(self): - """Read header lines from the incoming stream.""" - environ = self.environ - - while True: - line = self.rfile.readline() - if not line: - # No more data--illegal end of headers - raise ValueError("Illegal end of headers.") - - if line == '\r\n': - # Normal end of headers - break - - if line[0] in ' \t': - # It's a continuation line. - v = line.strip() - else: - k, v = line.split(":", 1) - k, v = k.strip().upper(), v.strip() - envname = "HTTP_" + k.replace("-", "_") - - if k in comma_separated_headers: - existing = environ.get(envname) - if existing: - v = ", ".join((existing, v)) - environ[envname] = v - - ct = environ.pop("HTTP_CONTENT_TYPE", None) - if ct is not None: - environ["CONTENT_TYPE"] = ct - cl = environ.pop("HTTP_CONTENT_LENGTH", None) - if cl is not None: - environ["CONTENT_LENGTH"] = cl - - def decode_chunked(self): - """Decode the 'chunked' transfer coding.""" - cl = 0 - data = StringIO.StringIO() - while True: - line = self.rfile.readline().strip().split(";", 1) - chunk_size = int(line.pop(0), 16) - if chunk_size <= 0: - break -## if line: chunk_extension = line[0] - cl += chunk_size - data.write(self.rfile.read(chunk_size)) - crlf = self.rfile.read(2) - if crlf != "\r\n": - self.simple_response("400 Bad Request", - "Bad chunked transfer coding " - "(expected '\\r\\n', got %r)" % crlf) - return - - # Grab any trailer headers - self.read_headers() - - data.seek(0) - self.environ["wsgi.input"] = data - self.environ["CONTENT_LENGTH"] = str(cl) or "" - return True - - def respond(self): - """Call the appropriate WSGI app and write its iterable output.""" - # Set rfile.maxlen to ensure we don't read past Content-Length. - # This will also be used to read the entire request body if errors - # are raised before the app can read the body. - if self.chunked_read: - # If chunked, Content-Length will be 0. - self.rfile.maxlen = self.max_request_body_size - else: - cl = int(self.environ.get("CONTENT_LENGTH", 0)) - if self.max_request_body_size: - self.rfile.maxlen = min(cl, self.max_request_body_size) - else: - self.rfile.maxlen = cl - self.rfile.bytes_read = 0 - - try: - self._respond() - except MaxSizeExceeded: - if not self.sent_headers: - self.simple_response("413 Request Entity Too Large") - return - - def _respond(self): - if self.chunked_read: - if not self.decode_chunked(): - self.close_connection = True - return - - response = self.wsgi_app(self.environ, self.start_response) - try: - for chunk in response: - # "The start_response callable must not actually transmit - # the response headers. Instead, it must store them for the - # server or gateway to transmit only after the first - # iteration of the application return value that yields - # a NON-EMPTY string, or upon the application's first - # invocation of the write() callable." (PEP 333) - if chunk: - self.write(chunk) - finally: - if hasattr(response, "close"): - response.close() - - if (self.ready and not self.sent_headers): - self.sent_headers = True - self.send_headers() - if self.chunked_write: - self.wfile.sendall("0\r\n\r\n") - - def simple_response(self, status, msg=""): - """Write a simple response back to the client.""" - status = str(status) - buf = ["%s %s\r\n" % (self.environ['ACTUAL_SERVER_PROTOCOL'], status), - "Content-Length: %s\r\n" % len(msg), - "Content-Type: text/plain\r\n"] - - if status[:3] == "413" and self.response_protocol == 'HTTP/1.1': - # Request Entity Too Large - self.close_connection = True - buf.append("Connection: close\r\n") - - buf.append("\r\n") - if msg: - buf.append(msg) - - try: - self.wfile.sendall("".join(buf)) - except socket.error, x: - if x.args[0] not in socket_errors_to_ignore: - raise - - def start_response(self, status, headers, exc_info = None): - """WSGI callable to begin the HTTP response.""" - # "The application may call start_response more than once, - # if and only if the exc_info argument is provided." - if self.started_response and not exc_info: - raise AssertionError("WSGI start_response called a second " - "time with no exc_info.") - - # "if exc_info is provided, and the HTTP headers have already been - # sent, start_response must raise an error, and should raise the - # exc_info tuple." - if self.sent_headers: - try: - raise exc_info[0], exc_info[1], exc_info[2] - finally: - exc_info = None - - self.started_response = True - self.status = status - self.outheaders.extend(headers) - return self.write - - def write(self, chunk): - """WSGI callable to write unbuffered data to the client. - - This method is also used internally by start_response (to write - data from the iterable returned by the WSGI application). - """ - if not self.started_response: - raise AssertionError("WSGI write called before start_response.") - - if not self.sent_headers: - self.sent_headers = True - self.send_headers() - - if self.chunked_write and chunk: - buf = [hex(len(chunk))[2:], "\r\n", chunk, "\r\n"] - self.wfile.sendall("".join(buf)) - else: - self.wfile.sendall(chunk) - - def send_headers(self): - """Assert, process, and send the HTTP response message-headers.""" - hkeys = [key.lower() for key, value in self.outheaders] - status = int(self.status[:3]) - - if status == 413: - # Request Entity Too Large. Close conn to avoid garbage. - self.close_connection = True - elif "content-length" not in hkeys: - # "All 1xx (informational), 204 (no content), - # and 304 (not modified) responses MUST NOT - # include a message-body." So no point chunking. - if status < 200 or status in (204, 205, 304): - pass - else: - if (self.response_protocol == 'HTTP/1.1' - and self.environ["REQUEST_METHOD"] != 'HEAD'): - # Use the chunked transfer-coding - self.chunked_write = True - self.outheaders.append(("Transfer-Encoding", "chunked")) - else: - # Closing the conn is the only way to determine len. - self.close_connection = True - - if "connection" not in hkeys: - if self.response_protocol == 'HTTP/1.1': - # Both server and client are HTTP/1.1 or better - if self.close_connection: - self.outheaders.append(("Connection", "close")) - else: - # Server and/or client are HTTP/1.0 - if not self.close_connection: - self.outheaders.append(("Connection", "Keep-Alive")) - - if (not self.close_connection) and (not self.chunked_read): - # Read any remaining request body data on the socket. - # "If an origin server receives a request that does not include an - # Expect request-header field with the "100-continue" expectation, - # the request includes a request body, and the server responds - # with a final status code before reading the entire request body - # from the transport connection, then the server SHOULD NOT close - # the transport connection until it has read the entire request, - # or until the client closes the connection. Otherwise, the client - # might not reliably receive the response message. However, this - # requirement is not be construed as preventing a server from - # defending itself against denial-of-service attacks, or from - # badly broken client implementations." - size = self.rfile.maxlen - self.rfile.bytes_read - if size > 0: - self.rfile.read(size) - - if "date" not in hkeys: - self.outheaders.append(("Date", rfc822.formatdate())) - - if "server" not in hkeys: - self.outheaders.append(("Server", self.environ['SERVER_SOFTWARE'])) - - buf = [self.environ['ACTUAL_SERVER_PROTOCOL'], " ", self.status, "\r\n"] - try: - buf += [k + ": " + v + "\r\n" for k, v in self.outheaders] - except TypeError: - if not isinstance(k, str): - raise TypeError("WSGI response header key %r is not a string.") - if not isinstance(v, str): - raise TypeError("WSGI response header value %r is not a string.") - else: - raise - buf.append("\r\n") - self.wfile.sendall("".join(buf)) - - -class NoSSLError(Exception): - """Exception raised when a client speaks HTTP to an HTTPS socket.""" - pass - - -class FatalSSLAlert(Exception): - """Exception raised when the SSL implementation signals a fatal alert.""" - pass - - -if not _fileobject_uses_str_type: - class CP_fileobject(socket._fileobject): - """Faux file object attached to a socket object.""" - - def sendall(self, data): - """Sendall for non-blocking sockets.""" - while data: - try: - bytes_sent = self.send(data) - data = data[bytes_sent:] - except socket.error, e: - if e.args[0] not in socket_errors_nonblocking: - raise - - def send(self, data): - return self._sock.send(data) - - def flush(self): - if self._wbuf: - buffer = "".join(self._wbuf) - self._wbuf = [] - self.sendall(buffer) - - def recv(self, size): - while True: - try: - return self._sock.recv(size) - except socket.error, e: - if (e.args[0] not in socket_errors_nonblocking - and e.args[0] not in socket_error_eintr): - raise - - def read(self, size=-1): - # Use max, disallow tiny reads in a loop as they are very inefficient. - # We never leave read() with any leftover data from a new recv() call - # in our internal buffer. - rbufsize = max(self._rbufsize, self.default_bufsize) - # Our use of StringIO rather than lists of string objects returned by - # recv() minimizes memory usage and fragmentation that occurs when - # rbufsize is large compared to the typical return value of recv(). - buf = self._rbuf - buf.seek(0, 2) # seek end - if size < 0: - # Read until EOF - self._rbuf = StringIO.StringIO() # reset _rbuf. we consume it via buf. - while True: - data = self.recv(rbufsize) - if not data: - break - buf.write(data) - return buf.getvalue() - else: - # Read until size bytes or EOF seen, whichever comes first - buf_len = buf.tell() - if buf_len >= size: - # Already have size bytes in our buffer? Extract and return. - buf.seek(0) - rv = buf.read(size) - self._rbuf = StringIO.StringIO() - self._rbuf.write(buf.read()) - return rv - - self._rbuf = StringIO.StringIO() # reset _rbuf. we consume it via buf. - while True: - left = size - buf_len - # recv() will malloc the amount of memory given as its - # parameter even though it often returns much less data - # than that. The returned data string is short lived - # as we copy it into a StringIO and free it. This avoids - # fragmentation issues on many platforms. - data = self.recv(left) - if not data: - break - n = len(data) - if n == size and not buf_len: - # Shortcut. Avoid buffer data copies when: - # - We have no data in our buffer. - # AND - # - Our call to recv returned exactly the - # number of bytes we were asked to read. - return data - if n == left: - buf.write(data) - del data # explicit free - break - assert n <= left, "recv(%d) returned %d bytes" % (left, n) - buf.write(data) - buf_len += n - del data # explicit free - #assert buf_len == buf.tell() - return buf.getvalue() - - def readline(self, size=-1): - buf = self._rbuf - buf.seek(0, 2) # seek end - if buf.tell() > 0: - # check if we already have it in our buffer - buf.seek(0) - bline = buf.readline(size) - if bline.endswith('\n') or len(bline) == size: - self._rbuf = StringIO.StringIO() - self._rbuf.write(buf.read()) - return bline - del bline - if size < 0: - # Read until \n or EOF, whichever comes first - if self._rbufsize <= 1: - # Speed up unbuffered case - buf.seek(0) - buffers = [buf.read()] - self._rbuf = StringIO.StringIO() # reset _rbuf. we consume it via buf. - data = None - recv = self.recv - while data != "\n": - data = recv(1) - if not data: - break - buffers.append(data) - return "".join(buffers) - - buf.seek(0, 2) # seek end - self._rbuf = StringIO.StringIO() # reset _rbuf. we consume it via buf. - while True: - data = self.recv(self._rbufsize) - if not data: - break - nl = data.find('\n') - if nl >= 0: - nl += 1 - buf.write(data[:nl]) - self._rbuf.write(data[nl:]) - del data - break - buf.write(data) - return buf.getvalue() - else: - # Read until size bytes or \n or EOF seen, whichever comes first - buf.seek(0, 2) # seek end - buf_len = buf.tell() - if buf_len >= size: - buf.seek(0) - rv = buf.read(size) - self._rbuf = StringIO.StringIO() - self._rbuf.write(buf.read()) - return rv - self._rbuf = StringIO.StringIO() # reset _rbuf. we consume it via buf. - while True: - data = self.recv(self._rbufsize) - if not data: - break - left = size - buf_len - # did we just receive a newline? - nl = data.find('\n', 0, left) - if nl >= 0: - nl += 1 - # save the excess data to _rbuf - self._rbuf.write(data[nl:]) - if buf_len: - buf.write(data[:nl]) - break - else: - # Shortcut. Avoid data copy through buf when returning - # a substring of our first recv(). - return data[:nl] - n = len(data) - if n == size and not buf_len: - # Shortcut. Avoid data copy through buf when - # returning exactly all of our first recv(). - return data - if n >= left: - buf.write(data[:left]) - self._rbuf.write(data[left:]) - break - buf.write(data) - buf_len += n - #assert buf_len == buf.tell() - return buf.getvalue() - -else: - class CP_fileobject(socket._fileobject): - """Faux file object attached to a socket object.""" - - def sendall(self, data): - """Sendall for non-blocking sockets.""" - while data: - try: - bytes_sent = self.send(data) - data = data[bytes_sent:] - except socket.error, e: - if e.args[0] not in socket_errors_nonblocking: - raise - - def send(self, data): - return self._sock.send(data) - - def flush(self): - if self._wbuf: - buffer = "".join(self._wbuf) - self._wbuf = [] - self.sendall(buffer) - - def recv(self, size): - while True: - try: - return self._sock.recv(size) - except socket.error, e: - if (e.args[0] not in socket_errors_nonblocking - and e.args[0] not in socket_error_eintr): - raise - - def read(self, size=-1): - if size < 0: - # Read until EOF - buffers = [self._rbuf] - self._rbuf = "" - if self._rbufsize <= 1: - recv_size = self.default_bufsize - else: - recv_size = self._rbufsize - - while True: - data = self.recv(recv_size) - if not data: - break - buffers.append(data) - return "".join(buffers) - else: - # Read until size bytes or EOF seen, whichever comes first - data = self._rbuf - buf_len = len(data) - if buf_len >= size: - self._rbuf = data[size:] - return data[:size] - buffers = [] - if data: - buffers.append(data) - self._rbuf = "" - while True: - left = size - buf_len - recv_size = max(self._rbufsize, left) - data = self.recv(recv_size) - if not data: - break - buffers.append(data) - n = len(data) - if n >= left: - self._rbuf = data[left:] - buffers[-1] = data[:left] - break - buf_len += n - return "".join(buffers) - - def readline(self, size=-1): - data = self._rbuf - if size < 0: - # Read until \n or EOF, whichever comes first - if self._rbufsize <= 1: - # Speed up unbuffered case - assert data == "" - buffers = [] - while data != "\n": - data = self.recv(1) - if not data: - break - buffers.append(data) - return "".join(buffers) - nl = data.find('\n') - if nl >= 0: - nl += 1 - self._rbuf = data[nl:] - return data[:nl] - buffers = [] - if data: - buffers.append(data) - self._rbuf = "" - while True: - data = self.recv(self._rbufsize) - if not data: - break - buffers.append(data) - nl = data.find('\n') - if nl >= 0: - nl += 1 - self._rbuf = data[nl:] - buffers[-1] = data[:nl] - break - return "".join(buffers) - else: - # Read until size bytes or \n or EOF seen, whichever comes first - nl = data.find('\n', 0, size) - if nl >= 0: - nl += 1 - self._rbuf = data[nl:] - return data[:nl] - buf_len = len(data) - if buf_len >= size: - self._rbuf = data[size:] - return data[:size] - buffers = [] - if data: - buffers.append(data) - self._rbuf = "" - while True: - data = self.recv(self._rbufsize) - if not data: - break - buffers.append(data) - left = size - buf_len - nl = data.find('\n', 0, left) - if nl >= 0: - nl += 1 - self._rbuf = data[nl:] - buffers[-1] = data[:nl] - break - n = len(data) - if n >= left: - self._rbuf = data[left:] - buffers[-1] = data[:left] - break - buf_len += n - return "".join(buffers) - - -class SSL_fileobject(CP_fileobject): - """SSL file object attached to a socket object.""" - - ssl_timeout = 3 - ssl_retry = .01 - - def _safe_call(self, is_reader, call, *args, **kwargs): - """Wrap the given call with SSL error-trapping. - - is_reader: if False EOF errors will be raised. If True, EOF errors - will return "" (to emulate normal sockets). - """ - start = time.time() - while True: - try: - return call(*args, **kwargs) - except SSL.WantReadError: - # Sleep and try again. This is dangerous, because it means - # the rest of the stack has no way of differentiating - # between a "new handshake" error and "client dropped". - # Note this isn't an endless loop: there's a timeout below. - time.sleep(self.ssl_retry) - except SSL.WantWriteError: - time.sleep(self.ssl_retry) - except SSL.SysCallError, e: - if is_reader and e.args == (-1, 'Unexpected EOF'): - return "" - - errnum = e.args[0] - if is_reader and errnum in socket_errors_to_ignore: - return "" - raise socket.error(errnum) - except SSL.Error, e: - if is_reader and e.args == (-1, 'Unexpected EOF'): - return "" - - thirdarg = None - try: - thirdarg = e.args[0][0][2] - except IndexError: - pass - - if thirdarg == 'http request': - # The client is talking HTTP to an HTTPS server. - raise NoSSLError() - raise FatalSSLAlert(*e.args) - except: - raise - - if time.time() - start > self.ssl_timeout: - raise socket.timeout("timed out") - - def recv(self, *args, **kwargs): - buf = [] - r = super(SSL_fileobject, self).recv - while True: - data = self._safe_call(True, r, *args, **kwargs) - buf.append(data) - p = self._sock.pending() - if not p: - return "".join(buf) - - def sendall(self, *args, **kwargs): - return self._safe_call(False, super(SSL_fileobject, self).sendall, *args, **kwargs) - - def send(self, *args, **kwargs): - return self._safe_call(False, super(SSL_fileobject, self).send, *args, **kwargs) - - -class HTTPConnection(object): - """An HTTP connection (active socket). - - socket: the raw socket object (usually TCP) for this connection. - wsgi_app: the WSGI application for this server/connection. - environ: a WSGI environ template. This will be copied for each request. - - rfile: a fileobject for reading from the socket. - send: a function for writing (+ flush) to the socket. - """ - - rbufsize = -1 - RequestHandlerClass = HTTPRequest - environ = {"wsgi.version": (1, 0), - "wsgi.url_scheme": "http", - "wsgi.multithread": True, - "wsgi.multiprocess": False, - "wsgi.run_once": False, - "wsgi.errors": sys.stderr, - } - - def __init__(self, sock, wsgi_app, environ): - self.socket = sock - self.wsgi_app = wsgi_app - - # Copy the class environ into self. - self.environ = self.environ.copy() - self.environ.update(environ) - - if SSL and isinstance(sock, SSL.ConnectionType): - timeout = sock.gettimeout() - self.rfile = SSL_fileobject(sock, "rb", self.rbufsize) - self.rfile.ssl_timeout = timeout - self.wfile = SSL_fileobject(sock, "wb", -1) - self.wfile.ssl_timeout = timeout - else: - self.rfile = CP_fileobject(sock, "rb", self.rbufsize) - self.wfile = CP_fileobject(sock, "wb", -1) - - # Wrap wsgi.input but not HTTPConnection.rfile itself. - # We're also not setting maxlen yet; we'll do that separately - # for headers and body for each iteration of self.communicate - # (if maxlen is 0 the wrapper doesn't check length). - self.environ["wsgi.input"] = SizeCheckWrapper(self.rfile, 0) - - def communicate(self): - """Read each request and respond appropriately.""" - try: - while True: - # (re)set req to None so that if something goes wrong in - # the RequestHandlerClass constructor, the error doesn't - # get written to the previous request. - req = None - req = self.RequestHandlerClass(self.wfile, self.environ, - self.wsgi_app) - - # This order of operations should guarantee correct pipelining. - req.parse_request() - if not req.ready: - return - - req.respond() - if req.close_connection: - return - - except socket.error, e: - errnum = e.args[0] - if errnum == 'timed out': - if req and not req.sent_headers: - req.simple_response("408 Request Timeout") - elif errnum not in socket_errors_to_ignore: - if req and not req.sent_headers: - req.simple_response("500 Internal Server Error", - format_exc()) - return - except (KeyboardInterrupt, SystemExit): - raise - except FatalSSLAlert, e: - # Close the connection. - return - except NoSSLError: - if req and not req.sent_headers: - # Unwrap our wfile - req.wfile = CP_fileobject(self.socket._sock, "wb", -1) - req.simple_response("400 Bad Request", - "The client sent a plain HTTP request, but " - "this server only speaks HTTPS on this port.") - self.linger = True - except Exception, e: - if req and not req.sent_headers: - req.simple_response("500 Internal Server Error", format_exc()) - - linger = False - - def close(self): - """Close the socket underlying this connection.""" - self.rfile.close() - - if not self.linger: - # Python's socket module does NOT call close on the kernel socket - # when you call socket.close(). We do so manually here because we - # want this server to send a FIN TCP segment immediately. Note this - # must be called *before* calling socket.close(), because the latter - # drops its reference to the kernel socket. - self.socket._sock.close() - self.socket.close() - else: - # On the other hand, sometimes we want to hang around for a bit - # to make sure the client has a chance to read our entire - # response. Skipping the close() calls here delays the FIN - # packet until the socket object is garbage-collected later. - # Someday, perhaps, we'll do the full lingering_close that - # Apache does, but not today. - pass - - -def format_exc(limit=None): - """Like print_exc() but return a string. Backport for Python 2.3.""" - try: - etype, value, tb = sys.exc_info() - return ''.join(traceback.format_exception(etype, value, tb, limit)) - finally: - etype = value = tb = None - - -_SHUTDOWNREQUEST = None - -class WorkerThread(threading.Thread): - """Thread which continuously polls a Queue for Connection objects. - - server: the HTTP Server which spawned this thread, and which owns the - Queue and is placing active connections into it. - ready: a simple flag for the calling server to know when this thread - has begun polling the Queue. - - Due to the timing issues of polling a Queue, a WorkerThread does not - check its own 'ready' flag after it has started. To stop the thread, - it is necessary to stick a _SHUTDOWNREQUEST object onto the Queue - (one for each running WorkerThread). - """ - - conn = None - - def __init__(self, server): - self.ready = False - self.server = server - threading.Thread.__init__(self) - - def run(self): - try: - self.ready = True - while True: - conn = self.server.requests.get() - if conn is _SHUTDOWNREQUEST: - return - - self.conn = conn - try: - conn.communicate() - finally: - conn.close() - self.conn = None - except (KeyboardInterrupt, SystemExit), exc: - self.server.interrupt = exc - - -class ThreadPool(object): - """A Request Queue for the CherryPyWSGIServer which pools threads. - - ThreadPool objects must provide min, get(), put(obj), start() - and stop(timeout) attributes. - """ - - def __init__(self, server, min=10, max=-1): - self.server = server - self.min = min - self.max = max - self._threads = [] - self._queue = Queue.Queue() - self.get = self._queue.get - - def start(self): - """Start the pool of threads.""" - for i in xrange(self.min): - self._threads.append(WorkerThread(self.server)) - for worker in self._threads: - worker.setName("CP WSGIServer " + worker.getName()) - worker.start() - for worker in self._threads: - while not worker.ready: - time.sleep(.1) - - def _get_idle(self): - """Number of worker threads which are idle. Read-only.""" - return len([t for t in self._threads if t.conn is None]) - idle = property(_get_idle, doc=_get_idle.__doc__) - - def put(self, obj): - self._queue.put(obj) - if obj is _SHUTDOWNREQUEST: - return - - def grow(self, amount): - """Spawn new worker threads (not above self.max).""" - for i in xrange(amount): - if self.max > 0 and len(self._threads) >= self.max: - break - worker = WorkerThread(self.server) - worker.setName("CP WSGIServer " + worker.getName()) - self._threads.append(worker) - worker.start() - - def shrink(self, amount): - """Kill off worker threads (not below self.min).""" - # Grow/shrink the pool if necessary. - # Remove any dead threads from our list - for t in self._threads: - if not t.isAlive(): - self._threads.remove(t) - amount -= 1 - - if amount > 0: - for i in xrange(min(amount, len(self._threads) - self.min)): - # Put a number of shutdown requests on the queue equal - # to 'amount'. Once each of those is processed by a worker, - # that worker will terminate and be culled from our list - # in self.put. - self._queue.put(_SHUTDOWNREQUEST) - - def stop(self, timeout=5): - # Must shut down threads here so the code that calls - # this method can know when all threads are stopped. - for worker in self._threads: - self._queue.put(_SHUTDOWNREQUEST) - - # Don't join currentThread (when stop is called inside a request). - current = threading.currentThread() - while self._threads: - worker = self._threads.pop() - if worker is not current and worker.isAlive(): - try: - if timeout is None or timeout < 0: - worker.join() - else: - worker.join(timeout) - if worker.isAlive(): - # We exhausted the timeout. - # Forcibly shut down the socket. - c = worker.conn - if c and not c.rfile.closed: - if SSL and isinstance(c.socket, SSL.ConnectionType): - # pyOpenSSL.socket.shutdown takes no args - c.socket.shutdown() - else: - c.socket.shutdown(socket.SHUT_RD) - worker.join() - except (AssertionError, - # Ignore repeated Ctrl-C. - # See http://www.cherrypy.org/ticket/691. - KeyboardInterrupt), exc1: - pass - - - -class SSLConnection: - """A thread-safe wrapper for an SSL.Connection. - - *args: the arguments to create the wrapped SSL.Connection(*args). - """ - - def __init__(self, *args): - self._ssl_conn = SSL.Connection(*args) - self._lock = threading.RLock() - - for f in ('get_context', 'pending', 'send', 'write', 'recv', 'read', - 'renegotiate', 'bind', 'listen', 'connect', 'accept', - 'setblocking', 'fileno', 'shutdown', 'close', 'get_cipher_list', - 'getpeername', 'getsockname', 'getsockopt', 'setsockopt', - 'makefile', 'get_app_data', 'set_app_data', 'state_string', - 'sock_shutdown', 'get_peer_certificate', 'want_read', - 'want_write', 'set_connect_state', 'set_accept_state', - 'connect_ex', 'sendall', 'settimeout'): - exec """def %s(self, *args): - self._lock.acquire() - try: - return self._ssl_conn.%s(*args) - finally: - self._lock.release() -""" % (f, f) - try: - import fcntl -except ImportError: - try: - from ctypes import windll, WinError - except ImportError: - def prevent_socket_inheritance(sock): - """Dummy function, since neither fcntl nor ctypes are available.""" - pass - else: - def prevent_socket_inheritance(sock): - """Mark the given socket fd as non-inheritable (Windows).""" - if not windll.kernel32.SetHandleInformation(sock.fileno(), 1, 0): - raise WinError() -else: - def prevent_socket_inheritance(sock): - """Mark the given socket fd as non-inheritable (POSIX).""" - fd = sock.fileno() - old_flags = fcntl.fcntl(fd, fcntl.F_GETFD) - fcntl.fcntl(fd, fcntl.F_SETFD, old_flags | fcntl.FD_CLOEXEC) - - -class CherryPyWSGIServer(object): - """An HTTP server for WSGI. - - bind_addr: The interface on which to listen for connections. - For TCP sockets, a (host, port) tuple. Host values may be any IPv4 - or IPv6 address, or any valid hostname. The string 'localhost' is a - synonym for '127.0.0.1' (or '::1', if your hosts file prefers IPv6). - The string '0.0.0.0' is a special IPv4 entry meaning "any active - interface" (INADDR_ANY), and '::' is the similar IN6ADDR_ANY for - IPv6. The empty string or None are not allowed. - - For UNIX sockets, supply the filename as a string. - wsgi_app: the WSGI 'application callable'; multiple WSGI applications - may be passed as (path_prefix, app) pairs. - numthreads: the number of worker threads to create (default 10). - server_name: the string to set for WSGI's SERVER_NAME environ entry. - Defaults to socket.gethostname(). - max: the maximum number of queued requests (defaults to -1 = no limit). - request_queue_size: the 'backlog' argument to socket.listen(); - specifies the maximum number of queued connections (default 5). - timeout: the timeout in seconds for accepted connections (default 10). - - nodelay: if True (the default since 3.1), sets the TCP_NODELAY socket - option. - - protocol: the version string to write in the Status-Line of all - HTTP responses. For example, "HTTP/1.1" (the default). This - also limits the supported features used in the response. - - - SSL/HTTPS - --------- - The OpenSSL module must be importable for SSL functionality. - You can obtain it from http://pyopenssl.sourceforge.net/ - - ssl_certificate: the filename of the server SSL certificate. - ssl_privatekey: the filename of the server's private key file. - - If either of these is None (both are None by default), this server - will not use SSL. If both are given and are valid, they will be read - on server start and used in the SSL context for the listening socket. - """ - - protocol = "HTTP/1.1" - _bind_addr = "127.0.0.1" - version = "CherryPy/3.1.2" - ready = False - _interrupt = None - - nodelay = True - - ConnectionClass = HTTPConnection - environ = {} - - # Paths to certificate and private key files - ssl_certificate = None - ssl_private_key = None - - def __init__(self, bind_addr, wsgi_app, numthreads=10, server_name=None, - max=-1, request_queue_size=5, timeout=10, shutdown_timeout=5): - self.requests = ThreadPool(self, min=numthreads or 1, max=max) - - if callable(wsgi_app): - # We've been handed a single wsgi_app, in CP-2.1 style. - # Assume it's mounted at "". - self.wsgi_app = wsgi_app - else: - # We've been handed a list of (path_prefix, wsgi_app) tuples, - # so that the server can call different wsgi_apps, and also - # correctly set SCRIPT_NAME. - warnings.warn("The ability to pass multiple apps is deprecated " - "and will be removed in 3.2. You should explicitly " - "include a WSGIPathInfoDispatcher instead.", - DeprecationWarning) - self.wsgi_app = WSGIPathInfoDispatcher(wsgi_app) - - self.bind_addr = bind_addr - if not server_name: - server_name = socket.gethostname() - self.server_name = server_name - self.request_queue_size = request_queue_size - - self.timeout = timeout - self.shutdown_timeout = shutdown_timeout - - def _get_numthreads(self): - return self.requests.min - def _set_numthreads(self, value): - self.requests.min = value - numthreads = property(_get_numthreads, _set_numthreads) - - def __str__(self): - return "%s.%s(%r)" % (self.__module__, self.__class__.__name__, - self.bind_addr) - - def _get_bind_addr(self): - return self._bind_addr - def _set_bind_addr(self, value): - if isinstance(value, tuple) and value[0] in ('', None): - # Despite the socket module docs, using '' does not - # allow AI_PASSIVE to work. Passing None instead - # returns '0.0.0.0' like we want. In other words: - # host AI_PASSIVE result - # '' Y 192.168.x.y - # '' N 192.168.x.y - # None Y 0.0.0.0 - # None N 127.0.0.1 - # But since you can get the same effect with an explicit - # '0.0.0.0', we deny both the empty string and None as values. - raise ValueError("Host values of '' or None are not allowed. " - "Use '0.0.0.0' (IPv4) or '::' (IPv6) instead " - "to listen on all active interfaces.") - self._bind_addr = value - bind_addr = property(_get_bind_addr, _set_bind_addr, - doc="""The interface on which to listen for connections. - - For TCP sockets, a (host, port) tuple. Host values may be any IPv4 - or IPv6 address, or any valid hostname. The string 'localhost' is a - synonym for '127.0.0.1' (or '::1', if your hosts file prefers IPv6). - The string '0.0.0.0' is a special IPv4 entry meaning "any active - interface" (INADDR_ANY), and '::' is the similar IN6ADDR_ANY for - IPv6. The empty string or None are not allowed. - - For UNIX sockets, supply the filename as a string.""") - - def start(self): - """Run the server forever.""" - # We don't have to trap KeyboardInterrupt or SystemExit here, - # because cherrpy.server already does so, calling self.stop() for us. - # If you're using this server with another framework, you should - # trap those exceptions in whatever code block calls start(). - self._interrupt = None - - # Select the appropriate socket - if isinstance(self.bind_addr, basestring): - # AF_UNIX socket - - # So we can reuse the socket... - try: os.unlink(self.bind_addr) - except: pass - - # So everyone can access the socket... - try: os.chmod(self.bind_addr, 0777) - except: pass - - info = [(socket.AF_UNIX, socket.SOCK_STREAM, 0, "", self.bind_addr)] - else: - # AF_INET or AF_INET6 socket - # Get the correct address family for our host (allows IPv6 addresses) - host, port = self.bind_addr - try: - info = socket.getaddrinfo(host, port, socket.AF_UNSPEC, - socket.SOCK_STREAM, 0, socket.AI_PASSIVE) - except socket.gaierror: - # Probably a DNS issue. Assume IPv4. - info = [(socket.AF_INET, socket.SOCK_STREAM, 0, "", self.bind_addr)] - - self.socket = None - msg = "No socket could be created" - for res in info: - af, socktype, proto, canonname, sa = res - try: - self.bind(af, socktype, proto) - except socket.error, msg: - if self.socket: - self.socket.close() - self.socket = None - continue - break - if not self.socket: - raise socket.error, msg - - # Timeout so KeyboardInterrupt can be caught on Win32 - self.socket.settimeout(1) - self.socket.listen(self.request_queue_size) - - # Create worker threads - self.requests.start() - - self.ready = True - while self.ready: - self.tick() - if self.interrupt: - while self.interrupt is True: - # Wait for self.stop() to complete. See _set_interrupt. - time.sleep(0.1) - if self.interrupt: - raise self.interrupt - - def bind(self, family, type, proto=0): - """Create (or recreate) the actual socket object.""" - self.socket = socket.socket(family, type, proto) - prevent_socket_inheritance(self.socket) - self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - if self.nodelay: - self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) - if self.ssl_certificate and self.ssl_private_key: - if SSL is None: - raise ImportError("You must install pyOpenSSL to use HTTPS.") - - # See http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/442473 - ctx = SSL.Context(SSL.SSLv23_METHOD) - ctx.use_privatekey_file(self.ssl_private_key) - ctx.use_certificate_file(self.ssl_certificate) - self.socket = SSLConnection(ctx, self.socket) - self.populate_ssl_environ() - - # If listening on the IPV6 any address ('::' = IN6ADDR_ANY), - # activate dual-stack. See http://www.cherrypy.org/ticket/871. - if (not isinstance(self.bind_addr, basestring) - and self.bind_addr[0] == '::' and family == socket.AF_INET6): - try: - self.socket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0) - except (AttributeError, socket.error): - # Apparently, the socket option is not available in - # this machine's TCP stack - pass - - self.socket.bind(self.bind_addr) - - def tick(self): - """Accept a new connection and put it on the Queue.""" - try: - s, addr = self.socket.accept() - prevent_socket_inheritance(s) - if not self.ready: - return - if hasattr(s, 'settimeout'): - s.settimeout(self.timeout) - - environ = self.environ.copy() - # SERVER_SOFTWARE is common for IIS. It's also helpful for - # us to pass a default value for the "Server" response header. - if environ.get("SERVER_SOFTWARE") is None: - environ["SERVER_SOFTWARE"] = "%s WSGI Server" % self.version - # set a non-standard environ entry so the WSGI app can know what - # the *real* server protocol is (and what features to support). - # See http://www.faqs.org/rfcs/rfc2145.html. - environ["ACTUAL_SERVER_PROTOCOL"] = self.protocol - environ["SERVER_NAME"] = self.server_name - - if isinstance(self.bind_addr, basestring): - # AF_UNIX. This isn't really allowed by WSGI, which doesn't - # address unix domain sockets. But it's better than nothing. - environ["SERVER_PORT"] = "" - else: - environ["SERVER_PORT"] = str(self.bind_addr[1]) - # optional values - # Until we do DNS lookups, omit REMOTE_HOST - environ["REMOTE_ADDR"] = addr[0] - environ["REMOTE_PORT"] = str(addr[1]) - - conn = self.ConnectionClass(s, self.wsgi_app, environ) - self.requests.put(conn) - except socket.timeout: - # The only reason for the timeout in start() is so we can - # notice keyboard interrupts on Win32, which don't interrupt - # accept() by default - return - except socket.error, x: - if x.args[0] in socket_error_eintr: - # I *think* this is right. EINTR should occur when a signal - # is received during the accept() call; all docs say retry - # the call, and I *think* I'm reading it right that Python - # will then go ahead and poll for and handle the signal - # elsewhere. See http://www.cherrypy.org/ticket/707. - return - if x.args[0] in socket_errors_nonblocking: - # Just try again. See http://www.cherrypy.org/ticket/479. - return - if x.args[0] in socket_errors_to_ignore: - # Our socket was closed. - # See http://www.cherrypy.org/ticket/686. - return - raise - - def _get_interrupt(self): - return self._interrupt - def _set_interrupt(self, interrupt): - self._interrupt = True - self.stop() - self._interrupt = interrupt - interrupt = property(_get_interrupt, _set_interrupt, - doc="Set this to an Exception instance to " - "interrupt the server.") - - def stop(self): - """Gracefully shutdown a server that is serving forever.""" - self.ready = False - - sock = getattr(self, "socket", None) - if sock: - if not isinstance(self.bind_addr, basestring): - # Touch our own socket to make accept() return immediately. - try: - host, port = sock.getsockname()[:2] - except socket.error, x: - if x.args[0] not in socket_errors_to_ignore: - raise - else: - # Note that we're explicitly NOT using AI_PASSIVE, - # here, because we want an actual IP to touch. - # localhost won't work if we've bound to a public IP, - # but it will if we bound to '0.0.0.0' (INADDR_ANY). - for res in socket.getaddrinfo(host, port, socket.AF_UNSPEC, - socket.SOCK_STREAM): - af, socktype, proto, canonname, sa = res - s = None - try: - s = socket.socket(af, socktype, proto) - # See http://groups.google.com/group/cherrypy-users/ - # browse_frm/thread/bbfe5eb39c904fe0 - s.settimeout(1.0) - s.connect((host, port)) - s.close() - except socket.error: - if s: - s.close() - if hasattr(sock, "close"): - sock.close() - self.socket = None - - self.requests.stop(self.shutdown_timeout) - - def populate_ssl_environ(self): - """Create WSGI environ entries to be merged into each request.""" - cert = open(self.ssl_certificate, 'rb').read() - cert = crypto.load_certificate(crypto.FILETYPE_PEM, cert) - ssl_environ = { - "wsgi.url_scheme": "https", - "HTTPS": "on", - # pyOpenSSL doesn't provide access to any of these AFAICT -## 'SSL_PROTOCOL': 'SSLv2', -## SSL_CIPHER string The cipher specification name -## SSL_VERSION_INTERFACE string The mod_ssl program version -## SSL_VERSION_LIBRARY string The OpenSSL program version - } - - # Server certificate attributes - ssl_environ.update({ - 'SSL_SERVER_M_VERSION': cert.get_version(), - 'SSL_SERVER_M_SERIAL': cert.get_serial_number(), -## 'SSL_SERVER_V_START': Validity of server's certificate (start time), -## 'SSL_SERVER_V_END': Validity of server's certificate (end time), - }) - - for prefix, dn in [("I", cert.get_issuer()), - ("S", cert.get_subject())]: - # X509Name objects don't seem to have a way to get the - # complete DN string. Use str() and slice it instead, - # because str(dn) == "" - dnstr = str(dn)[18:-2] - - wsgikey = 'SSL_SERVER_%s_DN' % prefix - ssl_environ[wsgikey] = dnstr - - # The DN should be of the form: /k1=v1/k2=v2, but we must allow - # for any value to contain slashes itself (in a URL). - while dnstr: - pos = dnstr.rfind("=") - dnstr, value = dnstr[:pos], dnstr[pos + 1:] - pos = dnstr.rfind("/") - dnstr, key = dnstr[:pos], dnstr[pos + 1:] - if key and value: - wsgikey = 'SSL_SERVER_%s_DN_%s' % (prefix, key) - ssl_environ[wsgikey] = value - - self.environ.update(ssl_environ) - + #__version__ = pkg_resources.get_distribution('cheroot').version + __version__ = 'cheroot/5.11.0' +except Exception: + __version__ = 'unknown' diff --git a/module/lib/wsgiserver/_compat.py b/module/lib/wsgiserver/_compat.py new file mode 100644 index 0000000000..e9de9a1169 --- /dev/null +++ b/module/lib/wsgiserver/_compat.py @@ -0,0 +1,62 @@ +"""Compatibility code for using Cheroot with various versions of Python.""" + +import re + +import six + +if six.PY3: + def ntob(n, encoding='ISO-8859-1'): + """Return the native string as bytes in the given encoding.""" + assert_native(n) + # In Python 3, the native string type is unicode + return n.encode(encoding) + + def ntou(n, encoding='ISO-8859-1'): + """Return the native string as unicode with the given encoding.""" + assert_native(n) + # In Python 3, the native string type is unicode + return n + + def bton(b, encoding='ISO-8859-1'): + """Return the byte string as native string in the given encoding.""" + return b.decode(encoding) +else: + # Python 2 + def ntob(n, encoding='ISO-8859-1'): + """Return the native string as bytes in the given encoding.""" + assert_native(n) + # In Python 2, the native string type is bytes. Assume it's already + # in the given encoding, which for ISO-8859-1 is almost always what + # was intended. + return n + + def ntou(n, encoding='ISO-8859-1'): + """Return the native string as unicode with the given encoding.""" + assert_native(n) + # In Python 2, the native string type is bytes. + # First, check for the special encoding 'escape'. The test suite uses + # this to signal that it wants to pass a string with embedded \uXXXX + # escapes, but without having to prefix it with u'' for Python 2, + # but no prefix for Python 3. + if encoding == 'escape': + return six.u( + re.sub(r'\\u([0-9a-zA-Z]{4})', + lambda m: six.unichr(int(m.group(1), 16)), + n.decode('ISO-8859-1'))) + # Assume it's already in the given encoding, which for ISO-8859-1 + # is almost always what was intended. + return n.decode(encoding) + + def bton(b, encoding='ISO-8859-1'): + """Return the byte string as native string in the given encoding.""" + return b + + +def assert_native(n): + """Check whether the input is of nativ ``str`` type. + + Raises: + TypeError: in case of failed check + """ + if not isinstance(n, str): + raise TypeError('n must be a native str (got %s)' % type(n).__name__) diff --git a/module/lib/wsgiserver/errors.py b/module/lib/wsgiserver/errors.py new file mode 100644 index 0000000000..974580c7d5 --- /dev/null +++ b/module/lib/wsgiserver/errors.py @@ -0,0 +1,55 @@ +"""Collection of exceptions raised and/or processed by Cheroot.""" + +import errno +import sys + + +class MaxSizeExceeded(Exception): + """Exception raised when a client sends more data then acceptable within limit. + + Depends on ``request.body.maxbytes`` config option if used within CherryPy + """ + + +class NoSSLError(Exception): + """Exception raised when a client speaks HTTP to an HTTPS socket.""" + + +class FatalSSLAlert(Exception): + """Exception raised when the SSL implementation signals a fatal alert.""" + + +def plat_specific_errors(*errnames): + """Return error numbers for all errors in errnames on this platform. + + The 'errno' module contains different global constants depending on + the specific platform (OS). This function will return the list of + numeric values for a given list of potential names. + """ + errno_names = dir(errno) + nums = [getattr(errno, k) for k in errnames if k in errno_names] + # de-dupe the list + return list(dict.fromkeys(nums).keys()) + + +socket_error_eintr = plat_specific_errors('EINTR', 'WSAEINTR') + +socket_errors_to_ignore = plat_specific_errors( + 'EPIPE', + 'EBADF', 'WSAEBADF', + 'ENOTSOCK', 'WSAENOTSOCK', + 'ETIMEDOUT', 'WSAETIMEDOUT', + 'ECONNREFUSED', 'WSAECONNREFUSED', + 'ECONNRESET', 'WSAECONNRESET', + 'ECONNABORTED', 'WSAECONNABORTED', + 'ENETRESET', 'WSAENETRESET', + 'EHOSTDOWN', 'EHOSTUNREACH', +) +socket_errors_to_ignore.append('timed out') +socket_errors_to_ignore.append('The read operation timed out') +socket_errors_nonblocking = plat_specific_errors( + 'EAGAIN', 'EWOULDBLOCK', 'WSAEWOULDBLOCK') + +if sys.platform == 'darwin': + socket_errors_to_ignore.extend(plat_specific_errors('EPROTOTYPE')) + socket_errors_nonblocking.extend(plat_specific_errors('EPROTOTYPE')) diff --git a/module/lib/wsgiserver/makefile.py b/module/lib/wsgiserver/makefile.py new file mode 100644 index 0000000000..145bc7170c --- /dev/null +++ b/module/lib/wsgiserver/makefile.py @@ -0,0 +1,384 @@ +"""Socket file object.""" + +import socket + +try: + # prefer slower Python-based io module + import _pyio as io +except ImportError: + # Python 2.6 + import io + +import six + +from . import errors + + +class BufferedWriter(io.BufferedWriter): + """Faux file object attached to a socket object.""" + + def write(self, b): + """Write bytes to buffer.""" + self._checkClosed() + if isinstance(b, str): + raise TypeError("can't write str to binary stream") + + with self._write_lock: + self._write_buf.extend(b) + self._flush_unlocked() + return len(b) + + def _flush_unlocked(self): + self._checkClosed('flush of closed file') + while self._write_buf: + try: + # ssl sockets only except 'bytes', not bytearrays + # so perhaps we should conditionally wrap this for perf? + n = self.raw.write(bytes(self._write_buf)) + except io.BlockingIOError as e: + n = e.characters_written + del self._write_buf[:n] + + +def MakeFile_PY3(sock, mode='r', bufsize=io.DEFAULT_BUFFER_SIZE): + """File object attached to a socket object.""" + if 'r' in mode: + return io.BufferedReader(socket.SocketIO(sock, mode), bufsize) + else: + return BufferedWriter(socket.SocketIO(sock, mode), bufsize) + + +class MakeFile_PY2(getattr(socket, '_fileobject', object)): + """Faux file object attached to a socket object.""" + + def __init__(self, *args, **kwargs): + """Initialize faux file object.""" + self.bytes_read = 0 + self.bytes_written = 0 + socket._fileobject.__init__(self, *args, **kwargs) + + def write(self, data): + """Sendall for non-blocking sockets.""" + while data: + try: + bytes_sent = self.send(data) + data = data[bytes_sent:] + except socket.error as e: + if e.args[0] not in errors.socket_errors_nonblocking: + raise + + def send(self, data): + """Send some part of message to the socket.""" + bytes_sent = self._sock.send(data) + self.bytes_written += bytes_sent + return bytes_sent + + def flush(self): + """Write all data from buffer to socket and reset write buffer.""" + if self._wbuf: + buffer = ''.join(self._wbuf) + self._wbuf = [] + self.write(buffer) + + def recv(self, size): + """Receive message of a size from the socket.""" + while True: + try: + data = self._sock.recv(size) + self.bytes_read += len(data) + return data + except socket.error as e: + what = ( + e.args[0] not in errors.socket_errors_nonblocking + and e.args[0] not in errors.socket_error_eintr + ) + if what: + raise + + class FauxSocket(object): + """Faux socket with the minimal interface required by pypy.""" + + def _reuse(self): + pass + + _fileobject_uses_str_type = six.PY2 and isinstance( + socket._fileobject(FauxSocket())._rbuf, six.string_types) + + # FauxSocket is no longer needed + del FauxSocket + + if not _fileobject_uses_str_type: + def read(self, size=-1): + """Read data from the socket to buffer.""" + # Use max, disallow tiny reads in a loop as they are very + # inefficient. + # We never leave read() with any leftover data from a new recv() + # call in our internal buffer. + rbufsize = max(self._rbufsize, self.default_bufsize) + # Our use of StringIO rather than lists of string objects returned + # by recv() minimizes memory usage and fragmentation that occurs + # when rbufsize is large compared to the typical return value of + # recv(). + buf = self._rbuf + buf.seek(0, 2) # seek end + if size < 0: + # Read until EOF + # reset _rbuf. we consume it via buf. + self._rbuf = io.BytesIO() + while True: + data = self.recv(rbufsize) + if not data: + break + buf.write(data) + return buf.getvalue() + else: + # Read until size bytes or EOF seen, whichever comes first + buf_len = buf.tell() + if buf_len >= size: + # Already have size bytes in our buffer? Extract and + # return. + buf.seek(0) + rv = buf.read(size) + self._rbuf = io.BytesIO() + self._rbuf.write(buf.read()) + return rv + + # reset _rbuf. we consume it via buf. + self._rbuf = io.BytesIO() + while True: + left = size - buf_len + # recv() will malloc the amount of memory given as its + # parameter even though it often returns much less data + # than that. The returned data string is short lived + # as we copy it into a StringIO and free it. This avoids + # fragmentation issues on many platforms. + data = self.recv(left) + if not data: + break + n = len(data) + if n == size and not buf_len: + # Shortcut. Avoid buffer data copies when: + # - We have no data in our buffer. + # AND + # - Our call to recv returned exactly the + # number of bytes we were asked to read. + return data + if n == left: + buf.write(data) + del data # explicit free + break + assert n <= left, 'recv(%d) returned %d bytes' % (left, n) + buf.write(data) + buf_len += n + del data # explicit free + # assert buf_len == buf.tell() + return buf.getvalue() + + def readline(self, size=-1): + """Read line from the socket to buffer.""" + buf = self._rbuf + buf.seek(0, 2) # seek end + if buf.tell() > 0: + # check if we already have it in our buffer + buf.seek(0) + bline = buf.readline(size) + if bline.endswith('\n') or len(bline) == size: + self._rbuf = io.BytesIO() + self._rbuf.write(buf.read()) + return bline + del bline + if size < 0: + # Read until \n or EOF, whichever comes first + if self._rbufsize <= 1: + # Speed up unbuffered case + buf.seek(0) + buffers = [buf.read()] + # reset _rbuf. we consume it via buf. + self._rbuf = io.BytesIO() + data = None + recv = self.recv + while data != '\n': + data = recv(1) + if not data: + break + buffers.append(data) + return ''.join(buffers) + + buf.seek(0, 2) # seek end + # reset _rbuf. we consume it via buf. + self._rbuf = io.BytesIO() + while True: + data = self.recv(self._rbufsize) + if not data: + break + nl = data.find('\n') + if nl >= 0: + nl += 1 + buf.write(data[:nl]) + self._rbuf.write(data[nl:]) + del data + break + buf.write(data) + return buf.getvalue() + else: + # Read until size bytes or \n or EOF seen, whichever comes + # first + buf.seek(0, 2) # seek end + buf_len = buf.tell() + if buf_len >= size: + buf.seek(0) + rv = buf.read(size) + self._rbuf = io.BytesIO() + self._rbuf.write(buf.read()) + return rv + # reset _rbuf. we consume it via buf. + self._rbuf = io.BytesIO() + while True: + data = self.recv(self._rbufsize) + if not data: + break + left = size - buf_len + # did we just receive a newline? + nl = data.find('\n', 0, left) + if nl >= 0: + nl += 1 + # save the excess data to _rbuf + self._rbuf.write(data[nl:]) + if buf_len: + buf.write(data[:nl]) + break + else: + # Shortcut. Avoid data copy through buf when + # returning a substring of our first recv(). + return data[:nl] + n = len(data) + if n == size and not buf_len: + # Shortcut. Avoid data copy through buf when + # returning exactly all of our first recv(). + return data + if n >= left: + buf.write(data[:left]) + self._rbuf.write(data[left:]) + break + buf.write(data) + buf_len += n + # assert buf_len == buf.tell() + return buf.getvalue() + else: + def read(self, size=-1): + """Read data from the socket to buffer.""" + if size < 0: + # Read until EOF + buffers = [self._rbuf] + self._rbuf = '' + if self._rbufsize <= 1: + recv_size = self.default_bufsize + else: + recv_size = self._rbufsize + + while True: + data = self.recv(recv_size) + if not data: + break + buffers.append(data) + return ''.join(buffers) + else: + # Read until size bytes or EOF seen, whichever comes first + data = self._rbuf + buf_len = len(data) + if buf_len >= size: + self._rbuf = data[size:] + return data[:size] + buffers = [] + if data: + buffers.append(data) + self._rbuf = '' + while True: + left = size - buf_len + recv_size = max(self._rbufsize, left) + data = self.recv(recv_size) + if not data: + break + buffers.append(data) + n = len(data) + if n >= left: + self._rbuf = data[left:] + buffers[-1] = data[:left] + break + buf_len += n + return ''.join(buffers) + + def readline(self, size=-1): + """Read line from the socket to buffer.""" + data = self._rbuf + if size < 0: + # Read until \n or EOF, whichever comes first + if self._rbufsize <= 1: + # Speed up unbuffered case + assert data == '' + buffers = [] + while data != '\n': + data = self.recv(1) + if not data: + break + buffers.append(data) + return ''.join(buffers) + nl = data.find('\n') + if nl >= 0: + nl += 1 + self._rbuf = data[nl:] + return data[:nl] + buffers = [] + if data: + buffers.append(data) + self._rbuf = '' + while True: + data = self.recv(self._rbufsize) + if not data: + break + buffers.append(data) + nl = data.find('\n') + if nl >= 0: + nl += 1 + self._rbuf = data[nl:] + buffers[-1] = data[:nl] + break + return ''.join(buffers) + else: + # Read until size bytes or \n or EOF seen, whichever comes + # first + nl = data.find('\n', 0, size) + if nl >= 0: + nl += 1 + self._rbuf = data[nl:] + return data[:nl] + buf_len = len(data) + if buf_len >= size: + self._rbuf = data[size:] + return data[:size] + buffers = [] + if data: + buffers.append(data) + self._rbuf = '' + while True: + data = self.recv(self._rbufsize) + if not data: + break + buffers.append(data) + left = size - buf_len + nl = data.find('\n', 0, left) + if nl >= 0: + nl += 1 + self._rbuf = data[nl:] + buffers[-1] = data[:nl] + break + n = len(data) + if n >= left: + self._rbuf = data[left:] + buffers[-1] = data[:left] + break + buf_len += n + return ''.join(buffers) + + +MakeFile = MakeFile_PY2 if six.PY2 else MakeFile_PY3 diff --git a/module/lib/wsgiserver/server.py b/module/lib/wsgiserver/server.py new file mode 100644 index 0000000000..c68b8bfd8d --- /dev/null +++ b/module/lib/wsgiserver/server.py @@ -0,0 +1,1800 @@ +""" +A high-speed, production ready, thread pooled, generic HTTP server. + +For those of you wanting to understand internals of this module, here's the +basic call flow. The server's listening thread runs a very tight loop, +sticking incoming connections onto a Queue:: + + server = HTTPServer(...) + server.start() + while True: + tick() + # This blocks until a request comes in: + child = socket.accept() + conn = HTTPConnection(child, ...) + server.requests.put(conn) + +Worker threads are kept in a pool and poll the Queue, popping off and then +handling each connection in turn. Each connection can consist of an arbitrary +number of requests and their responses, so we run a nested loop:: + + while True: + conn = server.requests.get() + conn.communicate() + -> while True: + req = HTTPRequest(...) + req.parse_request() + -> # Read the Request-Line, e.g. "GET /page HTTP/1.1" + req.rfile.readline() + read_headers(req.rfile, req.inheaders) + req.respond() + -> response = app(...) + try: + for chunk in response: + if chunk: + req.write(chunk) + finally: + if hasattr(response, "close"): + response.close() + if req.close_connection: + return + +And now for a trivial doctest to exercise the test suite + +>>> 'HTTPServer' in globals() +True + +""" + +import os +import io +import re +import email.utils +import socket +import sys +import time +import traceback as traceback_ +import logging +import platform + +import six +from six.moves import queue +from six.moves import urllib + +from . import errors, __version__ +from ._compat import bton, ntou +from .workers import threadpool +from .makefile import MakeFile + + +__all__ = ('HTTPRequest', 'HTTPConnection', 'HTTPServer', + 'SizeCheckWrapper', 'KnownLengthRFile', 'ChunkedRFile', + 'Gateway', 'get_ssl_adapter_class') + + +if 'win' in sys.platform and hasattr(socket, 'AF_INET6'): + if not hasattr(socket, 'IPPROTO_IPV6'): + socket.IPPROTO_IPV6 = 41 + if not hasattr(socket, 'IPV6_V6ONLY'): + socket.IPV6_V6ONLY = 27 + + +LF = b'\n' +CRLF = b'\r\n' +TAB = b'\t' +SPACE = b' ' +COLON = b':' +SEMICOLON = b';' +EMPTY = b'' +ASTERISK = b'*' +FORWARD_SLASH = b'/' +QUOTED_SLASH = b'%2F' +QUOTED_SLASH_REGEX = re.compile(b'(?i)' + QUOTED_SLASH) + + +comma_separated_headers = [ + b'Accept', b'Accept-Charset', b'Accept-Encoding', + b'Accept-Language', b'Accept-Ranges', b'Allow', b'Cache-Control', + b'Connection', b'Content-Encoding', b'Content-Language', b'Expect', + b'If-Match', b'If-None-Match', b'Pragma', b'Proxy-Authenticate', b'TE', + b'Trailer', b'Transfer-Encoding', b'Upgrade', b'Vary', b'Via', b'Warning', + b'WWW-Authenticate', +] + + +if not hasattr(logging, 'statistics'): + logging.statistics = {} + + +class HeaderReader(object): + """Object for reading headers from an HTTP request. + + Interface and default implementation. + """ + + def __call__(self, rfile, hdict=None): + """ + Read headers from the given stream into the given header dict. + + If hdict is None, a new header dict is created. Returns the populated + header dict. + + Headers which are repeated are folded together using a comma if their + specification so dictates. + + This function raises ValueError when the read bytes violate the HTTP + spec. + You should probably return "400 Bad Request" if this happens. + """ + if hdict is None: + hdict = {} + + while True: + line = rfile.readline() + if not line: + # No more data--illegal end of headers + raise ValueError('Illegal end of headers.') + + if line == CRLF: + # Normal end of headers + break + if not line.endswith(CRLF): + raise ValueError('HTTP requires CRLF terminators') + + if line[0] in (SPACE, TAB): + # It's a continuation line. + v = line.strip() + else: + try: + k, v = line.split(COLON, 1) + except ValueError: + raise ValueError('Illegal header line.') + v = v.strip() + k = self._transform_key(k) + hname = k + + if not self._allow_header(k): + continue + + if k in comma_separated_headers: + existing = hdict.get(hname) + if existing: + v = b', '.join((existing, v)) + hdict[hname] = v + + return hdict + + def _allow_header(self, key_name): + return True + + def _transform_key(self, key_name): + # TODO: what about TE and WWW-Authenticate? + return key_name.strip().title() + + +class DropUnderscoreHeaderReader(HeaderReader): + """Custom HeaderReader to exclude any headers with underscores in them.""" + + def _allow_header(self, key_name): + orig = super(DropUnderscoreHeaderReader, self)._allow_header(key_name) + return orig and '_' not in key_name + + +class SizeCheckWrapper(object): + """Wraps a file-like object, raising MaxSizeExceeded if too large.""" + + def __init__(self, rfile, maxlen): + """Initialize SizeCheckWrapper instance. + + Args: + rfile (file): file of a limited size + maxlen (int): maximum length of the file being read + """ + self.rfile = rfile + self.maxlen = maxlen + self.bytes_read = 0 + + def _check_length(self): + if self.maxlen and self.bytes_read > self.maxlen: + raise errors.MaxSizeExceeded() + + def read(self, size=None): + """Read a chunk from rfile buffer and return it. + + Args: + size (int): amount of data to read + + Returns: + bytes: Chunk from rfile, limited by size if specified. + """ + data = self.rfile.read(size) + self.bytes_read += len(data) + self._check_length() + return data + + def readline(self, size=None): + """Read a single line from rfile buffer and return it. + + Args: + size (int): minimum amount of data to read + + Returns: + bytes: One line from rfile. + """ + if size is not None: + data = self.rfile.readline(size) + self.bytes_read += len(data) + self._check_length() + return data + + # User didn't specify a size ... + # We read the line in chunks to make sure it's not a 100MB line ! + res = [] + while True: + data = self.rfile.readline(256) + self.bytes_read += len(data) + self._check_length() + res.append(data) + # See https://github.com/cherrypy/cherrypy/issues/421 + if len(data) < 256 or data[-1:] == LF: + return EMPTY.join(res) + + def readlines(self, sizehint=0): + """Read all lines from rfile buffer and return them. + + Args: + sizehint (int): hint of minimum amount of data to read + + Returns: + list[bytes]: Lines of bytes read from rfile. + """ + # Shamelessly stolen from StringIO + total = 0 + lines = [] + line = self.readline(sizehint) + while line: + lines.append(line) + total += len(line) + if 0 < sizehint <= total: + break + line = self.readline(sizehint) + return lines + + def close(self): + """Release resources allocated for rfile.""" + self.rfile.close() + + def __iter__(self): + """Return file iterator.""" + return self + + def __next__(self): + """Generate next file chunk.""" + data = next(self.rfile) + self.bytes_read += len(data) + self._check_length() + return data + + def next(self): + """Generate next file chunk.""" + data = self.rfile.next() + self.bytes_read += len(data) + self._check_length() + return data + + +class KnownLengthRFile(object): + """Wraps a file-like object, returning an empty string when exhausted.""" + + def __init__(self, rfile, content_length): + """Initialize KnownLengthRFile instance. + + Args: + rfile (file): file of a known size + content_length (int): length of the file being read + """ + self.rfile = rfile + self.remaining = content_length + + def read(self, size=None): + """Read a chunk from rfile buffer and return it. + + Args: + size (int): amount of data to read + + Returns: + bytes: Chunk from rfile, limited by size if specified. + """ + if self.remaining == 0: + return b'' + if size is None: + size = self.remaining + else: + size = min(size, self.remaining) + + data = self.rfile.read(size) + self.remaining -= len(data) + return data + + def readline(self, size=None): + """Read a single line from rfile buffer and return it. + + Args: + size (int): minimum amount of data to read + + Returns: + bytes: One line from rfile. + """ + if self.remaining == 0: + return b'' + if size is None: + size = self.remaining + else: + size = min(size, self.remaining) + + data = self.rfile.readline(size) + self.remaining -= len(data) + return data + + def readlines(self, sizehint=0): + """Read all lines from rfile buffer and return them. + + Args: + sizehint (int): hint of minimum amount of data to read + + Returns: + list[bytes]: Lines of bytes read from rfile. + """ + # Shamelessly stolen from StringIO + total = 0 + lines = [] + line = self.readline(sizehint) + while line: + lines.append(line) + total += len(line) + if 0 < sizehint <= total: + break + line = self.readline(sizehint) + return lines + + def close(self): + """Release resources allocated for rfile.""" + self.rfile.close() + + def __iter__(self): + """Return file iterator.""" + return self + + def __next__(self): + """Generate next file chunk.""" + data = next(self.rfile) + self.remaining -= len(data) + return data + + +class ChunkedRFile(object): + """Wraps a file-like object, returning an empty string when exhausted. + + This class is intended to provide a conforming wsgi.input value for + request entities that have been encoded with the 'chunked' transfer + encoding. + """ + + def __init__(self, rfile, maxlen, bufsize=8192): + """Initialize ChunkedRFile instance. + + Args: + rfile (file): file encoded with the 'chunked' transfer encoding + maxlen (int): maximum length of the file being read + bufsize (int): size of the buffer used to read the file + """ + self.rfile = rfile + self.maxlen = maxlen + self.bytes_read = 0 + self.buffer = EMPTY + self.bufsize = bufsize + self.closed = False + + def _fetch(self): + if self.closed: + return + + line = self.rfile.readline() + self.bytes_read += len(line) + + if self.maxlen and self.bytes_read > self.maxlen: + raise errors.MaxSizeExceeded( + 'Request Entity Too Large', self.maxlen) + + line = line.strip().split(SEMICOLON, 1) + + try: + chunk_size = line.pop(0) + chunk_size = int(chunk_size, 16) + except ValueError: + raise ValueError('Bad chunked transfer size: ' + repr(chunk_size)) + + if chunk_size <= 0: + self.closed = True + return + +# if line: chunk_extension = line[0] + + if self.maxlen and self.bytes_read + chunk_size > self.maxlen: + raise IOError('Request Entity Too Large') + + chunk = self.rfile.read(chunk_size) + self.bytes_read += len(chunk) + self.buffer += chunk + + crlf = self.rfile.read(2) + if crlf != CRLF: + raise ValueError( + "Bad chunked transfer coding (expected '\\r\\n', " + 'got ' + repr(crlf) + ')') + + def read(self, size=None): + """Read a chunk from rfile buffer and return it. + + Args: + size (int): amount of data to read + + Returns: + bytes: Chunk from rfile, limited by size if specified. + """ + data = EMPTY + + if size == 0: + return data + + while True: + if size and len(data) >= size: + return data + + if not self.buffer: + self._fetch() + if not self.buffer: + # EOF + return data + + if size: + remaining = size - len(data) + data += self.buffer[:remaining] + self.buffer = self.buffer[remaining:] + else: + data += self.buffer + self.buffer = EMPTY + + def readline(self, size=None): + """Read a single line from rfile buffer and return it. + + Args: + size (int): minimum amount of data to read + + Returns: + bytes: One line from rfile. + """ + data = EMPTY + + if size == 0: + return data + + while True: + if size and len(data) >= size: + return data + + if not self.buffer: + self._fetch() + if not self.buffer: + # EOF + return data + + newline_pos = self.buffer.find(LF) + if size: + if newline_pos == -1: + remaining = size - len(data) + data += self.buffer[:remaining] + self.buffer = self.buffer[remaining:] + else: + remaining = min(size - len(data), newline_pos) + data += self.buffer[:remaining] + self.buffer = self.buffer[remaining:] + else: + if newline_pos == -1: + data += self.buffer + self.buffer = EMPTY + else: + data += self.buffer[:newline_pos] + self.buffer = self.buffer[newline_pos:] + + def readlines(self, sizehint=0): + """Read all lines from rfile buffer and return them. + + Args: + sizehint (int): hint of minimum amount of data to read + + Returns: + list[bytes]: Lines of bytes read from rfile. + """ + # Shamelessly stolen from StringIO + total = 0 + lines = [] + line = self.readline(sizehint) + while line: + lines.append(line) + total += len(line) + if 0 < sizehint <= total: + break + line = self.readline(sizehint) + return lines + + def read_trailer_lines(self): + """Read HTTP headers and yield them. + + Returns: + Generator: yields CRLF separated lines. + """ + if not self.closed: + raise ValueError( + 'Cannot read trailers until the request body has been read.') + + while True: + line = self.rfile.readline() + if not line: + # No more data--illegal end of headers + raise ValueError('Illegal end of headers.') + + self.bytes_read += len(line) + if self.maxlen and self.bytes_read > self.maxlen: + raise IOError('Request Entity Too Large') + + if line == CRLF: + # Normal end of headers + break + if not line.endswith(CRLF): + raise ValueError('HTTP requires CRLF terminators') + + yield line + + def close(self): + """Release resources allocated for rfile.""" + self.rfile.close() + + +class HTTPRequest(object): + """An HTTP Request (and response). + + A single HTTP connection may consist of multiple request/response pairs. + """ + + server = None + """The HTTPServer object which is receiving this request.""" + + conn = None + """The HTTPConnection object on which this request connected.""" + + inheaders = {} + """A dict of request headers.""" + + outheaders = [] + """A list of header tuples to write in the response.""" + + ready = False + """When True, the request has been parsed and is ready to begin generating + the response. When False, signals the calling Connection that the response + should not be generated and the connection should close.""" + + close_connection = False + """Signals the calling Connection that the request should close. This does + not imply an error! The client and/or server may each request that the + connection be closed.""" + + chunked_write = False + """If True, output will be encoded with the "chunked" transfer-coding. + + This value is set automatically inside send_headers.""" + + header_reader = HeaderReader() + """ + A HeaderReader instance or compatible reader. + """ + + def __init__(self, server, conn, proxy_mode=False, strict_mode=True): + """Initialize HTTP request container instance. + + Args: + server (HTTPServer): web server object receiving this request + conn (HTTPConnection): HTTP connection object for this request + proxy_mode (bool): whether this HTTPServer should behave as a PROXY + server for certain requests + strict_mode (bool): whether we should return a 400 Bad Request when + we encounter a request that a HTTP compliant client should not be + making + """ + self.server = server + self.conn = conn + + self.ready = False + self.started_request = False + self.scheme = b'http' + if self.server.ssl_adapter is not None: + self.scheme = b'https' + # Use the lowest-common protocol in case read_request_line errors. + self.response_protocol = 'HTTP/1.0' + self.inheaders = {} + + self.status = '' + self.outheaders = [] + self.sent_headers = False + self.close_connection = self.__class__.close_connection + self.chunked_read = False + self.chunked_write = self.__class__.chunked_write + self.proxy_mode = proxy_mode + self.strict_mode = strict_mode + + def parse_request(self): + """Parse the next HTTP request start-line and message-headers.""" + self.rfile = SizeCheckWrapper(self.conn.rfile, + self.server.max_request_header_size) + try: + success = self.read_request_line() + except errors.MaxSizeExceeded: + self.simple_response( + '414 Request-URI Too Long', + 'The Request-URI sent with the request exceeds the maximum ' + 'allowed bytes.') + return + else: + if not success: + return + + try: + success = self.read_request_headers() + except errors.MaxSizeExceeded: + self.simple_response( + '413 Request Entity Too Large', + 'The headers sent with the request exceed the maximum ' + 'allowed bytes.') + return + else: + if not success: + return + + self.ready = True + + def read_request_line(self): + """Read and parse first line of the HTTP request. + + Returns: + bool: True if the request line is valid or False if it's malformed. + """ + # HTTP/1.1 connections are persistent by default. If a client + # requests a page, then idles (leaves the connection open), + # then rfile.readline() will raise socket.error("timed out"). + # Note that it does this based on the value given to settimeout(), + # and doesn't need the client to request or acknowledge the close + # (although your TCP stack might suffer for it: cf Apache's history + # with FIN_WAIT_2). + request_line = self.rfile.readline() + + # Set started_request to True so communicate() knows to send 408 + # from here on out. + self.started_request = True + if not request_line: + return False + + if request_line == CRLF: + # RFC 2616 sec 4.1: "...if the server is reading the protocol + # stream at the beginning of a message and receives a CRLF + # first, it should ignore the CRLF." + # But only ignore one leading line! else we enable a DoS. + request_line = self.rfile.readline() + if not request_line: + return False + + if not request_line.endswith(CRLF): + self.simple_response( + '400 Bad Request', 'HTTP requires CRLF terminators') + return False + + try: + method, uri, req_protocol = request_line.strip().split(SPACE, 2) + if not req_protocol.startswith(b'HTTP/'): + self.simple_response( + '400 Bad Request', 'Malformed Request-Line: bad protocol' + ) + return False + rp = req_protocol[5:].split(b'.', 1) + rp = tuple(map(int, rp)) # Minor.Major must be threat as integers + if rp > (1, 1): + self.simple_response( + '505 HTTP Version Not Supported', 'Cannot fulfill request' + ) + return False + except (ValueError, IndexError): + self.simple_response('400 Bad Request', 'Malformed Request-Line') + return False + + self.uri = uri + self.method = method.upper() + + if self.strict_mode and method != self.method: + resp = ( + 'Malformed method name: According to RFC 2616 ' + '(section 5.1.1) and its successors ' + 'RFC 7230 (section 3.1.1) and RFC 7231 (section 4.1) ' + 'method names are case-sensitive and uppercase.' + ) + self.simple_response('400 Bad Request', resp) + return False + + try: + if six.PY2: # FIXME: Figure out better way to do this + # Ref: https://stackoverflow.com/a/196392/595220 (like this?) + """This is a dummy check for unicode in URI.""" + ntou(bton(uri, 'ascii'), 'ascii') + scheme, authority, path, qs, fragment = urllib.parse.urlsplit(uri) + except UnicodeError: + self.simple_response('400 Bad Request', 'Malformed Request-URI') + return False + + if self.method == b'OPTIONS': + # TODO: cover this branch with tests + path = (uri + # https://tools.ietf.org/html/rfc7230#section-5.3.4 + if self.proxy_mode or uri == ASTERISK + else path) + elif self.method == b'CONNECT': + # TODO: cover this branch with tests + if not self.proxy_mode: + self.simple_response('405 Method Not Allowed') + return False + + # `urlsplit()` above parses "example.com:3128" as path part of URI. + # this is a workaround, which makes it detect netloc correctly + uri_split = urllib.parse.urlsplit(b'//' + uri) + _scheme, _authority, _path, _qs, _fragment = uri_split + _port = EMPTY + try: + _port = uri_split.port + except ValueError: + pass + + # FIXME: use third-party validation to make checks against RFC + # the validation doesn't take into account, that urllib parses + # invalid URIs without raising errors + # https://tools.ietf.org/html/rfc7230#section-5.3.3 + invalid_path = ( + _authority != uri + or not _port + or any((_scheme, _path, _qs, _fragment)) + ) + if invalid_path: + self.simple_response('400 Bad Request', + 'Invalid path in Request-URI: request-' + 'target must match authority-form.') + return False + + authority = path = _authority + scheme = qs = fragment = EMPTY + else: + uri_is_absolute_form = (scheme or authority) + + disallowed_absolute = ( + self.strict_mode + and not self.proxy_mode + and uri_is_absolute_form + ) + if disallowed_absolute: + # https://tools.ietf.org/html/rfc7230#section-5.3.2 + # (absolute form) + """Absolute URI is only allowed within proxies.""" + self.simple_response( + '400 Bad Request', + 'Absolute URI not allowed if server is not a proxy.', + ) + return False + + invalid_path = ( + self.strict_mode + and not uri.startswith(FORWARD_SLASH) + and not uri_is_absolute_form + ) + if invalid_path: + # https://tools.ietf.org/html/rfc7230#section-5.3.1 + # (origin_form) and + """Path should start with a forward slash.""" + resp = ( + 'Invalid path in Request-URI: request-target must contain ' + 'origin-form which starts with absolute-path (URI ' + 'starting with a slash "/").' + ) + self.simple_response('400 Bad Request', resp) + return False + + if fragment: + self.simple_response('400 Bad Request', + 'Illegal #fragment in Request-URI.') + return False + + if path is None: + # FIXME: It looks like this case cannot happen + self.simple_response('400 Bad Request', + 'Invalid path in Request-URI.') + return False + + # Unquote the path+params (e.g. "/this%20path" -> "/this path"). + # http://www.w3.org/Protocols/rfc2616/rfc2616-sec5.html#sec5.1.2 + # + # But note that "...a URI must be separated into its components + # before the escaped characters within those components can be + # safely decoded." http://www.ietf.org/rfc/rfc2396.txt, sec 2.4.2 + # Therefore, "/this%2Fpath" becomes "/this%2Fpath", not + # "/this/path". + try: + # TODO: Figure out whether exception can really happen here. + # It looks like it's caught on urlsplit() call above. + atoms = [ + urllib.parse.unquote_to_bytes(x) + for x in QUOTED_SLASH_REGEX.split(path) + ] + except ValueError as ex: + self.simple_response('400 Bad Request', ex.args[0]) + return False + path = QUOTED_SLASH.join(atoms) + + if not path.startswith(FORWARD_SLASH): + path = FORWARD_SLASH + path + + if scheme is not EMPTY: + self.scheme = scheme + self.authority = authority + self.path = path + + # Note that, like wsgiref and most other HTTP servers, + # we "% HEX HEX"-unquote the path but not the query string. + self.qs = qs + + # Compare request and server HTTP protocol versions, in case our + # server does not support the requested protocol. Limit our output + # to min(req, server). We want the following output: + # request server actual written supported response + # protocol protocol response protocol feature set + # a 1.0 1.0 1.0 1.0 + # b 1.0 1.1 1.1 1.0 + # c 1.1 1.0 1.0 1.0 + # d 1.1 1.1 1.1 1.1 + # Notice that, in (b), the response will be "HTTP/1.1" even though + # the client only understands 1.0. RFC 2616 10.5.6 says we should + # only return 505 if the _major_ version is different. + sp = int(self.server.protocol[5]), int(self.server.protocol[7]) + + if sp[0] != rp[0]: + self.simple_response('505 HTTP Version Not Supported') + return False + + self.request_protocol = req_protocol + self.response_protocol = 'HTTP/%s.%s' % min(rp, sp) + + return True + + def read_request_headers(self): + """Read self.rfile into self.inheaders. Return success.""" + # then all the http headers + try: + self.header_reader(self.rfile, self.inheaders) + except ValueError as ex: + self.simple_response('400 Bad Request', ex.args[0]) + return False + + mrbs = self.server.max_request_body_size + if mrbs and int(self.inheaders.get(b'Content-Length', 0)) > mrbs: + self.simple_response( + '413 Request Entity Too Large', + 'The entity sent with the request exceeds the maximum ' + 'allowed bytes.') + return False + + # Persistent connection support + if self.response_protocol == 'HTTP/1.1': + # Both server and client are HTTP/1.1 + if self.inheaders.get(b'Connection', b'') == b'close': + self.close_connection = True + else: + # Either the server or client (or both) are HTTP/1.0 + if self.inheaders.get(b'Connection', b'') != b'Keep-Alive': + self.close_connection = True + + # Transfer-Encoding support + te = None + if self.response_protocol == 'HTTP/1.1': + te = self.inheaders.get(b'Transfer-Encoding') + if te: + te = [x.strip().lower() for x in te.split(b',') if x.strip()] + + self.chunked_read = False + + if te: + for enc in te: + if enc == b'chunked': + self.chunked_read = True + else: + # Note that, even if we see "chunked", we must reject + # if there is an extension we don't recognize. + self.simple_response('501 Unimplemented') + self.close_connection = True + return False + + # From PEP 333: + # "Servers and gateways that implement HTTP 1.1 must provide + # transparent support for HTTP 1.1's "expect/continue" mechanism. + # This may be done in any of several ways: + # 1. Respond to requests containing an Expect: 100-continue request + # with an immediate "100 Continue" response, and proceed normally. + # 2. Proceed with the request normally, but provide the application + # with a wsgi.input stream that will send the "100 Continue" + # response if/when the application first attempts to read from + # the input stream. The read request must then remain blocked + # until the client responds. + # 3. Wait until the client decides that the server does not support + # expect/continue, and sends the request body on its own. + # (This is suboptimal, and is not recommended.) + # + # We used to do 3, but are now doing 1. Maybe we'll do 2 someday, + # but it seems like it would be a big slowdown for such a rare case. + if self.inheaders.get(b'Expect', b'') == b'100-continue': + # Don't use simple_response here, because it emits headers + # we don't want. See + # https://github.com/cherrypy/cherrypy/issues/951 + msg = self.server.protocol.encode('ascii') + msg += b' 100 Continue\r\n\r\n' + try: + self.conn.wfile.write(msg) + except socket.error as ex: + if ex.args[0] not in errors.socket_errors_to_ignore: + raise + return True + + def respond(self): + """Call the gateway and write its iterable output.""" + mrbs = self.server.max_request_body_size + if self.chunked_read: + self.rfile = ChunkedRFile(self.conn.rfile, mrbs) + else: + cl = int(self.inheaders.get(b'Content-Length', 0)) + if mrbs and mrbs < cl: + if not self.sent_headers: + self.simple_response( + '413 Request Entity Too Large', + 'The entity sent with the request exceeds the ' + 'maximum allowed bytes.') + return + self.rfile = KnownLengthRFile(self.conn.rfile, cl) + + self.server.gateway(self).respond() + + if (self.ready and not self.sent_headers): + self.sent_headers = True + self.send_headers() + if self.chunked_write: + self.conn.wfile.write(b'0\r\n\r\n') + + def simple_response(self, status, msg=''): + """Write a simple response back to the client.""" + status = str(status) + proto_status = '%s %s\r\n' % (self.server.protocol, status) + content_length = 'Content-Length: %s\r\n' % len(msg) + content_type = 'Content-Type: text/plain\r\n' + buf = [ + proto_status.encode('ISO-8859-1'), + content_length.encode('ISO-8859-1'), + content_type.encode('ISO-8859-1'), + ] + + if status[:3] in ('413', '414'): + # Request Entity Too Large / Request-URI Too Long + self.close_connection = True + if self.response_protocol == 'HTTP/1.1': + # This will not be true for 414, since read_request_line + # usually raises 414 before reading the whole line, and we + # therefore cannot know the proper response_protocol. + buf.append(b'Connection: close\r\n') + else: + # HTTP/1.0 had no 413/414 status nor Connection header. + # Emit 400 instead and trust the message body is enough. + status = '400 Bad Request' + + buf.append(CRLF) + if msg: + if isinstance(msg, six.text_type): + msg = msg.encode('ISO-8859-1') + buf.append(msg) + + try: + self.conn.wfile.write(EMPTY.join(buf)) + except socket.error as ex: + if ex.args[0] not in errors.socket_errors_to_ignore: + raise + + def write(self, chunk): + """Write unbuffered data to the client.""" + if self.chunked_write and chunk: + chunk_size_hex = hex(len(chunk))[2:].encode('ascii') + buf = [chunk_size_hex, CRLF, chunk, CRLF] + self.conn.wfile.write(EMPTY.join(buf)) + else: + self.conn.wfile.write(chunk) + + def send_headers(self): + """Assert, process, and send the HTTP response message-headers. + + You must set self.status, and self.outheaders before calling this. + """ + hkeys = [key.lower() for key, value in self.outheaders] + status = int(self.status[:3]) + + if status == 413: + # Request Entity Too Large. Close conn to avoid garbage. + self.close_connection = True + elif b'content-length' not in hkeys: + # "All 1xx (informational), 204 (no content), + # and 304 (not modified) responses MUST NOT + # include a message-body." So no point chunking. + if status < 200 or status in (204, 205, 304): + pass + else: + needs_chunked = ( + self.response_protocol == 'HTTP/1.1' + and self.method != b'HEAD' + ) + if needs_chunked: + # Use the chunked transfer-coding + self.chunked_write = True + self.outheaders.append((b'Transfer-Encoding', b'chunked')) + else: + # Closing the conn is the only way to determine len. + self.close_connection = True + + if b'connection' not in hkeys: + if self.response_protocol == 'HTTP/1.1': + # Both server and client are HTTP/1.1 or better + if self.close_connection: + self.outheaders.append((b'Connection', b'close')) + else: + # Server and/or client are HTTP/1.0 + if not self.close_connection: + self.outheaders.append((b'Connection', b'Keep-Alive')) + + if (not self.close_connection) and (not self.chunked_read): + # Read any remaining request body data on the socket. + # "If an origin server receives a request that does not include an + # Expect request-header field with the "100-continue" expectation, + # the request includes a request body, and the server responds + # with a final status code before reading the entire request body + # from the transport connection, then the server SHOULD NOT close + # the transport connection until it has read the entire request, + # or until the client closes the connection. Otherwise, the client + # might not reliably receive the response message. However, this + # requirement is not be construed as preventing a server from + # defending itself against denial-of-service attacks, or from + # badly broken client implementations." + remaining = getattr(self.rfile, 'remaining', 0) + if remaining > 0: + self.rfile.read(remaining) + + if b'date' not in hkeys: + self.outheaders.append(( + b'Date', + email.utils.formatdate(usegmt=True).encode('ISO-8859-1'), + )) + + if b'server' not in hkeys: + self.outheaders.append(( + b'Server', + self.server.server_name.encode('ISO-8859-1'), + )) + + proto = self.server.protocol.encode('ascii') + buf = [proto + SPACE + self.status + CRLF] + for k, v in self.outheaders: + buf.append(k + COLON + SPACE + v + CRLF) + buf.append(CRLF) + self.conn.wfile.write(EMPTY.join(buf)) + + +class HTTPConnection(object): + """An HTTP connection (active socket).""" + + remote_addr = None + remote_port = None + ssl_env = None + rbufsize = io.DEFAULT_BUFFER_SIZE + wbufsize = io.DEFAULT_BUFFER_SIZE + RequestHandlerClass = HTTPRequest + + def __init__(self, server, sock, makefile=MakeFile): + """Initialize HTTPConnection instance. + + Args: + server (HTTPServer): web server object receiving this request + socket (socket._socketobject): the raw socket object (usually + TCP) for this connection + makefile (file): a fileobject class for reading from the socket + """ + self.server = server + self.socket = sock + self.rfile = makefile(sock, 'rb', self.rbufsize) + self.wfile = makefile(sock, 'wb', self.wbufsize) + self.requests_seen = 0 + + def communicate(self): + """Read each request and respond appropriately.""" + request_seen = False + try: + while True: + # (re)set req to None so that if something goes wrong in + # the RequestHandlerClass constructor, the error doesn't + # get written to the previous request. + req = None + req = self.RequestHandlerClass(self.server, self) + + # This order of operations should guarantee correct pipelining. + req.parse_request() + if self.server.stats['Enabled']: + self.requests_seen += 1 + if not req.ready: + # Something went wrong in the parsing (and the server has + # probably already made a simple_response). Return and + # let the conn close. + return + + request_seen = True + req.respond() + if req.close_connection: + return + except socket.error as ex: + errnum = ex.args[0] + # sadly SSL sockets return a different (longer) time out string + timeout_errs = 'timed out', 'The read operation timed out' + if errnum in timeout_errs: + # Don't error if we're between requests; only error + # if 1) no request has been started at all, or 2) we're + # in the middle of a request. + # See https://github.com/cherrypy/cherrypy/issues/853 + if (not request_seen) or (req and req.started_request): + self._conditional_error(req, '408 Request Timeout') + elif errnum not in errors.socket_errors_to_ignore: + self.server.error_log('socket.error %s' % repr(errnum), + level=logging.WARNING, traceback=True) + self._conditional_error(req, '500 Internal Server Error') + except (KeyboardInterrupt, SystemExit): + raise + except errors.FatalSSLAlert: + pass + except errors.NoSSLError: + self._handle_no_ssl(req) + except Exception as ex: + self.server.error_log( + repr(ex), level=logging.ERROR, traceback=True) + self._conditional_error(req, '500 Internal Server Error') + + linger = False + + def _handle_no_ssl(self, req): + if not req or req.sent_headers: + return + # Unwrap wfile + self.wfile = MakeFile(self.socket._sock, 'wb', self.wbufsize) + msg = ( + 'The client sent a plain HTTP request, but ' + 'this server only speaks HTTPS on this port.' + ) + req.simple_response('400 Bad Request', msg) + self.linger = True + + def _conditional_error(self, req, response): + """Respond with an error. + + Don't bother writing if a response + has already started being written. + """ + if not req or req.sent_headers: + return + + try: + req.simple_response('408 Request Timeout') + except errors.FatalSSLAlert: + pass + except errors.NoSSLError: + self._handle_no_ssl(req) + + def close(self): + """Close the socket underlying this connection.""" + self.rfile.close() + + if not self.linger: + self._close_kernel_socket() + self.socket.close() + else: + # On the other hand, sometimes we want to hang around for a bit + # to make sure the client has a chance to read our entire + # response. Skipping the close() calls here delays the FIN + # packet until the socket object is garbage-collected later. + # Someday, perhaps, we'll do the full lingering_close that + # Apache does, but not today. + pass + + def _close_kernel_socket(self): + """Close kernel socket in outdated Python versions. + + On old Python versions, + Python's socket module does NOT call close on the kernel + socket when you call socket.close(). We do so manually here + because we want this server to send a FIN TCP segment + immediately. Note this must be called *before* calling + socket.close(), because the latter drops its reference to + the kernel socket. + """ + if six.PY2 and hasattr(self.socket, '_sock'): + self.socket._sock.close() + + +try: + import fcntl +except ImportError: + try: + from ctypes import windll, WinError + import ctypes.wintypes + _SetHandleInformation = windll.kernel32.SetHandleInformation + _SetHandleInformation.argtypes = [ + ctypes.wintypes.HANDLE, + ctypes.wintypes.DWORD, + ctypes.wintypes.DWORD, + ] + _SetHandleInformation.restype = ctypes.wintypes.BOOL + except ImportError: + def prevent_socket_inheritance(sock): + """Dummy function, since neither fcntl nor ctypes are available.""" + pass + else: + def prevent_socket_inheritance(sock): + """Mark the given socket fd as non-inheritable (Windows).""" + if not _SetHandleInformation(sock.fileno(), 1, 0): + raise WinError() +else: + def prevent_socket_inheritance(sock): + """Mark the given socket fd as non-inheritable (POSIX).""" + fd = sock.fileno() + old_flags = fcntl.fcntl(fd, fcntl.F_GETFD) + fcntl.fcntl(fd, fcntl.F_SETFD, old_flags | fcntl.FD_CLOEXEC) + + +class HTTPServer(object): + """An HTTP server.""" + + _bind_addr = '127.0.0.1' + _interrupt = None + + gateway = None + """A Gateway instance.""" + + minthreads = None + """The minimum number of worker threads to create (default 10).""" + + maxthreads = None + """The maximum number of worker threads to create. + + (default -1 = no limit)""" + + server_name = None + """The name of the server; defaults to ``self.version``.""" + + protocol = 'HTTP/1.1' + """The version string to write in the Status-Line of all HTTP responses. + + For example, "HTTP/1.1" is the default. This also limits the supported + features used in the response.""" + + request_queue_size = 5 + """The 'backlog' arg to socket.listen(); max queued connections. + + (default 5).""" + + shutdown_timeout = 5 + """The total time to wait for worker threads to cleanly exit. + + Specified in seconds.""" + + timeout = 10 + """The timeout in seconds for accepted connections (default 10).""" + + version = 'Cheroot/' + __version__ + """A version string for the HTTPServer.""" + + software = None + """The value to set for the SERVER_SOFTWARE entry in the WSGI environ. + + If None, this defaults to ``'%s Server' % self.version``. + """ + + ready = False + """Internal flag which indicating the socket is accepting connections.""" + + max_request_header_size = 0 + """The maximum size, in bytes, for request headers, or 0 for no limit.""" + + max_request_body_size = 0 + """The maximum size, in bytes, for request bodies, or 0 for no limit.""" + + nodelay = True + """If True (the default since 3.1), sets the TCP_NODELAY socket option.""" + + ConnectionClass = HTTPConnection + """The class to use for handling HTTP connections.""" + + ssl_adapter = None + """An instance of ssl.Adapter (or a subclass). + + You must have the corresponding SSL driver library installed. + """ + + def __init__( + self, bind_addr, gateway, minthreads=10, maxthreads=-1, + server_name=None): + """Initialize HTTPServer instance. + + Args: + bind_addr (tuple): network interface to listen to + gateway (Gateway): gateway for processing HTTP requests + minthreads (int): minimum number of threads for HTTP thread pool + maxthreads (int): maximum number of threads for HTTP thread pool + server_name (str): web server name to be advertised via Server + HTTP header + """ + self.bind_addr = bind_addr + self.gateway = gateway + + self.requests = threadpool.ThreadPool( + self, min=minthreads or 1, max=maxthreads) + + if not server_name: + server_name = self.version + self.server_name = server_name + self.clear_stats() + + def clear_stats(self): + """Reset server stat counters..""" + self._start_time = None + self._run_time = 0 + self.stats = { + 'Enabled': False, + 'Bind Address': lambda s: repr(self.bind_addr), + 'Run time': lambda s: (not s['Enabled']) and -1 or self.runtime(), + 'Accepts': 0, + 'Accepts/sec': lambda s: s['Accepts'] / self.runtime(), + 'Queue': lambda s: getattr(self.requests, 'qsize', None), + 'Threads': lambda s: len(getattr(self.requests, '_threads', [])), + 'Threads Idle': lambda s: getattr(self.requests, 'idle', None), + 'Socket Errors': 0, + 'Requests': lambda s: (not s['Enabled']) and -1 or sum( + [w['Requests'](w) for w in s['Worker Threads'].values()], 0), + 'Bytes Read': lambda s: (not s['Enabled']) and -1 or sum( + [w['Bytes Read'](w) for w in s['Worker Threads'].values()], 0), + 'Bytes Written': lambda s: (not s['Enabled']) and -1 or sum( + [w['Bytes Written'](w) for w in s['Worker Threads'].values()], + 0), + 'Work Time': lambda s: (not s['Enabled']) and -1 or sum( + [w['Work Time'](w) for w in s['Worker Threads'].values()], 0), + 'Read Throughput': lambda s: (not s['Enabled']) and -1 or sum( + [w['Bytes Read'](w) / (w['Work Time'](w) or 1e-6) + for w in s['Worker Threads'].values()], 0), + 'Write Throughput': lambda s: (not s['Enabled']) and -1 or sum( + [w['Bytes Written'](w) / (w['Work Time'](w) or 1e-6) + for w in s['Worker Threads'].values()], 0), + 'Worker Threads': {}, + } + logging.statistics['Cheroot HTTPServer %d' % id(self)] = self.stats + + def runtime(self): + """Return server uptime.""" + if self._start_time is None: + return self._run_time + else: + return self._run_time + (time.time() - self._start_time) + + def __str__(self): + """Render Server instance representing bind address.""" + return '%s.%s(%r)' % (self.__module__, self.__class__.__name__, + self.bind_addr) + + def _get_bind_addr(self): + return self._bind_addr + + def _set_bind_addr(self, value): + if isinstance(value, tuple) and value[0] in ('', None): + # Despite the socket module docs, using '' does not + # allow AI_PASSIVE to work. Passing None instead + # returns '0.0.0.0' like we want. In other words: + # host AI_PASSIVE result + # '' Y 192.168.x.y + # '' N 192.168.x.y + # None Y 0.0.0.0 + # None N 127.0.0.1 + # But since you can get the same effect with an explicit + # '0.0.0.0', we deny both the empty string and None as values. + raise ValueError("Host values of '' or None are not allowed. " + "Use '0.0.0.0' (IPv4) or '::' (IPv6) instead " + 'to listen on all active interfaces.') + self._bind_addr = value + bind_addr = property( + _get_bind_addr, + _set_bind_addr, + doc="""The interface on which to listen for connections. + + For TCP sockets, a (host, port) tuple. Host values may be any IPv4 + or IPv6 address, or any valid hostname. The string 'localhost' is a + synonym for '127.0.0.1' (or '::1', if your hosts file prefers IPv6). + The string '0.0.0.0' is a special IPv4 entry meaning "any active + interface" (INADDR_ANY), and '::' is the similar IN6ADDR_ANY for + IPv6. The empty string or None are not allowed. + + For UNIX sockets, supply the filename as a string. + + Systemd socket activation is automatic and doesn't require tempering + with this variable""") + + def safe_start(self): + """Run the server forever, and stop it cleanly on exit.""" + try: + self.start() + except (KeyboardInterrupt, IOError): + # The time.sleep call might raise + # "IOError: [Errno 4] Interrupted function call" on KBInt. + self.error_log('Keyboard Interrupt: shutting down') + self.stop() + raise + except SystemExit: + self.error_log('SystemExit raised: shutting down') + self.stop() + raise + + def start(self): + """Run the server forever.""" + # We don't have to trap KeyboardInterrupt or SystemExit here, + # because cherrpy.server already does so, calling self.stop() for us. + # If you're using this server with another framework, you should + # trap those exceptions in whatever code block calls start(). + self._interrupt = None + + if self.software is None: + self.software = '%s Server' % self.version + + # Select the appropriate socket + self.socket = None + if os.getenv('LISTEN_PID', None): + # systemd socket activation + self.socket = socket.fromfd(3, socket.AF_INET, socket.SOCK_STREAM) + elif isinstance(self.bind_addr, six.string_types): + # AF_UNIX socket + + # So we can reuse the socket... + try: + os.unlink(self.bind_addr) + except Exception: + pass + + # So everyone can access the socket... + try: + os.chmod(self.bind_addr, 0o777) + except Exception: + pass + + info = [ + (socket.AF_UNIX, socket.SOCK_STREAM, 0, '', self.bind_addr)] + else: + # AF_INET or AF_INET6 socket + # Get the correct address family for our host (allows IPv6 + # addresses) + host, port = self.bind_addr + try: + info = socket.getaddrinfo( + host, port, socket.AF_UNSPEC, + socket.SOCK_STREAM, 0, socket.AI_PASSIVE) + except socket.gaierror: + if ':' in self.bind_addr[0]: + info = [(socket.AF_INET6, socket.SOCK_STREAM, + 0, '', self.bind_addr + (0, 0))] + else: + info = [(socket.AF_INET, socket.SOCK_STREAM, + 0, '', self.bind_addr)] + + if not self.socket: + msg = 'No socket could be created' + for res in info: + af, socktype, proto, canonname, sa = res + try: + self.bind(af, socktype, proto) + break + except socket.error as serr: + msg = '%s -- (%s: %s)' % (msg, sa, serr) + if self.socket: + self.socket.close() + self.socket = None + + if not self.socket: + raise socket.error(msg) + + # Timeout so KeyboardInterrupt can be caught on Win32 + self.socket.settimeout(1) + self.socket.listen(self.request_queue_size) + + # Create worker threads + self.requests.start() + + self.ready = True + self._start_time = time.time() + while self.ready: + try: + self.tick() + except (KeyboardInterrupt, SystemExit): + raise + except Exception: + self.error_log('Error in HTTPServer.tick', level=logging.ERROR, + traceback=True) + + if self.interrupt: + while self.interrupt is True: + # Wait for self.stop() to complete. See _set_interrupt. + time.sleep(0.1) + if self.interrupt: + raise self.interrupt + + def error_log(self, msg='', level=20, traceback=False): + """Write error message to log. + + Args: + msg (str): error message + level (int): logging level + traceback (bool): add traceback to output or not + """ + # Override this in subclasses as desired + sys.stderr.write(msg + '\n') + sys.stderr.flush() + if traceback: + tblines = traceback_.format_exc() + sys.stderr.write(tblines) + sys.stderr.flush() + + def bind(self, family, type, proto=0): + """Create (or recreate) the actual socket object.""" + self.socket = socket.socket(family, type, proto) + prevent_socket_inheritance(self.socket) + if platform.system() != 'Windows': + # Windows has different semantics for SO_REUSEADDR, + # so don't set it. + # https://msdn.microsoft.com/en-us/library/ms740621(v=vs.85).aspx + self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + if self.nodelay and not isinstance(self.bind_addr, str): + self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) + + if self.ssl_adapter is not None: + self.socket = self.ssl_adapter.bind(self.socket) + + host, port = self.bind_addr[:2] + + # If listening on the IPV6 any address ('::' = IN6ADDR_ANY), + # activate dual-stack. See + # https://github.com/cherrypy/cherrypy/issues/871. + listening_ipv6 = ( + hasattr(socket, 'AF_INET6') + and family == socket.AF_INET6 + and host in ('::', '::0', '::0.0.0.0') + ) + if listening_ipv6: + try: + self.socket.setsockopt( + socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0) + except (AttributeError, socket.error): + # Apparently, the socket option is not available in + # this machine's TCP stack + pass + + self.socket.bind(self.bind_addr) + + def tick(self): + """Accept a new connection and put it on the Queue.""" + try: + s, addr = self.socket.accept() + if self.stats['Enabled']: + self.stats['Accepts'] += 1 + if not self.ready: + return + + prevent_socket_inheritance(s) + if hasattr(s, 'settimeout'): + s.settimeout(self.timeout) + + mf = MakeFile + ssl_env = {} + # if ssl cert and key are set, we try to be a secure HTTP server + if self.ssl_adapter is not None: + try: + s, ssl_env = self.ssl_adapter.wrap(s) + except errors.NoSSLError: + msg = ('The client sent a plain HTTP request, but ' + 'this server only speaks HTTPS on this port.') + buf = ['%s 400 Bad Request\r\n' % self.protocol, + 'Content-Length: %s\r\n' % len(msg), + 'Content-Type: text/plain\r\n\r\n', + msg] + + sock_to_make = s if six.PY3 else s._sock + wfile = mf(sock_to_make, 'wb', io.DEFAULT_BUFFER_SIZE) + try: + wfile.write(''.join(buf).encode('ISO-8859-1')) + except socket.error as ex: + if ex.args[0] not in errors.socket_errors_to_ignore: + raise + return + if not s: + return + mf = self.ssl_adapter.makefile + # Re-apply our timeout since we may have a new socket object + if hasattr(s, 'settimeout'): + s.settimeout(self.timeout) + + conn = self.ConnectionClass(self, s, mf) + + if not isinstance(self.bind_addr, six.string_types): + # optional values + # Until we do DNS lookups, omit REMOTE_HOST + if addr is None: # sometimes this can happen + # figure out if AF_INET or AF_INET6. + if len(s.getsockname()) == 2: + # AF_INET + addr = ('0.0.0.0', 0) + else: + # AF_INET6 + addr = ('::', 0) + conn.remote_addr = addr[0] + conn.remote_port = addr[1] + + conn.ssl_env = ssl_env + + try: + self.requests.put(conn) + except queue.Full: + # Just drop the conn. TODO: write 503 back? + conn.close() + return + except socket.timeout: + # The only reason for the timeout in start() is so we can + # notice keyboard interrupts on Win32, which don't interrupt + # accept() by default + return + except socket.error as ex: + if self.stats['Enabled']: + self.stats['Socket Errors'] += 1 + if ex.args[0] in errors.socket_error_eintr: + # I *think* this is right. EINTR should occur when a signal + # is received during the accept() call; all docs say retry + # the call, and I *think* I'm reading it right that Python + # will then go ahead and poll for and handle the signal + # elsewhere. See + # https://github.com/cherrypy/cherrypy/issues/707. + return + if ex.args[0] in errors.socket_errors_nonblocking: + # Just try again. See + # https://github.com/cherrypy/cherrypy/issues/479. + return + if ex.args[0] in errors.socket_errors_to_ignore: + # Our socket was closed. + # See https://github.com/cherrypy/cherrypy/issues/686. + return + raise + + def _get_interrupt(self): + return self._interrupt + + def _set_interrupt(self, interrupt): + self._interrupt = True + self.stop() + self._interrupt = interrupt + interrupt = property(_get_interrupt, _set_interrupt, + doc='Set this to an Exception instance to ' + 'interrupt the server.') + + def stop(self): + """Gracefully shutdown a server that is serving forever.""" + self.ready = False + if self._start_time is not None: + self._run_time += (time.time() - self._start_time) + self._start_time = None + + sock = getattr(self, 'socket', None) + if sock: + if not isinstance(self.bind_addr, six.string_types): + # Touch our own socket to make accept() return immediately. + try: + host, port = sock.getsockname()[:2] + except socket.error as ex: + if ex.args[0] not in errors.socket_errors_to_ignore: + # Changed to use error code and not message + # See + # https://github.com/cherrypy/cherrypy/issues/860. + raise + else: + # Note that we're explicitly NOT using AI_PASSIVE, + # here, because we want an actual IP to touch. + # localhost won't work if we've bound to a public IP, + # but it will if we bound to '0.0.0.0' (INADDR_ANY). + for res in socket.getaddrinfo(host, port, socket.AF_UNSPEC, + socket.SOCK_STREAM): + af, socktype, proto, canonname, sa = res + s = None + try: + s = socket.socket(af, socktype, proto) + # See + # http://groups.google.com/group/cherrypy-users/ + # browse_frm/thread/bbfe5eb39c904fe0 + s.settimeout(1.0) + s.connect((host, port)) + s.close() + except socket.error: + if s: + s.close() + if hasattr(sock, 'close'): + sock.close() + self.socket = None + + self.requests.stop(self.shutdown_timeout) + + +class Gateway(object): + """Base class to interface HTTPServer with other systems, such as WSGI.""" + + def __init__(self, req): + """Initialize Gateway instance with request. + + Args: + req (HTTPRequest): current HTTP request + """ + self.req = req + + def respond(self): + """Process the current request. Must be overridden in a subclass.""" + raise NotImplementedError + + +# These may either be ssl.Adapter subclasses or the string names +# of such classes (in which case they will be lazily loaded). +ssl_adapters = { + 'builtin': 'cheroot.ssl.builtin.BuiltinSSLAdapter', + 'pyopenssl': 'cheroot.ssl.pyopenssl.pyOpenSSLAdapter', +} + + +def get_ssl_adapter_class(name='builtin'): + """Return an SSL adapter class for the given name.""" + adapter = ssl_adapters[name.lower()] + if isinstance(adapter, six.string_types): + last_dot = adapter.rfind('.') + attr_name = adapter[last_dot + 1:] + mod_path = adapter[:last_dot] + + try: + mod = sys.modules[mod_path] + if mod is None: + raise KeyError() + except KeyError: + # The last [''] is important. + mod = __import__(mod_path, globals(), locals(), ['']) + + # Let an AttributeError propagate outward. + try: + adapter = getattr(mod, attr_name) + except AttributeError: + raise AttributeError("'%s' object has no attribute '%s'" + % (mod_path, attr_name)) + + return adapter diff --git a/module/lib/wsgiserver/six.py b/module/lib/wsgiserver/six.py new file mode 100644 index 0000000000..5fe9f8e141 --- /dev/null +++ b/module/lib/wsgiserver/six.py @@ -0,0 +1,980 @@ +# Copyright (c) 2010-2020 Benjamin Peterson +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +"""Utilities for writing code that runs on Python 2 and 3""" + +from __future__ import absolute_import + +import functools +import itertools +import operator +import sys +import types + +__author__ = "Benjamin Peterson " +__version__ = "1.14.0" + + +# Useful for very coarse version differentiation. +PY2 = sys.version_info[0] == 2 +PY3 = sys.version_info[0] == 3 +PY34 = sys.version_info[0:2] >= (3, 4) + +if PY3: + string_types = str, + integer_types = int, + class_types = type, + text_type = str + binary_type = bytes + + MAXSIZE = sys.maxsize +else: + string_types = basestring, + integer_types = (int, long) + class_types = (type, types.ClassType) + text_type = unicode + binary_type = str + + if sys.platform.startswith("java"): + # Jython always uses 32 bits. + MAXSIZE = int((1 << 31) - 1) + else: + # It's possible to have sizeof(long) != sizeof(Py_ssize_t). + class X(object): + + def __len__(self): + return 1 << 31 + try: + len(X()) + except OverflowError: + # 32-bit + MAXSIZE = int((1 << 31) - 1) + else: + # 64-bit + MAXSIZE = int((1 << 63) - 1) + del X + + +def _add_doc(func, doc): + """Add documentation to a function.""" + func.__doc__ = doc + + +def _import_module(name): + """Import module, returning the module after the last dot.""" + __import__(name) + return sys.modules[name] + + +class _LazyDescr(object): + + def __init__(self, name): + self.name = name + + def __get__(self, obj, tp): + result = self._resolve() + setattr(obj, self.name, result) # Invokes __set__. + try: + # This is a bit ugly, but it avoids running this again by + # removing this descriptor. + delattr(obj.__class__, self.name) + except AttributeError: + pass + return result + + +class MovedModule(_LazyDescr): + + def __init__(self, name, old, new=None): + super(MovedModule, self).__init__(name) + if PY3: + if new is None: + new = name + self.mod = new + else: + self.mod = old + + def _resolve(self): + return _import_module(self.mod) + + def __getattr__(self, attr): + _module = self._resolve() + value = getattr(_module, attr) + setattr(self, attr, value) + return value + + +class _LazyModule(types.ModuleType): + + def __init__(self, name): + super(_LazyModule, self).__init__(name) + self.__doc__ = self.__class__.__doc__ + + def __dir__(self): + attrs = ["__doc__", "__name__"] + attrs += [attr.name for attr in self._moved_attributes] + return attrs + + # Subclasses should override this + _moved_attributes = [] + + +class MovedAttribute(_LazyDescr): + + def __init__(self, name, old_mod, new_mod, old_attr=None, new_attr=None): + super(MovedAttribute, self).__init__(name) + if PY3: + if new_mod is None: + new_mod = name + self.mod = new_mod + if new_attr is None: + if old_attr is None: + new_attr = name + else: + new_attr = old_attr + self.attr = new_attr + else: + self.mod = old_mod + if old_attr is None: + old_attr = name + self.attr = old_attr + + def _resolve(self): + module = _import_module(self.mod) + return getattr(module, self.attr) + + +class _SixMetaPathImporter(object): + + """ + A meta path importer to import six.moves and its submodules. + + This class implements a PEP302 finder and loader. It should be compatible + with Python 2.5 and all existing versions of Python3 + """ + + def __init__(self, six_module_name): + self.name = six_module_name + self.known_modules = {} + + def _add_module(self, mod, *fullnames): + for fullname in fullnames: + self.known_modules[self.name + "." + fullname] = mod + + def _get_module(self, fullname): + return self.known_modules[self.name + "." + fullname] + + def find_module(self, fullname, path=None): + if fullname in self.known_modules: + return self + return None + + def __get_module(self, fullname): + try: + return self.known_modules[fullname] + except KeyError: + raise ImportError("This loader does not know module " + fullname) + + def load_module(self, fullname): + try: + # in case of a reload + return sys.modules[fullname] + except KeyError: + pass + mod = self.__get_module(fullname) + if isinstance(mod, MovedModule): + mod = mod._resolve() + else: + mod.__loader__ = self + sys.modules[fullname] = mod + return mod + + def is_package(self, fullname): + """ + Return true, if the named module is a package. + + We need this method to get correct spec objects with + Python 3.4 (see PEP451) + """ + return hasattr(self.__get_module(fullname), "__path__") + + def get_code(self, fullname): + """Return None + + Required, if is_package is implemented""" + self.__get_module(fullname) # eventually raises ImportError + return None + get_source = get_code # same as get_code + +_importer = _SixMetaPathImporter(__name__) + + +class _MovedItems(_LazyModule): + + """Lazy loading of moved objects""" + __path__ = [] # mark as package + + +_moved_attributes = [ + MovedAttribute("cStringIO", "cStringIO", "io", "StringIO"), + MovedAttribute("filter", "itertools", "builtins", "ifilter", "filter"), + MovedAttribute("filterfalse", "itertools", "itertools", "ifilterfalse", "filterfalse"), + MovedAttribute("input", "__builtin__", "builtins", "raw_input", "input"), + MovedAttribute("intern", "__builtin__", "sys"), + MovedAttribute("map", "itertools", "builtins", "imap", "map"), + MovedAttribute("getcwd", "os", "os", "getcwdu", "getcwd"), + MovedAttribute("getcwdb", "os", "os", "getcwd", "getcwdb"), + MovedAttribute("getoutput", "commands", "subprocess"), + MovedAttribute("range", "__builtin__", "builtins", "xrange", "range"), + MovedAttribute("reload_module", "__builtin__", "importlib" if PY34 else "imp", "reload"), + MovedAttribute("reduce", "__builtin__", "functools"), + MovedAttribute("shlex_quote", "pipes", "shlex", "quote"), + MovedAttribute("StringIO", "StringIO", "io"), + MovedAttribute("UserDict", "UserDict", "collections"), + MovedAttribute("UserList", "UserList", "collections"), + MovedAttribute("UserString", "UserString", "collections"), + MovedAttribute("xrange", "__builtin__", "builtins", "xrange", "range"), + MovedAttribute("zip", "itertools", "builtins", "izip", "zip"), + MovedAttribute("zip_longest", "itertools", "itertools", "izip_longest", "zip_longest"), + MovedModule("builtins", "__builtin__"), + MovedModule("configparser", "ConfigParser"), + MovedModule("collections_abc", "collections", "collections.abc" if sys.version_info >= (3, 3) else "collections"), + MovedModule("copyreg", "copy_reg"), + MovedModule("dbm_gnu", "gdbm", "dbm.gnu"), + MovedModule("dbm_ndbm", "dbm", "dbm.ndbm"), + MovedModule("_dummy_thread", "dummy_thread", "_dummy_thread" if sys.version_info < (3, 9) else "_thread"), + MovedModule("http_cookiejar", "cookielib", "http.cookiejar"), + MovedModule("http_cookies", "Cookie", "http.cookies"), + MovedModule("html_entities", "htmlentitydefs", "html.entities"), + MovedModule("html_parser", "HTMLParser", "html.parser"), + MovedModule("http_client", "httplib", "http.client"), + MovedModule("email_mime_base", "email.MIMEBase", "email.mime.base"), + MovedModule("email_mime_image", "email.MIMEImage", "email.mime.image"), + MovedModule("email_mime_multipart", "email.MIMEMultipart", "email.mime.multipart"), + MovedModule("email_mime_nonmultipart", "email.MIMENonMultipart", "email.mime.nonmultipart"), + MovedModule("email_mime_text", "email.MIMEText", "email.mime.text"), + MovedModule("BaseHTTPServer", "BaseHTTPServer", "http.server"), + MovedModule("CGIHTTPServer", "CGIHTTPServer", "http.server"), + MovedModule("SimpleHTTPServer", "SimpleHTTPServer", "http.server"), + MovedModule("cPickle", "cPickle", "pickle"), + MovedModule("queue", "Queue"), + MovedModule("reprlib", "repr"), + MovedModule("socketserver", "SocketServer"), + MovedModule("_thread", "thread", "_thread"), + MovedModule("tkinter", "Tkinter"), + MovedModule("tkinter_dialog", "Dialog", "tkinter.dialog"), + MovedModule("tkinter_filedialog", "FileDialog", "tkinter.filedialog"), + MovedModule("tkinter_scrolledtext", "ScrolledText", "tkinter.scrolledtext"), + MovedModule("tkinter_simpledialog", "SimpleDialog", "tkinter.simpledialog"), + MovedModule("tkinter_tix", "Tix", "tkinter.tix"), + MovedModule("tkinter_ttk", "ttk", "tkinter.ttk"), + MovedModule("tkinter_constants", "Tkconstants", "tkinter.constants"), + MovedModule("tkinter_dnd", "Tkdnd", "tkinter.dnd"), + MovedModule("tkinter_colorchooser", "tkColorChooser", + "tkinter.colorchooser"), + MovedModule("tkinter_commondialog", "tkCommonDialog", + "tkinter.commondialog"), + MovedModule("tkinter_tkfiledialog", "tkFileDialog", "tkinter.filedialog"), + MovedModule("tkinter_font", "tkFont", "tkinter.font"), + MovedModule("tkinter_messagebox", "tkMessageBox", "tkinter.messagebox"), + MovedModule("tkinter_tksimpledialog", "tkSimpleDialog", + "tkinter.simpledialog"), + MovedModule("urllib_parse", __name__ + ".moves.urllib_parse", "urllib.parse"), + MovedModule("urllib_error", __name__ + ".moves.urllib_error", "urllib.error"), + MovedModule("urllib", __name__ + ".moves.urllib", __name__ + ".moves.urllib"), + MovedModule("urllib_robotparser", "robotparser", "urllib.robotparser"), + MovedModule("xmlrpc_client", "xmlrpclib", "xmlrpc.client"), + MovedModule("xmlrpc_server", "SimpleXMLRPCServer", "xmlrpc.server"), +] +# Add windows specific modules. +if sys.platform == "win32": + _moved_attributes += [ + MovedModule("winreg", "_winreg"), + ] + +for attr in _moved_attributes: + setattr(_MovedItems, attr.name, attr) + if isinstance(attr, MovedModule): + _importer._add_module(attr, "moves." + attr.name) +del attr + +_MovedItems._moved_attributes = _moved_attributes + +moves = _MovedItems(__name__ + ".moves") +_importer._add_module(moves, "moves") + + +class Module_six_moves_urllib_parse(_LazyModule): + + """Lazy loading of moved objects in six.moves.urllib_parse""" + + +_urllib_parse_moved_attributes = [ + MovedAttribute("ParseResult", "urlparse", "urllib.parse"), + MovedAttribute("SplitResult", "urlparse", "urllib.parse"), + MovedAttribute("parse_qs", "urlparse", "urllib.parse"), + MovedAttribute("parse_qsl", "urlparse", "urllib.parse"), + MovedAttribute("urldefrag", "urlparse", "urllib.parse"), + MovedAttribute("urljoin", "urlparse", "urllib.parse"), + MovedAttribute("urlparse", "urlparse", "urllib.parse"), + MovedAttribute("urlsplit", "urlparse", "urllib.parse"), + MovedAttribute("urlunparse", "urlparse", "urllib.parse"), + MovedAttribute("urlunsplit", "urlparse", "urllib.parse"), + MovedAttribute("quote", "urllib", "urllib.parse"), + MovedAttribute("quote_plus", "urllib", "urllib.parse"), + MovedAttribute("unquote", "urllib", "urllib.parse"), + MovedAttribute("unquote_plus", "urllib", "urllib.parse"), + MovedAttribute("unquote_to_bytes", "urllib", "urllib.parse", "unquote", "unquote_to_bytes"), + MovedAttribute("urlencode", "urllib", "urllib.parse"), + MovedAttribute("splitquery", "urllib", "urllib.parse"), + MovedAttribute("splittag", "urllib", "urllib.parse"), + MovedAttribute("splituser", "urllib", "urllib.parse"), + MovedAttribute("splitvalue", "urllib", "urllib.parse"), + MovedAttribute("uses_fragment", "urlparse", "urllib.parse"), + MovedAttribute("uses_netloc", "urlparse", "urllib.parse"), + MovedAttribute("uses_params", "urlparse", "urllib.parse"), + MovedAttribute("uses_query", "urlparse", "urllib.parse"), + MovedAttribute("uses_relative", "urlparse", "urllib.parse"), +] +for attr in _urllib_parse_moved_attributes: + setattr(Module_six_moves_urllib_parse, attr.name, attr) +del attr + +Module_six_moves_urllib_parse._moved_attributes = _urllib_parse_moved_attributes + +_importer._add_module(Module_six_moves_urllib_parse(__name__ + ".moves.urllib_parse"), + "moves.urllib_parse", "moves.urllib.parse") + + +class Module_six_moves_urllib_error(_LazyModule): + + """Lazy loading of moved objects in six.moves.urllib_error""" + + +_urllib_error_moved_attributes = [ + MovedAttribute("URLError", "urllib2", "urllib.error"), + MovedAttribute("HTTPError", "urllib2", "urllib.error"), + MovedAttribute("ContentTooShortError", "urllib", "urllib.error"), +] +for attr in _urllib_error_moved_attributes: + setattr(Module_six_moves_urllib_error, attr.name, attr) +del attr + +Module_six_moves_urllib_error._moved_attributes = _urllib_error_moved_attributes + +_importer._add_module(Module_six_moves_urllib_error(__name__ + ".moves.urllib.error"), + "moves.urllib_error", "moves.urllib.error") + + +class Module_six_moves_urllib_request(_LazyModule): + + """Lazy loading of moved objects in six.moves.urllib_request""" + + +_urllib_request_moved_attributes = [ + MovedAttribute("urlopen", "urllib2", "urllib.request"), + MovedAttribute("install_opener", "urllib2", "urllib.request"), + MovedAttribute("build_opener", "urllib2", "urllib.request"), + MovedAttribute("pathname2url", "urllib", "urllib.request"), + MovedAttribute("url2pathname", "urllib", "urllib.request"), + MovedAttribute("getproxies", "urllib", "urllib.request"), + MovedAttribute("Request", "urllib2", "urllib.request"), + MovedAttribute("OpenerDirector", "urllib2", "urllib.request"), + MovedAttribute("HTTPDefaultErrorHandler", "urllib2", "urllib.request"), + MovedAttribute("HTTPRedirectHandler", "urllib2", "urllib.request"), + MovedAttribute("HTTPCookieProcessor", "urllib2", "urllib.request"), + MovedAttribute("ProxyHandler", "urllib2", "urllib.request"), + MovedAttribute("BaseHandler", "urllib2", "urllib.request"), + MovedAttribute("HTTPPasswordMgr", "urllib2", "urllib.request"), + MovedAttribute("HTTPPasswordMgrWithDefaultRealm", "urllib2", "urllib.request"), + MovedAttribute("AbstractBasicAuthHandler", "urllib2", "urllib.request"), + MovedAttribute("HTTPBasicAuthHandler", "urllib2", "urllib.request"), + MovedAttribute("ProxyBasicAuthHandler", "urllib2", "urllib.request"), + MovedAttribute("AbstractDigestAuthHandler", "urllib2", "urllib.request"), + MovedAttribute("HTTPDigestAuthHandler", "urllib2", "urllib.request"), + MovedAttribute("ProxyDigestAuthHandler", "urllib2", "urllib.request"), + MovedAttribute("HTTPHandler", "urllib2", "urllib.request"), + MovedAttribute("HTTPSHandler", "urllib2", "urllib.request"), + MovedAttribute("FileHandler", "urllib2", "urllib.request"), + MovedAttribute("FTPHandler", "urllib2", "urllib.request"), + MovedAttribute("CacheFTPHandler", "urllib2", "urllib.request"), + MovedAttribute("UnknownHandler", "urllib2", "urllib.request"), + MovedAttribute("HTTPErrorProcessor", "urllib2", "urllib.request"), + MovedAttribute("urlretrieve", "urllib", "urllib.request"), + MovedAttribute("urlcleanup", "urllib", "urllib.request"), + MovedAttribute("URLopener", "urllib", "urllib.request"), + MovedAttribute("FancyURLopener", "urllib", "urllib.request"), + MovedAttribute("proxy_bypass", "urllib", "urllib.request"), + MovedAttribute("parse_http_list", "urllib2", "urllib.request"), + MovedAttribute("parse_keqv_list", "urllib2", "urllib.request"), +] +for attr in _urllib_request_moved_attributes: + setattr(Module_six_moves_urllib_request, attr.name, attr) +del attr + +Module_six_moves_urllib_request._moved_attributes = _urllib_request_moved_attributes + +_importer._add_module(Module_six_moves_urllib_request(__name__ + ".moves.urllib.request"), + "moves.urllib_request", "moves.urllib.request") + + +class Module_six_moves_urllib_response(_LazyModule): + + """Lazy loading of moved objects in six.moves.urllib_response""" + + +_urllib_response_moved_attributes = [ + MovedAttribute("addbase", "urllib", "urllib.response"), + MovedAttribute("addclosehook", "urllib", "urllib.response"), + MovedAttribute("addinfo", "urllib", "urllib.response"), + MovedAttribute("addinfourl", "urllib", "urllib.response"), +] +for attr in _urllib_response_moved_attributes: + setattr(Module_six_moves_urllib_response, attr.name, attr) +del attr + +Module_six_moves_urllib_response._moved_attributes = _urllib_response_moved_attributes + +_importer._add_module(Module_six_moves_urllib_response(__name__ + ".moves.urllib.response"), + "moves.urllib_response", "moves.urllib.response") + + +class Module_six_moves_urllib_robotparser(_LazyModule): + + """Lazy loading of moved objects in six.moves.urllib_robotparser""" + + +_urllib_robotparser_moved_attributes = [ + MovedAttribute("RobotFileParser", "robotparser", "urllib.robotparser"), +] +for attr in _urllib_robotparser_moved_attributes: + setattr(Module_six_moves_urllib_robotparser, attr.name, attr) +del attr + +Module_six_moves_urllib_robotparser._moved_attributes = _urllib_robotparser_moved_attributes + +_importer._add_module(Module_six_moves_urllib_robotparser(__name__ + ".moves.urllib.robotparser"), + "moves.urllib_robotparser", "moves.urllib.robotparser") + + +class Module_six_moves_urllib(types.ModuleType): + + """Create a six.moves.urllib namespace that resembles the Python 3 namespace""" + __path__ = [] # mark as package + parse = _importer._get_module("moves.urllib_parse") + error = _importer._get_module("moves.urllib_error") + request = _importer._get_module("moves.urllib_request") + response = _importer._get_module("moves.urllib_response") + robotparser = _importer._get_module("moves.urllib_robotparser") + + def __dir__(self): + return ['parse', 'error', 'request', 'response', 'robotparser'] + +_importer._add_module(Module_six_moves_urllib(__name__ + ".moves.urllib"), + "moves.urllib") + + +def add_move(move): + """Add an item to six.moves.""" + setattr(_MovedItems, move.name, move) + + +def remove_move(name): + """Remove item from six.moves.""" + try: + delattr(_MovedItems, name) + except AttributeError: + try: + del moves.__dict__[name] + except KeyError: + raise AttributeError("no such move, %r" % (name,)) + + +if PY3: + _meth_func = "__func__" + _meth_self = "__self__" + + _func_closure = "__closure__" + _func_code = "__code__" + _func_defaults = "__defaults__" + _func_globals = "__globals__" +else: + _meth_func = "im_func" + _meth_self = "im_self" + + _func_closure = "func_closure" + _func_code = "func_code" + _func_defaults = "func_defaults" + _func_globals = "func_globals" + + +try: + advance_iterator = next +except NameError: + def advance_iterator(it): + return it.next() +next = advance_iterator + + +try: + callable = callable +except NameError: + def callable(obj): + return any("__call__" in klass.__dict__ for klass in type(obj).__mro__) + + +if PY3: + def get_unbound_function(unbound): + return unbound + + create_bound_method = types.MethodType + + def create_unbound_method(func, cls): + return func + + Iterator = object +else: + def get_unbound_function(unbound): + return unbound.im_func + + def create_bound_method(func, obj): + return types.MethodType(func, obj, obj.__class__) + + def create_unbound_method(func, cls): + return types.MethodType(func, None, cls) + + class Iterator(object): + + def next(self): + return type(self).__next__(self) + + callable = callable +_add_doc(get_unbound_function, + """Get the function out of a possibly unbound function""") + + +get_method_function = operator.attrgetter(_meth_func) +get_method_self = operator.attrgetter(_meth_self) +get_function_closure = operator.attrgetter(_func_closure) +get_function_code = operator.attrgetter(_func_code) +get_function_defaults = operator.attrgetter(_func_defaults) +get_function_globals = operator.attrgetter(_func_globals) + + +if PY3: + def iterkeys(d, **kw): + return iter(d.keys(**kw)) + + def itervalues(d, **kw): + return iter(d.values(**kw)) + + def iteritems(d, **kw): + return iter(d.items(**kw)) + + def iterlists(d, **kw): + return iter(d.lists(**kw)) + + viewkeys = operator.methodcaller("keys") + + viewvalues = operator.methodcaller("values") + + viewitems = operator.methodcaller("items") +else: + def iterkeys(d, **kw): + return d.iterkeys(**kw) + + def itervalues(d, **kw): + return d.itervalues(**kw) + + def iteritems(d, **kw): + return d.iteritems(**kw) + + def iterlists(d, **kw): + return d.iterlists(**kw) + + viewkeys = operator.methodcaller("viewkeys") + + viewvalues = operator.methodcaller("viewvalues") + + viewitems = operator.methodcaller("viewitems") + +_add_doc(iterkeys, "Return an iterator over the keys of a dictionary.") +_add_doc(itervalues, "Return an iterator over the values of a dictionary.") +_add_doc(iteritems, + "Return an iterator over the (key, value) pairs of a dictionary.") +_add_doc(iterlists, + "Return an iterator over the (key, [values]) pairs of a dictionary.") + + +if PY3: + def b(s): + return s.encode("latin-1") + + def u(s): + return s + unichr = chr + import struct + int2byte = struct.Struct(">B").pack + del struct + byte2int = operator.itemgetter(0) + indexbytes = operator.getitem + iterbytes = iter + import io + StringIO = io.StringIO + BytesIO = io.BytesIO + del io + _assertCountEqual = "assertCountEqual" + if sys.version_info[1] <= 1: + _assertRaisesRegex = "assertRaisesRegexp" + _assertRegex = "assertRegexpMatches" + _assertNotRegex = "assertNotRegexpMatches" + else: + _assertRaisesRegex = "assertRaisesRegex" + _assertRegex = "assertRegex" + _assertNotRegex = "assertNotRegex" +else: + def b(s): + return s + # Workaround for standalone backslash + + def u(s): + return unicode(s.replace(r'\\', r'\\\\'), "unicode_escape") + unichr = unichr + int2byte = chr + + def byte2int(bs): + return ord(bs[0]) + + def indexbytes(buf, i): + return ord(buf[i]) + iterbytes = functools.partial(itertools.imap, ord) + import StringIO + StringIO = BytesIO = StringIO.StringIO + _assertCountEqual = "assertItemsEqual" + _assertRaisesRegex = "assertRaisesRegexp" + _assertRegex = "assertRegexpMatches" + _assertNotRegex = "assertNotRegexpMatches" +_add_doc(b, """Byte literal""") +_add_doc(u, """Text literal""") + + +def assertCountEqual(self, *args, **kwargs): + return getattr(self, _assertCountEqual)(*args, **kwargs) + + +def assertRaisesRegex(self, *args, **kwargs): + return getattr(self, _assertRaisesRegex)(*args, **kwargs) + + +def assertRegex(self, *args, **kwargs): + return getattr(self, _assertRegex)(*args, **kwargs) + + +def assertNotRegex(self, *args, **kwargs): + return getattr(self, _assertNotRegex)(*args, **kwargs) + + +if PY3: + exec_ = getattr(moves.builtins, "exec") + + def reraise(tp, value, tb=None): + try: + if value is None: + value = tp() + if value.__traceback__ is not tb: + raise value.with_traceback(tb) + raise value + finally: + value = None + tb = None + +else: + def exec_(_code_, _globs_=None, _locs_=None): + """Execute code in a namespace.""" + if _globs_ is None: + frame = sys._getframe(1) + _globs_ = frame.f_globals + if _locs_ is None: + _locs_ = frame.f_locals + del frame + elif _locs_ is None: + _locs_ = _globs_ + exec("""exec _code_ in _globs_, _locs_""") + + exec_("""def reraise(tp, value, tb=None): + try: + raise tp, value, tb + finally: + tb = None +""") + + +if sys.version_info[:2] > (3,): + exec_("""def raise_from(value, from_value): + try: + raise value from from_value + finally: + value = None +""") +else: + def raise_from(value, from_value): + raise value + + +print_ = getattr(moves.builtins, "print", None) +if print_ is None: + def print_(*args, **kwargs): + """The new-style print function for Python 2.4 and 2.5.""" + fp = kwargs.pop("file", sys.stdout) + if fp is None: + return + + def write(data): + if not isinstance(data, basestring): + data = str(data) + # If the file has an encoding, encode unicode with it. + if (isinstance(fp, file) and + isinstance(data, unicode) and + fp.encoding is not None): + errors = getattr(fp, "errors", None) + if errors is None: + errors = "strict" + data = data.encode(fp.encoding, errors) + fp.write(data) + want_unicode = False + sep = kwargs.pop("sep", None) + if sep is not None: + if isinstance(sep, unicode): + want_unicode = True + elif not isinstance(sep, str): + raise TypeError("sep must be None or a string") + end = kwargs.pop("end", None) + if end is not None: + if isinstance(end, unicode): + want_unicode = True + elif not isinstance(end, str): + raise TypeError("end must be None or a string") + if kwargs: + raise TypeError("invalid keyword arguments to print()") + if not want_unicode: + for arg in args: + if isinstance(arg, unicode): + want_unicode = True + break + if want_unicode: + newline = unicode("\n") + space = unicode(" ") + else: + newline = "\n" + space = " " + if sep is None: + sep = space + if end is None: + end = newline + for i, arg in enumerate(args): + if i: + write(sep) + write(arg) + write(end) +if sys.version_info[:2] < (3, 3): + _print = print_ + + def print_(*args, **kwargs): + fp = kwargs.get("file", sys.stdout) + flush = kwargs.pop("flush", False) + _print(*args, **kwargs) + if flush and fp is not None: + fp.flush() + +_add_doc(reraise, """Reraise an exception.""") + +if sys.version_info[0:2] < (3, 4): + # This does exactly the same what the :func:`py3:functools.update_wrapper` + # function does on Python versions after 3.2. It sets the ``__wrapped__`` + # attribute on ``wrapper`` object and it doesn't raise an error if any of + # the attributes mentioned in ``assigned`` and ``updated`` are missing on + # ``wrapped`` object. + def _update_wrapper(wrapper, wrapped, + assigned=functools.WRAPPER_ASSIGNMENTS, + updated=functools.WRAPPER_UPDATES): + for attr in assigned: + try: + value = getattr(wrapped, attr) + except AttributeError: + continue + else: + setattr(wrapper, attr, value) + for attr in updated: + getattr(wrapper, attr).update(getattr(wrapped, attr, {})) + wrapper.__wrapped__ = wrapped + return wrapper + _update_wrapper.__doc__ = functools.update_wrapper.__doc__ + + def wraps(wrapped, assigned=functools.WRAPPER_ASSIGNMENTS, + updated=functools.WRAPPER_UPDATES): + return functools.partial(_update_wrapper, wrapped=wrapped, + assigned=assigned, updated=updated) + wraps.__doc__ = functools.wraps.__doc__ + +else: + wraps = functools.wraps + + +def with_metaclass(meta, *bases): + """Create a base class with a metaclass.""" + # This requires a bit of explanation: the basic idea is to make a dummy + # metaclass for one level of class instantiation that replaces itself with + # the actual metaclass. + class metaclass(type): + + def __new__(cls, name, this_bases, d): + if sys.version_info[:2] >= (3, 7): + # This version introduced PEP 560 that requires a bit + # of extra care (we mimic what is done by __build_class__). + resolved_bases = types.resolve_bases(bases) + if resolved_bases is not bases: + d['__orig_bases__'] = bases + else: + resolved_bases = bases + return meta(name, resolved_bases, d) + + @classmethod + def __prepare__(cls, name, this_bases): + return meta.__prepare__(name, bases) + return type.__new__(metaclass, 'temporary_class', (), {}) + + +def add_metaclass(metaclass): + """Class decorator for creating a class with a metaclass.""" + def wrapper(cls): + orig_vars = cls.__dict__.copy() + slots = orig_vars.get('__slots__') + if slots is not None: + if isinstance(slots, str): + slots = [slots] + for slots_var in slots: + orig_vars.pop(slots_var) + orig_vars.pop('__dict__', None) + orig_vars.pop('__weakref__', None) + if hasattr(cls, '__qualname__'): + orig_vars['__qualname__'] = cls.__qualname__ + return metaclass(cls.__name__, cls.__bases__, orig_vars) + return wrapper + + +def ensure_binary(s, encoding='utf-8', errors='strict'): + """Coerce **s** to six.binary_type. + + For Python 2: + - `unicode` -> encoded to `str` + - `str` -> `str` + + For Python 3: + - `str` -> encoded to `bytes` + - `bytes` -> `bytes` + """ + if isinstance(s, text_type): + return s.encode(encoding, errors) + elif isinstance(s, binary_type): + return s + else: + raise TypeError("not expecting type '%s'" % type(s)) + + +def ensure_str(s, encoding='utf-8', errors='strict'): + """Coerce *s* to `str`. + + For Python 2: + - `unicode` -> encoded to `str` + - `str` -> `str` + + For Python 3: + - `str` -> `str` + - `bytes` -> decoded to `str` + """ + if not isinstance(s, (text_type, binary_type)): + raise TypeError("not expecting type '%s'" % type(s)) + if PY2 and isinstance(s, text_type): + s = s.encode(encoding, errors) + elif PY3 and isinstance(s, binary_type): + s = s.decode(encoding, errors) + return s + + +def ensure_text(s, encoding='utf-8', errors='strict'): + """Coerce *s* to six.text_type. + + For Python 2: + - `unicode` -> `unicode` + - `str` -> `unicode` + + For Python 3: + - `str` -> `str` + - `bytes` -> decoded to `str` + """ + if isinstance(s, binary_type): + return s.decode(encoding, errors) + elif isinstance(s, text_type): + return s + else: + raise TypeError("not expecting type '%s'" % type(s)) + + +def python_2_unicode_compatible(klass): + """ + A class decorator that defines __unicode__ and __str__ methods under Python 2. + Under Python 3 it does nothing. + + To support Python 2 and 3 with a single code base, define a __str__ method + returning text and apply this decorator to the class. + """ + if PY2: + if '__str__' not in klass.__dict__: + raise ValueError("@python_2_unicode_compatible cannot be applied " + "to %s because it doesn't define __str__()." % + klass.__name__) + klass.__unicode__ = klass.__str__ + klass.__str__ = lambda self: self.__unicode__().encode('utf-8') + return klass + + +# Complete the moves implementation. +# This code is at the end of this module to speed up module loading. +# Turn this module into a package. +__path__ = [] # required for PEP 302 and PEP 451 +__package__ = __name__ # see PEP 366 @ReservedAssignment +if globals().get("__spec__") is not None: + __spec__.submodule_search_locations = [] # PEP 451 @UndefinedVariable +# Remove other six meta path importers, since they cause problems. This can +# happen if six is removed from sys.modules and then reloaded. (Setuptools does +# this for some reason.) +if sys.meta_path: + for i, importer in enumerate(sys.meta_path): + # Here's some real nastiness: Another "instance" of the six module might + # be floating around. Therefore, we can't use isinstance() to check for + # the six meta path importer, since the other six instance will have + # inserted an importer with different class. + if (type(importer).__name__ == "_SixMetaPathImporter" and + importer.name == __name__): + del sys.meta_path[i] + break + del i, importer +# Finally, add the importer to the meta path import hook. +sys.meta_path.append(_importer) diff --git a/module/lib/wsgiserver/ssl/__init__.py b/module/lib/wsgiserver/ssl/__init__.py new file mode 100644 index 0000000000..e581706630 --- /dev/null +++ b/module/lib/wsgiserver/ssl/__init__.py @@ -0,0 +1,48 @@ +"""Implementation of the SSL adapter base interface.""" + +from abc import ABCMeta, abstractmethod + +from six import add_metaclass + + +@add_metaclass(ABCMeta) +class Adapter(object): + """Base class for SSL driver library adapters. + + Required methods: + + * ``wrap(sock) -> (wrapped socket, ssl environ dict)`` + * ``makefile(sock, mode='r', bufsize=DEFAULT_BUFFER_SIZE) -> + socket file object`` + """ + + @abstractmethod + def __init__( + self, certificate, private_key, certificate_chain=None, + ciphers=None): + """Set up certificates, private key ciphers and reset context.""" + self.certificate = certificate + self.private_key = private_key + self.certificate_chain = certificate_chain + self.ciphers = ciphers + self.context = None + + @abstractmethod + def bind(self, sock): + """Wrap and return the given socket.""" + return sock + + @abstractmethod + def wrap(self, sock): + """Wrap and return the given socket, plus WSGI environ entries.""" + raise NotImplementedError + + @abstractmethod + def get_environ(self): + """Return WSGI environ entries to be merged into each request.""" + raise NotImplementedError + + @abstractmethod + def makefile(self, sock, mode='r', bufsize=-1): + """Return socket file object.""" + raise NotImplementedError diff --git a/module/lib/wsgiserver/ssl/builtin.py b/module/lib/wsgiserver/ssl/builtin.py new file mode 100644 index 0000000000..e6209d95e9 --- /dev/null +++ b/module/lib/wsgiserver/ssl/builtin.py @@ -0,0 +1,134 @@ +""" +A library for integrating Python's builtin ``ssl`` library with Cheroot. + +The ssl module must be importable for SSL functionality. + +To use this module, set ``HTTPServer.ssl_adapter`` to an instance of +``BuiltinSSLAdapter``. +""" + +try: + import ssl +except ImportError: + ssl = None + +try: + from _pyio import DEFAULT_BUFFER_SIZE +except ImportError: + try: + from io import DEFAULT_BUFFER_SIZE + except ImportError: + DEFAULT_BUFFER_SIZE = -1 + +from . import Adapter +from .. import errors +from ..makefile import MakeFile + + +class BuiltinSSLAdapter(Adapter): + """A wrapper for integrating Python's builtin ssl module with Cheroot.""" + + certificate = None + """The filename of the server SSL certificate.""" + + private_key = None + """The filename of the server's private key file.""" + + certificate_chain = None + """The filename of the certificate chain file.""" + + context = None + """The ssl.SSLContext that will be used to wrap sockets where available + (on Python > 2.7.9 / 3.3) + """ + + ciphers = None + """The ciphers list of SSL.""" + + def __init__( + self, certificate, private_key, certificate_chain=None, + ciphers=None): + """Set up context in addition to base class properties if available.""" + if ssl is None: + raise ImportError('You must install the ssl module to use HTTPS.') + + super(BuiltinSSLAdapter, self).__init__( + certificate, private_key, certificate_chain, ciphers) + + if hasattr(ssl, 'create_default_context'): + self.context = ssl.create_default_context( + purpose=ssl.Purpose.CLIENT_AUTH, + cafile=certificate_chain + ) + self.context.load_cert_chain(certificate, private_key) + if self.ciphers is not None: + self.context.set_ciphers(ciphers) + + def bind(self, sock): + """Wrap and return the given socket.""" + return super(BuiltinSSLAdapter, self).bind(sock) + + def wrap(self, sock): + """Wrap and return the given socket, plus WSGI environ entries.""" + try: + if self.context is not None: + s = self.context.wrap_socket( + sock, do_handshake_on_connect=True, server_side=True) + else: + s = ssl.wrap_socket(sock, do_handshake_on_connect=True, + server_side=True, + certfile=self.certificate, + keyfile=self.private_key, + ssl_version=ssl.PROTOCOL_SSLv23, + ca_certs=self.certificate_chain) + except ssl.SSLError as ex: + if ex.errno == ssl.SSL_ERROR_EOF: + # This is almost certainly due to the cherrypy engine + # 'pinging' the socket to assert it's connectable; + # the 'ping' isn't SSL. + return None, {} + elif ex.errno == ssl.SSL_ERROR_SSL: + if 'http request' in ex.args[1]: + # The client is speaking HTTP to an HTTPS server. + raise errors.NoSSLError + + # Check if it's one of the known errors + # Errors that are caught by PyOpenSSL, but thrown by + # built-in ssl + _block_errors = ( + 'unknown protocol', 'unknown ca', 'unknown_ca', + 'unknown error', + 'https proxy request', 'inappropriate fallback', + 'wrong version number', + 'no shared cipher', 'certificate unknown', + 'ccs received early', + ) + for error_text in _block_errors: + if error_text in ex.args[1].lower(): + # Accepted error, let's pass + return None, {} + elif 'handshake operation timed out' in ex.args[0]: + # This error is thrown by builtin SSL after a timeout + # when client is speaking HTTP to an HTTPS server. + # The connection can safely be dropped. + return None, {} + raise + return s, self.get_environ(s) + + # TODO: fill this out more with mod ssl env + def get_environ(self, sock): + """Create WSGI environ entries to be merged into each request.""" + cipher = sock.cipher() + ssl_environ = { + 'wsgi.url_scheme': 'https', + 'HTTPS': 'on', + 'SSL_PROTOCOL': cipher[1], + 'SSL_CIPHER': cipher[0] + # SSL_VERSION_INTERFACE string The mod_ssl program version + # SSL_VERSION_LIBRARY string The OpenSSL program version + } + return ssl_environ + + def makefile(self, sock, mode='r', bufsize=DEFAULT_BUFFER_SIZE): + """Return socket file object.""" + return MakeFile(sock, mode, bufsize) diff --git a/module/lib/wsgiserver/ssl/pyopenssl.py b/module/lib/wsgiserver/ssl/pyopenssl.py new file mode 100644 index 0000000000..b32ada281b --- /dev/null +++ b/module/lib/wsgiserver/ssl/pyopenssl.py @@ -0,0 +1,264 @@ +""" +A library for integrating pyOpenSSL with Cheroot. + +The OpenSSL module must be importable for SSL functionality. +You can obtain it from `here `_. + +To use this module, set HTTPServer.ssl_adapter to an instance of +ssl.Adapter. There are two ways to use SSL: + +Method One +---------- + + * ``ssl_adapter.context``: an instance of SSL.Context. + +If this is not None, it is assumed to be an SSL.Context instance, +and will be passed to SSL.Connection on bind(). The developer is +responsible for forming a valid Context object. This approach is +to be preferred for more flexibility, e.g. if the cert and key are +streams instead of files, or need decryption, or SSL.SSLv3_METHOD +is desired instead of the default SSL.SSLv23_METHOD, etc. Consult +the pyOpenSSL documentation for complete options. + +Method Two (shortcut) +--------------------- + + * ``ssl_adapter.certificate``: the filename of the server SSL certificate. + * ``ssl_adapter.private_key``: the filename of the server's private key file. + +Both are None by default. If ssl_adapter.context is None, but .private_key +and .certificate are both given and valid, they will be read, and the +context will be automatically created from them. +""" + +import socket +import threading +import time + +try: + from OpenSSL import SSL + from OpenSSL import crypto +except ImportError: + SSL = None + +from . import Adapter +from .. import errors, server as cheroot_server +from ..makefile import MakeFile + + +class SSL_fileobject(MakeFile): + """SSL file object attached to a socket object.""" + + ssl_timeout = 3 + ssl_retry = .01 + + def _safe_call(self, is_reader, call, *args, **kwargs): + """Wrap the given call with SSL error-trapping. + + is_reader: if False EOF errors will be raised. If True, EOF errors + will return "" (to emulate normal sockets). + """ + start = time.time() + while True: + try: + return call(*args, **kwargs) + except SSL.WantReadError: + # Sleep and try again. This is dangerous, because it means + # the rest of the stack has no way of differentiating + # between a "new handshake" error and "client dropped". + # Note this isn't an endless loop: there's a timeout below. + time.sleep(self.ssl_retry) + except SSL.WantWriteError: + time.sleep(self.ssl_retry) + except SSL.SysCallError as e: + if is_reader and e.args == (-1, 'Unexpected EOF'): + return '' + + errnum = e.args[0] + if is_reader and errnum in errors.socket_errors_to_ignore: + return '' + raise socket.error(errnum) + except SSL.Error as e: + if is_reader and e.args == (-1, 'Unexpected EOF'): + return '' + + thirdarg = None + try: + thirdarg = e.args[0][0][2] + except IndexError: + pass + + if thirdarg == 'http request': + # The client is talking HTTP to an HTTPS server. + raise errors.NoSSLError() + + raise errors.FatalSSLAlert(*e.args) + + if time.time() - start > self.ssl_timeout: + raise socket.timeout('timed out') + + def recv(self, size): + """Receive message of a size from the socket.""" + return self._safe_call(True, super(SSL_fileobject, self).recv, size) + + def sendall(self, *args, **kwargs): + """Send whole message to the socket.""" + return self._safe_call(False, super(SSL_fileobject, self).sendall, + *args, **kwargs) + + def send(self, *args, **kwargs): + """Send some part of message to the socket.""" + return self._safe_call(False, super(SSL_fileobject, self).send, + *args, **kwargs) + + +class SSLConnection: + """A thread-safe wrapper for an SSL.Connection. + + ``*args``: the arguments to create the wrapped ``SSL.Connection(*args)``. + """ + + def __init__(self, *args): + """Initialize SSLConnection instance.""" + self._ssl_conn = SSL.Connection(*args) + self._lock = threading.RLock() + + for f in ('get_context', 'pending', 'send', 'write', 'recv', 'read', + 'renegotiate', 'bind', 'listen', 'connect', 'accept', + 'setblocking', 'fileno', 'close', 'get_cipher_list', + 'getpeername', 'getsockname', 'getsockopt', 'setsockopt', + 'makefile', 'get_app_data', 'set_app_data', 'state_string', + 'sock_shutdown', 'get_peer_certificate', 'want_read', + 'want_write', 'set_connect_state', 'set_accept_state', + 'connect_ex', 'sendall', 'settimeout', 'gettimeout'): + exec("""def %s(self, *args): + self._lock.acquire() + try: + return self._ssl_conn.%s(*args) + finally: + self._lock.release() +""" % (f, f)) + + def shutdown(self, *args): + """Shutdown the SSL connection. + + Ignore all incoming args since pyOpenSSL.socket.shutdown takes no args. + """ + self._lock.acquire() + try: + return self._ssl_conn.shutdown() + finally: + self._lock.release() + + +class pyOpenSSLAdapter(Adapter): + """A wrapper for integrating pyOpenSSL with Cheroot.""" + + certificate = None + """The filename of the server SSL certificate.""" + + private_key = None + """The filename of the server's private key file.""" + + certificate_chain = None + """Optional. The filename of CA's intermediate certificate bundle. + + This is needed for cheaper "chained root" SSL certificates, and should be + left as None if not required.""" + + context = None + """An instance of SSL.Context.""" + + ciphers = None + """The ciphers list of SSL.""" + + def __init__( + self, certificate, private_key, certificate_chain=None, + ciphers=None): + """Initialize OpenSSL Adapter instance.""" + if SSL is None: + raise ImportError('You must install pyOpenSSL to use HTTPS.') + + super(pyOpenSSLAdapter, self).__init__( + certificate, private_key, certificate_chain, ciphers) + + self._environ = None + + def bind(self, sock): + """Wrap and return the given socket.""" + if self.context is None: + self.context = self.get_context() + conn = SSLConnection(self.context, sock) + self._environ = self.get_environ() + return conn + + def wrap(self, sock): + """Wrap and return the given socket, plus WSGI environ entries.""" + return sock, self._environ.copy() + + def get_context(self): + """Return an SSL.Context from self attributes.""" + # See http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/442473 + c = SSL.Context(SSL.SSLv23_METHOD) + c.use_privatekey_file(self.private_key) + if self.certificate_chain: + c.load_verify_locations(self.certificate_chain) + c.use_certificate_file(self.certificate) + return c + + def get_environ(self): + """Return WSGI environ entries to be merged into each request.""" + ssl_environ = { + 'HTTPS': 'on', + # pyOpenSSL doesn't provide access to any of these AFAICT + # 'SSL_PROTOCOL': 'SSLv2', + # SSL_CIPHER string The cipher specification name + # SSL_VERSION_INTERFACE string The mod_ssl program version + # SSL_VERSION_LIBRARY string The OpenSSL program version + } + + if self.certificate: + # Server certificate attributes + cert = open(self.certificate, 'rb').read() + cert = crypto.load_certificate(crypto.FILETYPE_PEM, cert) + ssl_environ.update({ + 'SSL_SERVER_M_VERSION': cert.get_version(), + 'SSL_SERVER_M_SERIAL': cert.get_serial_number(), + # 'SSL_SERVER_V_START': + # Validity of server's certificate (start time), + # 'SSL_SERVER_V_END': + # Validity of server's certificate (end time), + }) + + for prefix, dn in [('I', cert.get_issuer()), + ('S', cert.get_subject())]: + # X509Name objects don't seem to have a way to get the + # complete DN string. Use str() and slice it instead, + # because str(dn) == "" + dnstr = str(dn)[18:-2] + + wsgikey = 'SSL_SERVER_%s_DN' % prefix + ssl_environ[wsgikey] = dnstr + + # The DN should be of the form: /k1=v1/k2=v2, but we must allow + # for any value to contain slashes itself (in a URL). + while dnstr: + pos = dnstr.rfind('=') + dnstr, value = dnstr[:pos], dnstr[pos + 1:] + pos = dnstr.rfind('/') + dnstr, key = dnstr[:pos], dnstr[pos + 1:] + if key and value: + wsgikey = 'SSL_SERVER_%s_DN_%s' % (prefix, key) + ssl_environ[wsgikey] = value + + return ssl_environ + + def makefile(self, sock, mode='r', bufsize=-1): + """Return socket file object.""" + if SSL and isinstance(sock, SSL.ConnectionType): + timeout = sock.gettimeout() + f = SSL_fileobject(sock, mode, bufsize) + f.ssl_timeout = timeout + return f + else: + return cheroot_server.CP_fileobject(sock, mode, bufsize) diff --git a/module/lib/wsgiserver/workers/__init__.py b/module/lib/wsgiserver/workers/__init__.py new file mode 100644 index 0000000000..098b8f25ff --- /dev/null +++ b/module/lib/wsgiserver/workers/__init__.py @@ -0,0 +1 @@ +"""HTTP workers pool.""" diff --git a/module/lib/wsgiserver/workers/threadpool.py b/module/lib/wsgiserver/workers/threadpool.py new file mode 100644 index 0000000000..110fbdaf2d --- /dev/null +++ b/module/lib/wsgiserver/workers/threadpool.py @@ -0,0 +1,267 @@ +"""A thread-based worker pool.""" + + +import threading +import time +import socket + +from six.moves import queue + + +__all__ = ('WorkerThread', 'ThreadPool') + + +class TrueyZero(object): + """Object which equals and does math like the integer 0 but evals True.""" + + def __add__(self, other): + return other + + def __radd__(self, other): + return other + + +trueyzero = TrueyZero() + +_SHUTDOWNREQUEST = None + + +class WorkerThread(threading.Thread): + """Thread which continuously polls a Queue for Connection objects. + + Due to the timing issues of polling a Queue, a WorkerThread does not + check its own 'ready' flag after it has started. To stop the thread, + it is necessary to stick a _SHUTDOWNREQUEST object onto the Queue + (one for each running WorkerThread). + """ + + conn = None + """The current connection pulled off the Queue, or None.""" + + server = None + """The HTTP Server which spawned this thread, and which owns the + Queue and is placing active connections into it.""" + + ready = False + """A simple flag for the calling server to know when this thread + has begun polling the Queue.""" + + def __init__(self, server): + """Initialize WorkerThread instance. + + Args: + server (cheroot.server.HTTPServer): web server object + receiving this request + """ + self.ready = False + self.server = server + + self.requests_seen = 0 + self.bytes_read = 0 + self.bytes_written = 0 + self.start_time = None + self.work_time = 0 + self.stats = { + 'Requests': lambda s: self.requests_seen + ( + (self.start_time is None) and + trueyzero or + self.conn.requests_seen + ), + 'Bytes Read': lambda s: self.bytes_read + ( + (self.start_time is None) and + trueyzero or + self.conn.rfile.bytes_read + ), + 'Bytes Written': lambda s: self.bytes_written + ( + (self.start_time is None) and + trueyzero or + self.conn.wfile.bytes_written + ), + 'Work Time': lambda s: self.work_time + ( + (self.start_time is None) and + trueyzero or + time.time() - self.start_time + ), + 'Read Throughput': lambda s: s['Bytes Read'](s) / ( + s['Work Time'](s) or 1e-6), + 'Write Throughput': lambda s: s['Bytes Written'](s) / ( + s['Work Time'](s) or 1e-6), + } + threading.Thread.__init__(self) + + def run(self): + """Process incoming HTTP connections. + + Retrieves incoming connections from thread pool. + """ + self.server.stats['Worker Threads'][self.getName()] = self.stats + try: + self.ready = True + while True: + conn = self.server.requests.get() + if conn is _SHUTDOWNREQUEST: + return + + self.conn = conn + if self.server.stats['Enabled']: + self.start_time = time.time() + try: + conn.communicate() + finally: + conn.close() + if self.server.stats['Enabled']: + self.requests_seen += self.conn.requests_seen + self.bytes_read += self.conn.rfile.bytes_read + self.bytes_written += self.conn.wfile.bytes_written + self.work_time += time.time() - self.start_time + self.start_time = None + self.conn = None + except (KeyboardInterrupt, SystemExit) as ex: + self.server.interrupt = ex + + +class ThreadPool(object): + """A Request Queue for an HTTPServer which pools threads. + + ThreadPool objects must provide min, get(), put(obj), start() + and stop(timeout) attributes. + """ + + def __init__( + self, server, min=10, max=-1, accepted_queue_size=-1, + accepted_queue_timeout=10): + """Initialize HTTP requests queue instance. + + Args: + server (cheroot.server.HTTPServer): web server object + receiving this request + min (int): minimum number of worker threads + max (int): maximum number of worker threads + accepted_queue_size (int): maximum number of active + requests in queue + accepted_queue_timeout (int): timeout for putting request + into queue + """ + self.server = server + self.min = min + self.max = max + self._threads = [] + self._queue = queue.Queue(maxsize=accepted_queue_size) + self._queue_put_timeout = accepted_queue_timeout + self.get = self._queue.get + + def start(self): + """Start the pool of threads.""" + for i in range(self.min): + self._threads.append(WorkerThread(self.server)) + for worker in self._threads: + worker.setName('CP Server ' + worker.getName()) + worker.start() + for worker in self._threads: + while not worker.ready: + time.sleep(.1) + + def _get_idle(self): + """Number of worker threads which are idle. Read-only.""" + return len([t for t in self._threads if t.conn is None]) + idle = property(_get_idle, doc=_get_idle.__doc__) + + def put(self, obj): + """Put request into queue. + + Args: + obj (cheroot.server.HTTPConnection): HTTP connection + waiting to be processed + """ + self._queue.put(obj, block=True, timeout=self._queue_put_timeout) + if obj is _SHUTDOWNREQUEST: + return + + def grow(self, amount): + """Spawn new worker threads (not above self.max).""" + if self.max > 0: + budget = max(self.max - len(self._threads), 0) + else: + # self.max <= 0 indicates no maximum + budget = float('inf') + + n_new = min(amount, budget) + + workers = [self._spawn_worker() for i in range(n_new)] + while not all(worker.ready for worker in workers): + time.sleep(.1) + self._threads.extend(workers) + + def _spawn_worker(self): + worker = WorkerThread(self.server) + worker.setName('CP Server ' + worker.getName()) + worker.start() + return worker + + def shrink(self, amount): + """Kill off worker threads (not below self.min).""" + # Grow/shrink the pool if necessary. + # Remove any dead threads from our list + for t in self._threads: + if not t.isAlive(): + self._threads.remove(t) + amount -= 1 + + # calculate the number of threads above the minimum + n_extra = max(len(self._threads) - self.min, 0) + + # don't remove more than amount + n_to_remove = min(amount, n_extra) + + # put shutdown requests on the queue equal to the number of threads + # to remove. As each request is processed by a worker, that worker + # will terminate and be culled from the list. + for n in range(n_to_remove): + self._queue.put(_SHUTDOWNREQUEST) + + def stop(self, timeout=5): + """Terminate all worker threads. + + Args: + timeout (int): time to wait for threads to stop gracefully + """ + # Must shut down threads here so the code that calls + # this method can know when all threads are stopped. + for worker in self._threads: + self._queue.put(_SHUTDOWNREQUEST) + + # Don't join currentThread (when stop is called inside a request). + current = threading.currentThread() + if timeout is not None and timeout >= 0: + endtime = time.time() + timeout + while self._threads: + worker = self._threads.pop() + if worker is not current and worker.isAlive(): + try: + if timeout is None or timeout < 0: + worker.join() + else: + remaining_time = endtime - time.time() + if remaining_time > 0: + worker.join(remaining_time) + if worker.isAlive(): + # We exhausted the timeout. + # Forcibly shut down the socket. + c = worker.conn + if c and not c.rfile.closed: + try: + c.socket.shutdown(socket.SHUT_RD) + except TypeError: + # pyOpenSSL sockets don't take an arg + c.socket.shutdown() + worker.join() + except (AssertionError, + # Ignore repeated Ctrl-C. + # See + # https://github.com/cherrypy/cherrypy/issues/691. + KeyboardInterrupt): + pass + + def _get_qsize(self): + return self._queue.qsize() + qsize = property(_get_qsize) diff --git a/module/lib/wsgiserver/wsgi.py b/module/lib/wsgiserver/wsgi.py new file mode 100644 index 0000000000..3a9f3fe15e --- /dev/null +++ b/module/lib/wsgiserver/wsgi.py @@ -0,0 +1,394 @@ +"""This class holds Cheroot WSGI server implementation. + +Simplest example on how to use this server:: + + from cheroot import wsgi + + def my_crazy_app(environ, start_response): + status = '200 OK' + response_headers = [('Content-type','text/plain')] + start_response(status, response_headers) + return ['Hello world!'] + + addr = '0.0.0.0', 8070 + server = wsgi.Server(addr, my_crazy_app) + server.start() + +The Cheroot WSGI server can serve as many WSGI applications +as you want in one instance by using a PathInfoDispatcher:: + + path_map = { + '/': my_crazy_app, + '/blog': my_blog_app, + } + d = wsgi.PathInfoDispatcher(path_map) + server = wsgi.Server(addr, d) +""" + +import sys + +import six +from six.moves import filter + +from . import server +from .workers import threadpool +from ._compat import ntob, bton + + +class Server(server.HTTPServer): + """A subclass of HTTPServer which calls a WSGI application.""" + + wsgi_version = (1, 0) + """The version of WSGI to produce.""" + + def __init__(self, bind_addr, wsgi_app, numthreads=10, server_name=None, + max=-1, request_queue_size=5, timeout=10, shutdown_timeout=5, + accepted_queue_size=-1, accepted_queue_timeout=10): + """Initialize WSGI Server instance. + + Args: + bind_addr (tuple): network interface to listen to + wsgi_app (callable): WSGI application callable + numthreads (int): number of threads for WSGI thread pool + server_name (str): web server name to be advertised via + Server HTTP header + max (int): maximum number of worker threads + request_queue_size (int): the 'backlog' arg to + socket.listen(); max queued connections + timeout (int): the timeout in seconds for accepted connections + shutdown_timeout (int): the total time, in seconds, to + wait for worker threads to cleanly exit + accepted_queue_size (int): maximum number of active + requests in queue + accepted_queue_timeout (int): timeout for putting request + into queue + """ + super(Server, self).__init__( + bind_addr, + gateway=wsgi_gateways[self.wsgi_version], + server_name=server_name, + ) + self.wsgi_app = wsgi_app + self.request_queue_size = request_queue_size + self.timeout = timeout + self.shutdown_timeout = shutdown_timeout + self.requests = threadpool.ThreadPool( + self, min=numthreads or 1, max=max, + accepted_queue_size=accepted_queue_size, + accepted_queue_timeout=accepted_queue_timeout) + + def _get_numthreads(self): + return self.requests.min + + def _set_numthreads(self, value): + self.requests.min = value + numthreads = property(_get_numthreads, _set_numthreads) + + +class Gateway(server.Gateway): + """A base class to interface HTTPServer with WSGI.""" + + def __init__(self, req): + """Initialize WSGI Gateway instance with request. + + Args: + req (HTTPRequest): current HTTP request + """ + super(Gateway, self).__init__(req) + self.started_response = False + self.env = self.get_environ() + self.remaining_bytes_out = None + + @classmethod + def gateway_map(cls): + """Create a mapping of gateways and their versions. + + Returns: + dict[tuple[int,int],class]: map of gateway version and + corresponding class + """ + return dict( + (gw.version, gw) + for gw in cls.__subclasses__() + ) + + def get_environ(self): + """Return a new environ dict targeting the given wsgi.version.""" + raise NotImplementedError + + def respond(self): + """Process the current request. + + From PEP 333: + + The start_response callable must not actually transmit + the response headers. Instead, it must store them for the + server or gateway to transmit only after the first + iteration of the application return value that yields + a NON-EMPTY string, or upon the application's first + invocation of the write() callable. + """ + response = self.req.server.wsgi_app(self.env, self.start_response) + try: + for chunk in filter(None, response): + if not isinstance(chunk, six.binary_type): + raise ValueError('WSGI Applications must yield bytes') + self.write(chunk) + finally: + if hasattr(response, 'close'): + response.close() + + def start_response(self, status, headers, exc_info=None): + """WSGI callable to begin the HTTP response.""" + # "The application may call start_response more than once, + # if and only if the exc_info argument is provided." + if self.started_response and not exc_info: + raise AssertionError('WSGI start_response called a second ' + 'time with no exc_info.') + self.started_response = True + + # "if exc_info is provided, and the HTTP headers have already been + # sent, start_response must raise an error, and should raise the + # exc_info tuple." + if self.req.sent_headers: + try: + six.reraise(*exc_info) + finally: + exc_info = None + + self.req.status = self._encode_status(status) + + for k, v in headers: + if not isinstance(k, str): + raise TypeError( + 'WSGI response header key %r is not of type str.' % k) + if not isinstance(v, str): + raise TypeError( + 'WSGI response header value %r is not of type str.' % v) + if k.lower() == 'content-length': + self.remaining_bytes_out = int(v) + out_header = ntob(k), ntob(v) + self.req.outheaders.append(out_header) + + return self.write + + @staticmethod + def _encode_status(status): + """Cast status to bytes representation of current Python version. + + According to PEP 3333, when using Python 3, the response status + and headers must be bytes masquerading as unicode; that is, they + must be of type "str" but are restricted to code points in the + "latin-1" set. + """ + if six.PY2: + return status + if not isinstance(status, str): + raise TypeError('WSGI response status is not of type str.') + return status.encode('ISO-8859-1') + + def write(self, chunk): + """WSGI callable to write unbuffered data to the client. + + This method is also used internally by start_response (to write + data from the iterable returned by the WSGI application). + """ + if not self.started_response: + raise AssertionError('WSGI write called before start_response.') + + chunklen = len(chunk) + rbo = self.remaining_bytes_out + if rbo is not None and chunklen > rbo: + if not self.req.sent_headers: + # Whew. We can send a 500 to the client. + self.req.simple_response( + '500 Internal Server Error', + 'The requested resource returned more bytes than the ' + 'declared Content-Length.') + else: + # Dang. We have probably already sent data. Truncate the chunk + # to fit (so the client doesn't hang) and raise an error later. + chunk = chunk[:rbo] + + if not self.req.sent_headers: + self.req.sent_headers = True + self.req.send_headers() + + self.req.write(chunk) + + if rbo is not None: + rbo -= chunklen + if rbo < 0: + raise ValueError( + 'Response body exceeds the declared Content-Length.') + + +class Gateway_10(Gateway): + """A Gateway class to interface HTTPServer with WSGI 1.0.x.""" + + version = 1, 0 + + def get_environ(self): + """Return a new environ dict targeting the given wsgi.version.""" + req = self.req + env = { + # set a non-standard environ entry so the WSGI app can know what + # the *real* server protocol is (and what features to support). + # See http://www.faqs.org/rfcs/rfc2145.html. + 'ACTUAL_SERVER_PROTOCOL': req.server.protocol, + 'PATH_INFO': bton(req.path), + 'QUERY_STRING': bton(req.qs), + 'REMOTE_ADDR': req.conn.remote_addr or '', + 'REMOTE_PORT': str(req.conn.remote_port or ''), + 'REQUEST_METHOD': bton(req.method), + 'REQUEST_URI': bton(req.uri), + 'SCRIPT_NAME': '', + 'SERVER_NAME': req.server.server_name, + # Bah. "SERVER_PROTOCOL" is actually the REQUEST protocol. + 'SERVER_PROTOCOL': bton(req.request_protocol), + 'SERVER_SOFTWARE': req.server.software, + 'wsgi.errors': sys.stderr, + 'wsgi.input': req.rfile, + 'wsgi.multiprocess': False, + 'wsgi.multithread': True, + 'wsgi.run_once': False, + 'wsgi.url_scheme': bton(req.scheme), + 'wsgi.version': self.version, + } + + if isinstance(req.server.bind_addr, six.string_types): + # AF_UNIX. This isn't really allowed by WSGI, which doesn't + # address unix domain sockets. But it's better than nothing. + env['SERVER_PORT'] = '' + else: + env['SERVER_PORT'] = str(req.server.bind_addr[1]) + + # Request headers + env.update( + ('HTTP_' + bton(k).upper().replace('-', '_'), bton(v)) + for k, v in req.inheaders.items() + ) + + # CONTENT_TYPE/CONTENT_LENGTH + ct = env.pop('HTTP_CONTENT_TYPE', None) + if ct is not None: + env['CONTENT_TYPE'] = ct + cl = env.pop('HTTP_CONTENT_LENGTH', None) + if cl is not None: + env['CONTENT_LENGTH'] = cl + + if req.conn.ssl_env: + env.update(req.conn.ssl_env) + + return env + + +class Gateway_u0(Gateway_10): + """A Gateway class to interface HTTPServer with WSGI u.0. + + WSGI u.0 is an experimental protocol, which uses unicode for keys + and values in both Python 2 and Python 3. + """ + + version = 'u', 0 + + def get_environ(self): + """Return a new environ dict targeting the given wsgi.version.""" + req = self.req + env_10 = super(Gateway_u0, self).get_environ() + env = dict(map(self._decode_key, env_10.items())) + + # Request-URI + enc = env.setdefault(six.u('wsgi.url_encoding'), six.u('utf-8')) + try: + env['PATH_INFO'] = req.path.decode(enc) + env['QUERY_STRING'] = req.qs.decode(enc) + except UnicodeDecodeError: + # Fall back to latin 1 so apps can transcode if needed. + env['wsgi.url_encoding'] = 'ISO-8859-1' + env['PATH_INFO'] = env_10['PATH_INFO'] + env['QUERY_STRING'] = env_10['QUERY_STRING'] + + env.update(map(self._decode_value, env.items())) + + return env + + @staticmethod + def _decode_key(item): + k, v = item + if six.PY2: + k = k.decode('ISO-8859-1') + return k, v + + @staticmethod + def _decode_value(item): + k, v = item + skip_keys = 'REQUEST_URI', 'wsgi.input' + if six.PY3 or not isinstance(v, bytes) or k in skip_keys: + return k, v + return k, v.decode('ISO-8859-1') + + +wsgi_gateways = Gateway.gateway_map() + + +class PathInfoDispatcher(object): + """A WSGI dispatcher for dispatch based on the PATH_INFO.""" + + def __init__(self, apps): + """Initialize path info WSGI app dispatcher. + + Args: + apps (dict[str,object]|list[tuple[str,object]]): URI prefix + and WSGI app pairs + """ + try: + apps = list(apps.items()) + except AttributeError: + pass + + # Sort the apps by len(path), descending + def by_path_len(app): + return len(app[0]) + apps.sort(key=by_path_len, reverse=True) + + # The path_prefix strings must start, but not end, with a slash. + # Use "" instead of "/". + self.apps = [(p.rstrip('/'), a) for p, a in apps] + + def __call__(self, environ, start_response): + """Process incoming WSGI request. + + Ref: PEP 3333 + + Args: + environ (Mapping): a dict containing WSGI environment variables + start_response (callable): function, which sets response + status and headers + + Returns: + list[bytes]: iterable containing bytes to be returned in + HTTP response body + """ + path = environ['PATH_INFO'] or '/' + for p, app in self.apps: + # The apps list should be sorted by length, descending. + if path.startswith(p + '/') or path == p: + environ = environ.copy() + environ['SCRIPT_NAME'] = environ['SCRIPT_NAME'] + p + environ['PATH_INFO'] = path[len(p):] + return app(environ, start_response) + + start_response('404 Not Found', [('Content-Type', 'text/plain'), + ('Content-Length', '0')]) + return [''] + + +# compatibility aliases +globals().update( + WSGIServer=Server, + WSGIGateway=Gateway, + WSGIGateway_u0=Gateway_u0, + WSGIGateway_10=Gateway_10, + WSGIPathInfoDispatcher=PathInfoDispatcher, +) diff --git a/module/network/Browser.py b/module/network/Browser.py index d68a236870..5a25308685 100644 --- a/module/network/Browser.py +++ b/module/network/Browser.py @@ -79,13 +79,15 @@ def abortDownloads(self): self._size = self.dl.size self.dl.abort = True - def httpDownload(self, url, filename, get={}, post={}, ref=True, cookies=True, chunks=1, resume=False, - progressNotify=None, disposition=False): + def httpDownload(self, url, filename, size=0, get={}, post={}, ref=True, cookies=True, chunks=1, resume=False, + status_notify=None, disposition=False): """ this can also download ftp """ self._size = 0 - self.dl = HTTPDownload(url, filename, get, post, self.lastEffectiveURL if ref else None, - self.cj if cookies else None, self.bucket, self.options, progressNotify, disposition) + self.dl = HTTPDownload(url, filename, size=size, get=get, post=post, referer=self.lastEffectiveURL if ref else None, + cj=self.cj if cookies else None, bucket=self.bucket, options=self.options, + status_notify=status_notify, disposition=disposition) name = self.dl.download(chunks, resume) + self.http.code = self.dl.code self._size = self.dl.size self.dl = None diff --git a/module/network/HTTPChunk.py b/module/network/HTTPChunk.py index b637aef327..33427086a9 100644 --- a/module/network/HTTPChunk.py +++ b/module/network/HTTPChunk.py @@ -16,15 +16,20 @@ @author: RaNaN """ -from os import remove, stat, fsync -from os.path import exists -from time import sleep -from re import search -from module.utils import fs_encode import codecs -import pycurl +import os +import re +import time +import urllib +from cgi import parse_header +from email.header import decode_header +from ntpath import basename as ntpath_basename +from posixpath import basename as posixpath_basename +import pycurl from HTTPRequest import HTTPRequest +from module.utils import decode, fs_encode, parse_name + class WrongFormat(Exception): pass @@ -32,7 +37,7 @@ class WrongFormat(Exception): class ChunkInfo(): def __init__(self, name): - self.name = unicode(name) + self.name = decode(name) self.size = 0 self.resume = False self.chunks = [] @@ -63,7 +68,6 @@ def createChunks(self, chunks): self.addChunk("%s.chunk%s" % (self.name, i), (current, end)) current += chunk_size + 1 - def save(self): fs_name = fs_encode("%s.chunks" % self.name) fh = codecs.open(fs_name, "w", "utf_8") @@ -78,7 +82,7 @@ def save(self): @staticmethod def load(name): fs_name = fs_encode("%s.chunks" % name) - if not exists(fs_name): + if not os.path.exists(fs_name): raise IOError() fh = codecs.open(fs_name, "r", "utf_8") name = fh.readline()[:-1] @@ -93,7 +97,7 @@ def load(name): ci.loaded = True ci.setSize(size) while True: - if not fh.readline(): #skip line + if not fh.readline(): # skip line break name = fh.readline()[1:-1] range = fh.readline()[1:-1] @@ -103,18 +107,19 @@ def load(name): else: raise WrongFormat() - ci.addChunk(name, (long(range[0]), long(range[1]))) + ci.addChunk(name, (int(range[0]), int(range[1]))) fh.close() return ci def remove(self): fs_name = fs_encode("%s.chunks" % self.name) - if exists(fs_name): remove(fs_name) + if os.path.exists(fs_name): + os.remove(fs_name) def getCount(self): return len(self.chunks) - def getChunkName(self, index): + def getChunkFilename(self, index): return self.chunks[index][0] def getChunkRange(self, index): @@ -124,8 +129,8 @@ def getChunkRange(self, index): class HTTPChunk(HTTPRequest): def __init__(self, id, parent, range=None, resume=False): self.id = id - self.p = parent # HTTPDownload instance - self.range = range # tuple (start, end) + self.p = parent # HTTPDownload instance + self.range = range # tuple (start, end) self.resume = resume self.log = parent.log @@ -133,21 +138,23 @@ def __init__(self, id, parent, range=None, resume=False): self.arrived = 0 self.lastURL = self.p.referer + self.aborted = False # indicates that the chunk aborted gracefully + self.c = pycurl.Curl() self.header = "" - self.headerParsed = False #indicates if the header has been processed + self.headerParsed = False # indicates if the header has been processed - self.fp = None #file handle + self.fp = None # file handle self.initHandle() self.setInterface(self.p.options) - self.BOMChecked = False # check and remove byte order mark + self.BOMChecked = False # check and remove byte order mark self.rep = None - self.sleep = 0.000 + self.sleep = 0.0 self.lastSize = 0 def __repr__(self): @@ -157,8 +164,22 @@ def __repr__(self): def cj(self): return self.p.cj + def formatRange(self): + if self.id == len(self.p.info.chunks) - 1: # as last chunk dont set end range, so we get everything + if self.resume: + range = "%i-" % (self.arrived + self.range[0]) + else: + range = "%i-" % self.range[0] + else: + if self.id == 0 and not self.resume: # special case for first chunk + range = "%i-%i" % (0, min(self.range[1] + 1, self.p.size - 1)) + else: + range = "%i-%i" % (self.arrived + self.range[0], min(self.range[1] + 1, self.p.size - 1)) + + return range + def getHandle(self): - """ returns a Curl handle ready to use for perform/multiperform """ + """ returns a Curl handle ready to use for perform/multi-perform """ self.setRequestContext(self.p.url, self.p.get, self.p.post, self.p.referer, self.p.cj) self.c.setopt(pycurl.WRITEFUNCTION, self.writeBody) @@ -166,23 +187,21 @@ def getHandle(self): # request all bytes, since some servers in russia seems to have a defect arihmetic unit - fs_name = fs_encode(self.p.info.getChunkName(self.id)) + fs_name = fs_encode(self.p.info.getChunkFilename(self.id)) if self.resume: self.fp = open(fs_name, "ab") self.arrived = self.fp.tell() if not self.arrived: - self.arrived = stat(fs_name).st_size + self.arrived = os.stat(fs_name).st_size if self.range: - #do nothing if chunk already finished - if self.arrived + self.range[0] >= self.range[1]: return None + # do nothing if chunk already finished + if self.arrived + self.range[0] >= self.range[1]: + return None - if self.id == len(self.p.info.chunks) - 1: #as last chunk dont set end range, so we get everything - range = "%i-" % (self.arrived + self.range[0]) - else: - range = "%i-%i" % (self.arrived + self.range[0], min(self.range[1] + 1, self.p.size - 1)) + range = self.formatRange() - self.log.debug("Chunked resume with range %s" % range) + self.log.debug("Chunked resume with range %s" % self.formatRange()) self.c.setopt(pycurl.RANGE, range) else: self.log.debug("Resume File from %i" % self.arrived) @@ -190,10 +209,7 @@ def getHandle(self): else: if self.range: - if self.id == len(self.p.info.chunks) - 1: # see above - range = "%i-" % self.range[0] - else: - range = "%i-%i" % (self.range[0], min(self.range[1] + 1, self.p.size - 1)) + range = self.formatRange() self.log.debug("Chunked with range %s" % range) self.c.setopt(pycurl.RANGE, range) @@ -204,22 +220,24 @@ def getHandle(self): def writeHeader(self, buf): self.header += buf - #@TODO forward headers?, this is possibly unneeeded, when we just parse valid 200 headers + # @TODO forward headers?, this is possibly unneeeded, when we just parse valid 200 headers # as first chunk, we will parse the headers if not self.range and self.header.endswith("\r\n\r\n"): self.parseHeader() - elif not self.range and buf.startswith("150") and "data connection" in buf: #ftp file size parsing - size = search(r"(\d+) bytes", buf) + elif not self.range and buf.startswith("150") and "data connection" in buf: # ftp file size parsing + size = re.search(r'(\d+) bytes', buf) if size: self.p.size = int(size.group(1)) self.p.chunkSupport = True - self.headerParsed = True + self.headerParsed = True + + return None #: All is fine def writeBody(self, buf): - #ignore BOM, it confuses unrar + #: Ignore BOM, it confuses unrar if not self.BOMChecked: - if [ord(b) for b in buf[:3]] == [239, 187, 191]: + if buf[:3] == codecs.BOM_UTF8: buf = buf[3:] self.BOMChecked = True @@ -230,7 +248,8 @@ def writeBody(self, buf): self.fp.write(buf) if self.p.bucket: - sleep(self.p.bucket.consumed(size)) + time.sleep(self.p.bucket.consumed(size)) + else: # Avoid small buffers, increasing sleep time slowly if buffer size gets smaller # otherwise reduce sleep time percentual (values are based on tests) @@ -243,33 +262,108 @@ def writeBody(self, buf): self.lastSize = size - sleep(self.sleep) + time.sleep(self.sleep) if self.range and self.arrived > self.size: - return 0 #close if we have enough data + self.aborted = True #: Tell parent to ignore the pycurl Exception + return 0 #: Close if we have enough data + return None #: All is fine def parseHeader(self): """parse data from recieved header""" - for orgline in self.decodeResponse(self.header).splitlines(): + location = None + for orgline in self.header.splitlines(): line = orgline.strip().lower() if line.startswith("accept-ranges") and "bytes" in line: self.p.chunkSupport = True - if line.startswith("content-disposition") and "filename=" in line: - name = orgline.partition("filename=")[2] - name = name.replace('"', "").replace("'", "").replace(";", "").strip() - self.p.nameDisposition = name - self.log.debug("Content-Disposition: %s" % name) - - if not self.resume and line.startswith("content-length"): + elif line.startswith("location"): + location = orgline.split(":", 1)[1].strip() + + elif line.startswith("content-disposition"): + disposition_value = orgline.split(":", 1)[1].strip() + disposition_type, disposition_params = parse_header(disposition_value) + + fname = None + if 'filename*' in disposition_params: + fname = disposition_params['filename*'] + m = re.search(r'=\?([^?]+)\?([QB])\?([^?]*)\?=', fname, re.I) #: rfc2047 + if m is not None: + fname, enc = decode_header(fname)[0] + try: + fname = fname.decode(enc) + except LookupError: + self.log.warning("Content-Disposition: | error: No decoder found for %s" % enc) + fname = None + except UnicodeEncodeError: + self.log.warning("Content-Disposition: | error: Error when decoding string from %s." % enc) + fname = None + + else: + m = re.search(r'(.+?)\'(.*)\'(.+)', fname) + if m is not None: + enc, lang, data = m.groups() + try: + fname = urllib.unquote(data).decode(enc) + except LookupError: + self.log.warning("Content-Disposition: | error: No decoder found for %s" % enc) + fname = None + except UnicodeEncodeError: + self.log.warning("Content-Disposition: | error: Error when decoding string from %s." % enc) + fname = None + + else: + fname = None + + if fname is None: + if 'filename' in disposition_params: + fname = disposition_params['filename'] + m = re.search(r'=\?([^?]+)\?([QB])\?([^?]*)\?=', fname, re.I) #: rfc2047 + if m is not None: + fname, enc = decode_header(m.group(0))[0] + try: + fname = fname.decode(enc) + except LookupError: + self.log.warning("Content-Disposition: | error: No decoder found for %s" % enc) + continue + except UnicodeEncodeError: + self.log.warning("Content-Disposition: | error: Error when decoding string from %s." % enc) + continue + else: + try: + fname = urllib.unquote(fname).decode('iso-8859-1') + except UnicodeEncodeError: + self.log.warning("Content-Disposition: | error: Error when decoding string from iso-8859-1.") + continue + + elif disposition_type.lower() == "attachment": + if location is not None: + fname = parse_name(location) + else: + fname = parse_name(self.p.url) + + else: + continue + + #: Drop unsafe characters + fname = posixpath_basename(fname) + fname = ntpath_basename(fname) + for badc in '<>:"/\\|?*\0': + fname = fname.replace(badc, "") + fname = fname.lstrip('.') + + self.log.debug("Content-Disposition: %s" % fname) + self.p.updateDisposition(fname) + + elif not self.resume and line.startswith("content-length"): self.p.size = int(line.split(":")[1]) self.headerParsed = True def stop(self): """The download will not proceed after next call of writeBody""" - self.range = [0,0] + self.range = [0, 0] self.size = 0 def resetRange(self): @@ -279,15 +373,16 @@ def resetRange(self): def setRange(self, range): self.range = range self.size = range[1] - range[0] + self.log.debug("Chunked with range %s" % self.formatRange()) def flushFile(self): """ flush and close file """ self.fp.flush() - fsync(self.fp.fileno()) #make sure everything was written to disk - self.fp.close() #needs to be closed, or merging chunks will fail + os.fsync(self.fp.fileno()) # make sure everything was written to disk + self.fp.close() # needs to be closed, or merging chunks will fail def close(self): """ closes everything, unusable after this """ if self.fp: self.fp.close() self.c.close() - if hasattr(self, "p"): del self.p \ No newline at end of file + if hasattr(self, "p"): del self.p diff --git a/module/network/HTTPDownload.py b/module/network/HTTPDownload.py index fe80755390..3e5ab5a0bb 100644 --- a/module/network/HTTPDownload.py +++ b/module/network/HTTPDownload.py @@ -31,25 +31,28 @@ from module.plugins.Plugin import Abort from module.utils import save_join, fs_encode + class HTTPDownload(): """ loads a url http + ftp """ - def __init__(self, url, filename, get={}, post={}, referer=None, cj=None, bucket=None, - options={}, progressNotify=None, disposition=False): + def __init__(self, url, filename, size=0, get={}, post={}, referer=None, cj=None, bucket=None, + options={}, status_notify=None, disposition=False): self.url = url - self.filename = filename #complete file destination, not only name + self.filename = filename # complete file destination, not only name self.get = get self.post = post self.referer = referer - self.cj = cj #cookiejar if cookies are needed + self.cj = cj # cookiejar if cookies are needed self.bucket = bucket self.options = options self.disposition = disposition # all arguments + self.code = 0 #: last http code, would be set from the first chunk + self.abort = False - self.size = 0 - self.nameDisposition = None #will be parsed from content disposition + self.size = size + self.nameDisposition = None # will be parsed from content disposition self.chunks = [] @@ -57,7 +60,7 @@ def __init__(self, url, filename, get={}, post={}, referer=None, cj=None, bucket try: self.info = ChunkInfo.load(filename) - self.info.resume = True #resume is only possible with valid info file + self.info.resume = True # resume is only possible with valid info file self.size = self.info.size self.infoSaved = True except IOError: @@ -66,12 +69,13 @@ def __init__(self, url, filename, get={}, post={}, referer=None, cj=None, bucket self.chunkSupport = None self.m = pycurl.CurlMulti() - #needed for speed calculation + # needed for speed calculation self.lastArrived = [] self.speeds = [] self.lastSpeeds = [0, 0] - self.progressNotify = progressNotify + # notifications callback + self.status_notify = status_notify if callable(status_notify) else None @property def speed(self): @@ -88,18 +92,17 @@ def percent(self): return (self.arrived * 100) / self.size def _copyChunks(self): - init = fs_encode(self.info.getChunkName(0)) #initial chunk name + init = fs_encode(self.info.getChunkFilename(0)) # initial chunk name if self.info.getCount() > 1: - fo = open(init, "rb+") #first chunkfile + fo = open(init, "rb+") # first chunkfile for i in range(1, self.info.getCount()): - #input file - fo.seek( - self.info.getChunkRange(i - 1)[1] + 1) #seek to beginning of chunk, to get rid of overlapping chunks + # input file + fo.seek(self.info.getChunkRange(i - 1)[1] + 1) # seek to beginning of chunk, to get rid of overlapping chunks fname = fs_encode("%s.chunk%d" % (self.filename, i)) fi = open(fname, "rb") buf = 32 * 1024 - while True: #copy in chunks, consumes less memory + while True: # copy in chunks, consumes less memory data = fi.read(buf) if not data: break @@ -108,16 +111,16 @@ def _copyChunks(self): if fo.tell() < self.info.getChunkRange(i)[1]: fo.close() remove(init) - self.info.remove() #there are probably invalid chunks + self.info.remove() # there are probably invalid chunks raise Exception("Downloaded content was smaller than expected. Try to reduce download connections.") - remove(fname) #remove chunk + remove(fname) # remove chunk fo.close() if self.nameDisposition and self.disposition: self.filename = save_join(dirname(self.filename), self.nameDisposition) move(init, fs_encode(self.filename)) - self.info.remove() #remove info file + self.info.remove() # remove info file def download(self, chunks=1, resume=False): """ returns new filename or None """ @@ -128,13 +131,13 @@ def download(self, chunks=1, resume=False): try: self._download(chunks, resume) except pycurl.error, e: - #code 33 - no resume + # code 33 - no resume code = e.args[0] if code == 33: # try again without resume self.log.debug("Errno 33 -> Restart without resume") - #remove old handles + # remove old handles for chunk in self.chunks: self.closeChunk(chunk) @@ -144,17 +147,19 @@ def download(self, chunks=1, resume=False): finally: self.close() - if self.nameDisposition and self.disposition: return self.nameDisposition - return None + if self.nameDisposition and self.disposition: + return self.nameDisposition + else: + return None def _download(self, chunks, resume): if not resume: self.info.clear() - self.info.addChunk("%s.chunk0" % self.filename, (0, 0)) #create an initial entry + self.info.addChunk("%s.chunk0" % self.filename, (0, 0)) # create an initial entry self.chunks = [] - init = HTTPChunk(0, self, None, resume) #initial chunk that will load complete file (if needed) + init = HTTPChunk(0, self, None, resume) # initial chunk that will load complete file (if needed) self.chunks.append(init) self.m.add_handle(init.getHandle()) @@ -168,8 +173,8 @@ def _download(self, chunks, resume): self.chunkSupport = True while 1: - #need to create chunks - if not chunksCreated and self.chunkSupport and self.size: #will be setted later by first chunk + # do we need to create chunks? + if not chunksCreated and self.chunkSupport and self.size: # will be set later by first chunk if not resume: self.info.setSize(self.size) @@ -188,7 +193,7 @@ def _download(self, chunks, resume): self.chunks.append(c) self.m.add_handle(handle) else: - #close immediatly + # close immediatly self.log.debug("Invalid curl handle -> closed") c.close() @@ -211,9 +216,10 @@ def _download(self, chunks, resume): for c in ok_list: chunk = self.findChunk(c) try: # check if the header implies success, else add it to failed list - chunk.verifyHeader() + chunk.code = chunk.verifyHeader() except BadHeader, e: self.log.debug("Chunk %d failed: %s" % (chunk.id + 1, str(e))) + chunk.code = e.code failed.append(chunk) ex = e else: @@ -222,36 +228,38 @@ def _download(self, chunks, resume): for c in err_list: curl, errno, msg = c chunk = self.findChunk(curl) - #test if chunk was finished - if errno != 23 or "0 !=" not in msg: + # test if chunk was finished + if errno != pycurl.E_WRITE_ERROR or not chunk.aborted: failed.append(chunk) ex = pycurl.error(errno, msg) self.log.debug("Chunk %d failed: %s" % (chunk.id + 1, str(ex))) continue try: # check if the header implies success, else add it to failed list - chunk.verifyHeader() + chunk.code = chunk.verifyHeader() except BadHeader, e: self.log.debug("Chunk %d failed: %s" % (chunk.id + 1, str(e))) + chunk.code = e.code failed.append(chunk) ex = e else: chunksDone.add(curl) - if not num_q: # no more infos to get + + if num_q == 0: # no more infos to get # check if init is not finished so we reset download connections # note that other chunks are closed and downloaded with init too if failed and init not in failed and init.c not in chunksDone: self.log.error(_("Download chunks failed, fallback to single connection | %s" % (str(ex)))) - #list of chunks to clean and remove + # list of chunks to clean and remove to_clean = filter(lambda x: x is not init, self.chunks) for chunk in to_clean: self.closeChunk(chunk) self.chunks.remove(chunk) - remove(fs_encode(self.info.getChunkName(chunk.id))) + remove(fs_encode(self.info.getChunkFilename(chunk.id))) - #let first chunk load the rest and update the info file + # let first chunk load the rest and update the info file init.resetRange() self.info.clear() self.info.addChunk("%s.chunk0" % self.filename, (0, self.size)) @@ -264,12 +272,12 @@ def _download(self, chunks, resume): if len(chunksDone) >= len(self.chunks): if len(chunksDone) > len(self.chunks): self.log.warning("Finished download chunks size incorrect, please report bug.") - done = True #all chunks loaded + done = True # all chunks loaded break if done: - break #all chunks loaded + break # all chunks loaded # calc speed once per second, averaging over 3 seconds if lastTimeCheck + 1 < t: @@ -286,17 +294,27 @@ def _download(self, chunks, resume): if self.abort: raise Abort() - #sleep(0.003) #supress busy waiting - limits dl speed to (1 / x) * buffersize + #sleep(0.003) # supress busy waiting - limits dl speed to (1 / x) * buffersize self.m.select(1) for chunk in self.chunks: - chunk.flushFile() #make sure downloads are written to disk + chunk.flushFile() # make sure downloads are written to disk self._copyChunks() + self.code = init.code + def updateProgress(self): - if self.progressNotify: - self.progressNotify(self.percent) + if self.status_notify: + self.status_notify({'progress': self.percent}) + + def updateDisposition(self, disposition): + self.nameDisposition = disposition + if self.disposition: + if self.status_notify: + self.status_notify({'disposition': disposition}) + else: + self.log.debug("Ignoring Content-Disposition header") def findChunk(self, handle): """ linear search to find a chunk (should be ok since chunk size is usually low) """ diff --git a/module/network/HTTPRequest.py b/module/network/HTTPRequest.py index 4747d937f3..9a601c6131 100644 --- a/module/network/HTTPRequest.py +++ b/module/network/HTTPRequest.py @@ -17,16 +17,20 @@ @author: RaNaN """ -import pycurl +from __future__ import with_statement -from codecs import getincrementaldecoder, lookup, BOM_UTF8 -from urllib import quote, urlencode +import cStringIO +import mimetypes +from codecs import BOM_UTF8, getincrementaldecoder, lookup from httplib import responses from logging import getLogger -from cStringIO import StringIO +from os.path import abspath, basename, exists +from urllib import quote, urlencode +import pycurl from module.plugins.Plugin import Abort + def myquote(url): return quote(url.encode('utf_8') if isinstance(url, unicode) else url, safe="%/:=&?~#+!$,;'@()*[]") @@ -37,17 +41,44 @@ def myurlencode(data): bad_headers = range(400, 404) + range(405, 418) + range(500, 506) +unofficial_responses = { + 440: "Login Timeout - The client's session has expired and must log in again.", + 449: 'Retry With - The server cannot honour the request because the user has not provided the required information', + 451: 'Redirect - Unsupported Redirect Header', + 509: 'Bandwidth Limit Exceeded', + 520: 'Unknown Error', + 521: 'Web Server Is Down - The origin server has refused the connection from CloudFlare', + 522: 'Connection Timed Out - CloudFlare could not negotiate a TCP handshake with the origin server', + 523: 'Origin Is Unreachable - CloudFlare could not reach the origin server', + 524: 'A Timeout Occurred - CloudFlare did not receive a timely HTTP response', + 525: 'SSL Handshake Failed - CloudFlare could not negotiate a SSL/TLS handshake with the origin server', + 526: 'Invalid SSL Certificate - CloudFlare could not validate the SSL/TLS certificate that the origin server presented', + 527: 'Railgun Error - CloudFlare requests timeout or failed after the WAN connection has been established', + 530: 'Site Is Frozen - Used by the Pantheon web platform to indicate a site that has been frozen due to inactivity'} + class BadHeader(Exception): - def __init__(self, code, content=""): - Exception.__init__(self, "Bad server response: %s %s" % (code, responses[int(code)])) - self.code = code + def __init__(self, code, header="", content=""): + int_code = int(code) + Exception.__init__(self, "Bad server response: %s %s" % + (code, responses.get(int_code, unofficial_responses.get(int_code, "unknown error code")))) + self.code = int_code + self.header = header self.content = content +class FormFile(): + def __init__(self, filename, data=None, mimetype=None): + self.filename = abspath(filename) + self.data = data + self.mimetype = mimetype or \ + mimetypes.guess_type(filename)[0] if not data and exists(filename) else None or \ + 'application/octet-stream' + + class HTTPRequest(): def __init__(self, cookies=None, options=None): self.c = pycurl.Curl() - self.rep = StringIO() + self.rep = None self.cj = cookies #cookiejar @@ -79,13 +110,14 @@ def initHandle(self): if hasattr(pycurl, "AUTOREFERER"): self.c.setopt(pycurl.AUTOREFERER, 1) self.c.setopt(pycurl.SSL_VERIFYPEER, 0) - self.c.setopt(pycurl.LOW_SPEED_TIME, 30) + self.c.setopt(pycurl.LOW_SPEED_TIME, 60) self.c.setopt(pycurl.LOW_SPEED_LIMIT, 5) #self.c.setopt(pycurl.VERBOSE, 1) + #self.c.setopt(pycurl.HTTP_VERSION, pycurl.CURL_HTTP_VERSION_1_1) self.c.setopt(pycurl.USERAGENT, - "Mozilla/5.0 (Windows NT 6.1; Win64; x64;en; rv:5.0) Gecko/20110619 Firefox/5.0") + "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:75.0) Gecko/20100101 Firefox/75.0") if pycurl.version_info()[7]: self.c.setopt(pycurl.ENCODING, "gzip, deflate") self.c.setopt(pycurl.HTTPHEADER, ["Accept: */*", @@ -96,19 +128,23 @@ def initHandle(self): "Expect:"]) def setInterface(self, options): - interface, proxy, ipv6 = options["interface"], options["proxies"], options["ipv6"] if interface and interface.lower() != "none": self.c.setopt(pycurl.INTERFACE, str(interface)) if proxy: - if proxy["type"] == "socks4": - self.c.setopt(pycurl.PROXYTYPE, pycurl.PROXYTYPE_SOCKS4) - elif proxy["type"] == "socks5": - self.c.setopt(pycurl.PROXYTYPE, pycurl.PROXYTYPE_SOCKS5) - else: + if proxy["type"] == "http": self.c.setopt(pycurl.PROXYTYPE, pycurl.PROXYTYPE_HTTP) + elif proxy["type"] == "https": + self.c.setopt(pycurl.PROXYTYPE, pycurl.PROXYTYPE_HTTPS) + self.c.setopt(pycurl.PROXY_SSL_VERIFYPEER, 0) + elif proxy["type"] == "socks4": + self.c.setopt(pycurl.PROXYTYPE, + pycurl.PROXYTYPE_SOCKS4A if proxy["socksResolveDns"] else pycurl.PROXYTYPE_SOCKS4) + elif proxy["type"] == "socks5": + self.c.setopt(pycurl.PROXYTYPE, + pycurl.PROXYTYPE_SOCKS5_HOSTNAME if proxy["socksResolveDns"] else pycurl.PROXYTYPE_SOCKS5) self.c.setopt(pycurl.PROXY, str(proxy["address"])) self.c.setopt(pycurl.PROXYPORT, proxy["port"]) @@ -146,6 +182,8 @@ def clearCookies(self): def setRequestContext(self, url, get, post, referer, cookies, multipart=False): """ sets everything needed for the request """ + self.rep = cStringIO.StringIO() + url = myquote(url) if get: @@ -158,7 +196,9 @@ def setRequestContext(self, url, get, post, referer, cookies, multipart=False): if post: self.c.setopt(pycurl.POST, 1) if not multipart: - if type(post) == unicode: + if post is True: + post = "" + elif type(post) == unicode: post = str(post) #unicode not allowed elif type(post) == str: pass @@ -167,8 +207,27 @@ def setRequestContext(self, url, get, post, referer, cookies, multipart=False): self.c.setopt(pycurl.POSTFIELDS, post) else: - post = [(x, y.encode('utf8') if type(y) == unicode else y ) for x, y in post.iteritems()] - self.c.setopt(pycurl.HTTPPOST, post) + multipart_post = [] + for k, v in post.iteritems(): + if isinstance(v, (basestring, bool, int)): + multipart_post.append((k, v.encode('utf8') if type(v) == unicode else str(v))) + + elif isinstance(v, FormFile): + filename = basename(v.filename) + filename = filename.encode('utf8') if type(filename) == unicode else filename + data = v.data + if data is None: + if not exists(v.filename): + continue + else: + with open(v.filename, "rb") as f: #: workaround for pycurl.FORM_FILE UnicodeEncodeError + data = f.read() + multipart_post.append((k, (pycurl.FORM_BUFFER, filename, + pycurl.FORM_BUFFERPTR, data, + pycurl.FORM_CONTENTTYPE, str(v.mimetype)))) + + self.c.setopt(pycurl.HTTPPOST, multipart_post) + else: self.c.setopt(pycurl.POST, 0) @@ -200,15 +259,27 @@ def load(self, url, get={}, post={}, referer=True, cookies=True, just_header=Fal self.c.setopt(pycurl.NOBODY, 0) else: - self.c.perform() + try: + self.c.perform() + except pycurl.error, e: + if e.args[0] == pycurl.E_WRITE_ERROR and self.abort: #: Ignore write error on abort + pass + else: + raise rep = self.getResponse() self.c.setopt(pycurl.POSTFIELDS, "") self.lastEffectiveURL = self.c.getinfo(pycurl.EFFECTIVE_URL) - self.code = self.verifyHeader() self.addCookies() + try: + self.code = self.verifyHeader() + + finally: + self.rep.close() + self.rep = None + if decode: rep = self.decodeResponse(rep) @@ -219,7 +290,7 @@ def verifyHeader(self): code = int(self.c.getinfo(pycurl.RESPONSE_CODE)) if code in bad_headers: #404 will NOT raise an exception - raise BadHeader(code, self.getResponse()) + raise BadHeader(code, self.header, self.getResponse()) return code def checkHeader(self): @@ -228,11 +299,11 @@ def checkHeader(self): def getResponse(self): """ retrieve response from string io """ - if self.rep is None: return "" - value = self.rep.getvalue() - self.rep.close() - self.rep = StringIO() - return value + if self.rep is None: + return "" + + else: + return self.rep.getvalue() def decodeResponse(self, rep): """ decode with correct encoding, relies on header """ @@ -272,7 +343,8 @@ def write(self, buf): """ writes response """ if self.rep.tell() > 1000000 or self.abort: rep = self.getResponse() - if self.abort: raise Abort() + if self.abort: + raise Abort() f = open("response.dump", "wb") f.write(rep) f.close() @@ -292,15 +364,18 @@ def clearHeaders(self): def close(self): """ cleanup, unusable after this """ - self.rep.close() + if self.rep: + self.rep.close() + del self.rep + if hasattr(self, "cj"): del self.cj + if hasattr(self, "c"): self.c.close() del self.c if __name__ == "__main__": - url = "http://pyload.org" + url = "http://pyload.net" c = HTTPRequest() print c.load(url) - diff --git a/module/network/RequestFactory.py b/module/network/RequestFactory.py index 5b15282818..fc01addf3e 100644 --- a/module/network/RequestFactory.py +++ b/module/network/RequestFactory.py @@ -13,7 +13,7 @@ You should have received a copy of the GNU General Public License along with this program; if not, see . - + @author: mkaay, RaNaN """ @@ -26,6 +26,7 @@ from XDCCRequest import XDCCRequest + class RequestFactory(): def __init__(self, core): self.lock = Lock() @@ -37,19 +38,23 @@ def __init__(self, core): def iface(self): return self.core.config["download"]["interface"] - def getRequest(self, pluginName, account=None, type="HTTP"): + def getRequest(self, pluginName, account=None, type="HTTP", **kwargs): self.lock.acquire() - if type == "XDCC": - return XDCCRequest(proxies=self.getProxies()) + options = self.getOptions() + options.update(kwargs) # submit kwargs as additional options - req = Browser(self.bucket, self.getOptions()) + if type == "XDCC": + req = XDCCRequest(self.bucket, options) - if account: - cj = self.getCookieJar(pluginName, account) - req.setCookieJar(cj) else: - req.setCookieJar(CookieJar(pluginName)) + req = Browser(self.bucket, options) + + if account: + cj = self.getCookieJar(pluginName, account) + req.setCookieJar(cj) + else: + req.setCookieJar(CookieJar(pluginName)) self.lock.release() return req @@ -57,7 +62,7 @@ def getRequest(self, pluginName, account=None, type="HTTP"): def getHTTPRequest(self, **kwargs): """ returns a http request, dont forget to close it ! """ options = self.getOptions() - options.update(kwargs) # submit kwargs as additional options + options.update(kwargs) # submit kwargs as additional options return HTTPRequest(CookieJar(None), options) def getURL(self, *args, **kwargs): @@ -67,7 +72,7 @@ def getURL(self, *args, **kwargs): rep = h.load(*args, **kwargs) finally: h.close() - + return rep def getCookieJar(self, pluginName, account=None): @@ -75,40 +80,39 @@ def getCookieJar(self, pluginName, account=None): return self.cookiejars[(pluginName, account)] cj = CookieJar(pluginName, account) - self.cookiejars[(pluginName, account)] = cj + if account: + self.cookiejars[(pluginName, account)] = cj return cj + def removeCookieJar(self, plugin_name, account): + self.cookiejars.pop((plugin_name, account), None) + def getProxies(self): - """ returns a proxy list for the request classes """ - if not self.core.config["proxy"]["proxy"]: + """ returns proxy related options """ + proxy = self.core.config["proxy"] + if not proxy["proxy"]: return {} else: - type = "http" - setting = self.core.config["proxy"]["type"].lower() - if setting == "socks4": type = "socks4" - elif setting == "socks5": type = "socks5" - - username = None - if self.core.config["proxy"]["username"] and self.core.config["proxy"]["username"].lower() != "none": - username = self.core.config["proxy"]["username"] - - pw = None - if self.core.config["proxy"]["password"] and self.core.config["proxy"]["password"].lower() != "none": - pw = self.core.config["proxy"]["password"] + proxy_type = proxy["type"] + socks_resolve_dns = proxy["socksResolveDns"] + proxy_username = proxy["username"] or None + proxy_password = proxy["password"] or None return { - "type": type, - "address": self.core.config["proxy"]["address"], - "port": self.core.config["proxy"]["port"], - "username": username, - "password": pw, - } + "type" : proxy_type, + "socksResolveDns": socks_resolve_dns, + "address" : proxy["address"], + "port" : proxy["port"], + "username": proxy_username, + "password": proxy_password, + } + def getOptions(self): - """returns options needed for pycurl""" + """ returns options needed for pycurl """ return {"interface": self.iface(), - "proxies": self.getProxies(), - "ipv6": self.core.config["download"]["ipv6"]} + "proxies" : self.getProxies(), + "ipv6" : self.core.config["download"]["ipv6"]} def updateBucket(self): """ set values in the bucket according to settings""" @@ -117,6 +121,7 @@ def updateBucket(self): else: self.bucket.setRate(self.core.config["download"]["max_speed"] * 1024) + # needs pyreq in global namespace def getURL(*args, **kwargs): return pyreq.getURL(*args, **kwargs) diff --git a/module/network/XDCCRequest.py b/module/network/XDCCRequest.py index f03798c171..7afb4d4188 100644 --- a/module/network/XDCCRequest.py +++ b/module/network/XDCCRequest.py @@ -13,150 +13,195 @@ You should have received a copy of the GNU General Public License along with this program; if not, see . - - @author: jeix + + @authors: jeix, GammaC0de """ +import errno +import os +import select import socket -import re - -from os import remove -from os.path import exists - -from time import time - import struct -from select import select +import time from module.plugins.Plugin import Abort +from module.utils import fs_encode + + +class XDCCRequest: + def __init__(self, bucket=None, options={}): + self.proxies = options.get('proxies', {}) + self.bucket = bucket + self.fh = None + self.dccsock = None -class XDCCRequest(): - def __init__(self, timeout=30, proxies={}): - - self.proxies = proxies - self.timeout = timeout - self.filesize = 0 - self.recv = 0 - self.speed = 0 - + self.received = 0 + self.speeds = [0.0, 0.0, 0.0] + + self.sleep = 0.000 + self.last_recv_size = 0 + self.send_64bits_ack = False + self.abort = False - + self.status_notify = None + def createSocket(self): # proxytype = None # proxy = None # if self.proxies.has_key("socks5"): - # proxytype = socks.PROXY_TYPE_SOCKS5 - # proxy = self.proxies["socks5"] + # proxytype = socks.PROXY_TYPE_SOCKS5 + # proxy = self.proxies["socks5"] # elif self.proxies.has_key("socks4"): - # proxytype = socks.PROXY_TYPE_SOCKS4 - # proxy = self.proxies["socks4"] + # proxytype = socks.PROXY_TYPE_SOCKS4 + # proxy = self.proxies["socks4"] # if proxytype: - # sock = socks.socksocket() - # t = _parse_proxy(proxy) - # sock.setproxy(proxytype, addr=t[3].split(":")[0], port=int(t[3].split(":")[1]), username=t[1], password=t[2]) + # sock = socks.socksocket() + # t = _parse_proxy(proxy) + # sock.setproxy(proxytype, addr=t[3].split(":")[0], port=int(t[3].split(":")[1]), username=t[1], password=t[2]) # else: - # sock = socket.socket() + # sock = socket.socket() # return sock - - return socket.socket() - - def download(self, ip, port, filename, irc, progressNotify=None): - - ircbuffer = "" - lastUpdate = time() - cumRecvLen = 0 - - dccsock = self.createSocket() - - dccsock.settimeout(self.timeout) - dccsock.connect((ip, port)) - - if exists(filename): - i = 0 - nameParts = filename.rpartition(".") - while True: - newfilename = "%s-%d%s%s" % (nameParts[0], i, nameParts[1], nameParts[2]) - i += 1 - - if not exists(newfilename): - filename = newfilename - break - - fh = open(filename, "wb") - + + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + # sock.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 16384) + + return sock + + def _write_func(self, buf): + size = len(buf) + + self.received += size + + self.fh.write(buf) + + if self.bucket: + time.sleep(self.bucket.consumed(size)) + + else: + # Avoid small buffers, increasing sleep time slowly if buffer size gets smaller + # otherwise reduce sleep time percentequal (values are based on tests) + # So in general cpu time is saved without reducing bandwidth too much + + if size < self.last_recv_size: + self.sleep += 0.002 + else: + self.sleep *= 0.7 + + self.last_recv_size = size + + time.sleep(self.sleep) + + def _send_ack(self): + # acknowledge data by sending number of received bytes + try: + self.dccsock.send(struct.pack('!Q' if self.send_64bits_ack else '!I', self.received)) + + except socket.error: + pass + + def download(self, ip, port, filename, status_notify=None, resume=None): + self.status_notify = status_notify if callable(status_notify) else None + self.send_64bits_ack = False if self.filesize < 1 << 32 else True + + chunk_name = fs_encode(filename + ".chunk0") + + if resume and os.path.exists(chunk_name): + self.fh = open(chunk_name, "ab") + resume_position = self.fh.tell() + if not resume_position: + resume_position = os.stat(chunk_name).st_size + + resume_position = resume(resume_position) + self.fh.truncate(resume_position) + self.received = resume_position + + else: + self.fh = open(chunk_name, "wb") + + lastUpdate = time.time() + numRecvLen = 0 + + self.dccsock = self.createSocket() + + recv_list = [self.dccsock] + self.dccsock.connect((ip, port)) + self.dccsock.setblocking(0) + # recv loop for dcc socket while True: if self.abort: - dccsock.close() - fh.close() - remove(filename) + self.dccsock.close() + self.fh.close() raise Abort() - - self._keepAlive(irc, ircbuffer) - - data = dccsock.recv(4096) - dataLen = len(data) - self.recv += dataLen - - cumRecvLen += dataLen - - now = time() + + fdset = select.select(recv_list, [], [], 0.1) + if self.dccsock in fdset[0]: + try: + data = self.dccsock.recv(16384) + + except socket.error, e: + if e.errno == errno.EAGAIN or e.errno == errno.EWOULDBLOCK: + continue + + else: + raise + + data_len = len(data) + if data_len == 0 or self.filesize and self.received + data_len > self.filesize: + break + + numRecvLen += data_len + + self._write_func(data) + self._send_ack() + + now = time.time() timespan = now - lastUpdate - if timespan > 1: - self.speed = cumRecvLen / timespan - cumRecvLen = 0 + if timespan > 1: + # calc speed once per second, averaging over 3 seconds + self.speeds[2] = self.speeds[1] + self.speeds[1] = self.speeds[0] + self.speeds[0] = float(numRecvLen) / timespan + + numRecvLen = 0 lastUpdate = now - - if progressNotify: - progressNotify(self.percent) - - - if not data: - break - - fh.write(data) - - # acknowledge data by sending number of recceived bytes - dccsock.send(struct.pack('!I', self.recv)) - - dccsock.close() - fh.close() - + + self.updateProgress() + + self.dccsock.close() + self.fh.close() + + os.rename(chunk_name, fs_encode(filename)) + return filename - - def _keepAlive(self, sock, readbuffer): - fdset = select([sock], [], [], 0) - if sock not in fdset[0]: - return - - readbuffer += sock.recv(1024) - temp = readbuffer.split("\n") - readbuffer = temp.pop() - - for line in temp: - line = line.rstrip() - first = line.split() - if first[0] == "PING": - sock.send("PONG %s\r\n" % first[1]) def abortDownloads(self): self.abort = True - + + def updateProgress(self): + if self.status_notify: + self.status_notify({'progress': self.percent}) + @property def size(self): return self.filesize @property def arrived(self): - return self.recv + return self.received + + @property + def speed(self): + speeds = [x for x in self.speeds if x] + return sum(speeds) / len(speeds) @property def percent(self): if not self.filesize: return 0 - return (self.recv * 100) / self.filesize + return (self.received * 100) / self.filesize def close(self): pass diff --git a/module/plugins/Account.py b/module/plugins/Account.py deleted file mode 100644 index c147404e09..0000000000 --- a/module/plugins/Account.py +++ /dev/null @@ -1,292 +0,0 @@ -# -*- 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 . - - @author: mkaay -""" - -from random import choice -from time import time -from traceback import print_exc -from threading import RLock - -from Plugin import Base -from module.utils import compare_time, parseFileSize, lock - -class WrongPassword(Exception): - pass - - -class Account(Base): - """ - Base class for every Account plugin. - Just overwrite `login` and cookies will be stored and account becomes accessible in\ - associated hoster plugin. Plugin should also provide `loadAccountInfo` - """ - __name__ = "Account" - __version__ = "0.2" - __type__ = "account" - __description__ = """Account Plugin""" - __author_name__ = ("mkaay") - __author_mail__ = ("mkaay@mkaay.de") - - #: after that time [in minutes] pyload will relogin the account - login_timeout = 600 - #: account data will be reloaded after this time - info_threshold = 600 - - - def __init__(self, manager, accounts): - Base.__init__(self, manager.core) - - self.manager = manager - self.accounts = {} - self.infos = {} # cache for account information - self.lock = RLock() - - self.timestamps = {} - self.setAccounts(accounts) - self.init() - - def init(self): - pass - - def login(self, user, data, req): - """login into account, the cookies will be saved so user can be recognized - - :param user: loginname - :param data: data dictionary - :param req: `Request` instance - """ - pass - - @lock - def _login(self, user, data): - # set timestamp for login - self.timestamps[user] = time() - - req = self.getAccountRequest(user) - try: - self.login(user, data, req) - except WrongPassword: - self.logWarning( - _("Could not login with account %(user)s | %(msg)s") % {"user": user - , "msg": _("Wrong Password")}) - data["valid"] = False - - except Exception, e: - self.logWarning( - _("Could not login with account %(user)s | %(msg)s") % {"user": user - , "msg": e}) - data["valid"] = False - if self.core.debug: - print_exc() - finally: - if req: req.close() - - def relogin(self, user): - req = self.getAccountRequest(user) - if req: - req.cj.clear() - req.close() - if user in self.infos: - del self.infos[user] #delete old information - - self._login(user, self.accounts[user]) - - def setAccounts(self, accounts): - self.accounts = accounts - for user, data in self.accounts.iteritems(): - self._login(user, data) - self.infos[user] = {} - - def updateAccounts(self, user, password=None, options={}): - """ updates account and return true if anything changed """ - - if user in self.accounts: - self.accounts[user]["valid"] = True #do not remove or accounts will not login - if password: - self.accounts[user]["password"] = password - self.relogin(user) - return True - if options: - before = self.accounts[user]["options"] - self.accounts[user]["options"].update(options) - return self.accounts[user]["options"] != before - else: - self.accounts[user] = {"password": password, "options": options, "valid": True} - self._login(user, self.accounts[user]) - return True - - def removeAccount(self, user): - if user in self.accounts: - del self.accounts[user] - if user in self.infos: - del self.infos[user] - if user in self.timestamps: - del self.timestamps[user] - - @lock - def getAccountInfo(self, name, force=False): - """retrieve account infos for an user, do **not** overwrite this method!\\ - just use it to retrieve infos in hoster plugins. see `loadAccountInfo` - - :param name: username - :param force: reloads cached account information - :return: dictionary with information - """ - data = Account.loadAccountInfo(self, name) - - if force or name not in self.infos: - self.logDebug("Get Account Info for %s" % name) - req = self.getAccountRequest(name) - - try: - infos = self.loadAccountInfo(name, req) - if not type(infos) == dict: - raise Exception("Wrong return format") - except Exception, e: - infos = {"error": str(e)} - - if req: req.close() - - self.logDebug("Account Info: %s" % str(infos)) - - infos["timestamp"] = time() - self.infos[name] = infos - elif "timestamp" in self.infos[name] and self.infos[name][ - "timestamp"] + self.info_threshold * 60 < time(): - self.logDebug("Reached timeout for account data") - self.scheduleRefresh(name) - - data.update(self.infos[name]) - return data - - def isPremium(self, user): - info = self.getAccountInfo(user) - return info["premium"] - - def loadAccountInfo(self, name, req=None): - """this should be overwritten in account plugin,\ - and retrieving account information for user - - :param name: - :param req: `Request` instance - :return: - """ - return { - "validuntil": None, # -1 for unlimited - "login": name, - #"password": self.accounts[name]["password"], #@XXX: security - "options": self.accounts[name]["options"], - "valid": self.accounts[name]["valid"], - "trafficleft": None, # in kb, -1 for unlimited - "maxtraffic": None, - "premium": True, #useful for free accounts - "timestamp": 0, #time this info was retrieved - "type": self.__name__, - } - - def getAllAccounts(self, force=False): - return [self.getAccountInfo(user, force) for user, data in self.accounts.iteritems()] - - def getAccountRequest(self, user=None): - if not user: - user, data = self.selectAccount() - if not user: - return None - - req = self.core.requestFactory.getRequest(self.__name__, user) - return req - - def getAccountCookies(self, user=None): - if not user: - user, data = self.selectAccount() - if not user: - return None - - cj = self.core.requestFactory.getCookieJar(self.__name__, user) - return cj - - def getAccountData(self, user): - return self.accounts[user] - - def selectAccount(self): - """ returns an valid account name and data""" - usable = [] - for user, data in self.accounts.iteritems(): - if not data["valid"]: continue - - if "time" in data["options"] and data["options"]["time"]: - time_data = "" - try: - time_data = data["options"]["time"][0] - start, end = time_data.split("-") - if not compare_time(start.split(":"), end.split(":")): - continue - except: - self.logWarning(_("Your Time %s has wrong format, use: 1:22-3:44") % time_data) - - if user in self.infos: - if "validuntil" in self.infos[user]: - if self.infos[user]["validuntil"] > 0 and time() > self.infos[user]["validuntil"]: - continue - if "trafficleft" in self.infos[user]: - if self.infos[user]["trafficleft"] == 0: - continue - - usable.append((user, data)) - - if not usable: return None, None - return choice(usable) - - def canUse(self): - return False if self.selectAccount() == (None, None) else True - - def parseTraffic(self, string): #returns kbyte - return parseFileSize(string) / 1024 - - def wrongPassword(self): - raise WrongPassword - - def empty(self, user): - if user in self.infos: - self.logWarning(_("Account %s has not enough traffic, checking again in 30min") % user) - - self.infos[user].update({"trafficleft": 0}) - self.scheduleRefresh(user, 30 * 60) - - def expired(self, user): - if user in self.infos: - self.logWarning(_("Account %s is expired, checking again in 1h") % user) - - self.infos[user].update({"validuntil": time() - 1}) - self.scheduleRefresh(user, 60 * 60) - - def scheduleRefresh(self, user, time=0, force=True): - """ add task to refresh account info to sheduler """ - self.logDebug("Scheduled Account refresh for %s in %s seconds." % (user, time)) - self.core.scheduler.addJob(time, self.getAccountInfo, [user, force]) - - @lock - def checkLogin(self, user): - """ checks if user is still logged in """ - if user in self.timestamps: - if self.timestamps[user] + self.login_timeout * 60 < time(): - self.logDebug("Reached login timeout for %s" % user) - self.relogin(user) - return False - - return True diff --git a/module/plugins/AccountManager.py b/module/plugins/AccountManager.py index fc521d36c2..3dc85ec4f1 100644 --- a/module/plugins/AccountManager.py +++ b/module/plugins/AccountManager.py @@ -52,7 +52,11 @@ def getAccountPlugin(self, plugin): """get account instance for plugin or None if anonymous""" if plugin in self.accounts: if plugin not in self.plugins: - self.plugins[plugin] = self.core.pluginManager.loadClass("accounts", plugin)(self, self.accounts[plugin]) + klass = self.core.pluginManager.loadClass("accounts", plugin) + if klass: + self.plugins[plugin] = klass(self, self.accounts[plugin]) + else: + return None return self.plugins[plugin] else: @@ -113,6 +117,8 @@ def loadAccounts(self): elif ":" in line: name, sep, pw = line.partition(":") + name = name.replace(r"\x3a", ":") + pw = pw.replace(r"\x3a", ":") self.accounts[plugin][name] = {"password": pw, "options": {}, "valid": True} #---------------------------------------------------------------------- def saveAccounts(self): @@ -125,10 +131,13 @@ def saveAccounts(self): f.write("\n") f.write(plugin+":\n") - for name,data in accounts.iteritems(): - f.write("\n\t%s:%s\n" % (name,data["password"]) ) - if data["options"]: - for option, values in data["options"].iteritems(): + for name, data in accounts.iteritems(): + pw = data['password'] + name = name.replace(":", r"\x3a") + pw = pw.replace(":", r"\x3a") + f.write("\n\t%s:%s\n" % (name, pw)) + if data['options']: + for option, values in data['options'].iteritems(): f.write("\t@%s %s\n" % (option, " ".join(values))) f.close() @@ -144,7 +153,7 @@ def initAccountPlugins(self): @lock def updateAccount(self, plugin , user, password=None, options={}): """add or update account""" - if plugin in self.accounts: + if plugin in self.accounts and user: p = self.getAccountPlugin(plugin) updated = p.updateAccounts(user, password, options) #since accounts is a ref in plugin self.accounts doesnt need to be updated here @@ -170,12 +179,16 @@ def getAccountInfos(self, force=True, refresh=False): self.core.scheduler.addJob(0, self.core.accountManager.getAccountInfos) force = False - for p in self.accounts.keys(): - if self.accounts[p]: - p = self.getAccountPlugin(p) - data[p.__name__] = p.getAllAccounts(force) + for k in self.accounts.keys(): + if self.accounts[k]: + p = self.getAccountPlugin(k) + if p: + data[p.__name__] = p.getAllAccounts(force) + else: + self.core.log.error(_("Bad or missing plugin: ACCOUNT %s") % k) + data[k] = [] else: - data[p] = [] + data[k] = [] e = AccountUpdateEvent() self.core.pullManager.addEvent(e) return data diff --git a/module/plugins/Container.py b/module/plugins/Container.py deleted file mode 100644 index c233d37103..0000000000 --- a/module/plugins/Container.py +++ /dev/null @@ -1,75 +0,0 @@ -# -*- 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 . - - @author: mkaay -""" - -from module.plugins.Crypter import Crypter - -from os.path import join, exists, basename -from os import remove -import re - -class Container(Crypter): - __name__ = "Container" - __version__ = "0.1" - __pattern__ = None - __type__ = "container" - __description__ = """Base container plugin""" - __author_name__ = ("mkaay") - __author_mail__ = ("mkaay@mkaay.de") - - - def preprocessing(self, thread): - """prepare""" - - self.setup() - self.thread = thread - - self.loadToDisk() - - self.decrypt(self.pyfile) - self.deleteTmp() - - self.createPackages() - - - def loadToDisk(self): - """loads container to disk if its stored remotely and overwrite url, - or check existent on several places at disk""" - - if self.pyfile.url.startswith("http"): - self.pyfile.name = re.findall("([^\/=]+)", self.pyfile.url)[-1] - content = self.load(self.pyfile.url) - self.pyfile.url = join(self.config["general"]["download_folder"], self.pyfile.name) - f = open(self.pyfile.url, "wb" ) - f.write(content) - f.close() - - else: - self.pyfile.name = basename(self.pyfile.url) - if not exists(self.pyfile.url): - if exists(join(pypath, self.pyfile.url)): - self.pyfile.url = join(pypath, self.pyfile.url) - else: - self.fail(_("File not exists.")) - - - def deleteTmp(self): - if self.pyfile.name.startswith("tmp_"): - remove(self.pyfile.url) - - diff --git a/module/plugins/Crypter.py b/module/plugins/Crypter.py deleted file mode 100644 index d1549fe809..0000000000 --- a/module/plugins/Crypter.py +++ /dev/null @@ -1,72 +0,0 @@ -# -*- 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 . - - @author: mkaay -""" - -from module.plugins.Plugin import Plugin - -class Crypter(Plugin): - __name__ = "Crypter" - __version__ = "0.1" - __pattern__ = None - __type__ = "container" - __description__ = """Base crypter plugin""" - __author_name__ = ("mkaay") - __author_mail__ = ("mkaay@mkaay.de") - - def __init__(self, pyfile): - Plugin.__init__(self, pyfile) - - #: Put all packages here. It's a list of tuples like: ( name, [list of links], folder ) - self.packages = [] - - #: List of urls, pyLoad will generate packagenames - self.urls = [] - - self.multiDL = True - self.limitDL = 0 - - - def preprocessing(self, thread): - """prepare""" - self.setup() - self.thread = thread - - self.decrypt(self.pyfile) - - self.createPackages() - - - def decrypt(self, pyfile): - raise NotImplementedError - - def createPackages(self): - """ create new packages from self.packages """ - for pack in self.packages: - - self.log.debug("Parsed package %(name)s with %(len)d links" % { "name" : pack[0], "len" : len(pack[1]) } ) - - links = [x.decode("utf-8") for x in pack[1]] - - pid = self.core.api.addPackage(pack[0], links, self.pyfile.package().queue) - - if self.pyfile.package().password: - self.core.api.setPackageData(pid, {"password": self.pyfile.package().password}) - - if self.urls: - self.core.api.generateAndAddPackages(self.urls) - diff --git a/module/plugins/Hook.py b/module/plugins/Hook.py deleted file mode 100644 index 5efd08baef..0000000000 --- a/module/plugins/Hook.py +++ /dev/null @@ -1,161 +0,0 @@ -# -*- 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 . - - @author: mkaay - @interface-version: 0.2 -""" - -from traceback import print_exc - -from Plugin import Base - -class Expose(object): - """ used for decoration to declare rpc services """ - - def __new__(cls, f, *args, **kwargs): - hookManager.addRPC(f.__module__, f.func_name, f.func_doc) - return f - -def threaded(f): - def run(*args,**kwargs): - hookManager.startThread(f, *args, **kwargs) - return run - -class Hook(Base): - """ - Base class for hook plugins. - """ - __name__ = "Hook" - __version__ = "0.2" - __type__ = "hook" - __threaded__ = [] - __config__ = [ ("name", "type", "desc" , "default") ] - __description__ = """interface for hook""" - __author_name__ = ("mkaay", "RaNaN") - __author_mail__ = ("mkaay@mkaay.de", "RaNaN@pyload.org") - - #: automatically register event listeners for functions, attribute will be deleted dont use it yourself - event_map = None - - # Alternative to event_map - #: List of events the plugin can handle, name the functions exactly like eventname. - event_list = None # dont make duplicate entries in event_map - - - #: periodic call interval in secondc - interval = 60 - - def __init__(self, core, manager): - Base.__init__(self, core) - - #: Provide information in dict here, usable by API `getInfo` - self.info = None - - #: Callback of periodical job task, used by hookmanager - self.cb = None - - #: `HookManager` - self.manager = manager - - #register events - if self.event_map: - for event, funcs in self.event_map.iteritems(): - if type(funcs) in (list, tuple): - for f in funcs: - self.manager.addEvent(event, getattr(self,f)) - else: - self.manager.addEvent(event, getattr(self,funcs)) - - #delete for various reasons - self.event_map = None - - if self.event_list: - for f in self.event_list: - self.manager.addEvent(f, getattr(self,f)) - - self.event_list = None - - self.initPeriodical() - self.setup() - - def initPeriodical(self): - if self.interval >=1: - self.cb = self.core.scheduler.addJob(0, self._periodical, threaded=False) - - def _periodical(self): - try: - if self.isActivated(): self.periodical() - except Exception, e: - self.core.log.error(_("Error executing hooks: %s") % str(e)) - if self.core.debug: - print_exc() - - self.cb = self.core.scheduler.addJob(self.interval, self._periodical, threaded=False) - - - def __repr__(self): - return "" % self.__name__ - - def setup(self): - """ more init stuff if needed """ - pass - - def unload(self): - """ called when hook was deactivated """ - pass - - def isActivated(self): - """ checks if hook is activated""" - return self.config.getPlugin(self.__name__, "activated") - - - #event methods - overwrite these if needed - def coreReady(self): - pass - - def coreExiting(self): - pass - - def downloadPreparing(self, pyfile): - pass - - def downloadFinished(self, pyfile): - pass - - def downloadFailed(self, pyfile): - pass - - def packageFinished(self, pypack): - pass - - def beforeReconnecting(self, ip): - pass - - def afterReconnecting(self, ip): - pass - - def periodical(self): - pass - - def newCaptchaTask(self, task): - """ new captcha task for the plugin, it MUST set the handler and timeout or will be ignored """ - pass - - def captchaCorrect(self, task): - pass - - def captchaInvalid(self, task): - pass \ No newline at end of file diff --git a/module/plugins/Hoster.py b/module/plugins/Hoster.py deleted file mode 100644 index 814a709492..0000000000 --- a/module/plugins/Hoster.py +++ /dev/null @@ -1,33 +0,0 @@ -# -*- 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 . - - @author: mkaay -""" - -from module.plugins.Plugin import Plugin - -def getInfo(self): - #result = [ .. (name, size, status, url) .. ] - return - -class Hoster(Plugin): - __name__ = "Hoster" - __version__ = "0.1" - __pattern__ = None - __type__ = "hoster" - __description__ = """Base hoster plugin""" - __author_name__ = ("mkaay") - __author_mail__ = ("mkaay@mkaay.de") diff --git a/module/plugins/Plugin.py b/module/plugins/Plugin.py index 15bf3971f2..94cab51013 100644 --- a/module/plugins/Plugin.py +++ b/module/plugins/Plugin.py @@ -148,7 +148,7 @@ class Plugin(Base): __config__ = [("name", "type", "desc", "default")] __description__ = """Base Plugin""" __author_name__ = ("RaNaN", "spoob", "mkaay") - __author_mail__ = ("RaNaN@pyload.org", "spoob@pyload.org", "mkaay@mkaay.de") + __author_mail__ = ("RaNaN@pyload.net", "spoob@pyload.net", "mkaay@mkaay.de") def __init__(self, pyfile): Base.__init__(self, pyfile.m.core) diff --git a/module/plugins/PluginManager.py b/module/plugins/PluginManager.py index f3f5f47bca..7ef0831d2a 100644 --- a/module/plugins/PluginManager.py +++ b/module/plugins/PluginManager.py @@ -24,7 +24,6 @@ from os.path import isfile, join, exists, abspath from sys import version_info from itertools import chain -from traceback import print_exc from module.lib.SafeEval import const_eval as literal_eval from module.ConfigParser import IGNORE @@ -36,7 +35,7 @@ class PluginManager: PATTERN = re.compile(r'__pattern__.*=.*r("|\')([^"\']+)') VERSION = re.compile(r'__version__.*=.*("|\')([0-9.]+)') - CONFIG = re.compile(r'__config__.*=.*\[([^\]]+)', re.MULTILINE) + CONFIG = re.compile(r'__config__.*=.*(\[[^\]]+\])', re.MULTILINE) DESC = re.compile(r'__description__.?=.?("|"""|\')([^"\']+)') @@ -56,6 +55,18 @@ def __init__(self, core): def createIndex(self): """create information for all plugins available""" + def merge(dst, src, overwrite=False): + """merge dict of dicts""" + for name in src: + if name in dst: + if overwrite is True: + dst[name].update(src[name]) + else: + for _k in set(src[name].keys()) - set(dst[name].keys()): + dst[name][_k] = src[name][_k] + else: + dst[name] = src[name] + sys.path.append(abspath("")) if not exists("userplugins"): @@ -64,14 +75,41 @@ def createIndex(self): f = open(join("userplugins", "__init__.py"), "wb") f.close() - self.plugins["crypter"] = self.crypterPlugins = self.parse("crypter", pattern=True) - self.plugins["container"] = self.containerPlugins = self.parse("container", pattern=True) - self.plugins["hoster"] = self.hosterPlugins = self.parse("hoster", pattern=True) + self.crypterPlugins , config = self.parse("crypter", pattern=True) + self.plugins["crypter"] = self.crypterPlugins + default_config = config + + self.containerPlugins, config = self.parse("container", pattern=True) + self.plugins["container"] = self.containerPlugins + merge(default_config, config) + + self.hosterPlugins, config = self.parse("hoster", pattern=True) + self.plugins["hoster"] = self.hosterPlugins + merge(default_config, config) + + self.hookPlugins, config = self.parse("hooks") + self.plugins["hooks"] = self.hookPlugins + merge(default_config, config) + + self.captchaPlugins, config = self.parse("captcha") + self.plugins["captcha"] = self.captchaPlugins + merge(default_config, config) + + self.accountPlugins, config = self.parse("accounts") + self.plugins["accounts"] = self.accountPlugins + merge(default_config, config) + + self.internalPlugins, config = self.parse("internal") + self.plugins["internal"] = self.internalPlugins + merge(default_config, config) - self.plugins["captcha"] = self.captchaPlugins = self.parse("captcha") - self.plugins["accounts"] = self.accountPlugins = self.parse("accounts") - self.plugins["hooks"] = self.hookPlugins = self.parse("hooks") - self.plugins["internal"] = self.internalPlugins = self.parse("internal") + for name, config in default_config.items(): + desc = config.pop('desc', "") + config = [[k] + list(v) for k, v in config.items()] + try: + self.core.config.addPluginConfig(name, config, desc) + except: + self.log.error("Invalid config in %s: %s" % (name, config)) self.log.debug("created index of plugins") @@ -97,9 +135,11 @@ def parse(self, folder, pattern=False, home={}): else: pfolder = join(pypath, "module", "plugins", folder) + configs = {} for f in listdir(pfolder): - if (isfile(join(pfolder, f)) and f.endswith(".py") or f.endswith("_25.pyc") or f.endswith( - "_26.pyc") or f.endswith("_27.pyc")) and not f.startswith("_"): + if (isfile(join(pfolder, f)) and f.endswith(".py") or f.endswith("_25.pyc") or + f.endswith("_26.pyc") or f.endswith("_27.pyc")) and not f.startswith("_"): + data = open(join(pfolder, f)) content = data.read() data.close() @@ -121,7 +161,7 @@ def parse(self, folder, pattern=False, home={}): version = 0 # home contains plugins from pyload root - if home and name in home: + if isinstance(home, dict) and name in home: if home[name]["v"] >= version: continue @@ -151,7 +191,7 @@ def parse(self, folder, pattern=False, home={}): plugins[name]["re"] = re.compile(pattern) except: self.log.error(_("%s has a invalid pattern.") % name) - + plugins[name]["re"] = re.compile(r'(?!.*)') # internals have no config if folder == "internal": @@ -164,39 +204,32 @@ def parse(self, folder, pattern=False, home={}): desc = self.DESC.findall(content) desc = desc[0][1] if desc else "" - if type(config[0]) == tuple: - config = [list(x) for x in config] + if type(config) == list and all(type(c) == tuple for c in config): + config = dict((x[0], x[1:]) for x in config) else: - config = [list(config)] - - if folder == "hooks": - append = True - for item in config: - if item[0] == "activated": append = False + self.log.error("Invalid config in %s: %s" % (name, config)) + continue - # activated flag missing - if append: config.append(["activated", "bool", "Activated", False]) + if folder == "hooks" and "activated" not in config: + config['activated'] = ["bool", "Activated", False] - try: - self.core.config.addPluginConfig(name, config, desc) - except: - self.log.error("Invalid config in %s: %s" % (name, config)) + config['desc'] = desc + configs[name] = config - elif folder == "hooks": #force config creation + elif folder == "hooks": # force config creation desc = self.DESC.findall(content) desc = desc[0][1] if desc else "" - config = (["activated", "bool", "Activated", False],) + config['activated'] = ["bool", "Activated", False] - try: - self.core.config.addPluginConfig(name, config, desc) - except: - self.log.error("Invalid config in %s: %s" % (name, config)) + config['desc'] = desc + configs[name] = config if not home: - temp = self.parse(folder, pattern, plugins) - plugins.update(temp) + temp_plugins, temp_configs = self.parse(folder, pattern, plugins or True) + plugins.update(temp_plugins) + configs.update(temp_configs) - return plugins + return plugins, configs def parseUrls(self, urls): @@ -269,9 +302,11 @@ def loadModule(self, type, name): plugins[name]["module"] = module #cache import, maybe unneeded return module except Exception, e: - self.log.error(_("Error importing %(name)s: %(msg)s") % {"name": name, "msg": str(e)}) - if self.core.debug: - print_exc() + self.log.error(_("Error importing %(name)s: %(msg)s") % {"name": name, "msg": str(e)}, exc_info=self.core.debug) + + else: + self.log.debug("Plugin %s not found" % name) + self.log.debug("Available plugins : %s" % str(plugins)) def loadClass(self, type, name): """Returns the class of a plugin with the same name""" @@ -324,6 +359,20 @@ def load_module(self, name, replace=True): def reloadPlugins(self, type_plugins): """ reloads and reindexes plugins """ + + def merge(dst, src, overwrite=False): + """merge dict of dicts""" + for name in src: + if name in dst: + if overwrite is True: + dst[name].update(src[name]) + else: + for _k in set(src[name].keys()) - set(dst[name].keys()): + dst[name][_k] = src[name][_k] + else: + dst[name] = src[name] + + if not type_plugins: return False self.log.debug("Request reload of plugins: %s" % type_plugins) @@ -347,11 +396,36 @@ def reloadPlugins(self, type_plugins): reload(self.plugins[type][plugin]["module"]) #index creation - self.plugins["crypter"] = self.crypterPlugins = self.parse("crypter", pattern=True) - self.plugins["container"] = self.containerPlugins = self.parse("container", pattern=True) - self.plugins["hoster"] = self.hosterPlugins = self.parse("hoster", pattern=True) - self.plugins["captcha"] = self.captchaPlugins = self.parse("captcha") - self.plugins["accounts"] = self.accountPlugins = self.parse("accounts") + self.crypterPlugins , config = self.parse("crypter", pattern=True) + self.plugins["crypter"] = self.crypterPlugins + default_config = config + + self.containerPlugins, config = self.parse("container", pattern=True) + self.plugins["container"] = self.containerPlugins + merge(default_config, config) + + self.hosterPlugins, config = self.parse("hoster", pattern=True) + self.plugins["hoster"] = self.hosterPlugins + merge(default_config, config) + + temp, config = self.parse("hooks") + merge(default_config, config) + + self.captchaPlugins, config = self.parse("captcha") + self.plugins["captcha"] = self.captchaPlugins + merge(default_config, config) + + self.accountPlugins, config = self.parse("accounts") + self.plugins["accounts"] = self.accountPlugins + merge(default_config, config) + + for name, config in default_config.items(): + desc = config.pop('desc', "") + config = [[k] + list(v) for k, v in config.items()] + try: + self.core.config.addPluginConfig(name, config, desc) + except: + self.log.error("Invalid config in %s: %s" % (name, config)) if "accounts" in as_dict: #accounts needs to be reloaded self.core.accountManager.initPlugins() diff --git a/module/plugins/ReCaptcha.py b/module/plugins/ReCaptcha.py deleted file mode 100644 index 6f7ebe22ce..0000000000 --- a/module/plugins/ReCaptcha.py +++ /dev/null @@ -1,21 +0,0 @@ -import re - -class ReCaptcha(): - def __init__(self, plugin): - self.plugin = plugin - - def challenge(self, id): - js = self.plugin.req.load("http://www.google.com/recaptcha/api/challenge", get={"k":id}, cookies=True) - - try: - challenge = re.search("challenge : '(.*?)',", js).group(1) - server = re.search("server : '(.*?)',", js).group(1) - except: - self.plugin.fail("recaptcha error") - result = self.result(server,challenge) - - return challenge, result - - def result(self, server, challenge): - return self.plugin.decryptCaptcha("%simage"%server, get={"c":challenge}, cookies=True, imgtype="jpg") - diff --git a/module/plugins/accounts/AccioDebridCom.py b/module/plugins/accounts/AccioDebridCom.py new file mode 100644 index 0000000000..4f8b86e176 --- /dev/null +++ b/module/plugins/accounts/AccioDebridCom.py @@ -0,0 +1,115 @@ +# -*- coding: utf-8 -*- + +import pycurl +from module.network.HTTPRequest import BadHeader + +from ..internal.misc import decode, json +from ..internal.MultiAccount import MultiAccount + + +def args(**kwargs): + return kwargs + + +class AccioDebridCom(MultiAccount): + __name__ = "AccioDebridCom" + __type__ = "account" + __version__ = "0.02" + __status__ = "testing" + + __config__ = [("mh_mode", "all;listed;unlisted", "Filter hosters to use", "all"), + ("mh_list", "str", "Hoster list (comma separated)", ""), + ("mh_interval", "int", "Reload interval in hours", 12)] + + __description__ = """Accio-debrid.com account plugin""" + __license__ = "GPLv3" + __authors__ = [("PlugPlus", "accio.debrid@gmail.com")] + + LOGIN_TIMEOUT = -1 + + API_URL = "https://www.accio-debrid.com/apiv2/" + + def api_response(self, action, get={}, post={}): + get['action'] = action + + # Better use pyLoad User-Agent so we don't get blocked + self.req.http.c.setopt(pycurl.USERAGENT, "pyLoad/%s" % self.pyload.version) + + json_data = self.load(self.API_URL, get=get, post=post) + + return json.loads(json_data) + + def grab_hosters(self, user, password, data): + res = self.api_response("getHostsList") + + if res['success']: + return res['value'] + + else: + return [] + + def grab_info(self, user, password, data): + validuntil = None + trafficleft = None + premium = False + + cache_info = data.get('cache_info', {}) + if user in cache_info: + validuntil = float(cache_info[user]['vip_end']) + premium = validuntil > 0 + trafficleft = -1 + + return {'validuntil': validuntil, + 'trafficleft': trafficleft, + 'premium': premium} + + def signin(self, user, password, data): + cache_info = self.db.retrieve("cache_info", {}) + if user in cache_info: + data['cache_info'] = cache_info + self.skip_login() + + try: + res = self.api_response("login", args(login=user, password=password)) + + except BadHeader, e: + if e.code == 401: + self.fail_login() + + elif e.code == 405: + self.fail(_("Banned IP")) + + else: + raise + + if res['response_code'] != "ok": + cache_info.pop(user, None) + data['cache_info'] = cache_info + self.db.store("cache_info", cache_info) + + if res['response_code'] == "INCORRECT_PASSWORD": + self.fail_login() + + elif res['response_code'] == "UNALLOWED_IP": + self.fail_login(_("Banned IP")) + + else: + self.log_error(res['response_text']) + self.fail_login(res['response_text']) + + else: + cache_info[user] = {'vip_end': res['vip_end'], + 'token': res['token']} + data['cache_info'] = cache_info + + self.db.store("cache_info", cache_info) + + def relogin(self): + if self.req: + cache_info = self.info['data'].get('cache_info', {}) + + cache_info.pop(self.user, None) + self.info['data']['cache_info'] = cache_info + self.db.store("cache_info", cache_info) + + return MultiAccount.relogin(self) \ No newline at end of file diff --git a/module/plugins/accounts/AlldebridCom.py b/module/plugins/accounts/AlldebridCom.py index 6156605a70..c435f04b85 100644 --- a/module/plugins/accounts/AlldebridCom.py +++ b/module/plugins/accounts/AlldebridCom.py @@ -1,52 +1,84 @@ -import xml.dom.minidom as dom -from time import time -import re -import urllib +# -*- coding: utf-8 -*- -from module.plugins.Account import Account -from BeautifulSoup import BeautifulSoup +from module.network.HTTPRequest import BadHeader +from ..internal.misc import json +from ..internal.MultiAccount import MultiAccount -class AlldebridCom(Account): + +class AlldebridCom(MultiAccount): __name__ = "AlldebridCom" - __version__ = "0.22" __type__ = "account" + __version__ = "0.47" + __status__ = "testing" + + __config__ = [("mh_mode", "all;listed;unlisted", "Filter hosters to use", "all"), + ("mh_list", "str", "Hoster list (comma separated)", ""), + ("mh_interval", "int", "Reload interval in hours", 12), + ("ignore_status", "bool", "Treat all hosters as available (ignore status field)", False), + ("streams_also", "bool", "Also download from stream hosters", False)] + __description__ = """AllDebrid.com account plugin""" - __author_name__ = ("Andy, Voigt") - __author_mail__ = ("spamsales@online.de") - - def loadAccountInfo(self, user, req): - data = self.getAccountData(user) - page = req.load("http://www.alldebrid.com/account/") - soup = BeautifulSoup(page) - #Try to parse expiration date directly from the control panel page (better accuracy) - try: - time_text = soup.find('div', attrs={'class': 'remaining_time_text'}).strong.string - self.logDebug("Account expires in: %s" % time_text) - p = re.compile('\d+') - exp_data = p.findall(time_text) - exp_time = time() + int(exp_data[0]) * 24 * 60 * 60 + int( - exp_data[1]) * 60 * 60 + (int(exp_data[2]) - 1) * 60 - #Get expiration date from API - except: - data = self.getAccountData(user) - page = req.load("http://www.alldebrid.com/api.php?action=info_user&login=%s&pw=%s" % (user, - data["password"])) - self.logDebug(page) - xml = dom.parseString(page) - exp_time = time() + int(xml.getElementsByTagName("date")[0].childNodes[0].nodeValue) * 86400 - account_info = {"validuntil": exp_time, "trafficleft": -1} - return account_info - - def login(self, user, data, req): - urlparams = urllib.urlencode({'action': 'login', 'login_login': user, 'login_password': data["password"]}) - page = req.load("http://www.alldebrid.com/register/?%s" % urlparams) - - if "This login doesn't exist" in page: - self.wrongPassword() - - if "The password is not valid" in page: - self.wrongPassword() - - if "Invalid captcha" in page: - self.wrongPassword() + __license__ = "GPLv3" + __authors__ = [("Andy Voigt", "spamsales@online.de"), + ("GammaC0de", "nitzo2001[AT]yahoo[DOT]com")] + + # See https://docs.alldebrid.com/ + API_URL = "https://api.alldebrid.com/v4.1/" + + def api_response(self, method, get={}, post={}, multipart=False): + get.update({'agent': "pyLoad", + 'version': self.pyload.version}) + json_data = json.loads(self.load(self.API_URL + method, get=get, post=post, multipart=multipart)) + if json_data['status'] == "success": + return json_data['data'] + else: + return json_data + + def grab_hosters(self, user, password, data): + api_data = self.api_response("user/hosts", + get={'apikey': password}) + if api_data.get("error", False): + self.log_error(api_data['error']['message']) + return [] + + else: + valid_statuses = (True, False) if self.config.get("ignore_status") is True else (True,) + valid_hosters = api_data["hosts"].values() + ( + api_data["streams"].values() + if self.config.get("streams_also") is True + else [] + ) + return reduce(lambda x, y: x + y, + [_h['domains'] + for _h in valid_hosters + if _h.get('status', False) in valid_statuses or _h.get('type') == "free"]) + + def grab_info(self, user, password, data): + api_data = self.api_response("user", + get={'apikey': password}) + + if api_data.get("error", False): + self.log_error(api_data['error']['message']) + premium = False + validuntil = -1 + + else: + premium = api_data['user']['isPremium'] + validuntil = api_data['user']['premiumUntil'] or -1 + + return {'validuntil': validuntil, + 'trafficleft': -1, + 'premium': premium} + + def signin(self, user, password, data): + api_data = self.api_response("user", + get={'apikey': password}) + + if api_data.get("error", False): + self.log_error(_("v4 API token required - use GetAlldebridTokenV4.py to get it: https://github.com/pyload/pyload/files/4489732/GetAlldebridTokenV4.zip")) + self.fail_login(api_data['error']['message']) + + elif api_data['user']['username'] != user: + self.log_error(_("username for alldebrid.com should be your alldebrid.com username")) + self.fail_login() diff --git a/module/plugins/accounts/AniStreamCom.py b/module/plugins/accounts/AniStreamCom.py new file mode 100644 index 0000000000..7c316e436e --- /dev/null +++ b/module/plugins/accounts/AniStreamCom.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- + +from ..internal.XFSAccount import XFSAccount + + +class AniStreamCom(XFSAccount): + __name__ = "AniStreamCom" + __type__ = "account" + __version__ = "0.05" + __status__ = "testing" + + __description__ = """Ani-Stream.com account plugin""" + __license__ = "GPLv3" + __authors__ = [("Walter Purcaro", "vuolter@gmail.com")] + + PLUGIN_DOMAIN = "ani-stream.com" diff --git a/module/plugins/accounts/ArchiveOrg.py b/module/plugins/accounts/ArchiveOrg.py new file mode 100644 index 0000000000..f6bbbdb25a --- /dev/null +++ b/module/plugins/accounts/ArchiveOrg.py @@ -0,0 +1,51 @@ +# -*- coding: utf-8 -*- + +import json + +from module.network.HTTPRequest import BadHeader + +from ..internal.Account import Account +from ..internal.misc import json + + +class ArchiveOrg(Account): + __name__ = "ArchiveOrg" + __type__ = "account" + __version__ = "0.02" + __status__ = "testing" + + __description__ = """Archive.org account plugin""" + __license__ = "GPLv3" + __authors__ = [("GammaC0de", "nitzo2001[AT]yahoo[DOT]com")] + + LOGIN_URL = "https://archive.org/account/login" + LOGIN_CHECK_URL = "https://archive.org/account/index.php?settings=1" + + def grab_info(self, user, password, data): + return {'validuntil': None, + 'trafficleft': None, + 'premium': False} + + def signin(self, user, password, data): + html = self.load(self.LOGIN_CHECK_URL) + if "You must be logged in to change your settings." not in html: + self.skip_login() + + else: + self.load(self.LOGIN_URL) + try: + html = self.load(self.LOGIN_URL, post={ + "username": user, + "password": password, + "remember": "true", + "referer": "https://archive.org/", + "login": "true", + "submit_by_js": "true" + }) + except BadHeader as exc: + self.fail_login(str(exc)) + + else: + json_data = json.loads(html) + if json_data["status"] != "ok": + self.fail_login() diff --git a/module/plugins/accounts/BackinNet.py b/module/plugins/accounts/BackinNet.py new file mode 100644 index 0000000000..fdb31a72df --- /dev/null +++ b/module/plugins/accounts/BackinNet.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- + +from ..internal.XFSAccount import XFSAccount + + +class BackinNet(XFSAccount): + __name__ = "BackinNet" + __type__ = "account" + __version__ = "0.06" + __status__ = "testing" + + __description__ = """Backin.net account plugin""" + __license__ = "GPLv3" + __authors__ = [("Walter Purcaro", "vuolter@gmail.com")] + + PLUGIN_DOMAIN = "backin.net" diff --git a/module/plugins/accounts/BayfilesCom.py b/module/plugins/accounts/BayfilesCom.py deleted file mode 100644 index bf5cc54afb..0000000000 --- a/module/plugins/accounts/BayfilesCom.py +++ /dev/null @@ -1,51 +0,0 @@ -# -*- 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 . - - @author: zoidberg -""" - -from time import time - -from module.plugins.Account import Account -from module.common.json_layer import json_loads - - -class BayfilesCom(Account): - __name__ = "BayfilesCom" - __version__ = "0.02" - __type__ = "account" - __description__ = """bayfiles.com account plugin""" - __author_name__ = ("zoidberg") - __author_mail__ = ("zoidberg@mujmail.cz") - - def loadAccountInfo(self, user, req): - for i in range(2): - response = json_loads(req.load("http://api.bayfiles.com/v1/account/info")) - self.logDebug(response) - if not response["error"]: - break - self.logWarning(response["error"]) - self.relogin() - - return {"premium": bool(response['premium']), "trafficleft": -1, - "validuntil": response['expires'] if response['expires'] >= int(time()) else -1} - - def login(self, user, data, req): - response = json_loads(req.load("http://api.bayfiles.com/v1/account/login/%s/%s" % (user, data["password"]))) - self.logDebug(response) - if response["error"]: - self.logError(response["error"]) - self.wrongPassword() diff --git a/module/plugins/accounts/BitshareCom.py b/module/plugins/accounts/BitshareCom.py deleted file mode 100644 index de1e19f514..0000000000 --- a/module/plugins/accounts/BitshareCom.py +++ /dev/null @@ -1,45 +0,0 @@ -# -*- 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 . - - @author: pking -""" - -from module.plugins.Account import Account - - -class BitshareCom(Account): - __name__ = "BitshareCom" - __version__ = "0.12" - __type__ = "account" - __description__ = """Bitshare account plugin""" - __author_name__ = ("Paul King") - - def loadAccountInfo(self, user, req): - page = req.load("http://bitshare.com/mysettings.html") - - if "\"http://bitshare.com/myupgrade.html\">Free" in page: - return {"validuntil": -1, "trafficleft": -1, "premium": False} - - if not '' in page: - self.logWarning(_("Activate direct Download in your Bitshare Account")) - - return {"validuntil": -1, "trafficleft": -1, "premium": True} - - def login(self, user, data, req): - page = req.load("http://bitshare.com/login.html", - post={"user": user, "password": data["password"], "submit": "Login"}, cookies=True) - if "login" in req.lastEffectiveURL: - self.wrongPassword() diff --git a/module/plugins/accounts/CloudsharesNet.py b/module/plugins/accounts/CloudsharesNet.py new file mode 100644 index 0000000000..78a5f1c4c0 --- /dev/null +++ b/module/plugins/accounts/CloudsharesNet.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- + +from ..internal.XFSAccount import XFSAccount + + +class CloudsharesNet(XFSAccount): + __name__ = "CloudsharesNet" + __type__ = "account" + __version__ = "0.03" + __status__ = "testing" + + __description__ = """Cloudshares.net account plugin""" + __license__ = "GPLv3" + __authors__ = [("Walter Purcaro", "vuolter@gmail.com")] + + PLUGIN_DOMAIN = "cloudshares.net" diff --git a/module/plugins/accounts/CloudsixMe.py b/module/plugins/accounts/CloudsixMe.py new file mode 100644 index 0000000000..a5de87da3d --- /dev/null +++ b/module/plugins/accounts/CloudsixMe.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- + +from ..internal.XFSAccount import XFSAccount + + +class CloudsixMe(XFSAccount): + __name__ = "CloudsixMe" + __type__ = "account" + __version__ = "0.05" + __status__ = "testing" + + __description__ = """Cloudsix.me account plugin""" + __license__ = "GPLv3" + __authors__ = [("Walter Purcaro", "vuolter@gmail.com")] + + PLUGIN_DOMAIN = "cloudsix.me" diff --git a/module/plugins/accounts/CloudzillaTo.py b/module/plugins/accounts/CloudzillaTo.py new file mode 100644 index 0000000000..db9cea5562 --- /dev/null +++ b/module/plugins/accounts/CloudzillaTo.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- + +import re + +from ..internal.Account import Account + + +class CloudzillaTo(Account): + __name__ = "CloudzillaTo" + __type__ = "account" + __version__ = "0.09" + __status__ = "testing" + + __description__ = """Cloudzilla.to account plugin""" + __license__ = "GPLv3" + __authors__ = [("Walter Purcaro", "vuolter@gmail.com")] + + PREMIUM_PATTERN = r'

account type

\s*Premium Account' + + def grab_info(self, user, password, data): + html = self.load("http://www.cloudzilla.to/") + + premium = True if re.search(self.PREMIUM_PATTERN, html) else False + + return {'validuntil': -1, 'trafficleft': -1, 'premium': premium} + + def signin(self, user, password, data): + html = self.load("https://www.cloudzilla.to/", + post={'lusername': user, + 'lpassword': password, + 'w': "dologin"}) + + if "ERROR" in html: + self.fail_login() diff --git a/module/plugins/accounts/CramitIn.py b/module/plugins/accounts/CramitIn.py index b0334b191c..db9e523cd7 100644 --- a/module/plugins/accounts/CramitIn.py +++ b/module/plugins/accounts/CramitIn.py @@ -1,13 +1,16 @@ # -*- coding: utf-8 -*- -from module.plugins.internal.XFSPAccount import XFSPAccount +from ..internal.XFSAccount import XFSAccount -class CramitIn(XFSPAccount): + +class CramitIn(XFSAccount): __name__ = "CramitIn" - __version__ = "0.01" __type__ = "account" - __description__ = """cramit.in account plugin""" - __author_name__ = ("zoidberg") - __author_mail__ = ("zoidberg@mujmail.cz") + __version__ = "0.08" + __status__ = "testing" + + __description__ = """Cramit.in account plugin""" + __license__ = "GPLv3" + __authors__ = [("zoidberg", "zoidberg@mujmail.cz")] - MAIN_PAGE = "http://cramit.in/" + PLUGIN_DOMAIN = "cramit.in" diff --git a/module/plugins/accounts/CyberlockerCh.py b/module/plugins/accounts/CyberlockerCh.py deleted file mode 100644 index 0eaa262ebf..0000000000 --- a/module/plugins/accounts/CyberlockerCh.py +++ /dev/null @@ -1,32 +0,0 @@ -# -*- coding: utf-8 -*- -from module.plugins.internal.XFSPAccount import XFSPAccount -from module.plugins.internal.SimpleHoster import parseHtmlForm - - -class CyberlockerCh(XFSPAccount): - __name__ = "CyberlockerCh" - __version__ = "0.01" - __type__ = "account" - __description__ = """CyberlockerCh account plugin""" - __author_name__ = ("stickell") - __author_mail__ = ("l.stickell@yahoo.it") - - MAIN_PAGE = "http://cyberlocker.ch/" - - def login(self, user, data, req): - html = req.load(self.MAIN_PAGE + 'login.html', decode=True) - - action, inputs = parseHtmlForm('name="FL"', html) - if not inputs: - inputs = {"op": "login", - "redirect": self.MAIN_PAGE} - - inputs.update({"login": user, - "password": data['password']}) - - # Without this a 403 Forbidden is returned - req.http.lastURL = self.MAIN_PAGE + 'login.html' - html = req.load(self.MAIN_PAGE, post=inputs, decode=True) - - if 'Incorrect Login or Password' in html or '>Error<' in html: - self.wrongPassword() diff --git a/module/plugins/accounts/CzshareCom.py b/module/plugins/accounts/CzshareCom.py index 7b1a8edc54..a05aada1a4 100644 --- a/module/plugins/accounts/CzshareCom.py +++ b/module/plugins/accounts/CzshareCom.py @@ -1,57 +1,58 @@ # -*- 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 . - - @author: zoidberg -""" - -from time import mktime, strptime import re +import time -from module.plugins.Account import Account +from ..internal.Account import Account class CzshareCom(Account): __name__ = "CzshareCom" - __version__ = "0.13" __type__ = "account" - __description__ = """czshare.com account plugin""" - __author_name__ = ("zoidberg", "stickell") - __author_mail__ = ("zoidberg@mujmail.cz", "l.stickell@yahoo.it") + __version__ = "0.28" + __status__ = "testing" - CREDIT_LEFT_PATTERN = r'\s*([0-9 ,]+) (KiB|MiB|GiB)\s*([^<]*)\s*' + __description__ = """Czshare.com account plugin, now Sdilej.cz""" + __license__ = "GPLv3" + __authors__ = [("zoidberg", "zoidberg@mujmail.cz"), + ("stickell", "l.stickell@yahoo.it"), + ("ondrej", "git@ondrej.it"), ] - def loadAccountInfo(self, user, req): - html = req.load("http://czshare.com/prehled_kreditu/") + CREDIT_LEFT_PATTERN = r'^\s+
\s+\n.+([\d,]+)(KB|MB|GB)\s+\n.+\s+$' + VALID_UNTIL_PATTERN = r'^\s+\s+\n.+\n\s+([\d\.: ]+)\s+$' + + def grab_info(self, user, password, data): + premium = False + validuntil = None + trafficleft = None + + html = self.load("http://sdilej.cz/prehled_kreditu/") + + try: + m = re.search(self.CREDIT_LEFT_PATTERN, html, re.MULTILINE) + trafficleft = self.parse_traffic(m.group(1), m.group(2)) + + v = re.search(self.VALID_UNTIL_PATTERN, html, re.MULTILINE) + validuntil = time.mktime( + time.strptime( + v.group(1), + '%d.%m.%y %H:%M')) + + except Exception, e: + self.log_error(e, trace=True) - found = re.search(self.CREDIT_LEFT_PATTERN, html) - if not found: - return {"validuntil": 0, "trafficleft": 0} else: - credits = float(found.group(1).replace(' ', '').replace(',', '.')) - credits = credits * 1024 ** {'KiB': 0, 'MiB': 1, 'GiB': 2}[found.group(2)] - validuntil = mktime(strptime(found.group(3), '%d.%m.%y %H:%M')) - return {"validuntil": validuntil, "trafficleft": credits} + premium = True - def login(self, user, data, req): + return {'premium': premium, + 'validuntil': validuntil, + 'trafficleft': trafficleft} - html = req.load('https://czshare.com/index.php', post={ - "Prihlasit": "Prihlasit", - "login-password": data["password"], - "login-name": user - }) + def signin(self, user, password, data): + html = self.load('https://sdilej.cz/index.php', + post={'Prihlasit': "Prihlasit", + "login-password": password, + "login-name": user}) if '\s*
\s*(?:(?P[^<>]+))?(?P-?\d+|[Uu]nlimited)\s*
' + VALID_UNTIL_PATTERN = r'class="expires">([\w ]+)<' + + def setup(self): + super(DdownloadCom, self).setup() + self.req.http.c.setopt(pycurl.USERAGENT, "pyLoad/%s" % self.pyload.version) diff --git a/module/plugins/accounts/DebridItaliaCom.py b/module/plugins/accounts/DebridItaliaCom.py index 82acd8f8ed..ac5dc83920 100644 --- a/module/plugins/accounts/DebridItaliaCom.py +++ b/module/plugins/accounts/DebridItaliaCom.py @@ -1,49 +1,53 @@ # -*- coding: utf-8 -*- -############################################################################ -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU Affero 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 Affero General Public License for more details. # -# # -# You should have received a copy of the GNU Affero General Public License # -# along with this program. If not, see . # -############################################################################ - import re -import time -from module.plugins.Account import Account +from ..internal.MultiAccount import MultiAccount -class DebridItaliaCom(Account): +class DebridItaliaCom(MultiAccount): __name__ = "DebridItaliaCom" - __version__ = "0.1" __type__ = "account" - __description__ = """debriditalia.com account plugin""" - __author_name__ = ("stickell") - __author_mail__ = ("l.stickell@yahoo.it") + __version__ = "0.22" + __status__ = "testing" + + __config__ = [("mh_mode", "all;listed;unlisted", "Filter hosters to use", "all"), + ("mh_list", "str", "Hoster list (comma separated)", ""), + ("mh_interval", "int", "Reload interval in hours", 12)] + + __description__ = """Debriditalia.com account plugin""" + __license__ = "GPLv3" + __authors__ = [("stickell", "l.stickell@yahoo.it"), + ("Walter Purcaro", "vuolter@gmail.com"), + ("GammaC0de", "nitzo2001[AT]yahoo[DOT]com")] + + API_URL = "https://debriditalia.com/api.php" + + def api_response(self, method, **kwargs): + kwargs[method] = "" + return self.load(self.API_URL, get=kwargs) - WALID_UNTIL_PATTERN = r"Premium valid till: (?P[^|]+) \|" + def grab_hosters(self, user, password, data): + return self.api_response("hosts").replace('"', '').split(',') - def loadAccountInfo(self, user, req): - if 'Account premium not activated' in self.html: - return {"premium": False, "validuntil": None, "trafficleft": None} + def grab_info(self, user, password, data): + validuntil = None + + html = self.api_response("check", u=user, p=password) + + m = re.search(r'(.+?)', html) + if m is not None: + validuntil = int(m.group(1)) - m = re.search(self.WALID_UNTIL_PATTERN, self.html) - if m: - validuntil = int(time.mktime(time.strptime(m.group('D'), "%d/%m/%Y %H:%M"))) - return {"premium": True, "validuntil": validuntil, "trafficleft": -1} else: - self.logError('Unable to retrieve account information - Plugin may be out of date') + self.log_error(_("Unable to retrieve account information")) + + return {'validuntil': validuntil, + 'trafficleft': -1, + 'premium': True} + + def signin(self, user, password, data): + html = self.api_response("check", u=user, p=password) - def login(self, user, data, req): - self.html = req.load("http://debriditalia.com/login.php", - get={"u": user, "p": data["password"]}) - if 'NO' in self.html: - self.wrongPassword() + if "valid" not in html: + self.fail_login() diff --git a/module/plugins/accounts/DebridlinkFr.py b/module/plugins/accounts/DebridlinkFr.py new file mode 100644 index 0000000000..9b7ae0ca63 --- /dev/null +++ b/module/plugins/accounts/DebridlinkFr.py @@ -0,0 +1,100 @@ +# -*- coding: utf-8 -*- + +import time + +import pycurl +from module.network.HTTPRequest import BadHeader + +from ..hoster.DebridlinkFr import error_description +from ..internal.misc import json +from ..internal.MultiAccount import MultiAccount + + +class DebridlinkFr(MultiAccount): + __name__ = "DebridlinkFr" + __type__ = "account" + __version__ = "0.06" + __status__ = "testing" + + __config__ = [("mh_mode", "all;listed;unlisted", "Filter hosters to use", "all"), + ("mh_list", "str", "Hoster list (comma separated)", ""), + ("mh_interval", "int", "Reload interval in hours", 12)] + + __description__ = """Debridlink.fr account plugin""" + __license__ = "GPLv3" + __authors__ = [("GammaC0de", "nitzo2001[AT]yahoo[DOT]com")] + + TUNE_TIMEOUT = False + + #: See https://debrid-link.fr/api_doc/v2 + API_URL = "https://debrid-link.fr/api/" + + def api_request(self, method, get={}, post={}): + api_token = self.info['data'].get('api_token', None) + if api_token and method != "oauth/token": + self.req.http.c.setopt(pycurl.HTTPHEADER, ["Authorization: Bearer " + api_token]) + self.req.http.c.setopt(pycurl.USERAGENT, "pyLoad/%s" % self.pyload.version) + try: + json_data = self.load(self.API_URL + method, get=get, post=post) + except BadHeader, e: + json_data = e.content + + return json.loads(json_data) + + def _refresh_token(self, client_id, refresh_token): + api_data = self.api_request("oauth/token", + post={'client_id': client_id, + 'refresh_token': refresh_token, + 'grant_type': "refresh_token"}) + + if 'error' in api_data: + if api_data['error'] == 'invalid_request': + self.log_error(_("You have to use GetDebridlinkToken.py to authorize pyLoad: https://github.com/pyload/pyload/files/9353788/GetDebridlinkToken.zip")) + else: + self.log_error(api_data.get('error_description', error_description(api_data["error"]))) + self.fail_login() + + return api_data['access_token'], api_data['expires_in'] + + def grab_hosters(self, user, password, data): + api_data = self.api_request("v2/downloader/hostnames") + + if api_data['success']: + return api_data['value'] + + else: + return [] + + def grab_info(self, user, password, data): + api_data = self.api_request("v2/account/infos") + + if api_data['success']: + premium = api_data['value']['premiumLeft'] > 0 + validuntil = api_data['value']['premiumLeft'] + time.time() + + else: + self.log_error(_("Unable to retrieve account information"), + api_data.get('error_description', error_description(api_data["error"]))) + validuntil = None + premium = None + + return {'validuntil': validuntil, + 'trafficleft': -1, + 'premium': premium} + + def signin(self, user, password, data): + if 'token' not in data: + api_token, timeout = self._refresh_token(user, password) + data['api_token'] = api_token + self.timeout = timeout - 5 * 60 #: Five minutes less to be on the safe side + + api_data = self.api_request("v2/account/infos") + if 'error' in api_data: + if api_data['error'] == 'badToken': #: Token expired? try to refresh + api_token, timeout = self._refresh_token(user, password) + data['api_token'] = api_token + self.timeout = timeout - 5 * 60 #: Five minutes less to be on the safe side + + else: + self.log_error(api_data.get('error_description', error_description(api_data["error"]))) + self.fail_login() diff --git a/module/plugins/accounts/DebridplanetCom.py b/module/plugins/accounts/DebridplanetCom.py new file mode 100644 index 0000000000..10ad91f22b --- /dev/null +++ b/module/plugins/accounts/DebridplanetCom.py @@ -0,0 +1,70 @@ +# -*- coding: utf-8 -*- + +import hashlib +import json +import time + +import pycurl +from ..internal.MultiAccount import MultiAccount + + +class DebridplanetCom(MultiAccount): + __name__ = "DebridplanetCom" + __type__ = "account" + __version__ = "0.01" + __status__ = "testing" + + __config__ = [ + ("mh_mode", "all;listed;unlisted", "Filter downloaders to use", "all"), + ("mh_list", "str", "Downloader list (comma separated)", ""), + ("mh_interval", "int", "Reload interval in hours", 12), + ] + + __description__ = """Debridplanet.com account plugin""" + __license__ = "GPLv3" + __authors__ = [("GammaC0de", "nitzo2001[AT]yahoo[DOT]com")] + + API_URL = "https://debridplanet.com/v1/" + + def api_request(self, method, **kwargs): + token = self.info["data"].get("token") + if token is not None: + self.req.http.c.setopt( + pycurl.HTTPHEADER, ["Authorization: Bearer " + token] + ) + json_data = self.load("%s%s.php" % (self.API_URL, method), + post=json.dumps(kwargs)) + return json.loads(json_data) + + def grab_hosters(self, user, password, data): + api_data = self.api_request("supportedhosts") + hosts = [ + h["host"] + for h in api_data["supportedhosts"] + if h["currently_working"] + ] + return hosts + + def grab_info(self, user, password, data): + validuntil = None + premium = False + + api_data = self.api_request("user-info") + if api_data.get("success", False): + premium = api_data["user"]["account_type"] == "premium" + validuntil = time.mktime(time.strptime(api_data["user"]["expire"], "%Y-%m-%dT%H:%M:%S")) + + return {"validuntil": validuntil, "trafficleft": -1, "premium": premium} + + def signin(self, user, password, data): + api_data = self.api_request("user-info") + if api_data.get("success", False): + self.skip_login() + + data["token"] = None + api_data = self.api_request("login", username=user, password=hashlib.sha256(password).hexdigest()) + if api_data.get("success", False): + data["token"] = api_data["token"] + + else: + self.fail_login() diff --git a/module/plugins/accounts/DepositfilesCom.py b/module/plugins/accounts/DepositfilesCom.py index 5f2408e724..d383024c4a 100644 --- a/module/plugins/accounts/DepositfilesCom.py +++ b/module/plugins/accounts/DepositfilesCom.py @@ -1,47 +1,41 @@ # -*- 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 . - - @author: mkaay -""" - import re -from time import strptime, mktime +import time -from module.plugins.Account import Account +from ..internal.Account import Account class DepositfilesCom(Account): __name__ = "DepositfilesCom" - __version__ = "0.2" __type__ = "account" - __description__ = """depositfiles.com account plugin""" - __author_name__ = ("mkaay", "stickell") - __author_mail__ = ("mkaay@mkaay.de", "l.stickell@yahoo.it") - - def loadAccountInfo(self, user, req): - src = req.load("http://depositfiles.com/de/gold/") - validuntil = re.search(r"Sie haben Gold Zugang bis: (.*?)
", src).group(1) - - validuntil = int(mktime(strptime(validuntil, "%Y-%m-%d %H:%M:%S"))) - - return {"validuntil": validuntil, "trafficleft": -1} - - def login(self, user, data, req): - req.load("http://depositfiles.com/de/gold/payment.php") - src = req.load("http://depositfiles.com/de/login.php", get={"return": "/de/gold/payment.php"}, - post={"login": user, "password": data["password"]}) - if r'
Sie haben eine falsche Benutzername-Passwort-Kombination verwendet.
' in src: - self.wrongPassword() + __version__ = "0.39" + __status__ = "testing" + + __description__ = """Depositfiles.com account plugin""" + __license__ = "GPLv3" + __authors__ = [("mkaay", "mkaay@mkaay.de"), + ("stickell", "l.stickell@yahoo.it"), + ("Walter Purcaro", "vuolter@gmail.com")] + + def grab_info(self, user, password, data): + html = self.load("https://dfiles.eu/de/gold/") + validuntil = re.search( + r'Sie haben Gold Zugang bis: (.*?)', + html).group(1) + + validuntil = time.mktime( + time.strptime( + validuntil, + "%Y-%m-%d %H:%M:%S")) + + return {'validuntil': validuntil, 'trafficleft': -1} + + def signin(self, user, password, data): + html = self.load("https://dfiles.eu/de/login.php", + get={'return': "/de/gold/payment.php"}, + post={'login': user, + 'password': password}) + + if r'
Sie haben eine falsche Benutzername-Passwort-Kombination verwendet.
' in html: + self.fail_login() diff --git a/module/plugins/accounts/DownsterNet.py b/module/plugins/accounts/DownsterNet.py new file mode 100644 index 0000000000..13333cf6e7 --- /dev/null +++ b/module/plugins/accounts/DownsterNet.py @@ -0,0 +1,117 @@ +# -*- coding: utf-8 -*- + +import string +import random +import time + +import pycurl +from module.network.HTTPRequest import BadHeader + +from ..internal.misc import json +from ..internal.MultiAccount import MultiAccount + + +class DownsterApi(object): + API_URL = "https://downster.net/api/" + + def __init__(self, plugin): + self.plugin = plugin + + if hasattr(self.plugin, 'account'): + self.account_plugin = self.plugin.account + + else: + self.account_plugin = self.plugin + + def request(self, method, get={}, **kwargs): + self.plugin.req.http.c.setopt(pycurl.HTTPHEADER, [ + "Accept: application/json, text/plain, */", + "Content-Type: application/json", + "X-Flow-ID: " + self.flow_id(), + ]) + self.plugin.req.http.c.setopt(pycurl.USERAGENT, + "User-Agent: pyLoad/" + self.plugin.pyload.version + + " DownsterNet/" + self.account_plugin.__version__) + + try: + res = self.plugin.load(self.API_URL + method, + get=get, + post=json.dumps(kwargs)) + except BadHeader, e: + res = e.content + + res = json.loads(res) + + return res + + def rnd(self): + return ''.join([random.choice(string.ascii_lowercase + string.digits) for n in range(5)]) + + def flow_id(self): + user_flow_id = self.account_plugin.info['data'].get('user_flow_id') + self.plugin.log_debug("User flow id: %s" % user_flow_id) + if not user_flow_id: + self.account_plugin.info['data']['user_flow_id'] = self.rnd() + self.plugin.log_info('Created user flow id: %s' % self.account_plugin.info['data']['user_flow_id']) + + return 'PYL_' + self.account_plugin.info['data']['user_flow_id'] + '_' + self.rnd() + + +class DownsterNet(MultiAccount): + __name__ = "DownsterNet" + __type__ = "account" + __version__ = "0.04" + __status__ = "testing" + + __config__ = [("mh_mode", "all;listed;unlisted", "Filter hosters to use", "all"), + ("mh_list", "str", "Hoster list (comma separated)", ""), + ("mh_interval", "int", "Reload interval in hours", 12)] + + __description__ = """Downster.net account plugin""" + __license__ = "GPLv3" + __authors__ = [(None, None)] + + api = None + + def grab_hosters(self, user, password, data): + api_data = self.api.request("download/usage") + if not api_data['success']: + self.log_error('Could not get hoster info: ' + api_data['error']) + return [] + + else: + return [hoster['hoster'] for hoster in api_data['data']] + + def grab_info(self, user, password, data): + api_data = self.api.request("user/info") + + if not api_data['success']: + validuntil = None + trafficleft = None + premium = False + + self.log_error('Could not get user info: ' + api_data['error']) + + else: + validuntil = time.mktime(time.strptime(api_data['data']['premiumUntil'], "%Y-%m-%dT%H:%M:%S.%f+00:00")) + trafficleft = -1 + premium = validuntil > time.time() + + return {'validuntil': validuntil, + 'trafficleft': trafficleft, + 'premium': premium} + + def signin(self, user, password, data): + if self.api is None: + self.api = DownsterApi(self) + + api_data = self.api.request('user/info') + if api_data['success']: + self.skip_login() + + api_data = self.api.request("user/authenticate", + email=user, + password=password) + + if not api_data['success']: + self.fail_login(api_data['error']) diff --git a/module/plugins/accounts/EasybytezCom.py b/module/plugins/accounts/EasybytezCom.py index 72f37b6992..176c0bdb38 100644 --- a/module/plugins/accounts/EasybytezCom.py +++ b/module/plugins/accounts/EasybytezCom.py @@ -1,76 +1,17 @@ # -*- 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. +from ..internal.XFSAccount import XFSAccount - 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 . - - @author: zoidberg -""" - -import re -from time import mktime, strptime - -from module.plugins.Account import Account -from module.plugins.internal.SimpleHoster import parseHtmlForm -from module.utils import parseFileSize - - -class EasybytezCom(Account): +class EasybytezCom(XFSAccount): __name__ = "EasybytezCom" - __version__ = "0.03" __type__ = "account" - __description__ = """EasyBytez.com account plugin""" - __author_name__ = ("zoidberg") - __author_mail__ = ("zoidberg@mujmail.cz") - - VALID_UNTIL_PATTERN = r'Premium account expire:([^<]+)' - TRAFFIC_LEFT_PATTERN = r'Traffic available today:(?P[^<]+)' + __version__ = "0.20" + __status__ = "testing" - def loadAccountInfo(self, user, req): - html = req.load("http://www.easybytez.com/?op=my_account", decode=True) - - validuntil = trafficleft = None - premium = False - - found = re.search(self.VALID_UNTIL_PATTERN, html) - if found: - premium = True - trafficleft = -1 - try: - self.logDebug("Expire date: " + found.group(1)) - validuntil = mktime(strptime(found.group(1), "%d %B %Y")) - except Exception, e: - self.logError(e) - else: - found = re.search(self.TRAFFIC_LEFT_PATTERN, html) - if found: - trafficleft = found.group(1) - if "Unlimited" in trafficleft: - premium = True - trafficleft = -1 - else: - trafficleft = parseFileSize(trafficleft) / 1024 - - return {"validuntil": validuntil, "trafficleft": trafficleft, "premium": premium} - - def login(self, user, data, req): - html = req.load('http://www.easybytez.com/login.html', decode=True) - action, inputs = parseHtmlForm('name="FL"', html) - inputs.update({"login": user, - "password": data['password'], - "redirect": "http://www.easybytez.com/"}) - - html = req.load(action, post=inputs, decode=True) + __description__ = """EasyBytez.com account plugin""" + __license__ = "GPLv3" + __authors__ = [("zoidberg", "zoidberg@mujmail.cz"), + ("guidobelix", "guidobelix@hotmail.it")] - if 'Incorrect Login or Password' in html or '>Error<' in html: - self.wrongPassword() + PLUGIN_DOMAIN = "easybytez.com" diff --git a/module/plugins/accounts/EgoFilesCom.py b/module/plugins/accounts/EgoFilesCom.py deleted file mode 100644 index 9c2b918c32..0000000000 --- a/module/plugins/accounts/EgoFilesCom.py +++ /dev/null @@ -1,42 +0,0 @@ -# -*- coding: utf-8 -*- - -import re -import time - -from module.plugins.Account import Account -from module.utils import parseFileSize - - -class EgoFilesCom(Account): - __name__ = "EgoFilesCom" - __version__ = "0.2" - __type__ = "account" - __description__ = """egofiles.com account plugin""" - __author_name__ = ("stickell") - __author_mail__ = ("l.stickell@yahoo.it") - - PREMIUM_ACCOUNT_PATTERN = '
\s*Premium: (?P

[^/]*) / Traffic left: (?P[\d.]*) (?P\w*)\s*\\n\s*
' - - def loadAccountInfo(self, user, req): - html = req.load("http://egofiles.com") - if 'You are logged as a Free User' in html: - return {"premium": False, "validuntil": None, "trafficleft": None} - - m = re.search(self.PREMIUM_ACCOUNT_PATTERN, html) - if m: - validuntil = int(time.mktime(time.strptime(m.group('P'), "%Y-%m-%d %H:%M:%S"))) - trafficleft = parseFileSize(m.group('T'), m.group('U')) / 1024 - return {"premium": True, "validuntil": validuntil, "trafficleft": trafficleft} - else: - self.logError('Unable to retrieve account information - Plugin may be out of date') - - def login(self, user, data, req): - # Set English language - req.load("https://egofiles.com/ajax/lang.php?lang=en", just_header=True) - - html = req.load("http://egofiles.com/ajax/register.php", - post={"log": 1, - "loginV": user, - "passV": data["password"]}) - if 'Login successful' not in html: - self.wrongPassword() diff --git a/module/plugins/accounts/EuroshareEu.py b/module/plugins/accounts/EuroshareEu.py index 830c1db3f5..f1f88d43b0 100644 --- a/module/plugins/accounts/EuroshareEu.py +++ b/module/plugins/accounts/EuroshareEu.py @@ -1,56 +1,54 @@ # -*- 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 . - - @author: zoidberg -""" - -from time import mktime, strptime import re +import time -from module.plugins.Account import Account +from ..internal.Account import Account +from ..internal.misc import json class EuroshareEu(Account): __name__ = "EuroshareEu" - __version__ = "0.01" __type__ = "account" - __description__ = """euroshare.eu account plugin""" - __author_name__ = ("zoidberg") - __author_mail__ = ("zoidberg@mujmail.cz") - - def loadAccountInfo(self, user, req): - self.relogin(user) - html = req.load("http://euroshare.eu/customer-zone/settings/") - - found = re.search('id="input_expire_date" value="(\d+\.\d+\.\d+ \d+:\d+)"', html) - if found is None: - premium, validuntil = False, -1 + __version__ = "0.12" + __status__ = "testing" + + __description__ = """Euroshare.eu account plugin""" + __license__ = "GPLv3" + __authors__ = [("zoidberg", "zoidberg@mujmail.cz"), + ("GammaC0de", "nitzo2001[AT]yahoo[DOT]com")] + + def grab_info(self, user, password, data): + html = self.load("http://euroshare.eu/", + get={'lang': "en"}) + + m = re.search( + r'Premium account until: (\d+/\d+/\d+ \d+:\d+:\d+)<', + html) + if m is None: + premium = False + validuntil = -1 else: premium = True - validuntil = mktime(strptime(found.group(1), "%d.%m.%Y %H:%M")) + validuntil = time.mktime( + time.strptime( + m.group(1), + "%d/%m/%Y %H:%M:%S")) + + return {'validuntil': validuntil, + 'trafficleft': -1, 'premium': premium} - return {"validuntil": validuntil, "trafficleft": -1, "premium": premium} + def signin(self, user, password, data): + html = self.load("http://euroshare.eu/login.html") - def login(self, user, data, req): + if r'href="http://euroshare.eu/logout.html"' in html: + self.skip_login() - html = req.load('http://euroshare.eu/customer-zone/login/', post={ - "trvale": "1", - "login": user, - "password": data["password"] - }, decode=True) + json_data = json.loads(self.load("http://euroshare.eu/ajax/_account_login.ajax.php", + post={'username': user, + 'password': password, + 'remember': "false", + 'backlink': ""})) - if u">Nesprávne prihlasovacie meno alebo heslo" in html: - self.wrongPassword() + if json_data.get("login_status") != "success": + self.fail_login() diff --git a/module/plugins/accounts/ExashareCom.py b/module/plugins/accounts/ExashareCom.py new file mode 100644 index 0000000000..9cb23c2479 --- /dev/null +++ b/module/plugins/accounts/ExashareCom.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- + +from ..internal.XFSAccount import XFSAccount + + +class ExashareCom(XFSAccount): + __name__ = "ExashareCom" + __type__ = "account" + __version__ = "0.06" + __status__ = "testing" + + __description__ = """Exashare.com account plugin""" + __license__ = "GPLv3" + __authors__ = [("Walter Purcaro", "vuolter@gmail.com")] + + PLUGIN_DOMAIN = "exashare.com" diff --git a/module/plugins/accounts/ExtmatrixCom.py b/module/plugins/accounts/ExtmatrixCom.py new file mode 100644 index 0000000000..460b122f19 --- /dev/null +++ b/module/plugins/accounts/ExtmatrixCom.py @@ -0,0 +1,94 @@ +# -*- coding: utf-8 -*- + +import re +import time +import urlparse + +from module.PyFile import PyFile + +from ..internal.Account import Account +from ..internal.Captcha import Captcha + + +class ExtmatrixCom(Account): + __name__ = "ExtmatrixCom" + __type__ = "account" + __version__ = "0.02" + __status__ = "testing" + + __description__ = """Extmatrix.com account plugin""" + __license__ = "GPLv3" + __authors__ = [("GammaC0de", "nitzo2001[AT]yahoo[DOT]com")] + + VALID_UNTIL_PATTERN = r'>Premium End:\s*([\d-]+)' + + def grab_info(self, user, password, data): + html = self.load("https://www.extmatrix.com") + + premium = '>Premium Member<' in html + + m = re.search(self.VALID_UNTIL_PATTERN, html) + if m is not None: + validuntil = time.mktime(time.strptime(m.group(1) + " 23:59:59", '%Y-%m-%d %H:%M:%S')) + + else: + self.log_error(_("VALID_UNTIL_PATTERN not found")) + validuntil = None + + return {'validuntil': validuntil, + 'trafficleft': None, + 'premium': premium} + + def signin(self, user, password, data): + html = self.load("https://www.extmatrix.com/login.php") + if 'href="./logout.php"' in html: + self.skip_login() + + + # dummy pyfile + pyfile = PyFile(self.pyload.files, -1, "https://www.extmatrix.com", "https://www.extmatrix.com", 0, 0, "", self.classname, -1, -1) + pyfile.plugin = self + + for i in range(5): + m = re.search(r' 0: - account_info = {"validuntil": -1, "trafficleft": kb} + account_info = {'validuntil': -1, + 'trafficleft': trafficleft} else: - account_info = {"validuntil": None, "trafficleft": None, "premium": False} + account_info = { + 'validuntil': None, + 'trafficleft': None, + 'premium': False} return account_info - def login(self, user, data, req): - page = req.load("http://fastix.ru/api_v2/?sub=get_apikey&email=%s&password=%s" % (user, data["password"])) - api = json_loads(page) - api = api['apikey'] - data["api"] = api - if "error_code" in page: - self.wrongPassword() + def signin(self, user, password, data): + html = self.load("https://fastix.ru/api_v2/", + get={'sub': "get_apikey", + 'email': user, + 'password': password}) + api = json.loads(html) + + if 'error' in api: + self.fail_login(api['error_txt']) + + else: + data['apikey'] = api['apikey'] diff --git a/module/plugins/accounts/FastshareCz.py b/module/plugins/accounts/FastshareCz.py index c047ff7663..b0fe8a55b1 100644 --- a/module/plugins/accounts/FastshareCz.py +++ b/module/plugins/accounts/FastshareCz.py @@ -1,56 +1,74 @@ # -*- 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 . - - @author: zoidberg -""" - import re -from module.plugins.Account import Account -from module.utils import parseFileSize +import time + +from ..internal.Account import Account +from ..internal.misc import set_cookie class FastshareCz(Account): __name__ = "FastshareCz" - __version__ = "0.03" __type__ = "account" - __description__ = """fastshare.cz account plugin""" - __author_name__ = ("zoidberg", "stickell") - __author_mail__ = ("zoidberg@mujmail.cz", "l.stickell@yahoo.it") + __version__ = "0.19" + __status__ = "testing" + + __description__ = """Fastshare.cz account plugin""" + __license__ = "GPLv3" + __authors__ = [("zoidberg", "zoidberg@mujmail.cz"), + ("stickell", "l.stickell@yahoo.it"), + ("ondrej", "git@ondrej.it"), + ("GammaC0de", "nitzo2001[AT]yahoo[DOT]com")] + + TRAFFICLEFT_PATTERN = r'([\d\.]+) ([KMGT]B)\s*' + VALID_UNTILL_PATTERN = r">Active until ([\d.]+)<" - CREDIT_PATTERN = r'(?:Kredit|Credit)\s*\s*]*>([\d. \w]+) ' + def grab_info(self, user, password, data): + html = self.load("https://fastshare.cz/user") - def loadAccountInfo(self, user, req): - html = req.load("http://www.fastshare.cz/user", decode=True) + m = re.search(self.VALID_UNTILL_PATTERN, html) + if m is not None: + validuntil = time.mktime( + time.strptime(m.group(1) + " 23:59:59", "%d.%m.%Y %H:%M:%S") + ) + premium = True + trafficleft = -1 - found = re.search(self.CREDIT_PATTERN, html) - if found: - trafficleft = parseFileSize(found.group(1)) / 1024 - premium = True if trafficleft else False else: - trafficleft = None - premium = False + validuntil = -1 + m = re.search(self.TRAFFICLEFT_PATTERN, html) + if m is not None: + trafficleft = self.parse_traffic(m.group(1), m.group(2)) + premium = bool(trafficleft) + if not premium: + trafficleft = None + + elif ">Unlimited downloading<" in html: + premium = True + trafficleft = -1 + validuntil = None + + else: + premium = False + trafficleft = None + + return { + "validuntil": validuntil, + "trafficleft": trafficleft, + "premium": premium, + } + + def signin(self, user, password, data): + set_cookie(self.req.cj, "fastshare.cz", "lang", "en") - return {"validuntil": -1, "trafficleft": trafficleft, "premium": premium} + html = self.load("https://fastshare.cz/user") + if 'href="/logout.php"' in html: + self.skip_login() - def login(self, user, data, req): - req.load('http://www.fastshare.cz/login') # Do not remove or it will not login - html = req.load('http://www.fastshare.cz/sql.php', post={ - "heslo": data['password'], - "login": user - }, decode=True) + html = self.load( + "https://fastshare.cz/sql.php", + post={"login": user, "heslo": password}, + ) - if u'>Špatné uživatelské jméno nebo heslo.<' in html: - self.wrongPassword() \ No newline at end of file + if 'href="/logout.php"' not in html: + self.fail_login() diff --git a/module/plugins/accounts/FikperCom.py b/module/plugins/accounts/FikperCom.py new file mode 100644 index 0000000000..934c9b73d3 --- /dev/null +++ b/module/plugins/accounts/FikperCom.py @@ -0,0 +1,65 @@ +# -*- coding: utf-8 -*- + +import pycurl +from module.network.HTTPRequest import BadHeader + +from ..internal.Account import Account +from ..internal.misc import json + +class FikperCom(Account): + __name__ = "FikperCom" + __type__ = "account" + __version__ = "0.01" + __status__ = "testing" + + __description__ = """Fikper.com account plugin""" + __license__ = "GPLv3" + __authors__ = [("GammaC0de", "nitzo2001[AT]yahoo[DOT]com")] + + API_URL = "https://sapi.fikper.com/" + + # See https://sapi.fikper.com/api/reference/ + def api_request(self, method, api_key=None, **kwargs): + if api_key is not None: + self.req.http.c.setopt(pycurl.HTTPHEADER, ["x-api-key: %s" % api_key]) + + try: + json_data = self.load(self.API_URL + method, post=kwargs) + return json.loads(json_data) + + except json.JSONDecodeError: + return json_data + + except BadHeader, exc: + return json.loads(exc.content) + + def grab_info(self, user, password, data): + api_data = self.api_request("api/account/info", api_key=password) + + premium = api_data["accountType"] == "premium" + if premium: + validuntil = api_data["premiumExpire"] / 1000 + + else: + validuntil = -1 + + trafficleft = api_data["totalBandwidth"] - api_data["usedBandwidth"] + + return { + "premium": premium, + "validuntil": validuntil, + "trafficleft": trafficleft, + } + + def signin(self, user, password, data): + api_data = self.api_request("api/account/info", api_key=password) + if api_data.get("code") is not None: + self.log_error(_("Password for fikper.com should be the API token")) + self.log_error(_("API error"), api_data) + self.fail_login() + + elif api_data["email"] != user: + self.log_error( + self._("username for fikper.com should be your fikper.com email") + ) + self.fail_login() diff --git a/module/plugins/accounts/File4SafeCom.py b/module/plugins/accounts/File4SafeCom.py new file mode 100644 index 0000000000..d3a94be466 --- /dev/null +++ b/module/plugins/accounts/File4SafeCom.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- + +from ..internal.XFSAccount import XFSAccount + + +class File4SafeCom(XFSAccount): + __name__ = "File4SafeCom" + __type__ = "account" + __version__ = "0.11" + __status__ = "testing" + + __description__ = """File4Safe.com account plugin""" + __license__ = "GPLv3" + __authors__ = [("stickell", "l.stickell@yahoo.it")] + + PLUGIN_DOMAIN = "file4safe.com" + + LOGIN_FAIL_PATTERN = r'input_login' diff --git a/module/plugins/accounts/FileStoreTo.py b/module/plugins/accounts/FileStoreTo.py new file mode 100644 index 0000000000..bb23cc94da --- /dev/null +++ b/module/plugins/accounts/FileStoreTo.py @@ -0,0 +1,45 @@ +# -*- coding: utf-8 -*- + +import re +import time + +from ..internal.Account import Account + + +class FileStoreTo(Account): + __name__ = "FileStoreTo" + __type__ = "account" + __version__ = "0.02" + __status__ = "testing" + + __description__ = """Filestore.to account plugin""" + __license__ = "GPLv3" + __authors__ = [("GammaC0de", "nitzo2001[AT]yahoo[DOT]com")] + + LOGIN_URL = "https://filestore.to/login" + VALID_UNTIL_PATTERN = r'Premium-Status

([\d\.]+? - [\d:]+)' + + def grab_info(self, user, password, data): + premium = False + validuntil = None + + html = self.load("https://filestore.to/konto") + m = re.search(self.VALID_UNTIL_PATTERN, html) + if m is not None: + validuntil = time.mktime(time.strptime(m.group(1), "%d.%m.%Y - %H:%M")) + premium = validuntil > time.time() + + return {"validuntil": validuntil, "trafficleft": -1, "premium": premium} + + def signin(self, user, password, data): + html = self.load(self.LOGIN_URL) + if 'href="logout"' in html: + self.skip_login() + + else: + html = self.load( + self.LOGIN_URL, + post={"Email": user, "Password": password, "Aktion": "Login"}, + ) + if 'href="logout"' not in html: + self.fail_login() diff --git a/module/plugins/accounts/FileboomMe.py b/module/plugins/accounts/FileboomMe.py new file mode 100644 index 0000000000..fdd4e76ef5 --- /dev/null +++ b/module/plugins/accounts/FileboomMe.py @@ -0,0 +1,168 @@ +# -*- coding: utf-8 -*- + +import re + +from module.network.HTTPRequest import BadHeader +from module.PyFile import PyFile + +from ..captcha.ReCaptcha import ReCaptcha +from ..internal.Account import Account +from ..internal.Captcha import Captcha +from ..internal.misc import json + + +class FileboomMe(Account): + __name__ = "FileboomMe" + __type__ = "account" + __version__ = "0.04" + __status__ = "testing" + + __description__ = """Fileboom.me account plugin""" + __license__ = "GPLv3" + __authors__ = [("GammaC0de", "nitzo2001[AT]yahoo[DOT]com")] + + RECAPTCHA_KEY = "6LcYcN0SAAAAABtMlxKj7X0hRxOY8_2U86kI1vbb" + + API_URL = "https://fileboom.me/api/v2/" + #: Actually this is Keep2ShareCc API, see https://github.com/keep2share/api + + def api_response(self, method, **kwargs): + html = self.load(self.API_URL + method, + post=json.dumps(kwargs)) + return json.loads(html) + + def grab_info(self, user, password, data): + json_data = self.api_response("AccountInfo", auth_token=data['token']) + + return {'validuntil': json_data['account_expires'], + 'trafficleft': json_data['available_traffic'], + 'premium': True if json_data['account_expires'] else False} + + def signin(self, user, password, data): + if 'token' in data: + try: + json_data = self.api_response("test", auth_token=data['token']) + + except BadHeader, e: + if e.code == 403: #: Session expired + pass + + else: + raise + else: + self.skip_login() + + try: + json_data = self.api_response("login", username=user, password=password) + + except BadHeader, e: + if e.code == 406: #: Captcha needed + # dummy pyfile + pyfile = PyFile(self.pyload.files, -1, "https://fileboom.me", "https://fileboom.me", 0, 0, "", self.classname, -1, -1) + pyfile.plugin = self + + errors = [json.loads(m.group(0)).get('errorCode', 0) for m in re.finditer(r'{[^}]+}', e.content)] + if 33 in errors: #: ERROR_RE_CAPTCHA_REQUIRED + #: Recaptcha + self.captcha = ReCaptcha(pyfile) + for i in range(10): + json_data = self.api_response("RequestReCaptcha") + if json_data['code'] != 200: + self.log_error(_("Request reCAPTCHA API failed")) + self.fail_login(_("Request reCAPTCHA API failed")) + + re_captcha_response = self.captcha.challenge(self.RECAPTCHA_KEY, version="2js", secure_token=False) + try: + json_data = self.api_response("login", + username=user, + password=password, + re_captcha_challenge=json_data['challenge'], + re_captcha_response=re_captcha_response) + + except BadHeader, e: + if e.code == 406: + errors = [json.loads(m.group(0)).get('errorCode', 0) for m in re.finditer(r'{[^}]+}', e.content)] + if 31 in errors: #: ERROR_CAPTCHA_INVALID + self.captcha.invalid() + continue + + else: + self.log_error(e.content) + self.fail_login(e.content) + + else: + self.log_error(e.content) + self.fail_login(e.content) + + else: + self.captcha.correct() + data['token'] = json_data['auth_token'] + break + + else: + self.log_error(_("Max captcha retries reached")) + self.fail_login(_("Max captcha retries reached")) + + elif 30 in errors: #: ERROR_CAPTCHA_REQUIRED + #: Normal captcha + self.captcha = Captcha(pyfile) + for i in range(10): + json_data = self.api_response("RequestCaptcha") + if json_data['code'] != 200: + self.log_error(_("Request captcha API failed")) + self.fail_login(_("Request captcha API failed")) + + captcha_response = self.captcha.decrypt(json_data['captcha_url']) + try: + json_data = self.api_response("login", + username=user, + password=password, + captcha_challenge=json_data['challenge'], + captcha_response=captcha_response) + + except BadHeader, e: + if e.code == 406: + errors = [json.loads(m.group(0)).get('errorCode', 0) for m in re.finditer(r'{[^}]+}', e.content)] + if 31 in errors: #: ERROR_CAPTCHA_INVALID + self.captcha.invalid() + continue + + else: + self.log_error(e.content) + self.fail_login(e.content) + + else: + self.log_error(e.content) + self.fail_login(e.content) + + else: + self.captcha.correct() + data['token'] = json_data['auth_token'] + break + + else: + self.log_error(_("Max captcha retries reached")) + self.fail_login(_("Max captcha retries reached")) + + else: + self.log_error(e.content) + self.fail_login(e.content) + + else: + self.log_error(e.content) + self.fail_login(e.content) + + else: + #: No captcha + data['token'] = json_data['auth_token'] + + """ + @NOTE: below are methods + necessary for captcha to work with account plugins + """ + def check_status(self): + pass + + def retry_captcha(self, attemps=10, wait=1, msg=_("Max captcha retries reached")): + self.captcha.invalid() + self.fail_login(msg=_("Invalid captcha")) diff --git a/module/plugins/accounts/FilecloudIo.py b/module/plugins/accounts/FilecloudIo.py index 93ae020067..709054f3c3 100644 --- a/module/plugins/accounts/FilecloudIo.py +++ b/module/plugins/accounts/FilecloudIo.py @@ -1,72 +1,58 @@ # -*- 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 . - - @author: zoidberg -""" - -from module.plugins.Account import Account -from module.common.json_layer import json_loads +from ..internal.Account import Account +from ..internal.misc import json, set_cookie class FilecloudIo(Account): __name__ = "FilecloudIo" - __version__ = "0.02" __type__ = "account" - __description__ = """FilecloudIo account plugin""" - __author_name__ = ("zoidberg", "stickell") - __author_mail__ = ("zoidberg@mujmail.cz", "l.stickell@yahoo.it") + __version__ = "0.13" + __status__ = "testing" - def loadAccountInfo(self, user, req): - # It looks like the first API request always fails, so we retry 5 times, it should work on the second try - for _ in range(5): - rep = req.load("https://secure.filecloud.io/api-fetch_apikey.api", - post={"username": user, "password": self.accounts[user]['password']}) - rep = json_loads(rep) - if rep['status'] == 'ok': + __description__ = """FilecloudIo account plugin""" + __license__ = "GPLv3" + __authors__ = [("zoidberg", "zoidberg@mujmail.cz"), + ("stickell", "l.stickell@yahoo.it")] + + def grab_info(self, user, password, data): + #: It looks like the first API request always fails, so we retry 5 times, it should work on the second try + for _i in range(5): + rep = self.load("https://secure.filecloud.io/api-fetch_apikey.api", + post={'username': user, 'password': password}) + rep = json.loads(rep) + if rep['status'] == "ok": break - elif rep['status'] == 'error' and rep['message'] == 'no such user or wrong password': - self.logError("Wrong username or password") - return {"valid": False, "premium": False} + elif rep['status'] == "error" and rep['message'] == "no such user or wrong password": + self.log_error(_("Wrong username or password")) + return {'valid': False, 'premium': False} else: - return {"premium": False} + return {'premium': False} akey = rep['akey'] - self.accounts[user]['akey'] = akey # Saved for hoster plugin - rep = req.load("http://api.filecloud.io/api-fetch_account_details.api", - post={"akey": akey}) - rep = json_loads(rep) + self.accounts[user]['akey'] = akey #: Saved for hoster plugin + rep = self.load("http://api.filecloud.io/api-fetch_account_details.api", + post={'akey': akey}) + rep = json.loads(rep) if rep['is_premium'] == 1: - return {"validuntil": int(rep["premium_until"]), "trafficleft": -1} + return {'validuntil': float( + rep['premium_until']), 'trafficleft': -1} else: - return {"premium": False} + return {'premium': False} - def login(self, user, data, req): - req.cj.setCookie("secure.filecloud.io", "lang", "en") - html = req.load('https://secure.filecloud.io/user-login.html') + def signin(self, user, password, data): + set_cookie(self.req.cj, "secure.filecloud.io", "lang", "en") + html = self.load('https://secure.filecloud.io/user-login.html') if not hasattr(self, "form_data"): self.form_data = {} - self.form_data["username"] = user - self.form_data["password"] = data['password'] + self.form_data['username'] = user + self.form_password = password - html = req.load('https://secure.filecloud.io/user-login_p.html', - post=self.form_data, - multipart=True) + html = self.load('https://secure.filecloud.io/user-login_p.html', + post=self.form_data) - self.logged_in = True if "you have successfully logged in - filecloud.io" in html else False - self.form_data = {} + if "you have successfully logged in" not in html: + self.fail_login() diff --git a/module/plugins/accounts/FilefactoryCom.py b/module/plugins/accounts/FilefactoryCom.py index 04ba0ea869..883afb40fe 100644 --- a/module/plugins/accounts/FilefactoryCom.py +++ b/module/plugins/accounts/FilefactoryCom.py @@ -1,59 +1,53 @@ # -*- coding: utf-8 -*- -############################################################################ -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU Affero 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 Affero General Public License for more details. # -# # -# You should have received a copy of the GNU Affero General Public License # -# along with this program. If not, see . # -############################################################################ - import re -from time import mktime, strptime - -from pycurl import REFERER +import time -from module.plugins.Account import Account +from ..internal.Account import Account class FilefactoryCom(Account): __name__ = "FilefactoryCom" - __version__ = "0.14" __type__ = "account" - __description__ = """filefactory.com account plugin""" - __author_name__ = ("zoidberg", "stickell") - __author_mail__ = ("zoidberg@mujmail.cz", "l.stickell@yahoo.it") + __version__ = "0.23" + __status__ = "testing" - VALID_UNTIL_PATTERN = r'Premium valid until: (?P\d{1,2})\w{1,2} (?P\w{3}), (?P\d{4})' + __description__ = """Filefactory.com account plugin""" + __license__ = "GPLv3" + __authors__ = [("zoidberg", "zoidberg@mujmail.cz"), + ("stickell", "l.stickell@yahoo.it"), + ("GammaC0de", "nitzo2001[AT]yahoo[DOT]com")] - def loadAccountInfo(self, user, req): - html = req.load("http://www.filefactory.com/account/") + VALID_UNTIL_PATTERN = r'Premium valid until: (?P\d{1,2})\w{1,2} (?P\w{3}), (?P\d{4})' + + def grab_info(self, user, password, data): + html = self.load("http://www.filefactory.com/account/") m = re.search(self.VALID_UNTIL_PATTERN, html) - if m: + if m is not None: premium = True - validuntil = re.sub(self.VALID_UNTIL_PATTERN, '\g \g \g', m.group(0)) - validuntil = mktime(strptime(validuntil, "%d %b %Y")) + validuntil = re.sub( + self.VALID_UNTIL_PATTERN, + '\g \g \g', + m.group(0)) + validuntil = time.mktime(time.strptime(validuntil, "%d %b %Y")) + else: premium = False validuntil = -1 - return {"premium": premium, "trafficleft": -1, "validuntil": validuntil} + return {'premium': premium, 'trafficleft': - + 1, 'validuntil': validuntil} - def login(self, user, data, req): - req.http.c.setopt(REFERER, "http://www.filefactory.com/member/login.php") + def signin(self, user, password, data): + html = self.load("https://www.filefactory.com/member/signin.php") + if "/member/signout.php" in html: + self.skip_login() - html = req.load("http://www.filefactory.com/member/signin.php", post={ - "loginEmail": user, - "loginPassword": data["password"], - "Submit": "Sign In"}) + html = self.load("https://www.filefactory.com/member/signin.php", + post={'loginEmail': user, + 'loginPassword': password, + 'Submit': "Sign In"}) - if req.lastEffectiveURL != "http://www.filefactory.com/account/": - self.wrongPassword() + if "/member/signout.php" not in html: + self.fail_login() diff --git a/module/plugins/accounts/FilejokerNet.py b/module/plugins/accounts/FilejokerNet.py new file mode 100644 index 0000000000..cbb35e14f0 --- /dev/null +++ b/module/plugins/accounts/FilejokerNet.py @@ -0,0 +1,49 @@ +# -*- coding: utf-8 -*- + +import time + +from ..internal.Account import Account +from ..internal.misc import json + + +class FilejokerNet(Account): + __name__ = "FilejokerNet" + __type__ = "account" + __version__ = "0.04" + __status__ = "testing" + + __description__ = """Filejoker.net account plugin""" + __license__ = "GPLv3" + __authors__ = [("GammaC0de", "nitzo2001[AT]yahoo[DOT]com")] + + API_URL = "https://filejoker.net/zapi" + + + def api_response(self, op, **kwargs): + args = {'op': op} + args.update(kwargs) + return json.loads(self.load(self.API_URL, get=args)) + + def grab_info(self, user, password, data): + res = self.api_response("my_account", session=data['session']) + premium_expire = res.get('usr_premium_expire') + + validuntil = time.mktime(time.strptime(premium_expire, "%Y-%m-%d %H:%M:%S")) if premium_expire else -1 + trafficleft = int(res['traffic_left']) * 1024 ** 2 if 'traffic_left' in res else None + premium = bool(premium_expire) + + return {'validuntil': validuntil, + 'trafficleft': trafficleft, + 'premium': premium} + + + def signin(self, user, password, data): + session = data.get('session') + if session and 'error' not in self.api_response("my_account", session=session): + self.skip_login() + + res = self.api_response("login", **{'email': user, 'pass': password}) + if 'error'in res: + self.fail_login() + + data['session'] = res['session'] diff --git a/module/plugins/accounts/FilejungleCom.py b/module/plugins/accounts/FilejungleCom.py deleted file mode 100644 index 2f2a6012d3..0000000000 --- a/module/plugins/accounts/FilejungleCom.py +++ /dev/null @@ -1,62 +0,0 @@ -# -*- 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 . - - @author: zoidberg -""" - -import re -from time import mktime, strptime - -from module.plugins.Account import Account - - -class FilejungleCom(Account): - __name__ = "FilejungleCom" - __version__ = "0.11" - __type__ = "account" - __description__ = """filejungle.com account plugin""" - __author_name__ = ("zoidberg") - __author_mail__ = ("zoidberg@mujmail.cz") - - login_timeout = 60 - - URL = "http://filejungle.com/" - TRAFFIC_LEFT_PATTERN = r'"/extend_premium\.php">Until (\d+ [A-Za-z]+ \d+)' - - def loadAccountInfo(self, user, req): - html = req.load(self.URL + "dashboard.php") - found = re.search(self.TRAFFIC_LEFT_PATTERN, html) - if found: - premium = True - validuntil = mktime(strptime(found.group(1), "%d %b %Y")) - else: - premium = False - validuntil = -1 - - return {"premium": premium, "trafficleft": -1, "validuntil": validuntil} - - def login(self, user, data, req): - html = req.load(self.URL + "login.php", post={ - "loginUserName": user, - "loginUserPassword": data["password"], - "loginFormSubmit": "Login", - "recaptcha_challenge_field": "", - "recaptcha_response_field": "", - "recaptcha_shortencode_field": ""}) - - if re.search(self.LOGIN_FAILED_PATTERN, html): - self.wrongPassword() diff --git a/module/plugins/accounts/FileomCom.py b/module/plugins/accounts/FileomCom.py new file mode 100644 index 0000000000..1329e4e059 --- /dev/null +++ b/module/plugins/accounts/FileomCom.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- + +from ..internal.XFSAccount import XFSAccount + + +class FileomCom(XFSAccount): + __name__ = "FileomCom" + __type__ = "account" + __version__ = "0.07" + __status__ = "testing" + + __description__ = """Fileom.com account plugin""" + __license__ = "GPLv3" + __authors__ = [("Walter Purcaro", "vuolter@gmail.com")] + + PLUGIN_DOMAIN = "fileom.com" diff --git a/module/plugins/accounts/FilerNet.py b/module/plugins/accounts/FilerNet.py index 45ce5ab379..749e17454d 100644 --- a/module/plugins/accounts/FilerNet.py +++ b/module/plugins/accounts/FilerNet.py @@ -1,62 +1,66 @@ # -*- coding: utf-8 -*- -############################################################################ -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU Affero 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 Affero General Public License for more details. # -# # -# You should have received a copy of the GNU Affero General Public License # -# along with this program. If not, see . # -############################################################################ - -import re +import datetime import time -from module.plugins.Account import Account -from module.utils import parseFileSize +from module.network.HTTPRequest import BadHeader + +from ..internal.Account import Account +from ..internal.misc import json class FilerNet(Account): __name__ = "FilerNet" - __version__ = "0.01" __type__ = "account" + __version__ = "0.16" + __status__ = "testing" + __description__ = """Filer.net account plugin""" - __author_name__ = ("stickell") - __author_mail__ = ("l.stickell@yahoo.it") + __license__ = "GPLv3" + __authors__ = [("stickell", "l.stickell@yahoo.it"), + ("GammaC0de", "nitzo2001[AT]yahoo[DOT]com")] - TOKEN_PATTERN = r'_csrf_token" value="([^"]+)" />' - WALID_UNTIL_PATTERN = r"Der Premium-Zugang ist gültig bis (.+)\.\s*" - TRAFFIC_PATTERN = r'Traffic\s*([^<]+)' - FREE_PATTERN = r'Account Status\s*\s*Free' + # See https://filer.net/api + API_URL = "https://filer.net/api/" - def loadAccountInfo(self, user, req): - self.html = req.load("https://filer.net/profile") + def api_request(self, method, **kwargs): + try: + json_data = self.load(self.API_URL + method, post=kwargs) + except BadHeader as exc: + json_data = exc.content - # Free user - if re.search(self.FREE_PATTERN, self.html): - return {"premium": False, "validuntil": None, "trafficleft": None} + return json.loads(json_data) + + def grab_info(self, user, password, data): + api_data = self.api_request("user/account") + + premium = api_data["status"] == "Premium" - until = re.search(self.WALID_UNTIL_PATTERN, self.html) - traffic = re.search(self.TRAFFIC_PATTERN, self.html) - if until and traffic: - validuntil = int(time.mktime(time.strptime(until.group(1), "%d.%m.%Y %H:%M:%S"))) - trafficleft = parseFileSize(traffic.group(1)) / 1024 - return {"premium": True, "validuntil": validuntil, "trafficleft": trafficleft} - else: - self.logError('Unable to retrieve account information - Plugin may be out of date') + #: Free user + if premium is False: return {"premium": False, "validuntil": None, "trafficleft": None} - def login(self, user, data, req): - self.html = req.load("https://filer.net/login") - token = re.search(self.TOKEN_PATTERN, self.html).group(1) - self.html = req.load("https://filer.net/login_check", - post={"_username": user, "_password": data["password"], - "_remember_me": "on", "_csrf_token": token, "_target_path": "https://filer.net/"}) - if 'Logout' not in self.html: - self.wrongPassword() + dt_part = api_data["premiumUntil"][:19] + tz_part = api_data["premiumUntil"][19:] + local_dt = datetime.datetime.strptime(dt_part, "%Y-%m-%d %H:%M:%S") + + # Parse offset: e.g., "+02:00" -> timedelta(hours=2) + sign = 1 if tz_part[0] == '+' else -1 + hours, minutes = map(int, tz_part[1:].split(':')) + tz_offset = datetime.timedelta(hours=sign * hours, minutes=sign * minutes) + utc_dt = local_dt - tz_offset + + validuntil = time.mktime(utc_dt.timetuple()) + trafficleft = self.parse_traffic(api_data["traffic"]) + + return {"premium": premium, "validuntil": validuntil, "trafficleft": trafficleft} + + def signin(self, user, password, data): + api_data = self.api_request("user/account") + if "message" not in api_data: + self.skip_login() + + api_data = self.api_request("user/login", email=user, password=password) + if api_data.get("message", "") != "Login successful": + self.log_error(api_data["message"]) + self.fail_login() diff --git a/module/plugins/accounts/FilerioCom.py b/module/plugins/accounts/FilerioCom.py index 8b0b5f54fc..2ccde5e249 100644 --- a/module/plugins/accounts/FilerioCom.py +++ b/module/plugins/accounts/FilerioCom.py @@ -1,13 +1,16 @@ # -*- coding: utf-8 -*- -from module.plugins.internal.XFSPAccount import XFSPAccount +from ..internal.XFSAccount import XFSAccount -class FilerioCom(XFSPAccount): + +class FilerioCom(XFSAccount): __name__ = "FilerioCom" - __version__ = "0.01" __type__ = "account" + __version__ = "0.08" + __status__ = "testing" + __description__ = """FileRio.in account plugin""" - __author_name__ = ("zoidberg") - __author_mail__ = ("zoidberg@mujmail.cz") + __license__ = "GPLv3" + __authors__ = [("zoidberg", "zoidberg@mujmail.cz")] - MAIN_PAGE = "http://filerio.in/" + PLUGIN_DOMAIN = "filerio.in" diff --git a/module/plugins/accounts/FilesMailRu.py b/module/plugins/accounts/FilesMailRu.py index ea976bd447..bd429cdee4 100644 --- a/module/plugins/accounts/FilesMailRu.py +++ b/module/plugins/accounts/FilesMailRu.py @@ -1,42 +1,29 @@ # -*- 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 . - - @author: RaNaN -""" - -from module.plugins.Account import Account +from ..internal.Account import Account class FilesMailRu(Account): __name__ = "FilesMailRu" - __version__ = "0.1" __type__ = "account" - __description__ = """filesmail.ru account plugin""" - __author_name__ = ("RaNaN") - __author_mail__ = ("RaNaN@pyload.org") + __version__ = "0.18" + __status__ = "testing" + + __description__ = """Filesmail.ru account plugin""" + __license__ = "GPLv3" + __authors__ = [("RaNaN", "RaNaN@pyload.net")] - def loadAccountInfo(self, user, req): - return {"validuntil": None, "trafficleft": None} + def grab_info(self, user, password, data): + return {'validuntil': None, 'trafficleft': None} - def login(self, user, data, req): + def signin(self, user, password, data): user, domain = user.split("@") - page = req.load("http://swa.mail.ru/cgi-bin/auth", None, - {"Domain": domain, "Login": user, "Password": data['password'], - "Page": "http://files.mail.ru/"}, cookies=True) + html = self.load("https://swa.mail.ru/cgi-bin/auth", + post={'Domain': domain, + 'Login': user, + 'Password': password, + 'Page': "http://files.mail.ru/"}) - if "Неверное имя пользователя или пароль" in page: # @TODO seems not to work - self.wrongPassword() + if "Неверное имя пользователя или пароль" in html: + self.fail_login() diff --git a/module/plugins/accounts/FileserveCom.py b/module/plugins/accounts/FileserveCom.py deleted file mode 100644 index d4056891a3..0000000000 --- a/module/plugins/accounts/FileserveCom.py +++ /dev/null @@ -1,58 +0,0 @@ -# -*- 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 . - - @author: mkaay -""" - -from time import mktime, strptime - -from module.plugins.Account import Account -from module.common.json_layer import json_loads - - -class FileserveCom(Account): - __name__ = "FileserveCom" - __version__ = "0.2" - __type__ = "account" - __description__ = """fileserve.com account plugin""" - __author_name__ = ("mkaay") - __author_mail__ = ("mkaay@mkaay.de") - - def loadAccountInfo(self, user, req): - data = self.getAccountData(user) - - page = req.load("http://app.fileserve.com/api/login/", post={"username": user, "password": data["password"], - "submit": "Submit+Query"}) - res = json_loads(page) - - if res["type"] == "premium": - validuntil = mktime(strptime(res["expireTime"], "%Y-%m-%d %H:%M:%S")) - return {"trafficleft": res["traffic"], "validuntil": validuntil} - else: - return {"premium": False, "trafficleft": None, "validuntil": None} - - def login(self, user, data, req): - page = req.load("http://app.fileserve.com/api/login/", post={"username": user, "password": data["password"], - "submit": "Submit+Query"}) - res = json_loads(page) - - if not res["type"]: - self.wrongPassword() - - #login at fileserv page - req.load("http://www.fileserve.com/login.php", - post={"loginUserName": user, "loginUserPassword": data["password"], "autoLogin": "checked", - "loginFormSubmit": "Login"}) diff --git a/module/plugins/accounts/FiregetCom.py b/module/plugins/accounts/FiregetCom.py new file mode 100644 index 0000000000..d50c5400bd --- /dev/null +++ b/module/plugins/accounts/FiregetCom.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- + +from ..internal.XFSAccount import XFSAccount + + +class FiregetCom(XFSAccount): + __name__ = "FiregetCom" + __type__ = "account" + __version__ = "0.01" + __status__ = "testing" + + __description__ = """fireget.com account plugin""" + __license__ = "GPLv3" + + PLUGIN_DOMAIN = "fireget.com" + PLUGIN_URL = "https://fireget.com/" diff --git a/module/plugins/accounts/FourSharedCom.py b/module/plugins/accounts/FourSharedCom.py index 69a465671a..0f8d48c7b6 100644 --- a/module/plugins/accounts/FourSharedCom.py +++ b/module/plugins/accounts/FourSharedCom.py @@ -1,49 +1,33 @@ # -*- 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 . - - @author: zoidberg -""" - -from module.plugins.Account import Account -from module.common.json_layer import json_loads +from ..internal.Account import Account +from ..internal.misc import set_cookie class FourSharedCom(Account): __name__ = "FourSharedCom" - __version__ = "0.01" __type__ = "account" - __description__ = """FourSharedCom account plugin""" - __author_name__ = ("zoidberg") - __author_mail__ = ("zoidberg@mujmail.cz") - - def loadAccountInfo(self, user, req): - #fixme - return {"validuntil": -1, "trafficleft": -1, "premium": False} - - def login(self, user, data, req): - req.cj.setCookie("www.4shared.com", "4langcookie", "en") - response = req.load('http://www.4shared.com/login', - post={"login": user, - "password": data['password'], - "remember": "false", - "doNotRedirect": "true"}) - self.logDebug(response) - response = json_loads(response) - - if not "ok" in response or response['ok'] != True: - if "rejectReason" in response and response['rejectReason'] != True: - self.logError(response['rejectReason']) - self.wrongPassword() + __version__ = "0.13" + __status__ = "testing" + + __description__ = """FourShared.com account plugin""" + __license__ = "GPLv3" + __authors__ = [("zoidberg", "zoidberg@mujmail.cz"), + ("stickell", "l.stickell@yahoo.it")] + + def grab_info(self, user, password, data): + #: Free mode only for now + return {'premium': False} + + def signin(self, user, password, data): + set_cookie(self.req.cj, "4shared.com", "4langcookie", "en") + + res = self.load("https://www.4shared.com/web/login", + post={'login': user, + 'password': password, + 'remember': "on", + '_remember': "on", + 'returnTo': "http://www.4shared.com/account/home.jsp"}) + + if 'Please log in to access your 4shared account' in res: + self.fail_login() diff --git a/module/plugins/accounts/FreakshareCom.py b/module/plugins/accounts/FreakshareCom.py deleted file mode 100644 index cdf45114aa..0000000000 --- a/module/plugins/accounts/FreakshareCom.py +++ /dev/null @@ -1,53 +0,0 @@ -# -*- 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 . - - @author: RaNaN -""" -import re -from time import strptime, mktime - -from module.plugins.Account import Account - - -class FreakshareCom(Account): - __name__ = "FreakshareCom" - __version__ = "0.1" - __type__ = "account" - __description__ = """freakshare.com account plugin""" - __author_name__ = ("RaNaN") - __author_mail__ = ("RaNaN@pyload.org") - - def loadAccountInfo(self, user, req): - page = req.load("http://freakshare.com/") - - validuntil = r"ltig bis:\s*([0-9 \-:.]+)" - validuntil = re.search(validuntil, page, re.MULTILINE) - validuntil = validuntil.group(1).strip() - validuntil = mktime(strptime(validuntil, "%d.%m.%Y - %H:%M")) - - traffic = r"Traffic verbleibend:\s*([^<]+)" - traffic = re.search(traffic, page, re.MULTILINE) - traffic = traffic.group(1).strip() - traffic = self.parseTraffic(traffic) - - return {"validuntil": validuntil, "trafficleft": traffic} - - def login(self, user, data, req): - page = req.load("http://freakshare.com/login.html", None, - {"submit": "Login", "user": user, "pass": data['password']}, cookies=True) - - if "Falsche Logindaten!" in page or "Wrong Username or Password!" in page: - self.wrongPassword() diff --git a/module/plugins/accounts/FshareVn.py b/module/plugins/accounts/FshareVn.py index 0d49b137d9..576f221763 100644 --- a/module/plugins/accounts/FshareVn.py +++ b/module/plugins/accounts/FshareVn.py @@ -1,74 +1,102 @@ # -*- 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. +import pycurl +from module.network.HTTPRequest import BadHeader - 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. +from ..internal.Account import Account +from ..internal.misc import json - You should have received a copy of the GNU General Public License - along with this program; if not, see . - @author: zoidberg -""" +class FshareVn(Account): + __name__ = "FshareVn" + __type__ = "account" + __version__ = "0.28" + __status__ = "testing" -from time import mktime, strptime -from pycurl import REFERER -import re + __description__ = """Fshare.vn account plugin""" + __license__ = "GPLv3" + __authors__ = [("zoidberg", "zoidberg@mujmail.cz"), + ("stickell", "l.stickell@yahoo.it"), + ("GammaC0de", "nitzo2001[AT]yahoo[DOT]com")] -from module.plugins.Account import Account + API_KEY = "dMnqMMZMUnN5YpvKENaEhdQQ5jxDqddt" + API_USERAGENT = "pyLoad-B1RS5N" + API_URL = "https://api.fshare.vn/api/" + # See https://www.fshare.vn/api-doc + def api_request(self, method, session_id=None, **kwargs): + self.req.http.c.setopt(pycurl.USERAGENT, self.API_USERAGENT) -class FshareVn(Account): - __name__ = "FshareVn" - __version__ = "0.06" - __type__ = "account" - __description__ = """fshare.vn account plugin""" - __author_name__ = ("zoidberg", "stickell") - __author_mail__ = ("zoidberg@mujmail.cz", "l.stickell@yahoo.it") + if len(kwargs) == 0: + json_data = self.load(self.API_URL + method, + cookies=[("fshare.vn", 'session_id', session_id)] if session_id else True) + + else: + self.req.http.c.setopt(pycurl.HTTPHEADER, ["Content-Type: application/json"]) + json_data = self.load(self.API_URL + method, + post=json.dumps(kwargs), + cookies=[("fshare.vn", 'session_id', session_id)] if session_id else True) + + return json.loads(json_data) - VALID_UNTIL_PATTERN = ur'
Thời hạn dùng:
\s*
([^<]+)
' - LIFETIME_PATTERN = ur'
Lần đăng nhập trước:
\s*
[^<]+
' - TRAFFIC_LEFT_PATTERN = ur'
Tổng Dung Lượng Tài Khoản
\s*]*>([0-9.]+) ([kKMG])B' - DIRECT_DOWNLOAD_PATTERN = ur']*)[^>]*/>Kích hoạt download trực tiếp' + def grab_info(self, user, password, data): + trafficleft = None + premium = False - def loadAccountInfo(self, user, req): - self.html = req.load("http://www.fshare.vn/account_info.php", decode=True) + api_data = self.api_request("user/get", session_id=data['session_id']) - if re.search(self.LIFETIME_PATTERN, self.html): - self.logDebug("Lifetime membership detected") - trafficleft = self.getTrafficLeft() - return {"validuntil": -1, "trafficleft": trafficleft, "premium": True} + expire_vip = unicode(api_data.get("expire_vip", "")) #: isnumeric() is only available for unicode strings + validuntil = float(expire_vip) if expire_vip.isnumeric() else None - found = re.search(self.VALID_UNTIL_PATTERN, self.html) - if found: + if validuntil: premium = True - validuntil = mktime(strptime(found.group(1), '%I:%M:%S %p %d-%m-%Y')) - trafficleft = self.getTrafficLeft() - else: - premium = False - validuntil = None - trafficleft = None - return {"validuntil": validuntil, "trafficleft": trafficleft, "premium": premium} + return {'validuntil': validuntil, + 'trafficleft': trafficleft, + 'premium': premium} + + def signin(self, user, password, data): + user = user.lower() + + fshare_session_cache = self.db.retrieve("fshare_session_cache") or {} + if user in fshare_session_cache: + data['token'] = fshare_session_cache[user]['token'] + data['session_id'] = fshare_session_cache[user]['session_id'] + + try: + api_data = self.api_request("user/get", session_id=data['session_id']) + + except BadHeader, e: + if e.code == 401: + del fshare_session_cache[user] + self.db.store("fshare_session_cache", fshare_session_cache) + + if api_data.get('email', "").lower() == user: + self.skip_login() + + else: + del fshare_session_cache[user] + self.db.store("fshare_session_cache", fshare_session_cache) + + data['token'] = None + data['session_id'] = None - def login(self, user, data, req): - req.http.c.setopt(REFERER, "https://www.fshare.vn/login.php") + try: + api_data = self.api_request("user/login", + app_key=self.API_KEY, + user_email=user, + password=password) + except BadHeader, e: + self.log_error(_("Login failed, error code %s") % e.code) + self.fail_login() - self.html = req.load('https://www.fshare.vn/login.php', post={ - "login_password": data['password'], - "login_useremail": user, - "url_refe": "https://www.fshare.vn/login.php" - }, referer=True, decode=True) + if api_data['code'] != 200: + self.log_error(api_data['msg']) + self.fail_login() - if not 'VIP b + 'premium': rc['status'] == 'premium'} + + def signin(self, user, password, data): + data['passwd_sha256'] = sha256(password.encode('ascii')).hexdigest() + self.req.http.c.setopt( + pycurl.USERAGENT, "pyLoad/{}".format(self.pyload.version).encode() + ) diff --git a/module/plugins/accounts/HellshareCz.py b/module/plugins/accounts/HellshareCz.py index 4718ade996..867348eab2 100644 --- a/module/plugins/accounts/HellshareCz.py +++ b/module/plugins/accounts/HellshareCz.py @@ -1,89 +1,85 @@ # -*- 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 . - - @author: zoidberg -""" - import re import time -from module.plugins.Account import Account +from ..internal.Account import Account class HellshareCz(Account): __name__ = "HellshareCz" - __version__ = "0.14" __type__ = "account" - __description__ = """hellshare.cz account plugin""" - __author_name__ = ("zoidberg") - __author_mail__ = ("zoidberg@mujmail.cz") + __version__ = "0.26" + __status__ = "testing" + + __description__ = """Hellshare.cz account plugin""" + __license__ = "GPLv3" + __authors__ = [("zoidberg", "zoidberg@mujmail.cz")] CREDIT_LEFT_PATTERN = r'.*?bgcolor="#EFEFEF">(?P.*?)' + PATTERN_DL_LINK_PAGE = r'"(dl_links_\d+_\d+\.html)"' + PATTERN_REDIRECT_LINKS = r'disabled\'" href="(.*)" id' + LIST_PWDIGNORE = ["Kein Passwort", "-"] + + def decrypt(self, pyfile): + #: Init + self.pyfile = pyfile + self.package = pyfile.package() + + #: Decrypt and add links + pack_name, self.urls, folder_name, pack_pwd = self.decrypt_links( + self.pyfile.url) + if pack_pwd: + self.pyfile.package().password = pack_pwd + self.packages = [(pack_name, self.urls, folder_name)] + + def decrypt_links(self, url): + linklist = [] + name = self.package.name + folder = self.package.folder + password = None + + if re.match(self.PATTERN_SUPPORTED_MAIN, url, re.I): + #: Processing main page + html = self.load(url) + links = re.findall(self.PATTERN_DL_LINK_PAGE, html, re.I) + for link in links: + linklist.append("http://sexuria.com/v1/" + link) + + elif re.match(self.PATTERN_SUPPORTED_REDIRECT, url, re.I): + #: Processing direct redirect link (out.php), redirecting to main page + id = re.search( + self.PATTERN_SUPPORTED_REDIRECT, + url, + re.I).group('ID') + if id: + linklist.append( + "http://sexuria.com/v1/Pornos_Kostenlos_liebe_%s.html" % + id) + + elif re.match(self.PATTERN_SUPPORTED_CRYPT, url, re.I): + #: Extract info from main file + id = re.search(self.PATTERN_SUPPORTED_CRYPT, url, re.I).group('ID') + html = self.load( + "http://sexuria.com/v1/Pornos_Kostenlos_info_%s.html" % + id) + #: Webpage title / Package name + titledata = re.search(self.PATTERN_TITLE, html, re.I) + if not titledata: + self.log_warning("No title data found, has site changed?") + else: + title = titledata.group('TITLE').strip() + if title: + name = folder = title + self.log_debug( + "Package info found, name [%s] and folder [%s]" % + (name, folder)) + #: Password + pwddata = re.search(self.PATTERN_PASSWORD, html, re.I | re.S) + if not pwddata: + self.log_warning("No password data found, has site changed?") + else: + pwd = pwddata.group('PWD').strip() + if pwd and not (pwd in self.LIST_PWDIGNORE): + password = pwd + self.log_debug( + "Package info found, password [%s]" % + password) + + #: Process links (dl_link) + html = self.load(url) + links = re.findall(self.PATTERN_REDIRECT_LINKS, html, re.I) + if not links: + self.log_error(_("Broken for link: %s") % link) + else: + for link in links: + link = link.replace( + "http://sexuria.com/", + "http://www.sexuria.com/") + finallink = self.load(link, just_header=True)['url'] + if not finallink or ("sexuria.com/" in finallink): + self.log_error(_("Broken for link: %s") % link) + else: + linklist.append(finallink) + + #: Log result + if not linklist: + self.fail(_("Unable to extract links (maybe plugin out of date?)")) + else: + for i, link in enumerate(linklist): + self.log_debug( + "Supported link %d/%d: %s" % + (i + 1, len(linklist), link)) + + #: All done, return to caller + return name, linklist, folder, password diff --git a/module/plugins/crypter/ShSt.py b/module/plugins/crypter/ShSt.py new file mode 100644 index 0000000000..454c15e247 --- /dev/null +++ b/module/plugins/crypter/ShSt.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- + + +import pycurl + +from ..internal.Crypter import Crypter + + +class ShSt(Crypter): + __name__ = "ShSt" + __type__ = "crypter" + __version__ = "0.09" + __status__ = "testing" + + __pattern__ = r'http://sh\.st/\w+' + __config__ = [("activated", "bool", "Activated", True)] + + __description__ = """Sh.St decrypter plugin""" + __license__ = "GPLv3" + __authors__ = [("Frederik Möllers", "fred-public@posteo.de")] + + NAME_PATTERN = r'(?P<N>.+?) -' + + def decrypt(self, pyfile): + #: If we use curl as a user agent, we will get a straight redirect (no waiting!) + self.req.http.c.setopt(pycurl.USERAGENT, "curl/7.42.1") + #: Fetch the target URL + header = self.load(self.pyfile.url, just_header=True, decode=False) + target_url = header.get('location') + self.links.append(target_url) diff --git a/module/plugins/crypter/ShareLinksBiz.py b/module/plugins/crypter/ShareLinksBiz.py deleted file mode 100644 index 09ac21873a..0000000000 --- a/module/plugins/crypter/ShareLinksBiz.py +++ /dev/null @@ -1,269 +0,0 @@ -# -*- coding: utf-8 -*- - -import base64 -import binascii -import re - -from Crypto.Cipher import AES -from module.plugins.Crypter import Crypter - - -class ShareLinksBiz(Crypter): - __name__ = "ShareLinksBiz" - __type__ = "crypter" - __pattern__ = r"(?P<base>http://[\w\.]*?(share-links|s2l)\.biz)/(?P<id>_?[0-9a-z]+)(/.*)?" - __version__ = "1.13" - __description__ = """Share-Links.biz Crypter""" - __author_name__ = ("fragonib") - __author_mail__ = ("fragonib[AT]yahoo[DOT]es") - - def setup(self): - self.baseUrl = None - self.fileId = None - self.package = None - self.html = None - self.captcha = False - - def decrypt(self, pyfile): - - # Init - self.initFile(pyfile) - - # Request package - url = self.baseUrl + '/' + self.fileId - self.html = self.load(url, decode=True) - - # Unblock server (load all images) - self.unblockServer() - - # Check for protection - if self.isPasswordProtected(): - self.unlockPasswordProtection() - self.handleErrors() - - if self.isCaptchaProtected(): - self.captcha = True - self.unlockCaptchaProtection() - self.handleErrors() - - # Extract package links - package_links = [] - package_links.extend(self.handleWebLinks()) - package_links.extend(self.handleContainers()) - package_links.extend(self.handleCNL2()) - package_links = set(package_links) - - # Get package info - package_name, package_folder = self.getPackageInfo() - - # Pack - self.packages = [(package_name, package_links, package_folder)] - - def initFile(self, pyfile): - url = pyfile.url - if 's2l.biz' in url: - url = self.load(url, just_header=True)['location'] - self.baseUrl = re.search(self.__pattern__, url).group(1) - self.fileId = re.match(self.__pattern__, url).group('id') - self.package = pyfile.package() - - def isOnline(self): - if "No usable content was found" in self.html: - self.logDebug("File not found") - return False - return True - - def isPasswordProtected(self): - if re.search(r'''<form.*?id="passwordForm".*?>''', self.html): - self.logDebug("Links are protected") - return True - return False - - def isCaptchaProtected(self): - if '<map id="captchamap"' in self.html: - self.logDebug("Links are captcha protected") - return True - return False - - def unblockServer(self): - imgs = re.findall(r"(/template/images/.*?\.gif)", self.html) - for img in imgs: - self.load(self.baseUrl + img) - - def unlockPasswordProtection(self): - password = self.getPassword() - self.logDebug("Submitting password [%s] for protected links" % password) - post = {"password": password, 'login': 'Submit form'} - url = self.baseUrl + '/' + self.fileId - self.html = self.load(url, post=post, decode=True) - - def unlockCaptchaProtection(self): - # Get captcha map - captchaMap = self._getCaptchaMap() - self.logDebug("Captcha map with [%d] positions" % len(captchaMap.keys())) - - # Request user for captcha coords - m = re.search(r'<img src="/captcha.gif\?d=(.*?)&PHPSESSID=(.*?)&legend=1"', self.html) - captchaUrl = self.baseUrl + '/captcha.gif?d=%s&PHPSESSID=%s' % (m.group(1), m.group(2)) - self.logDebug("Waiting user for correct position") - coords = self.decryptCaptcha(captchaUrl, forceUser=True, imgtype="gif", result_type='positional') - self.logDebug("Captcha resolved, coords [%s]" % str(coords)) - - # Resolve captcha - href = self._resolveCoords(coords, captchaMap) - if href is None: - self.logDebug("Invalid captcha resolving, retrying") - self.invalidCaptcha() - self.setWait(5, False) - self.wait() - self.retry() - url = self.baseUrl + href - self.html = self.load(url, decode=True) - - def _getCaptchaMap(self): - mapp = {} - for m in re.finditer(r'<area shape="rect" coords="(.*?)" href="(.*?)"', self.html): - rect = eval('(' + m.group(1) + ')') - href = m.group(2) - mapp[rect] = href - return mapp - - def _resolveCoords(self, coords, captchaMap): - x, y = coords - for rect, href in captchaMap.items(): - x1, y1, x2, y2 = rect - if (x >= x1 and x <= x2) and (y >= y1 and y <= y2): - return href - - def handleErrors(self): - if "The inserted password was wrong" in self.html: - self.logDebug("Incorrect password, please set right password on 'Edit package' form and retry") - self.fail("Incorrect password, please set right password on 'Edit package' form and retry") - - if self.captcha: - if "Your choice was wrong" in self.html: - self.logDebug("Invalid captcha, retrying") - self.invalidCaptcha() - self.setWait(5) - self.wait() - self.retry() - else: - self.correctCaptcha() - - def getPackageInfo(self): - name = folder = None - - # Extract from web package header - title_re = r'<h2><img.*?/>(.*)</h2>' - m = re.search(title_re, self.html, re.DOTALL) - if m is not None: - title = m.group(1).strip() - if 'unnamed' not in title: - name = folder = title - self.logDebug("Found name [%s] and folder [%s] in package info" % (name, folder)) - - # Fallback to defaults - if not name or not folder: - name = self.package.name - folder = self.package.folder - self.logDebug("Package info not found, defaulting to pyfile name [%s] and folder [%s]" % (name, folder)) - - # Return package info - return name, folder - - def handleWebLinks(self): - package_links = [] - self.logDebug("Handling Web links") - - #@TODO: Gather paginated web links - pattern = r"javascript:_get\('(.*?)', \d+, ''\)" - ids = re.findall(pattern, self.html) - self.logDebug("Decrypting %d Web links" % len(ids)) - for i, ID in enumerate(ids): - try: - self.logDebug("Decrypting Web link %d, [%s]" % (i + 1, ID)) - dwLink = self.baseUrl + "/get/lnk/" + ID - response = self.load(dwLink) - code = re.search(r'frm/(\d+)', response).group(1) - fwLink = self.baseUrl + "/get/frm/" + code - response = self.load(fwLink) - jscode = re.search(r'<script language="javascript">\s*eval\((.*)\)\s*</script>', response, - re.DOTALL).group(1) - jscode = self.js.eval("f = %s" % jscode) - jslauncher = "window=''; parent={frames:{Main:{location:{href:''}}},location:''}; %s; parent.frames.Main.location.href" - dlLink = self.js.eval(jslauncher % jscode) - self.logDebug("JsEngine returns value [%s] for redirection link" % dlLink) - package_links.append(dlLink) - except Exception, detail: - self.logDebug("Error decrypting Web link [%s], %s" % (ID, detail)) - return package_links - - def handleContainers(self): - package_links = [] - self.logDebug("Handling Container links") - - pattern = r"javascript:_get\('(.*?)', 0, '(rsdf|ccf|dlc)'\)" - containersLinks = re.findall(pattern, self.html) - self.logDebug("Decrypting %d Container links" % len(containersLinks)) - for containerLink in containersLinks: - link = "%s/get/%s/%s" % (self.baseUrl, containerLink[1], containerLink[0]) - package_links.append(link) - return package_links - - def handleCNL2(self): - package_links = [] - self.logDebug("Handling CNL2 links") - - if '/lib/cnl2/ClicknLoad.swf' in self.html: - try: - (crypted, jk) = self._getCipherParams() - package_links.extend(self._getLinks(crypted, jk)) - except: - self.fail("Unable to decrypt CNL2 links") - return package_links - - def _getCipherParams(self): - - # Request CNL2 - code = re.search(r'ClicknLoad.swf\?code=(.*?)"', self.html).group(1) - url = "%s/get/cnl2/%s" % (self.baseUrl, code) - response = self.load(url) - params = response.split(";;") - - # Get jk - strlist = list(base64.standard_b64decode(params[1])) - strlist.reverse() - jk = ''.join(strlist) - - # Get crypted - strlist = list(base64.standard_b64decode(params[2])) - strlist.reverse() - crypted = ''.join(strlist) - - # Log and return - return crypted, jk - - def _getLinks(self, crypted, jk): - - # Get key - jreturn = self.js.eval("%s f()" % jk) - self.logDebug("JsEngine returns value [%s]" % jreturn) - key = binascii.unhexlify(jreturn) - - # Decode crypted - crypted = base64.standard_b64decode(crypted) - - # Decrypt - Key = key - IV = key - obj = AES.new(Key, AES.MODE_CBC, IV) - text = obj.decrypt(crypted) - - # Extract links - text = text.replace("\x00", "").replace("\r", "") - links = text.split("\n") - links = filter(lambda x: x != "", links) - - # Log and return - self.logDebug("Block has %d links" % len(links)) - return links diff --git a/module/plugins/crypter/ShareRapidComFolder.py b/module/plugins/crypter/ShareRapidComFolder.py deleted file mode 100644 index 951c09d456..0000000000 --- a/module/plugins/crypter/ShareRapidComFolder.py +++ /dev/null @@ -1,15 +0,0 @@ -# -*- coding: utf-8 -*- - -from module.plugins.internal.SimpleCrypter import SimpleCrypter - - -class ShareRapidComFolder(SimpleCrypter): - __name__ = "ShareRapidComFolder" - __type__ = "crypter" - __pattern__ = r"http://(?:www\.)?((share(-?rapid\.(biz|com|cz|info|eu|net|org|pl|sk)|-(central|credit|free|net)\.cz|-ms\.net)|(s-?rapid|rapids)\.(cz|sk))|(e-stahuj|mediatack|premium-rapidshare|rapidshare-premium|qiuck)\.cz|kadzet\.com|stahuj-zdarma\.eu|strelci\.net|universal-share\.com)/(slozka/.+)" - __version__ = "0.01" - __description__ = """Share-Rapid.com Folder Plugin""" - __author_name__ = ("zoidberg") - __author_mail__ = ("zoidberg@mujmail.cz") - - LINK_PATTERN = r'<td class="soubor"[^>]*><a href="([^"]+)">' diff --git a/module/plugins/crypter/SpeedLoadOrgFolder.py b/module/plugins/crypter/SpeedLoadOrgFolder.py deleted file mode 100644 index 7472e28fe2..0000000000 --- a/module/plugins/crypter/SpeedLoadOrgFolder.py +++ /dev/null @@ -1,28 +0,0 @@ -# -*- coding: utf-8 -*- - -############################################################################ -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU Affero 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 Affero General Public License for more details. # -# # -# You should have received a copy of the GNU Affero General Public License # -# along with this program. If not, see <http://www.gnu.org/licenses/>. # -############################################################################ - -from module.plugins.internal.DeadCrypter import DeadCrypter - - -class SpeedLoadOrgFolder(DeadCrypter): - __name__ = "SpeedLoadOrgFolder" - __type__ = "crypter" - __pattern__ = r"http://(www\.)?speedload\.org/(\d+~f$|folder/\d+/)" - __version__ = "0.3" - __description__ = """Speedload Crypter Plugin""" - __author_name__ = ("stickell") - __author_mail__ = ("l.stickell@yahoo.it") diff --git a/module/plugins/crypter/StealthTo.py b/module/plugins/crypter/StealthTo.py deleted file mode 100644 index 45a14f5a24..0000000000 --- a/module/plugins/crypter/StealthTo.py +++ /dev/null @@ -1,49 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -import re - -from module.plugins.Crypter import Crypter - - -class StealthTo(Crypter): - __name__ = "StealthTo" - __type__ = "container" - __pattern__ = r"http://(www\.)?stealth.to/folder/" - __version__ = "0.1" - __description__ = """Stealth.to Container Plugin""" - __author_name__ = ("spoob") - __author_mail__ = ("spoob@pyload.org") - - def __init__(self, parent): - Crypter.__init__(self, parent) - self.parent = parent - self.html = None - - def file_exists(self): - """ returns True or False - """ - return True - - def proceed(self, url, location): - url = self.parent.url - self.html = self.req.load(url, cookies=True) - temp_links = [] - ids = [] - ats = [] # authenticity_token - inputs = re.findall(r"(<(input|form)[^>]+)", self.html) - for input in inputs: - if re.search(r"name=\"authenticity_token\"", input[0]): - ats.append(re.search(r"value=\"([^\"]+)", input[0]).group(1)) - if re.search(r"name=\"id\"", input[0]): - ids.append(re.search(r"value=\"([^\"]+)", input[0]).group(1)) - - for i in range(0, len(ids)): - self.req.load(url + "/web", - post={"authenticity_token": ats[i], "id": str(ids[i]), "link": ("download_" + str(ids[i]))}, - cookies=True) - new_html = self.req.load(url + "/web", post={"authenticity_token": ats[i], "id": str(ids[i]), "link": "1"}, - cookies=True) - temp_links.append(re.search(r"iframe src=\"(.*)\" frameborder", new_html).group(1)) - - self.links = temp_links diff --git a/module/plugins/crypter/SwisstransferComFolder.py b/module/plugins/crypter/SwisstransferComFolder.py new file mode 100644 index 0000000000..dc640193f3 --- /dev/null +++ b/module/plugins/crypter/SwisstransferComFolder.py @@ -0,0 +1,77 @@ +# -*- coding: utf-8 -*- + +import base64 +import json +import urllib + +import pycurl + +from ..internal.SimpleCrypter import SimpleCrypter + + +class SwisstransferComFolder(SimpleCrypter): + __name__ = "SwisstransferComFolder" + __type__ = "crypter" + __version__ = "0.01" + __status__ = "testing" + + __pattern__ = r"https?://(?:www\.)?swisstransfer\.com/d/(?P<ID>[0-9a-f\-]+)" + __config__ = [("activated", "bool", "Activated", True), + ("use_premium", "bool", "Use premium account if available", True), + ("folder_per_package", "Default;Yes;No", "Create folder for each package", "Default"), + ("max_wait", "int", "Reconnect if waiting time is greater than minutes", 10)] + + __description__ = """Swisstransfer.com decrypter plugin""" + __license__ = "GPLv3" + __authors__ = [("GammaC0de", "nitzo2001[AT]yahoo[DOT]com")] + + API_URL = "https://www.swisstransfer.com/api/" + + def api_request(self, method, auth=None, **kwargs): + headers = ["Content-Type: application/json"] + if auth: + headers.append("Authorization: %s" % auth) + self.req.http.c.setopt(pycurl.HTTPHEADER, headers) + + json_data = self.load(self.API_URL + method, + post=json.dumps(kwargs) if kwargs else {}) + return json.loads(json_data) + + def decrypt(self, pyfile): + folder_id = self.info["pattern"]["ID"] + api_data = self.api_request("links/%s" % folder_id) + if api_data.get("result") == "success": + if api_data["data"].get("type") == "expired": + self.fail(_("Download expired")) + + has_password = api_data["data"].get("type") == "need_password" + if has_password: + password = self.get_password() + if password: + auth = base64.b64encode(urllib.quote(password)) + api_data = self.api_request("links/%s" % folder_id, auth=auth) + if api_data["data"].get("type") == "wrong_password": + self.fail(_("Wrong password")) + + else: + self.fail(_("Download is password protected")) + + if api_data["data"]["downloadCounterCredit"] == 0: + self.fail(_("Authorized number of downloads has been reached.")) + + download_host = api_data["data"]["downloadHost"] + pack_links = [] + for file in api_data["data"]["container"]["files"]: + link = "https://%s/api/download/%s/%s" % (download_host, folder_id, file["UUID"]) + if has_password: + download_token = self.api_request( + "generateDownloadToken", + containerUUID=file["containerUUID"], + fileUUID=file["UUID"], + password=password + ) + link += "?token=%s" % download_token + pack_links.append(link) + + if pack_links: + self.packages.append((pyfile.package().name, pack_links, pyfile.package().name)) diff --git a/module/plugins/crypter/TNTVillageScambioeticoOrg.py b/module/plugins/crypter/TNTVillageScambioeticoOrg.py new file mode 100644 index 0000000000..701b259358 --- /dev/null +++ b/module/plugins/crypter/TNTVillageScambioeticoOrg.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- + +from ..internal.SimpleCrypter import SimpleCrypter + + +class TNTVillageScambioeticoOrg(SimpleCrypter): + __name__ = "TNTVillageScambioeticoOrg" + __type__ = "crypter" + __version__ = "0.07" + __status__ = "testing" + + __pattern__ = r'http://(?:www\.)?forum\.tntvillage\.scambioetico\.org/index\.php\?.*showtopic=\d+' + __config__ = [("activated", "bool", "Activated", True), + ("use_premium", "bool", "Use premium account if available", True), + ("folder_per_package", "Default;Yes;No", + "Create folder for each package", "Default"), + ("max_wait", "int", "Reconnect if waiting time is greater than minutes", 10)] + + __description__ = """TNTVillage.scambioetico.org decrypter plugin""" + __license__ = "GPLv3" + __authors__ = [("Walter Purcaro", "vuolter@gmail.com")] + + LINK_PATTERNS = [ + r'<th class="titlemedium"><a href=\'(.+?)\'', + r"<a href='(\./index\.php\?act.+?)'"] + + def get_links(self): + for p in self.LINK_PATTERNS: + self.LINK_PATTERN = p + links = SimpleCrypter.get_links(self) + if links: + return links diff --git a/module/plugins/crypter/TenluaVnFolder.py b/module/plugins/crypter/TenluaVnFolder.py new file mode 100644 index 0000000000..e7f321ac44 --- /dev/null +++ b/module/plugins/crypter/TenluaVnFolder.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- + +from ..internal.SimpleCrypter import SimpleCrypter +from ..internal.misc import json + + +class TenluaVnFolder(SimpleCrypter): + __name__ = "TenluaVnFolder" + __type__ = "crypter" + __version__ = "0.03" + __status__ = "testing" + + __pattern__ = r'https?://(?:www\.)?tenlua\.vn/folder/.+?/(?P<ID>[0-9a-f]+)/' + __config__ = [("activated", "bool", "Activated", True), + ("use_premium", "bool", "Use premium account if available", True), + ("folder_per_package", "Default;Yes;No", "Create folder for each package", "Default"), + ("max_wait", "int", "Reconnect if waiting time is greater than minutes", 10)] + + __description__ = """Tenlua.vn decrypter plugin""" + __license__ = "GPLv3" + __authors__ = [("GammaC0de", "nitzo2001[AT]yahoo[DOT]com")] + + API_URL = "https://api2.tenlua.vn/" + + def api_response(self, method, **kwargs): + kwargs['a'] = method + return json.loads(self.load(self.API_URL, post=json.dumps([kwargs]))) + + def decrypt(self, pyfile): + folder_info = self.api_response("filemanager_gettree", p=self.info['pattern']['ID'], download=1) + pack_links = ["https://www.tenlua.vn/download/%s/%s" % (x['h'], x['ns']) + for x in folder_info[0]['f'] + if 'h' in x and 'ns' in x] + + pack_name = folder_info[0]['f'][0].get('n') or self.pyfile.package().name + + if pack_links: + self.packages.append((pack_name, pack_links, pack_name)) + diff --git a/module/plugins/crypter/TinyurlCom.py b/module/plugins/crypter/TinyurlCom.py new file mode 100644 index 0000000000..f092d1a1ff --- /dev/null +++ b/module/plugins/crypter/TinyurlCom.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- + +from ..internal.SimpleCrypter import SimpleCrypter + + +class TinyurlCom(SimpleCrypter): + __name__ = "TinyurlCom" + __type__ = "crypter" + __version__ = "0.07" + __status__ = "testing" + + __pattern__ = r'https?://(?:www\.)?(preview\.)?tinyurl\.com/[\w\-]+' + __config__ = [("activated", "bool", "Activated", True), + ("use_premium", "bool", "Use premium account if available", True), + ("folder_per_package", "Default;Yes;No", + "Create folder for each package", "Default"), + ("max_wait", "int", "Reconnect if waiting time is greater than minutes", 10)] + + __description__ = """Tinyurl.com decrypter plugin""" + __license__ = "GPLv3" + __authors__ = [("Walter Purcaro", "vuolter@gmail.com")] + + URL_REPLACEMENTS = [(r'preview\.', r'')] + + OFFLINE_PATTERN = r">Error: Unable to find site's URL to redirect to" diff --git a/module/plugins/crypter/TnyCz.py b/module/plugins/crypter/TnyCz.py new file mode 100644 index 0000000000..eda41b77d3 --- /dev/null +++ b/module/plugins/crypter/TnyCz.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- + +import re + +from ..internal.SimpleCrypter import SimpleCrypter + + +class TnyCz(SimpleCrypter): + __name__ = "TnyCz" + __type__ = "crypter" + __version__ = "0.09" + __status__ = "testing" + + __pattern__ = r'http://(?:www\.)?tny\.cz/\w+' + __config__ = [("activated", "bool", "Activated", True), + ("use_premium", "bool", "Use premium account if available", True), + ("folder_per_package", "Default;Yes;No", + "Create folder for each package", "Default"), + ("max_wait", "int", "Reconnect if waiting time is greater than minutes", 10)] + + __description__ = """Tny.cz decrypter plugin""" + __license__ = "GPLv3" + __authors__ = [("Walter Purcaro", "vuolter@gmail.com")] + + NAME_PATTERN = r'<title>(?P<N>.+?) - .+' + + def get_links(self): + m = re.search( + r'', + self.data) + return re.findall(".+", self.load(m.group(1))) if m else None diff --git a/module/plugins/crypter/TorboxAppTorrent.py b/module/plugins/crypter/TorboxAppTorrent.py new file mode 100644 index 0000000000..8220a536bb --- /dev/null +++ b/module/plugins/crypter/TorboxAppTorrent.py @@ -0,0 +1,219 @@ +# -*- coding: utf-8 -*- + +import fnmatch +import os +import time +import urllib + +import pycurl + +from module.network.HTTPRequest import BadHeader, FormFile + +from ..internal.misc import exists, json, safejoin, uniqify +from ..internal.SimpleCrypter import SimpleCrypter + + +class TorboxAppTorrent(SimpleCrypter): + __name__ = "TorboxAppTorrent" + __type__ = "crypter" + __version__ = "0.01" + __status__ = "testing" + + __pattern__ = r"^unmatchable$" + __config__ = [ + ("activated", "bool", "Activated", True), + ("folder_per_package", "Default;Yes;No", "Create folder for each package", "Default"), + ("max_wait", "int", "Reconnect if waiting time is greater than minutes", 10), + ("include_filter", "str", "File types to include (e.g. *.iso;*.zip, leave empty to select none)", "*.*"), + ("exclude_filter", "str", "File types to exclude (e.g. *.exe;advertisement.txt, leave empty to select none)", "") + ] + + + __description__ = """Torbox.app torrents decrypter plugin""" + __license__ = "GPLv3" + __authors__ = [("GammaC0de", "nitzo2001[AT]yahoo[DOT]com")] + + # See https://api-docs.torbox.app/ + API_URL = "https://api.torbox.app/v1/api/" + + def api_request(self, method, api_key=None, get={}, post={}): + if api_key is not None: + self.req.http.c.setopt( + pycurl.HTTPHEADER, ["Authorization: Bearer " + api_key] + ) + multipart = any( + isinstance(x, FormFile) + for x in post.values() + ) + try: + json_data = self.load(self.API_URL + method, get=get, post=post, multipart=multipart) + except BadHeader as exc: + json_data = exc.content + + api_data = json.loads(json_data) + return api_data + + def sleep(self, sec): + for _i in range(sec): + if self.pyfile.abort: + break + time.sleep(1) + + def exit_error(self, msg): + if self.tmp_file: + os.remove(self.tmp_file) + + self.fail(msg) + + def send_request_to_server(self): + """ Send torrent/magnet to the server """ + + if self.pyfile.url.endswith(".torrent"): + #: torrent URL + if self.pyfile.url.startswith("http"): + #: remote URL, download the torrent to tmp directory + torrent_content = self.load(self.pyfile.url, decode=False) + torrent_filename = safejoin(self.pyload.tempdir, "tmp_{}.torrent".format(self.pyfile.package().name)) + with open(torrent_filename, "wb") as fp: + fp.write(torrent_content) + + else: + #: URL is a local torrent file (uploaded container) + torrent_filename = urllib.url2pathname(self.pyfile.url[7:]) #: trim the starting `file://` + if not exists(torrent_filename): + self.fail(self._("Torrent file does not exist")) + + self.tmp_file = torrent_filename + + #: Check if the torrent file path is inside pyLoad's temp directory + if os.path.abspath(torrent_filename).startswith(self.pyload.tempdir + os.sep): + #: yes, send the torrent content to the server + api_data = self.api_request("torrents/createtorrent", + api_key=self.api_key, + post={"file": FormFile(torrent_filename, mimetype="application/octet-stream")}) + + if not api_data.get("success", False): + error_msg = api_data["detail"] + self.exit_error(error_msg) + + else: + self.exit_error(self._("Illegal URL")) #: We don't allow files outside pyLoad's config directory + + else: + #: magnet URL, send it to the server + api_data = self.api_request("torrents/createtorrent", + api_key=self.api_key, + post={"magnet": self.pyfile.url}) + + if not api_data.get("success", False): + error_msg = api_data["detail"] + self.exit_error(error_msg) + + torrent_id = api_data["data"]["torrent_id"] + torrent_hash = api_data["data"]["hash"] + return torrent_id, torrent_hash + + + def wait_for_server_dl(self, torrent_id, torrent_hash): + """ Show progress while the server does the download """ + + exclude_filters = self.config.get("exclude_filter").split(';') + include_filters = self.config.get("include_filter").split(";") + + api_data = self.api_request("torrents/checkcached", + api_key=self.api_key, + get={ + "hash": torrent_hash, + "format": "object", + "bypass_cache": True, + }) + + if api_data.get("success", False) and api_data.get("data"): + self.pyfile.name = api_data["data"][torrent_hash]["name"] + self.pyfile.size = api_data["data"][torrent_hash]["size"] + + else: + self.pyfile.set_custom_status("torrent") + self.pyfile.set_progress(0) + while True: + api_data = self.api_request("torrents/mylist", + api_key=self.api_key, + get={ + "id": torrent_id, + "bypass_cache": True, + }) + + file_size = api_data["data"].get("size") + if file_size: + self.pyfile.size = file_size + file_name = api_data["data"].get("name") + if file_name: + self.pyfile.name = file_name + + progress = api_data["data"].get("progress", 0) * 100 + self.pyfile.set_progress(progress) + if api_data["data"].get("download_state") == "completed": + break + + self.sleep(5) + + self.pyfile.set_progress(100) + + api_data = self.api_request("torrents/mylist", + api_key=self.api_key, + get={ + "id": torrent_id, + "bypass_cache": True + }) + + #: Filter and select files for downloading + excluded_ids = [] + for _filter in exclude_filters: + excluded_ids.extend([_file["id"] for _file in api_data["data"].get("files", []) + if fnmatch.fnmatch(os.path.basename(_file["short_name"]), _filter)]) + + excluded_ids = uniqify(excluded_ids) + + included_ids = [] + for _filter in include_filters: + included_ids.extend([_file["id"] for _file in api_data["data"].get("files", []) + if fnmatch.fnmatch(os.path.basename(_file["short_name"]), _filter)]) + + included_ids = uniqify(included_ids) + + selected_ids = [ + str(_id) + for _id in sorted(included_ids) + if _id not in excluded_ids + ] + + torrent_urls = [ + "%storrents/requestdl?token=%s&torrent_id=%s&file_id=%s&redirect=true" % ( + self.API_URL, self.api_key, torrent_id, _id + ) + for _id in selected_ids + ] + + return torrent_urls + + def delete_torrent_from_server(self, torrent_id): + """ Remove the torrent from the server """ + + pass + + def decrypt(self, pyfile): + self.tmp_file = None + torrent_id = 0 + if "TorboxApp" not in self.pyload.accountManager.plugins: + self.fail(self._("This plugin requires an active Torbox.app account")) + + self.account = self.pyload.accountManager.getAccountPlugin("TorboxApp") + if len(self.account.accounts) == 0: + self.fail(self._("This plugin requires an active Torbox.app account")) + + self.api_key = self.account.accounts[list(self.account.accounts.keys())[0]]["password"] + + torrent_id, torrent_hash = self.send_request_to_server() + torrent_urls = self.wait_for_server_dl(torrent_id, torrent_hash) + + self.packages = [(pyfile.package().name, torrent_urls, pyfile.package().name)] diff --git a/module/plugins/crypter/TrailerzoneInfo.py b/module/plugins/crypter/TrailerzoneInfo.py deleted file mode 100644 index cdd84bbc63..0000000000 --- a/module/plugins/crypter/TrailerzoneInfo.py +++ /dev/null @@ -1,13 +0,0 @@ -# -*- coding: utf-8 -*- - -from module.plugins.internal.DeadCrypter import DeadCrypter - - -class TrailerzoneInfo(DeadCrypter): - __name__ = "TrailerzoneInfo" - __type__ = "crypter" - __pattern__ = r"http://(www\.)?trailerzone.info/.*?" - __version__ = "0.03" - __description__ = """TrailerZone.info Crypter Plugin""" - __author_name__ = ("godofdream") - __author_mail__ = ("soilfiction@gmail.com") diff --git a/module/plugins/crypter/TurbobitNetFolder.py b/module/plugins/crypter/TurbobitNetFolder.py index e172f8037a..1a090662b1 100644 --- a/module/plugins/crypter/TurbobitNetFolder.py +++ b/module/plugins/crypter/TurbobitNetFolder.py @@ -1,60 +1,43 @@ # -*- coding: utf-8 -*- -############################################################################ -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU Affero 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 Affero General Public License for more details. # -# # -# You should have received a copy of the GNU Affero General Public License # -# along with this program. If not, see . # -############################################################################ -import math -import re - -from module.plugins.internal.SimpleCrypter import SimpleCrypter -from module.common.json_layer import json_loads - - -def format_links(fid): - return 'http://turbobit.net/%s.html' % fid +from ..internal.misc import json +from ..internal.SimpleCrypter import SimpleCrypter class TurbobitNetFolder(SimpleCrypter): __name__ = "TurbobitNetFolder" __type__ = "crypter" - __pattern__ = r"http://(?:w{3}.)?turbobit\.net/download/folder/(?P\w+)" - __version__ = "0.01" - __description__ = """Turbobit.net Folder Plugin""" - __author_name__ = ("stickell") - __author_mail__ = ("l.stickell@yahoo.it") - - TITLE_PATTERN = r"(?P.+)</div>" - - def getLinks(self): - folder_id = re.search(self.__pattern__, self.pyfile.url).group('id') - grid = self.load('http://turbobit.net/downloadfolder/gridFile', - get={'id_folder': folder_id, 'rows': 200}, decode=True) - grid = json_loads(grid) - - links_count = grid["records"] - pages = int(math.ceil(links_count / 200.0)) - - ids = list() - for i in grid['rows']: - ids.append(i['id']) - - for p in range(2, pages + 1): - grid = self.load('http://turbobit.net/downloadfolder/gridFile', - get={'id_folder': folder_id, 'rows': 200, 'page': p}, decode=True) - grid = json_loads(grid) - for i in grid['rows']: - ids.append(i['id']) + __version__ = "0.11" + __status__ = "broken" + + __pattern__ = r'http://(?:www\.)?turbobit\.net/download/folder/(?P<ID>\w+)' + __config__ = [("activated", "bool", "Activated", True), + ("use_premium", "bool", "Use premium account if available", True), + ("folder_per_package", "Default;Yes;No", + "Create folder for each package", "Default"), + ("max_wait", "int", "Reconnect if waiting time is greater than minutes", 10)] - return map(format_links, ids) + __description__ = """Turbobit.net folder decrypter plugin""" + __license__ = "GPLv3" + __authors__ = [("stickell", "l.stickell@yahoo.it"), + ("Walter Purcaro", "vuolter@gmail.com")] + + NAME_PATTERN = r'src=\'/js/lib/grid/icon/folder.png\'> <span>(?P<N>.+?)</span>' + + def _get_links(self, id, page=1): + gridFile = self.load("http://turbobit.net/downloadfolder/gridFile", + get={'rootId': id, 'rows': 200, 'page': page}) + grid = json.loads(gridFile) + + if grid['rows']: + for i in grid['rows']: + yield i['id'] + for id in self._get_links(id, page + 1): + yield id + else: + return + + def get_links(self): + return ["http://turbobit.net/%s.html" % + id for id in self._get_links(self.info['pattern']['ID'])] diff --git a/module/plugins/crypter/TusfilesNetFolder.py b/module/plugins/crypter/TusfilesNetFolder.py new file mode 100644 index 0000000000..19ea1a0b87 --- /dev/null +++ b/module/plugins/crypter/TusfilesNetFolder.py @@ -0,0 +1,51 @@ +# -*- coding: utf-8 -*- + +import math +import re +import urlparse + +from ..internal.XFSCrypter import XFSCrypter + + +class TusfilesNetFolder(XFSCrypter): + __name__ = "TusfilesNetFolder" + __type__ = "crypter" + __version__ = "0.17" + __status__ = "testing" + + __pattern__ = r'https?://(?:www\.)?tusfiles\.net/go/(?P<ID>\w+)' + __config__ = [("activated", "bool", "Activated", True), + ("use_premium", "bool", "Use premium account if available", True), + ("folder_per_package", "Default;Yes;No", + "Create folder for each package", "Default"), + ("max_wait", "int", "Reconnect if waiting time is greater than minutes", 10)] + + __description__ = """Tusfiles.net folder decrypter plugin""" + __license__ = "GPLv3" + __authors__ = [("Walter Purcaro", "vuolter@gmail.com"), + ("stickell", "l.stickell@yahoo.it")] + + PLUGIN_DOMAIN = "tusfiles.net" + PAGES_PATTERN = r'>\((\d+) \w+\)<' + + URL_REPLACEMENTS = [ + (__pattern__ + ".*", + r'https://www.tusfiles.net/go/\g<ID>/')] + + def load_page(self, page_n): + return self.load(urlparse.urljoin(self.pyfile.url, str(page_n))) + + def handle_pages(self, pyfile): + pages = re.search(self.PAGES_PATTERN, self.data) + + if pages: + pages = int(math.ceil(int(pages.group('pages')) / 25.0)) + else: + return + + links = self.links + for p in range(2, pages + 1): + self.data = self.load_page(p) + links.append(self.get_links()) + + self.links = links diff --git a/module/plugins/crypter/UlozToFolder.py b/module/plugins/crypter/UlozToFolder.py index a5ccfc7538..9229b58bc0 100644 --- a/module/plugins/crypter/UlozToFolder.py +++ b/module/plugins/crypter/UlozToFolder.py @@ -1,42 +1,47 @@ # -*- coding: utf-8 -*- import re -from module.plugins.Crypter import Crypter + +from ..internal.Crypter import Crypter class UlozToFolder(Crypter): __name__ = "UlozToFolder" __type__ = "crypter" - __pattern__ = r"http://.*(uloz\.to|ulozto\.(cz|sk|net)|bagruj.cz|zachowajto.pl)/(m|soubory)/.*" - __version__ = "0.2" - __description__ = """Uloz.to Folder Plugin""" - __author_name__ = ("zoidberg") - __author_mail__ = ("zoidberg@mujmail.cz") + __version__ = "0.27" + __status__ = "testing" + + __pattern__ = r'http://(?:www\.)?(uloz\.to|ulozto\.(cz|sk|net)|bagruj\.cz|zachowajto\.pl)/(m|soubory)/.+' + __config__ = [("activated", "bool", "Activated", True), + ("use_premium", "bool", "Use premium account if available", True), + ("folder_per_package", "Default;Yes;No", "Create folder for each package", "Default")] + + __description__ = """Uloz.to folder decrypter plugin""" + __license__ = "GPLv3" + __authors__ = [("zoidberg", "zoidberg@mujmail.cz")] FOLDER_PATTERN = r'<ul class="profile_files">(.*?)</ul>' - LINK_PATTERN = r'<br /><a href="/([^"]+)">[^<]+</a>' - NEXT_PAGE_PATTERN = r'<a class="next " href="/([^"]+)"> </a>' + LINK_PATTERN = r'<br /><a href="/(.+?)">.+?</a>' + NEXT_PAGE_PATTERN = r'<a class="next " href="/(.+?)"> </a>' def decrypt(self, pyfile): - html = self.load(self.pyfile.url) + html = self.load(pyfile.url) new_links = [] for i in range(1, 100): - self.logInfo("Fetching links from page %i" % i) - found = re.search(self.FOLDER_PATTERN, html, re.DOTALL) - if found is None: - self.fail("Parse error (FOLDER)") - - new_links.extend(re.findall(self.LINK_PATTERN, found.group(1))) - found = re.search(self.NEXT_PAGE_PATTERN, html) - if found: - html = self.load("http://ulozto.net/" + found.group(1)) + self.log_info(_("Fetching links from page %i") % i) + m = re.search(self.FOLDER_PATTERN, html, re.S) + if m is None: + self.error(_("FOLDER_PATTERN not found")) + + new_links.extend(re.findall(self.LINK_PATTERN, m.group(1))) + m = re.search(self.NEXT_PAGE_PATTERN, html) + if m is not None: + html = self.load("http://ulozto.net/" + m.group(1)) else: break else: - self.logInfo("Limit of 99 pages reached, aborting") + self.log_info(_("Limit of 99 pages reached, aborting")) if new_links: - self.core.files.addLinks(map(lambda s: "http://ulozto.net/%s" % s, new_links), self.pyfile.package().id) - else: - self.fail('Could not extract any links') + self.links = [map(lambda s: "http://ulozto.net/%s" % s, new_links)] diff --git a/module/plugins/crypter/UploadedToFolder.py b/module/plugins/crypter/UploadedToFolder.py deleted file mode 100644 index 88d4e04e85..0000000000 --- a/module/plugins/crypter/UploadedToFolder.py +++ /dev/null @@ -1,50 +0,0 @@ -# -*- coding: utf-8 -*- - -############################################################################ -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU Affero 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 Affero General Public License for more details. # -# # -# You should have received a copy of the GNU Affero General Public License # -# along with this program. If not, see <http://www.gnu.org/licenses/>. # -############################################################################ - -import re - -from module.plugins.internal.SimpleCrypter import SimpleCrypter - - -class UploadedToFolder(SimpleCrypter): - __name__ = "UploadedToFolder" - __type__ = "crypter" - __pattern__ = r"http://(?:www\.)?(uploaded|ul)\.(to|net)/(f|folder|list)/(?P<id>\w+)" - __version__ = "0.3" - __description__ = """UploadedTo Crypter Plugin""" - __author_name__ = ("stickell") - __author_mail__ = ("l.stickell@yahoo.it") - - PLAIN_PATTERN = r'<small class="date"><a href="(?P<plain>[\w/]+)" onclick=' - TITLE_PATTERN = r'<title>(?P<title>[^<]+)' - - def decrypt(self, pyfile): - self.html = self.load(pyfile.url) - - package_name, folder_name = self.getPackageNameAndFolder() - - m = re.search(self.PLAIN_PATTERN, self.html) - if m: - plain_link = 'http://uploaded.net/' + m.group('plain') - else: - self.fail('Parse error - Unable to find plain url list') - - self.html = self.load(plain_link) - package_links = self.html.split('\n')[:-1] - self.logDebug('Package has %d links' % len(package_links)) - - self.packages = [(package_name, package_links, folder_name)] diff --git a/module/plugins/crypter/WebshareCzFolder.py b/module/plugins/crypter/WebshareCzFolder.py new file mode 100644 index 0000000000..bcfd0e0696 --- /dev/null +++ b/module/plugins/crypter/WebshareCzFolder.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- + +import re + +from ..internal.Crypter import Crypter + +class WebshareCzFolder(Crypter): + __name__ = "WebshareCzFolder" + __type__ = "decrypter" + __version__ = "0.01" + __status__ = "testing" + + __pattern__ = r"https?://(?:www\.)?(?:en\.)?webshare\.cz/(?:#/)?(?:folder/)(?P\w+)" + __config__ = [ + ("activated", "bool", "Activated", True), + ("use_premium", "bool", "Use premium account if available", True), + ("folder_per_package", "Default;Yes;No", "Create folder for each package", "Default"), + ] + + __description__ = """Webshare.cz decrypter plugin""" + __license__ = "GPLv3" + __authors__ = [("GammaC0de", "nitzo2001[AT]yahoo[DOT]com")] + + NAME_PATTERN = r"(.+?)" + LINK_PATTERN = r"(\w+?)" + + API_URL = "https://webshare.cz/api/" + + def api_request(self, method, **kwargs): + return self.load(self.API_URL + method + "/", post=kwargs) + + def decrypt(self, pyfile): + wst = self.account.get_data("wst") if self.account else "" + api_data = self.api_request("folder", ident=self.info["pattern"]["ID"], offset=0, wst=wst) + + m = re.search(self.NAME_PATTERN, api_data) + if m is not None: + name = m.group(1) + else: + name = pyfile.package().name + + urls = ["https://webshare.cz/#/file/%s" % link_id for link_id in re.findall(self.LINK_PATTERN, api_data)] + self.packages = [(name, urls, name)] diff --git a/module/plugins/crypter/WetransferComDereferer.py b/module/plugins/crypter/WetransferComDereferer.py new file mode 100644 index 0000000000..7f10ad548d --- /dev/null +++ b/module/plugins/crypter/WetransferComDereferer.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -* + +from ..internal.SimpleCrypter import SimpleCrypter + + +class WetransferComDereferer(SimpleCrypter): + __name__ = "WetransferComDereferer" + __type__ = "crypter" + __version__ = "0.01" + __status__ = "testing" + + __pattern__ = r"https?://we\.tl/[\-\w]{12}" + __config__ = [("activated", "bool", "Activated", True), + ("use_premium", "bool", "Use premium account if available", True), + ("folder_per_package", "Default;Yes;No", + "Create folder for each package", "Default"), + ("max_wait", "int", "Reconnect if waiting time is greater than minutes", 10)] + + __description__ = """we.tl dereferer plugin""" + __license__ = "GPLv3" + __authors__ = [("GammaC0de", "nitzo2001[AT]yahoo[DOT]com")] + + def decrypt(self, pyfile): + headers = self.load(pyfile.url, just_header=True) + link = headers.get("location") + if link is not None: + self.packages = [(pyfile.package().folder, [link], pyfile.package().name)] diff --git a/module/plugins/crypter/WiiReloadedOrg.py b/module/plugins/crypter/WiiReloadedOrg.py deleted file mode 100644 index df70c5aea1..0000000000 --- a/module/plugins/crypter/WiiReloadedOrg.py +++ /dev/null @@ -1,13 +0,0 @@ -# -*- coding: utf-8 -*- - -from module.plugins.internal.DeadCrypter import DeadCrypter - - -class WiiReloadedOrg(DeadCrypter): - __name__ = "WiiReloadedOrg" - __type__ = "crypter" - __pattern__ = r"http://www\.wii-reloaded\.org/protect/get\.php\?i=.+" - __version__ = "0.11" - __description__ = """Wii-Reloaded.org Crypter Plugin""" - __author_name__ = ("hzpz") - __author_mail__ = ("none") diff --git a/module/plugins/crypter/WorkuploadComFolder.py b/module/plugins/crypter/WorkuploadComFolder.py new file mode 100644 index 0000000000..5995c04b91 --- /dev/null +++ b/module/plugins/crypter/WorkuploadComFolder.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- +import re + +from ..internal.misc import uniqify +from ..internal.SimpleCrypter import SimpleCrypter + + +class WorkuploadComFolder(SimpleCrypter): + __name__ = "WorkuploadComFolder" + __type__ = "crypter" + __version__ = "0.01" + __status__ = "testing" + + __pattern__ = r'https?://workupload\.com/archive/(?P\w+)' + __config__ = [("activated", "bool", "Activated", True), + ("use_premium", "bool", "Use premium account if available", True), + ("folder_per_package", "Default;Yes;No", "Create folder for each package", "Default"), + ("max_wait", "int", "Reconnect if waiting time is greater than minutes", 10)] + + __description__ = """Workupload.com folder decrypter plugin""" + __license__ = "GPLv3" + __authors__ = [("GammaC0de", "nitzo2001[AT]yahoo[DOT]com")] + + LINK_PATTERN = r'. - - @author: Walter Purcaro -""" - -import re -import json - -from module.plugins.Crypter import Crypter -from os.path import join - -API_KEY = "AIzaSyCKnWLNlkX-L4oD1aEzqqhRw1zczeD6_k0" - - -class YoutubeBatch(Crypter): - __name__ = "YoutubeBatch" - __type__ = "container" - __pattern__ = r"https?://(?:[^/]*?)youtube\.com/(?:(view_play_list|playlist|.*?feature=PlayList|user)(?:.*?[?&](?:list|p)=|/))([a-zA-Z0-9-_]+)" - __version__ = "0.94" - __description__ = """Youtube.com Channel Download Plugin""" - __author_name__ = ("RaNaN", "Spoob", "zoidberg", "roland", "Walter Purcaro") - __author_mail__ = ("RaNaN@pyload.org", "spoob@pyload.org", "zoidberg@mujmail.cz", "roland@enkore.de", "vuolter@gmail.com") - - def json_response(self, api, req): - req.update({"key": API_KEY}) - url = "https://www.googleapis.com/youtube/v3/" + api - page = self.load(url, get=req) - return json.loads(page) - - def get_playlist_baseinfos(self, playlist_id): - res = self.json_response("playlists", {"part": "snippet", "id": playlist_id}) - - snippet = res["items"][0]["snippet"] - playlist_name = snippet["title"] - channel_title = snippet["channelTitle"] - return playlist_name, channel_title - - def get_channel_id(self, user_name): - res = self.json_response("channels", {"part": "id", "forUsername": user_name}) - return res["items"][0]["id"] - - def get_playlists(self, user_name, token=None): - channel_id = self.get_channel_id(user_name) - req = {"part": "id", "maxResults": "50", "channelId": channel_id} - if token: - req.update({"pageToken": token}) - res = self.json_response("playlists", req) - - for item in res["items"]: - yield item["id"] - - if "nextPageToken" in res: - for item in self.get_playlists(user_name, res["nextPageToken"]): - yield item - - def get_videos(self, playlist_id, token=None): - req = {"part": "snippet", "maxResults": "50", "playlistId": playlist_id} - if token: - req.update({"pageToken": token}) - res = self.json_response("playlistItems", req) - - for item in res["items"]: - yield "http://youtube.com/watch?v=" + item["snippet"]["resourceId"]["videoId"] - - if "nextPageToken" in res: - for item in self.get_videos(playlist_id, res["nextPageToken"]): - yield item - - def decrypt(self, pyfile): - match_obj = re.match(self.__pattern__, pyfile.url) - match_type, match_result = match_obj.group(1), match_obj.group(2) - playlist_ids = [] - - #: is a channel username or just a playlist id? - if match_type == "user": - ids = self.get_playlists(match_result) - playlist_ids.extend(ids) - else: - playlist_ids.append(match_result) - - self.logDebug("Playlist IDs = %s" % playlist_ids) - - if not playlist_ids: - self.fail("Wrong url") - - for id in playlist_ids: - self.logDebug("Processing playlist id: %s" % id) - - playlist_name, channel_title = self.get_playlist_baseinfos(id) - video_links = [x for x in self.get_videos(id)] - - self.logInfo("%s videos found on playlist \"%s\" (channel \"%s\")" % (len(video_links), playlist_name, channel_title)) - - if not video_links: - continue - - self.logDebug("Video links = %s" % video_links) - - folder = join(self.config['general']['download_folder'], channel_title, playlist_name) - self.packages.append((playlist_name, video_links, folder)) #Note: folder is NOT used actually! diff --git a/module/plugins/crypter/YoutubeComFolder.py b/module/plugins/crypter/YoutubeComFolder.py new file mode 100644 index 0000000000..c0f561a14c --- /dev/null +++ b/module/plugins/crypter/YoutubeComFolder.py @@ -0,0 +1,156 @@ +# -*- coding: utf-8 -*- + +from ..internal.Crypter import Crypter +from ..internal.misc import json + + +class YoutubeComFolder(Crypter): + __name__ = "YoutubeComFolder" + __type__ = "crypter" + __version__ = "1.12" + __status__ = "testing" + + __pattern__ = r'https?://(?:www\.|m\.)?youtube\.com/(?Puser|playlist|view_play_list)(/|.*?[?&](?:list|p)=)(?P[\w\-]+)' + __config__ = [("activated", "bool", "Activated", True), + ("use_premium", "bool", "Use premium account if available", True), + ("folder_per_package", "Default;Yes;No", "Create folder for each package", "Default"), + ("likes", "bool", "Grab user (channel) liked videos", False), + ("favorites", "bool", "Grab user (channel) favorite videos", False), + ("uploads", "bool", "Grab channel unplaylisted videos", True)] + + __description__ = """Youtube.com channel & playlist decrypter plugin""" + __license__ = "GPLv3" + __authors__ = [("Walter Purcaro", "vuolter@gmail.com"), + ("GammaC0de", "nitzo2001[AT]yahoo[DOT]com")] + + API_KEY = "AIzaSyB68u-qFPP9oBJpo1DWAPFE_VD2Sfy9hpk" + + def api_response(self, method, **kwargs): + kwargs['key'] = self.API_KEY + json_data = self.load("https://www.googleapis.com/youtube/v3/" + method, get=kwargs) + return json.loads(json_data) + + def get_channel(self, user): + channels = self.api_response("channels", + part="id,snippet,contentDetails", + forUsername=user, + maxResults=50) + if channels['items']: + channel = channels['items'][0] + return {'id': channel['id'], + 'title': channel['snippet']['title'], + 'relatedPlaylists': channel['contentDetails']['relatedPlaylists'], + 'user': user} #: One lone channel for user? + + def get_playlist(self, playlist_id): + playlists = self.api_response("playlists", + part="snippet", + id=playlist_id) + if playlists['items']: + playlist = playlists['items'][0] + return {'id': playlist_id, + 'title': playlist['snippet']['title'], + 'channelId': playlist['snippet']['channelId'], + 'channelTitle': playlist['snippet']['channelTitle']} + + def _get_playlists(self, playlist_id, token=None): + if token: + playlists = self.api_response("playlists", + part="id", + maxResults=50, + channelId=playlist_id, + pageToken=token) + else: + playlists = self.api_response("playlists", + part="id", + maxResults=50, + channelId=playlist_id) + + for playlist in playlists['items']: + yield playlist['id'] + + if "nextPageToken" in playlists: + for item in self._get_playlists(playlist_id, playlists['nextPageToken']): + yield item + + def get_playlists(self, ch_id): + return map(self.get_playlist, self._get_playlists(ch_id)) + + def _get_videos_id(self, playlist_id, token=None): + if token: + playlist = self.api_response("playlistItems", + part="contentDetails", + maxResults=50, + playlistId=playlist_id, + pageToken=token) + else: + playlist = self.api_response("playlistItems", + part="contentDetails", + maxResults=50, + playlistId=playlist_id) + + for item in playlist['items']: + yield item['contentDetails']['videoId'] + + if "nextPageToken" in playlist: + for item in self._get_videos_id(playlist_id, playlist['nextPageToken']): + yield item + + def get_videos_id(self, p_id): + return list(self._get_videos_id(p_id)) + + def decrypt(self, pyfile): + if self.info['pattern']['TYPE'] == "user": + self.log_debug("Url recognized as Channel") + channel = self.get_channel(self.info['pattern']['ID']) + + if channel: + playlists = self.get_playlists(channel['id']) + self.log_debug('%s playlists found on channel "%s"' % (len(playlists), channel['title'])) + + relatedplaylist = dict((p_name, self.get_playlist(p_id)) + for p_name, p_id in channel['relatedPlaylists'].items()) + + self.log_debug("Channel's related playlists found = %s" % relatedplaylist.keys()) + + relatedplaylist['uploads']['title'] = "Unplaylisted videos" + relatedplaylist['uploads']['checkDups'] = True #: checkDups flag + + for p_name, p_data in relatedplaylist.items(): + if self.config.get(p_name): + p_data['title'] += " of " + user + playlists.append(p_data) + + else: + playlists = [] + + else: + self.log_debug("Url recognized as Playlist") + playlists = [self.get_playlist(self.info['pattern']['ID'])] + + if not playlists: + self.fail(_("No playlist available")) + + addedvideos = [] + urlize = lambda x: "https://www.youtube.com/watch?v=" + x + for p in playlists: + p_name = p['title'] + p_videos = self.get_videos_id(p['id']) + + self.log_debug('%s videos found on playlist "%s"' % (len(p_videos), p_name)) + + if not p_videos: + continue + elif "checkDups" in p: + p_urls = [urlize(v_id) + for v_id in p_videos + if v_id not in addedvideos] + + self.log_debug('%s videos available on playlist "%s" after duplicates cleanup' % (len(p_urls), p_name)) + + else: + p_urls = map(urlize, p_videos) + + self.packages.append((p_name, p_urls, p_name)) + + addedvideos.extend(p_videos) diff --git a/module/plugins/hooks/AlldebridCom.py b/module/plugins/hooks/AlldebridCom.py deleted file mode 100644 index d0e9b1f779..0000000000 --- a/module/plugins/hooks/AlldebridCom.py +++ /dev/null @@ -1,29 +0,0 @@ -# -*- coding: utf-8 -*- - -# should be working - -from module.network.RequestFactory import getURL -from module.plugins.internal.MultiHoster import MultiHoster - - -class AlldebridCom(MultiHoster): - __name__ = "AlldebridCom" - __version__ = "0.13" - __type__ = "hook" - - __config__ = [("activated", "bool", "Activated", "False"), - ("https", "bool", "Enable HTTPS", "False"), - ("hosterListMode", "all;listed;unlisted", "Use for hosters (if supported)", "all"), - ("hosterList", "str", "Hoster list (comma separated)", ""), - ("unloadFailing", "bool", "Revert to stanard download if download fails", "False"), - ("interval", "int", "Reload interval in hours (0 to disable)", "24")] - - __description__ = """Real-Debrid.com hook plugin""" - __author_name__ = ("Andy, Voigt") - __author_mail__ = ("spamsales@online.de") - - def getHoster(self): - https = "https" if self.getConfig("https") else "http" - page = getURL(https + "://www.alldebrid.com/api.php?action=get_host").replace("\"", "").strip() - - return [x.strip() for x in page.split(",") if x.strip()] diff --git a/module/plugins/hooks/AntiCaptcha.py b/module/plugins/hooks/AntiCaptcha.py new file mode 100644 index 0000000000..c70c5cf568 --- /dev/null +++ b/module/plugins/hooks/AntiCaptcha.py @@ -0,0 +1,225 @@ +# -*- coding: utf-8 -*- + +import base64 +import time +import urlparse + +from ..internal.Addon import Addon +from ..internal.misc import json, threaded, fs_encode + + +class AntiCaptcha(Addon): + __name__ = "AntiCaptcha" + __type__ = "hook" + __version__ = "0.04" + __status__ = "testing" + + __config__ = [("activated", "bool", "Activated", False), + ("check_client", "bool", "Don't use if client is connected", True), + ("solve_image", "bool", "Solve image catcha", True), + ("solve_recaptcha", "bool", "Solve ReCaptcha", True), + ("solve_hcaptcha", "bool", "Solve HCaptcha", True), + ("solve_turnstile", "bool", "Solve Turnstile", True), + ("refund", "bool", "Request refund if result incorrect", False), + ("api_url", "str", "API base URL", "https://api.anti-captcha.com/"), + ("passkey", "password", "API key", ""), + ("timeout", "int", "Timeout in seconds (min 60, max 3999)", "900")] + + __description__ = """Send captchas to anti-captcha.com""" + __license__ = "GPLv3" + __authors__ = [("GammaC0de", "nitzo2001[AT]yahho[DOT]com")] + + TASK_TYPES = { + "ReCaptcha": "RecaptchaV2TaskProxyless", + "HCaptcha": "HCaptchaTaskProxyless", + "Turnstile": "TurnstileTaskProxyless" + } + + # See https://anti-captcha.com/apidoc + API_URL = "https://api.anti-captcha.com/" + + def api_request(self, method, post): + api_url = self.config.get("api_url", self.API_URL) + api_url += "/" if not api_url.endswith("/") else "" + json_data = self.load(api_url + method, post=json.dumps(post)) + return json.loads(json_data) + + def get_credits(self): + credits = self.db.retrieve("credits", {"balance": 0, "time": 0}) + + #: Docs says: "Please don't call this method more often than once in 30 seconds" + if time.time() - credits["time"] >= 30: + api_data = self.api_request( + "getBalance", {"clientKey": self.config.get("passkey")} + ) + if api_data["errorId"] != 0: + self.log_error(_("API error"), api_data["errorDescription"]) + return 0 + + credits = {"balance": api_data["balance"], "time": time.time()} + self.db.store("credits", credits) + + balance = credits["balance"] + self.log_info(_("Credits left: %.2f$") % balance) + + return balance + + @threaded + def _process_captcha(self, task): + url_p = urlparse.urlparse(task.captchaParams["url"]) + if task.isInteractive(): + if url_p.scheme not in ("http", "https"): + self.log_error(_("Invalid url")) + return + + api_data = self.api_request( + "createTask", + { + "clientKey": self.config.get("passkey"), + "softId": 976, + "task": { + "type": self.TASK_TYPES[task.captchaParams["captcha_plugin"]], + "websiteURL": r"%s://%s/" % (url_p.scheme, url_p.netloc), + "websiteKey": task.captchaParams["sitekey"], + "isInvisible": task.isInvisible(), + }, + }, + ) + else: + try: + with open(fs_encode(task.captchaParams["file"], "rb")) as fp: + data = fp.read() + + except IOError as exc: + self.log_error(exc) + return + + api_data = self.api_request( + "createTask", + { + "clientKey": self.config.get("passkey"), + "softId": 976, + "task": { + "type": "ImageToTextTask", + "body": base64.b64encode(data), + "case": True, + "websiteURL": r"%s://%s/" % (url_p.scheme, url_p.netloc), + }, + }, + ) + if api_data["errorId"] != 0: + task.error = api_data["errorDescription"] + self.log_error(_("API error"), api_data["errorDescription"]) + return + + ticket = api_data["taskId"] + self.log_debug("NewCaptchaID ticket: %s" % ticket, task.captchaParams.get("file", "")) + + task.data["ticket"] = ticket + + result = None + for i in range(int(self.config.get("timeout") / 5)): + api_data = self.api_request( + "getTaskResult", + {"clientKey": self.config.get("passkey"), "taskId": ticket}, + ) + if api_data["errorId"] != 0: + task.error = api_data["errorDescription"] + self.log_error(_("API error"), api_data["errorDescription"]) + break + + if api_data["status"] == "processing": + time.sleep(5) + else: + captcha_plugin = task.captchaParams["captcha_plugin"] + if captcha_plugin in ("HCaptcha", "ReCaptcha"): + result = api_data["solution"]["gRecaptchaResponse"] + + elif captcha_plugin == "Turnstile": + result = api_data["solution"]["token"] + + elif task.isTextual(): + result = api_data["solution"]["text"] + + break + + else: + self.log_debug("Could not get result: %s" % ticket) + + self.log_info(_("Captcha result for ticket %s: %s") % (ticket, result)) + + task.setResult(result) + + def captcha_task(self, task): + if task.isInteractive(): + captcha_plugin = task.captchaParams["captcha_plugin"] + if captcha_plugin == "ReCaptcha" and not self.config.get("solve_recaptcha"): + return + elif captcha_plugin == "HCaptcha" and not self.config.get("solve_hcaptcha"): + return + elif captcha_plugin == "Turnstile" and not self.config.get("solve_turnstile"): + return + + else: + if not task.isTextual(): + return + elif not self.config.get("solve_image"): + return + + if not self.config.get("passkey"): + return + + if self.pyload.isClientConnected() and self.config.get("check_client"): + return + + credits = self.get_credits() + if credits < 0.05: + self.log_error(_("Your captcha anti-captcha.com account has not enough credits")) + return + + timeout = min(max(self.config.get("timeout"), 300), 3999) + task.handler.append(self) + task.setWaiting(timeout) + + self._process_captcha(task) + + def _captcha_response(self, task, correct): + request_type = "correct" if correct else "refund" + + if "ticket" not in task.data: + self.log_debug("No CaptchaID for %s request (task: %s)" % (request_type, task)) + return + + if not self.config.get("refund", False) or correct: + return + + if task.captchaParams["captcha_plugin"] == "ReCaptcha": + method = "reportIncorrectRecaptcha" + elif task.captcha_params["captcha_plugin"] == "Hcaptcha": + method = "reportIncorrectHcaptcha" + elif task.isTextual(): + method = "reportIncorrectImageCaptcha" + else: + return + + for _ in range(3): + api_data = self.api_request( + method, + { + "clientKey": self.config.get("passkey"), + "taskId": task.data["ticket"], + }, + ) + + self.log_debug("Request %s: %s" %(request_type, api_data)) + if api_data["errorId"] == 0: + break + time.sleep(5) + else: + self.log_debug("Could not send %s request: %s" % (request_type, api_data["errorDescription"])) + + def captcha_correct(self, task): + self._captcha_response(task, True) + + def captcha_invalid(self, task): + self._captcha_response(task, False) diff --git a/module/plugins/hooks/AntiStandby.py b/module/plugins/hooks/AntiStandby.py new file mode 100644 index 0000000000..7370cee87e --- /dev/null +++ b/module/plugins/hooks/AntiStandby.py @@ -0,0 +1,160 @@ +# -*- coding: utf-8 -*- + +from __future__ import with_statement + +import os +import subprocess +import sys +import time + +from ..internal.Addon import Addon +from ..internal.misc import Expose, encode, fsjoin + +try: + import caffeine +except ImportError: + pass + + +class Kernel32(object): + ES_AWAYMODE_REQUIRED = 0x00000040 + ES_CONTINUOUS = 0x80000000 + ES_DISPLAY_REQUIRED = 0x00000002 + ES_SYSTEM_REQUIRED = 0x00000001 + ES_USER_PRESENT = 0x00000004 + + +class AntiStandby(Addon): + __name__ = "AntiStandby" + __type__ = "hook" + __version__ = "0.18" + __status__ = "testing" + + __config__ = [("activated", "bool", "Activated", False), + ("hdd", "bool", "Prevent HDD standby", True), + ("system", "bool", "Prevent OS standby", True), + ("display", "bool", "Prevent display standby", False), + ("interval", "int", "HDD touching interval in seconds", 25)] + + __description__ = """Prevent OS, HDD and display standby""" + __license__ = "GPLv3" + __authors__ = [("Walter Purcaro", "vuolter@gmail.com")] + + TMP_FILE = ".antistandby" + + def init(self): + self.pid = None + self.mtime = 0 + + def activate(self): + hdd = self.config.get('hdd') + system = not self.config.get('system') + display = not self.config.get('display') + + if hdd: + self.periodical.start(self.config.get('interval'), threaded=True) + + if os.name == "nt": + self.win_standby(system, display) + + elif sys.platform == "darwin": + self.osx_standby(system, display) + + else: + self.linux_standby(system, display) + + def deactivate(self): + self.remove(self.TMP_FILE, trash=False) + + if os.name == "nt": + self.win_standby(True) + + elif sys.platform == "darwin": + self.osx_standby(True) + + else: + self.linux_standby(True) + + @Expose + def win_standby(self, system=True, display=True): + import ctypes + + set = ctypes.windll.kernel32.SetThreadExecutionState + + if system: + if display: + set(Kernel32.ES_CONTINUOUS) + else: + set(Kernel32.ES_CONTINUOUS | Kernel32.ES_DISPLAY_REQUIRED) + else: + if display: + set(Kernel32.ES_CONTINUOUS | Kernel32.ES_SYSTEM_REQUIRED) + else: + set(Kernel32.ES_CONTINUOUS | Kernel32.ES_SYSTEM_REQUIRED | + Kernel32.ES_DISPLAY_REQUIRED) + + @Expose + def osx_standby(self, system=True, display=True): + try: + if system: + caffeine.off() + else: + caffeine.on(display) + + except NameError: + self.log_warning(_("Unable to change power state"), + _("caffeine lib not found")) + + except Exception, e: + self.log_warning(_("Unable to change power state"), e) + + @Expose + def linux_standby(self, system=True, display=True): + try: + if system: + if self.pid: + self.pid.kill() + + elif not self.pid: + self.pid = subprocess.Popen(["caffeine"]) + + except Exception, e: + self.log_warning(_("Unable to change system power state"), e) + + try: + if display: + subprocess.call(["xset", "+dpms", "s", "default"]) + else: + subprocess.call(["xset", "-dpms", "s", "off"]) + + except Exception, e: + self.log_warning(_("Unable to change display power state"), e) + + @Expose + def touch(self, path): + with open(path, 'w'): + os.utime(path, None) + + self.mtime = time.time() + + @Expose + def max_mtime(self, path): + return max(0, 0, + *(os.path.getmtime(fsjoin(root, file)) + for root, dirs, files in os.walk(encode(path), topdown=False) + for file in files)) + + def periodical_task(self): + if self.config.get('hdd') is False: + return + + if (self.pyload.threadManager.pause or + not self.pyload.api.isTimeDownload() or + not self.pyload.threadManager.getActiveFiles()): + return + + dl_folder = self.pyload.config.get('general', 'download_folder') + if (self.max_mtime(dl_folder) - self.mtime) < self.periodical.interval: + return + + self.touch(self.TMP_FILE) diff --git a/module/plugins/hooks/AntiVirus.py b/module/plugins/hooks/AntiVirus.py new file mode 100644 index 0000000000..a3ef19cfa9 --- /dev/null +++ b/module/plugins/hooks/AntiVirus.py @@ -0,0 +1,140 @@ +# -*- coding: utf-8 -*- + +import os +import shutil +import subprocess + +from ..internal.Addon import Addon +from ..internal.misc import Expose, Popen, exists, fs_encode, fsjoin, threaded + +try: + import send2trash +except ImportError: + pass + + +class AntiVirus(Addon): + __name__ = "AntiVirus" + __type__ = "hook" + __version__ = "0.22" + __status__ = "broken" + + #@TODO: add trash option (use Send2Trash lib) + __config__ = [("activated", "bool", "Activated", False), + ("action", "Antivirus default;Delete;Quarantine", + "Manage infected files", "Antivirus default"), + ("quardir", "folder", "Quarantine folder", ""), + ("deltotrash", "bool", "Move to trash instead delete", True), + ("scanfailed", "bool", "Scan failed downloads", False), + ("avfile", "file", "Antivirus executable", ""), + ("avargs", "str", "Executable arguments", ""), + ("avtarget", "file;folder", "Scan target", "file"), + ("ignore-err", "bool", "Ignore scan errors", False)] + + __description__ = """Scan downloaded files with antivirus program""" + __license__ = "GPLv3" + __authors__ = [("Walter Purcaro", "vuolter@gmail.com")] + + @Expose + @threaded + def scan(self, pyfile, thread): + avfile = fs_encode(self.config.get('avfile')) + avargs = fs_encode(self.config.get('avargs').strip()) + + if not os.path.isfile(avfile): + self.fail(_("Antivirus executable not found")) + + scanfolder = self.config.get('avtarget') == "folder" + + if scanfolder: + dl_folder = self.pyload.config.get("general", "download_folder") + package_folder = pyfile.package().folder if self.pyload.config.get("general", "folder_per_package") else "" + target = fsjoin(dl_folder, package_folder, pyfile.name) + target_repr = "Folder: " + package_folder or dl_folder + else: + target = fs_encode(pyfile.plugin.last_download) + target_repr = "File: " + os.path.basename(pyfile.plugin.last_download) + + if not exists(target): + return + + thread.addActive(pyfile) + pyfile.setCustomStatus(_("virus scanning")) + pyfile.setProgress(0) + + try: + p = Popen([avfile, avargs, target], + bufsize=-1, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + + out, err = map(str.strip, p.communicate()) + + if out: + self.log_info(target_repr, out) + + if err: + self.log_warning(target_repr, err) + if not self.config.get('ignore-err'): + self.log_debug( + "Delete/Quarantine task aborted due scan error") + return + + if p.returncode: + action = self.config.get('action') + + if scanfolder: + if action == "Antivirus default": + self.log_warning( + _("Delete/Quarantine task skipped in folder scan mode")) + return + + pyfile.error = _("Infected file") + + try: + if action == "Delete": + if not self.config.get('deltotrash'): + os.remove(file) + + else: + try: + send2trash.send2trash(file) + + except NameError: + self.log_warning( + _("Send2Trash lib not found, moving to quarantine instead")) + pyfile.setCustomStatus(_("file moving")) + shutil.move(file, self.config.get('quardir')) + + except Exception, e: + self.log_warning( + _("Unable to move file to trash: %s, moving to quarantine instead") % + e.message) + pyfile.setCustomStatus(_("file moving")) + shutil.move(file, self.config.get('quardir')) + + else: + self.log_debug( + "Successfully moved file to trash") + + elif action == "Quarantine": + pyfile.setCustomStatus(_("file moving")) + shutil.move(file, self.config.get('quardir')) + + except (IOError, shutil.Error), e: + self.log_error(target_repr, action + " action failed!", e) + + elif not err: + self.log_debug(target_repr, "No infected file found") + + finally: + pyfile.setProgress(100) + thread.finishFile(pyfile) + + def download_finished(self, pyfile): + return self.scan(pyfile) + + def download_failed(self, pyfile): + #: Check if pyfile is still "failed", maybe might has been restarted in meantime + if pyfile.status == 8 and self.config.get('scanfailed'): + return self.scan(pyfile) diff --git a/module/plugins/hooks/AppriseNotify.py b/module/plugins/hooks/AppriseNotify.py new file mode 100644 index 0000000000..b45595fc45 --- /dev/null +++ b/module/plugins/hooks/AppriseNotify.py @@ -0,0 +1,54 @@ +# -*- coding: utf-8 -*- + +from ..internal.misc import check_module +from ..internal.Notifier import Notifier + + +class AppriseNotify(Notifier): + __name__ = "AppriseNotify" + __type__ = "hook" + __version__ = "0.02" + __status__ = "testing" + + __config__ = [("activated", "bool" , "Activated", False), + ("configs", "string", "Configuration(s) path/URL (comma separated)", ""), + ("title", "string", "Notification title", "pyLoad Notification"), + ("captcha", "bool", "Notify captcha request", True), + ("reconnection", "bool", "Notify reconnection request", True), + ("downloadfinished", "bool", "Notify download finished", True), + ("downloadfailed", "bool" ,"Notify download failed", True), + ("alldownloadsfinished", "bool", "Notify all downloads finished", True), + ("alldownloadsprocessed", "bool", "Notify all downloads processed", True), + ("packagefinished", "bool", "Notify package finished", True), + ("packagefailed", "bool", "Notify package failed", True), + ("update", "bool", "Notify pyload update", False), + ("exit", "bool", "Notify pyload shutdown/restart", False), + ("sendinterval", "int", "Interval in seconds between notifications", 1), + ("sendpermin", "int", "Max notifications per minute", 60), + ("ignoreclient", "bool", "Send notifications if client is connected", True)] + + __description__ = "Send push notifications to apprise." + __license__ = "GPLv3" + __authors__ = [("GammaC0de", "nitzo2001[AT]yahoo[DOT]com")] + + def get_key(self): + return self.config.get("configs").split(',') + + def send(self, event, msg, key): + if not check_module("apprise"): + self.log_error(_("Cannot send notification: apprise is not installed."), + _("Install apprise by issuing 'pip install apprise' command.")) + return + + import apprise + + apprise_obj = apprise.Apprise() + apprise_config = apprise.AppriseConfig() + + for c in key: + apprise_config.add(c) + + apprise_obj.add(apprise_config) + + apprise_obj.notify(title=self.config.get("title"), + body="%s: %s" % (event, msg) if msg else event) diff --git a/module/plugins/hooks/BypassCaptcha.py b/module/plugins/hooks/BypassCaptcha.py index bd718ea7e4..c4209c35ef 100644 --- a/module/plugins/hooks/BypassCaptcha.py +++ b/module/plugins/hooks/BypassCaptcha.py @@ -1,37 +1,19 @@ # -*- 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 . - - @author: RaNaN, Godofdream, zoidberg -""" - -from thread import start_new_thread -from pycurl import FORM_FILE, LOW_SPEED_TIME - -from module.network.RequestFactory import getURL, getRequest +import pycurl from module.network.HTTPRequest import BadHeader +from module.network.RequestFactory import getRequest as get_request -from module.plugins.Hook import Hook - -PYLOAD_KEY = "4f771155b640970d5607f919a615bdefc67e7d32" +from ..internal.Addon import Addon +from ..internal.misc import threaded class BypassCaptchaException(Exception): + def __init__(self, err): self.err = err - def getCode(self): + def get_code(self): return self.err def __str__(self): @@ -41,99 +23,107 @@ def __repr__(self): return "" % self.err -class BypassCaptcha(Hook): +class BypassCaptcha(Addon): __name__ = "BypassCaptcha" - __version__ = "0.04" - __description__ = """send captchas to BypassCaptcha.com""" + __type__ = "hook" + __version__ = "0.14" + __status__ = "testing" + __config__ = [("activated", "bool", "Activated", False), - ("force", "bool", "Force BC even if client is connected", False), - ("passkey", "password", "Passkey", "")] - __author_name__ = ("RaNaN", "Godofdream", "zoidberg") - __author_mail__ = ("RaNaN@pyload.org", "soilfcition@gmail.com", "zoidberg@mujmail.cz") + ("passkey", "password", "Access key", ""), + ("check_client", "bool", "Don't use if client is connected", True)] + + __description__ = """Send captchas to BypassCaptcha.com""" + __license__ = "GPLv3" + __authors__ = [("RaNaN", "RaNaN@pyload.net"), + ("Godofdream", "soilfcition@gmail.com"), + ("zoidberg", "zoidberg@mujmail.cz")] + + PYLOAD_KEY = "4f771155b640970d5607f919a615bdefc67e7d32" SUBMIT_URL = "http://bypasscaptcha.com/upload.php" RESPOND_URL = "http://bypasscaptcha.com/check_value.php" GETCREDITS_URL = "http://bypasscaptcha.com/ex_left.php" - def setup(self): - self.info = {} - - def getCredits(self): - response = getURL(self.GETCREDITS_URL, post={"key": self.getConfig("passkey")}) + def get_credits(self): + res = self.load( + self.GETCREDITS_URL, post={ + 'key': self.config.get('passkey')}) - data = dict([x.split(' ', 1) for x in response.splitlines()]) + data = dict(x.split(' ', 1) for x in res.splitlines()) return int(data['Left']) def submit(self, captcha, captchaType="file", match=None): - req = getRequest() + req = get_request() - #raise timeout threshold - req.c.setopt(LOW_SPEED_TIME, 80) + #: Raise timeout threshold + req.c.setopt(pycurl.LOW_SPEED_TIME, 80) try: - response = req.load(self.SUBMIT_URL, - post={"vendor_key": PYLOAD_KEY, - "key": self.getConfig("passkey"), - "gen_task_id": "1", - "file": (FORM_FILE, captcha)}, - multipart=True) + res = self.load(self.SUBMIT_URL, + post={'vendor_key': self.PYLOAD_KEY, + 'key': self.config.get('passkey'), + 'gen_task_id': "1", + 'file': (pycurl.FORM_FILE, captcha)}, + req=req) finally: req.close() - data = dict([x.split(' ', 1) for x in response.splitlines()]) + data = dict(x.split(' ', 1) for x in res.splitlines()) if not data or "Value" not in data: - raise BypassCaptchaException(response) + raise BypassCaptchaException(res) result = data['Value'] ticket = data['TaskId'] - self.logDebug("result %s : %s" % (ticket, result)) + self.log_debug("Result %s : %s" % (ticket, result)) return ticket, result def respond(self, ticket, success): try: - response = getURL(self.RESPOND_URL, post={"task_id": ticket, "key": self.getConfig("passkey"), - "cv": 1 if success else 0}) + res = self.load(self.RESPOND_URL, post={'task_id': ticket, 'key': self.config.get('passkey'), + 'cv': 1 if success else 0}) except BadHeader, e: - self.logError("Could not send response.", str(e)) + self.log_error(_("Could not send response"), e) - def newCaptchaTask(self, task): + def captcha_task(self, task): if "service" in task.data: return False if not task.isTextual(): return False - if not self.getConfig("passkey"): + if not self.config.get('passkey'): return False - if self.core.isClientConnected() and not self.getConfig("force"): + if self.pyload.isClientConnected() and self.config.get('check_client'): return False - if self.getCredits() > 0: + if self.get_credits() > 0: task.handler.append(self) - task.data['service'] = self.__name__ + task.data['service'] = self.classname task.setWaiting(100) - start_new_thread(self.processCaptcha, (task,)) + self._process_captcha(task) else: - self.logInfo("Your %s account has not enough credits" % self.__name__) + self.log_info(_("Your account has not enough credits")) - def captchaCorrect(self, task): - if task.data['service'] == self.__name__ and "ticket" in task.data: - self.respond(task.data["ticket"], True) + def captcha_correct(self, task): + if task.data['service'] == self.classname and "ticket" in task.data: + self.respond(task.data['ticket'], True) - def captchaInvalid(self, task): - if task.data['service'] == self.__name__ and "ticket" in task.data: - self.respond(task.data["ticket"], False) + def captcha_invalid(self, task): + if task.data['service'] == self.classname and "ticket" in task.data: + self.respond(task.data['ticket'], False) - def processCaptcha(self, task): - c = task.captchaFile + @threaded + def _process_captcha(self, task): + c = task.captchaParams['file'] try: ticket, result = self.submit(c) except BypassCaptchaException, e: - task.error = e.getCode() + task.error = e.get_code() return - task.data["ticket"] = ticket + task.data['ticket'] = ticket task.setResult(result) diff --git a/module/plugins/hooks/Captcha9Kw.py b/module/plugins/hooks/Captcha9Kw.py new file mode 100644 index 0000000000..cb2382db01 --- /dev/null +++ b/module/plugins/hooks/Captcha9Kw.py @@ -0,0 +1,275 @@ +# -*- coding: utf-8 -*- + +from __future__ import with_statement + +import base64 +import re +import time +import urlparse + +from module.network.HTTPRequest import BadHeader + +from ..internal.Addon import Addon +from ..internal.misc import threaded, fs_encode + + +class Captcha9Kw(Addon): + __name__ = "Captcha9Kw" + __type__ = "hook" + __version__ = "0.46" + __status__ = "testing" + + __config__ = [("activated", "bool", "Activated", False), + ("check_client", "bool", "Don't use if client is connected", True), + ("confirm", "bool", "Confirm Captcha (cost +6 credits)", False), + ("captchaperhour", "int", "Captcha per hour", "9999"), + ("captchapermin", "int", "Captcha per minute", "9999"), + ("prio", "int", "Priority (max 10)(cost +0 -> +10 credits)", "0"), + ("queue", "int", "Max. Queue (max 999)", "50"), + ("hoster_options", "str", "Hoster options (format pluginname;prio 1;selfsolve 1;confirm 1;timeout 900|...)", ""), + ("selfsolve", "bool", "Selfsolve (manually solve your captcha in your 9kw client if active)", False), + ("solve_interactive", "bool", "Solve ReCaptcha Interactive (cost 30 credits)", True), + ("passkey", "password", "API key", ""), + ("timeout", "int", "Timeout in seconds (min 60, max 3999)", "900")] + + __description__ = """Send captchas to 9kw.eu""" + __license__ = "GPLv3" + __authors__ = [("RaNaN", "RaNaN@pyload.net"), + ("Walter Purcaro", "vuolter@gmail.com"), + ("GammaC0de", "nitzo2001[AT]yahho[DOT]com")] + + API_URL = "https://www.9kw.eu/index.cgi" + + INTERACTIVE_TYPES = {'ReCaptcha': "recaptchav2", + 'HCaptcha': "hcaptcha"} + + def get_credits(self): + res = self.load(self.API_URL, + get={'apikey': self.config.get('passkey'), + 'pyload': "1", + 'source': "pyload", + 'action': "usercaptchaguthaben"}) + + if res.isdigit(): + self.log_info(_("%s credits left") % res) + credits = self.info['credits'] = int(res) + return credits + else: + self.log_error(res) + return 0 + + @threaded + def _process_captcha(self, task): + pluginname = task.captchaParams['plugin'] + if task.isInteractive() or task.isInvisible(): + url_p = urlparse.urlparse(task.captchaParams['url']) + if url_p.scheme not in ("http", "https"): + self.log_error(_("Invalid url")) + return + + post_data = {'pageurl': "%s://%s/" % (url_p.scheme, url_p.netloc), + 'oldsource': self.INTERACTIVE_TYPES[task.captchaParams['captcha_plugin']], + 'captchachoice': self.INTERACTIVE_TYPES[task.captchaParams['captcha_plugin']], + "isInvisible": "INVISIBLE" if task.isInvisible() else "NORMAL", + 'data-sitekey': task.captchaParams['sitekey'], + 'securetoken': task.captchaParams.get('securetoken', "")} + + else: + try: + with open(fs_encode(task.captchaParams['file']), 'rb') as f: + data = f.read() + + except IOError, e: + self.log_error(e) + return + + post_data = {'file-upload-01': base64.b64encode(data), + 'oldsource': pluginname} + + option = {'min': 2, + 'max': 50, + 'phrase': 0, + 'numeric': 0, + 'case_sensitive': 0, + 'math': 0, + 'prio': min(max(self.config.get('prio'), 0), 10), + 'confirm': self.config.get('confirm'), + 'timeout': min(max(self.config.get('timeout'), 300), 3999), + 'selfsolve': self.config.get('selfsolve'), + 'cph': self.config.get('captchaperhour'), + 'cpm': self.config.get('captchapermin')} + + for opt in [x for x in self.config.get('hoster_options', "").split('|') if x]: + details = map(str.strip, opt.split(';')) + + if not details or details[0].lower() != pluginname.lower(): + continue + + for d in details: + hosteroption = d.split(" ") + + if len(hosteroption) < 2 or not hosteroption[1].isdigit(): + continue + + o = hosteroption[0].lower() + if o in option: + option[o] = hosteroption[1] + + break + + post_data.update({'apikey': self.config.get('passkey'), + 'prio': option['prio'], + 'confirm': option['confirm'], + 'maxtimeout': option['timeout'], + 'selfsolve': option['selfsolve'], + 'captchaperhour': option['cph'], + 'captchapermin': option['cpm'], + 'case-sensitive': option['case_sensitive'], + 'min_len': option['min'], + 'max_len': option['max'], + 'phrase': option['phrase'], + 'numeric': option['numeric'], + 'math': option['math'], + 'pyload': 1, + 'source': "pyload", + 'base64': 0 if task.isInteractive() else 1, + 'mouse': 1 if task.isPositional() else 0, + "interactive": 1 if task.isInteractive() else 0, + 'action': "usercaptchaupload"}) + + for _i in range(5): + try: + res = self.load(self.API_URL, post=post_data) + + except BadHeader, e: + res = e.content + time.sleep(3) + + else: + if res and res.isdigit(): + break + + else: + self.log_error(_("Bad request: %s") % res) + return + + self.log_debug("NewCaptchaID ticket: %s" % res, task.captchaParams.get('file', "")) + + task.data['ticket'] = res + + for _i in range(int(self.config.get('timeout') / 5)): + result = self.load(self.API_URL, + get={'apikey': self.config.get('passkey'), + 'id': res, + 'pyload': "1", + 'info': "1", + 'source': "pyload", + 'action': "usercaptchacorrectdata"}) + + if not result or result == "NO DATA": + time.sleep(5) + else: + break + + else: + self.log_debug("Could not send request: %s" % res) + result = None + + self.log_info(_("Captcha result for ticket %s: %s") % (res, result)) + + task.setResult(result) + + def captcha_task(self, task): + if not hasattr(task, "isInvisible"): + self.log_error(_("Please update pyLoad's core to support invisible captcha")) + task.isInvisible = lambda : False + + if task.isInteractive() or task.isInvisible(): + if task.captchaParams['captcha_plugin'] not in self.INTERACTIVE_TYPES.keys() or self.config.get('solve_interactive') is False: + return + else: + if not task.isTextual() and not task.isPositional(): + return + + if not self.config.get('passkey'): + return + + if self.pyload.isClientConnected() and self.config.get('check_client'): + return + + credits = self.get_credits() + + if credits <= 0: + self.log_error(_("Your captcha 9kw.eu account has not enough credits")) + return + + max_queue = min(self.config.get('queue'), 999) + timeout = min(max(self.config.get('timeout'), 300), 3999) + pluginname = task.captchaParams['plugin'] + + for _i in range(5): + servercheck = self.load("http://www.9kw.eu/grafik/servercheck.txt") + if max_queue > int(re.search(r'queue=(\d+)', servercheck).group(1)): + break + + time.sleep(10) + + else: + self.log_error(_("Too many captchas in queue")) + return + + for opt in [x for x in self.config.get('hoster_options', "").split('|') if x]: + details = [x.strip() for x in opt.split(':')] + + if not details or details[0].lower() != pluginname.lower(): + continue + + for d in details: + hosteroption = d.split("=") + + if len(hosteroption) > 1 and \ + hosteroption[0].lower() == "timeout" and \ + hosteroption[1].isdigit(): + timeout = int(hosteroption[1]) + + break + + task.handler.append(self) + + task.setWaiting(timeout) + + self._process_captcha(task) + + def _captcha_response(self, task, correct): + request_type = "correct" if correct else "refund" + + if 'ticket' not in task.data: + self.log_debug("No CaptchaID for %s request (task: %s)" % (request_type, task)) + return + + passkey = self.config.get('passkey') + + for _i in range(3): + res = self.load(self.API_URL, + get={'action': "usercaptchacorrectback", + 'apikey': passkey, + 'api_key': passkey, + 'correct': "1" if correct else "2", + 'pyload': "1", + 'source': "pyload", + 'id': task.data['ticket']}) + + self.log_debug("Request %s: %s" % (request_type, res)) + + if res == "OK": + break + + time.sleep(5) + else: + self.log_debug("Could not send %s request: %s" % (request_type, res)) + + def captcha_correct(self, task): + self._captcha_response(task, True) + + def captcha_invalid(self, task): + self._captcha_response(task, False) diff --git a/module/plugins/hooks/Captcha9kw.py b/module/plugins/hooks/Captcha9kw.py deleted file mode 100755 index 6a9de24de5..0000000000 --- a/module/plugins/hooks/Captcha9kw.py +++ /dev/null @@ -1,167 +0,0 @@ -# -*- 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 . - - @author: mkaay, RaNaN, zoidberg -""" -from __future__ import with_statement - -from thread import start_new_thread -from base64 import b64encode -import time - -from module.network.RequestFactory import getURL -from module.network.HTTPRequest import BadHeader - -from module.plugins.Hook import Hook - - -class Captcha9kw(Hook): - __name__ = "Captcha9kw" - __version__ = "0.09" - __description__ = """send captchas to 9kw.eu""" - __config__ = [("activated", "bool", "Activated", False), - ("force", "bool", "Force CT even if client is connected", True), - ("https", "bool", "Enable HTTPS", "False"), - ("confirm", "bool", "Confirm Captcha (Cost +6)", "False"), - ("captchaperhour", "int", "Captcha per hour (max. 9999)", "9999"), - ("prio", "int", "Prio 1-10 (Cost +1-10)", "0"), - ("selfsolve", "bool", - "If enabled and you have a 9kw client active only you will get your captcha to solve it (Selfsolve)", - "False"), - ("timeout", "int", "Timeout (max. 300)", "300"), - ("passkey", "password", "API key", ""), ] - __author_name__ = ("RaNaN") - __author_mail__ = ("RaNaN@pyload.org") - - API_URL = "://www.9kw.eu/index.cgi" - - def setup(self): - self.API_URL = "https" + self.API_URL if self.getConfig("https") else "http" + self.API_URL - self.info = {} - - def getCredits(self): - response = getURL(self.API_URL, get={"apikey": self.getConfig("passkey"), "pyload": "1", "source": "pyload", - "action": "usercaptchaguthaben"}) - - if response.isdigit(): - self.logInfo(_("%s credits left") % response) - self.info["credits"] = credits = int(response) - return credits - else: - self.logError(response) - return 0 - - def processCaptcha(self, task): - result = None - - with open(task.captchaFile, 'rb') as f: - data = f.read() - data = b64encode(data) - self.logDebug("%s : %s" % (task.captchaFile, data)) - if task.isPositional(): - mouse = 1 - else: - mouse = 0 - - response = getURL(self.API_URL, post={ - "apikey": self.getConfig("passkey"), - "prio": self.getConfig("prio"), - "confirm": self.getConfig("confirm"), - "captchaperhour": self.getConfig("captchaperhour"), - "maxtimeout": self.getConfig("timeout"), - "selfsolve": self.getConfig("selfsolve"), - "pyload": "1", - "source": "pyload", - "base64": "1", - "mouse": mouse, - "file-upload-01": data, - "action": "usercaptchaupload"}) - - if response.isdigit(): - self.logInfo(_("New CaptchaID from upload: %s : %s") % (response, task.captchaFile)) - - for i in range(1, 100, 1): - response2 = getURL(self.API_URL, get={"apikey": self.getConfig("passkey"), "id": response, - "pyload": "1", "source": "pyload", - "action": "usercaptchacorrectdata"}) - - if response2 != "": - break - - time.sleep(3) - - result = response2 - task.data["ticket"] = response - self.logInfo("result %s : %s" % (response, result)) - task.setResult(result) - else: - self.logError("Bad upload: %s" % response) - return False - - def newCaptchaTask(self, task): - if not task.isTextual() and not task.isPositional(): - return False - - if not self.getConfig("passkey"): - return False - - if self.core.isClientConnected() and not self.getConfig("force"): - return False - - if self.getCredits() > 0: - task.handler.append(self) - task.setWaiting(self.getConfig("timeout")) - start_new_thread(self.processCaptcha, (task,)) - - else: - self.logError(_("Your Captcha 9kw.eu Account has not enough credits")) - - def captchaCorrect(self, task): - if "ticket" in task.data: - - try: - response = getURL(self.API_URL, - post={"action": "usercaptchacorrectback", - "apikey": self.getConfig("passkey"), - "api_key": self.getConfig("passkey"), - "correct": "1", - "pyload": "1", - "source": "pyload", - "id": task.data["ticket"]}) - self.logInfo("Request correct: %s" % response) - - except BadHeader, e: - self.logError("Could not send correct request.", str(e)) - else: - self.logError("No CaptchaID for correct request (task %s) found." % task) - - def captchaInvalid(self, task): - if "ticket" in task.data: - - try: - response = getURL(self.API_URL, - post={"action": "usercaptchacorrectback", - "apikey": self.getConfig("passkey"), - "api_key": self.getConfig("passkey"), - "correct": "2", - "pyload": "1", - "source": "pyload", - "id": task.data["ticket"]}) - self.logInfo("Request refund: %s" % response) - - except BadHeader, e: - self.logError("Could not send refund request.", str(e)) - else: - self.logError("No CaptchaID for not correct request (task %s) found." % task) diff --git a/module/plugins/hooks/CaptchaBrotherhood.py b/module/plugins/hooks/CaptchaBrotherhood.py deleted file mode 100644 index 69af967050..0000000000 --- a/module/plugins/hooks/CaptchaBrotherhood.py +++ /dev/null @@ -1,167 +0,0 @@ -# -*- 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 . - - @author: mkaay, RaNaN, zoidberg -""" -from __future__ import with_statement - -from thread import start_new_thread - -import pycurl -import StringIO -from urllib import urlencode -from time import sleep -import Image - -from module.network.RequestFactory import getURL, getRequest -from module.plugins.Hook import Hook - - -class CaptchaBrotherhoodException(Exception): - def __init__(self, err): - self.err = err - - def getCode(self): - return self.err - - def __str__(self): - return "" % self.err - - def __repr__(self): - return "" % self.err - - -class CaptchaBrotherhood(Hook): - __name__ = "CaptchaBrotherhood" - __version__ = "0.04" - __description__ = """send captchas to CaptchaBrotherhood.com""" - __config__ = [("activated", "bool", "Activated", False), - ("username", "str", "Username", ""), - ("force", "bool", "Force CT even if client is connected", False), - ("passkey", "password", "Password", "")] - __author_name__ = ("RaNaN", "zoidberg") - __author_mail__ = ("RaNaN@pyload.org", "zoidberg@mujmail.cz") - - API_URL = "http://www.captchabrotherhood.com/" - - def setup(self): - self.info = {} - - def getCredits(self): - response = getURL(self.API_URL + "askCredits.aspx", - get={"username": self.getConfig("username"), "password": self.getConfig("passkey")}) - if not response.startswith("OK"): - raise CaptchaBrotherhoodException(response) - else: - credits = int(response[3:]) - self.logInfo(_("%d credits left") % credits) - self.info["credits"] = credits - return credits - - def submit(self, captcha, captchaType="file", match=None): - try: - img = Image.open(captcha) - output = StringIO.StringIO() - self.logDebug("CAPTCHA IMAGE", img, img.format, img.mode) - if img.format in ("GIF", "JPEG"): - img.save(output, img.format) - else: - if img.mode != "RGB": - img = img.convert("RGB") - img.save(output, "JPEG") - data = output.getvalue() - output.close() - except Exception, e: - raise CaptchaBrotherhoodException("Reading or converting captcha image failed: %s" % e) - - req = getRequest() - - url = "%ssendNewCaptcha.aspx?%s" % (self.API_URL, - urlencode({"username": self.getConfig("username"), - "password": self.getConfig("passkey"), - "captchaSource": "pyLoad", - "timeout": "80"})) - - req.c.setopt(pycurl.URL, url) - req.c.setopt(pycurl.POST, 1) - req.c.setopt(pycurl.POSTFIELDS, data) - req.c.setopt(pycurl.HTTPHEADER, ["Content-Type: text/html"]) - - try: - req.c.perform() - response = req.getResponse() - except Exception, e: - raise CaptchaBrotherhoodException("Submit captcha image failed") - - req.close() - - if not response.startswith("OK"): - raise CaptchaBrotherhoodException(response[1]) - - ticket = response[3:] - - for i in range(15): - sleep(5) - response = self.get_api("askCaptchaResult", ticket) - if response.startswith("OK-answered"): - return ticket, response[12:] - - raise CaptchaBrotherhoodException("No solution received in time") - - def get_api(self, api, ticket): - response = getURL("%s%s.aspx" % (self.API_URL, api), - get={"username": self.getConfig("username"), - "password": self.getConfig("passkey"), - "captchaID": ticket}) - if not response.startswith("OK"): - raise CaptchaBrotherhoodException("Unknown response: %s" % response) - - return response - - def newCaptchaTask(self, task): - if "service" in task.data: - return False - - if not task.isTextual(): - return False - - if not self.getConfig("username") or not self.getConfig("passkey"): - return False - - if self.core.isClientConnected() and not self.getConfig("force"): - return False - - if self.getCredits() > 10: - task.handler.append(self) - task.data['service'] = self.__name__ - task.setWaiting(100) - start_new_thread(self.processCaptcha, (task,)) - else: - self.logInfo("Your CaptchaBrotherhood Account has not enough credits") - - def captchaInvalid(self, task): - if task.data['service'] == self.__name__ and "ticket" in task.data: - response = self.get_api("complainCaptcha", task.data['ticket']) - - def processCaptcha(self, task): - c = task.captchaFile - try: - ticket, result = self.submit(c) - except CaptchaBrotherhoodException, e: - task.error = e.getCode() - return - - task.data["ticket"] = ticket - task.setResult(result) diff --git a/module/plugins/hooks/CaptchaTrader.py b/module/plugins/hooks/CaptchaTrader.py deleted file mode 100644 index 51bb75a17c..0000000000 --- a/module/plugins/hooks/CaptchaTrader.py +++ /dev/null @@ -1,156 +0,0 @@ -# -*- 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 . - - @author: mkaay, RaNaN -""" - -from thread import start_new_thread -from pycurl import FORM_FILE, LOW_SPEED_TIME - -from module.common.json_layer import json_loads -from module.network.RequestFactory import getURL, getRequest -from module.network.HTTPRequest import BadHeader -from module.plugins.Hook import Hook - -PYLOAD_KEY = "9f65e7f381c3af2b076ea680ae96b0b7" - - -class CaptchaTraderException(Exception): - def __init__(self, err): - self.err = err - - def getCode(self): - return self.err - - def __str__(self): - return "" % self.err - - def __repr__(self): - return "" % self.err - - -class CaptchaTrader(Hook): - __name__ = "CaptchaTrader" - __version__ = "0.16" - __description__ = """send captchas to captchatrader.com""" - __config__ = [("activated", "bool", "Activated", False), - ("username", "str", "Username", ""), - ("force", "bool", "Force CT even if client is connected", False), - ("passkey", "password", "Password", ""), ] - __author_name__ = ("RaNaN") - __author_mail__ = ("RaNaN@pyload.org") - - SUBMIT_URL = "http://api.captchatrader.com/submit" - RESPOND_URL = "http://api.captchatrader.com/respond" - GETCREDITS_URL = "http://api.captchatrader.com/get_credits/username:%(user)s/password:%(password)s/" - - def setup(self): - self.info = {} - - def getCredits(self): - json = getURL(CaptchaTrader.GETCREDITS_URL % {"user": self.getConfig("username"), - "password": self.getConfig("passkey")}) - response = json_loads(json) - if response[0] < 0: - raise CaptchaTraderException(response[1]) - else: - self.logInfo(_("%s credits left") % response[1]) - self.info["credits"] = response[1] - return response[1] - - def submit(self, captcha, captchaType="file", match=None): - if not PYLOAD_KEY: - raise CaptchaTraderException("No API Key Specified!") - - #if type(captcha) == str and captchaType == "file": - # raise CaptchaTraderException("Invalid Type") - assert captchaType in ("file", "url-jpg", "url-jpeg", "url-png", "url-bmp") - - req = getRequest() - - #raise timeout threshold - req.c.setopt(LOW_SPEED_TIME, 80) - - try: - json = req.load(CaptchaTrader.SUBMIT_URL, post={"api_key": PYLOAD_KEY, - "username": self.getConfig("username"), - "password": self.getConfig("passkey"), - "value": (FORM_FILE, captcha), - "type": captchaType}, multipart=True) - finally: - req.close() - - response = json_loads(json) - if response[0] < 0: - raise CaptchaTraderException(response[1]) - - ticket = response[0] - result = response[1] - self.logDebug("result %s : %s" % (ticket, result)) - - return ticket, result - - def respond(self, ticket, success): - try: - json = getURL(CaptchaTrader.RESPOND_URL, post={"is_correct": 1 if success else 0, - "username": self.getConfig("username"), - "password": self.getConfig("passkey"), - "ticket": ticket}) - - response = json_loads(json) - if response[0] < 0: - raise CaptchaTraderException(response[1]) - - except BadHeader, e: - self.logError(_("Could not send response."), str(e)) - - def newCaptchaTask(self, task): - if not task.isTextual(): - return False - - if not self.getConfig("username") or not self.getConfig("passkey"): - return False - - if self.core.isClientConnected() and not self.getConfig("force"): - return False - - if self.getCredits() > 10: - task.handler.append(self) - task.setWaiting(100) - start_new_thread(self.processCaptcha, (task,)) - - else: - self.logInfo(_("Your CaptchaTrader Account has not enough credits")) - - def captchaCorrect(self, task): - if "ticket" in task.data: - ticket = task.data["ticket"] - self.respond(ticket, True) - - def captchaInvalid(self, task): - if "ticket" in task.data: - ticket = task.data["ticket"] - self.respond(ticket, False) - - def processCaptcha(self, task): - c = task.captchaFile - try: - ticket, result = self.submit(c) - except CaptchaTraderException, e: - task.error = e.getCode() - return - - task.data["ticket"] = ticket - task.setResult(result) diff --git a/module/plugins/hooks/Checksum.py b/module/plugins/hooks/Checksum.py index 7f2f63e8f5..12a4cb9035 100644 --- a/module/plugins/hooks/Checksum.py +++ b/module/plugins/hooks/Checksum.py @@ -1,185 +1,351 @@ # -*- 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. +from __future__ import with_statement - 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. +import hashlib +import os +import re +import time +import zlib +from threading import Event - You should have received a copy of the GNU General Public License - along with this program; if not, see . +from ..internal.Addon import Addon +from ..internal.misc import format_time, fs_encode, fsjoin, threaded - @author: zoidberg -""" -from __future__ import with_statement -import hashlib -import zlib -from os import remove -from os.path import getsize, isfile, splitext -import re +def compute_checksum(local_file, algorithm, progress_notify=None, abort=None): + file_size = os.stat(local_file).st_size + processed = 0 + if progress_notify: + progress_notify(0) + + try: + if algorithm in getattr(hashlib, "algorithms", ("md5", "sha1", "sha224", "sha256", "sha384", "sha512")): + h = getattr(hashlib, algorithm)() + + with open(fs_encode(local_file), 'rb') as f: + for chunk in iter(lambda: f.read(128 * h.block_size), ''): + if abort and abort(): + return False -from module.utils import save_join, fs_encode -from module.plugins.Hook import Hook + h.update(chunk) + processed += len(chunk) + if progress_notify: + progress_notify(processed * 100 / file_size) -def computeChecksum(local_file, algorithm): - if algorithm in getattr(hashlib, "algorithms", ("md5", "sha1", "sha224", "sha256", "sha384", "sha512")): - h = getattr(hashlib, algorithm)() + return h.hexdigest() - with open(local_file, 'rb') as f: - for chunk in iter(lambda: f.read(128 * h.block_size), b''): - h.update(chunk) + elif algorithm in ("adler32", "crc32"): + hf = getattr(zlib, algorithm) + last = 0 - return h.hexdigest() + with open(fs_encode(local_file), 'rb') as f: + for chunk in iter(lambda: f.read(8192), ''): + if abort and abort(): + return False - elif algorithm in ("adler32", "crc32"): - hf = getattr(zlib, algorithm) - last = 0 + last = hf(chunk, last) + processed += len(chunk) - with open(local_file, 'rb') as f: - for chunk in iter(lambda: f.read(8192), b''): - last = hf(chunk, last) + if progress_notify: + progress_notify(processed * 100 / file_size) - return "%x" % last + return "%x" % ((2**32 + last) & 0xFFFFFFFF) #: zlib sometimes return negative value - else: - return None + else: + return None + + finally: + if progress_notify: + progress_notify(100) -class Checksum(Hook): +class Checksum(Addon): __name__ = "Checksum" - __version__ = "0.12" - __description__ = "Verify downloaded file size and checksum" + __type__ = "hook" + __version__ = "0.36" + __status__ = "testing" + __config__ = [("activated", "bool", "Activated", False), + ("check_checksum", "bool", "Check checksum? (If False only size will be verified)", True), ("check_action", "fail;retry;nothing", "What to do if check fails?", "retry"), ("max_tries", "int", "Number of retries", 2), ("retry_action", "fail;nothing", "What to do if all retries fail?", "fail"), ("wait_time", "int", "Time to wait before each retry (seconds)", 1)] - __author_name__ = ("zoidberg", "Walter Purcaro") - __author_mail__ = ("zoidberg@mujmail.cz", "vuolter@gmail.com") - methods = {'sfv': 'crc32', 'crc': 'crc32', 'hash': 'md5'} - regexps = {'sfv': r'^(?P[^;].+)\s+(?P[0-9A-Fa-f]{8})$', - 'md5': r'^(?P[0-9A-Fa-f]{32}) (?P.+)$', - 'crc': r'filename=(?P.+)\nsize=(?P\d+)\ncrc32=(?P[0-9A-Fa-f]{8})$', - 'default': r'^(?P[0-9A-Fa-f]+)\s+\*?(?P.+)$'} + __description__ = """Verify downloaded file size and checksum""" + __license__ = "GPLv3" + __authors__ = [("zoidberg", "zoidberg@mujmail.cz"), + ("Walter Purcaro", "vuolter@gmail.com"), + ("stickell", "l.stickell@yahoo.it"), + ("GammaC0de", "nitzo2001[AT]yahoo[DOT]com")] - def coreReady(self): - if not self.config['general']['checksum']: - self.logInfo("Checksum validation is disabled in general configuration") + _methodmap = {'sfv': 'crc32', + 'crc': 'crc32', + 'hash': 'md5'} - def setup(self): + _regexmap = {'sfv': r'^(?P[^;].+)\s+(?P[0-9A-Fa-f]{8})$', + 'md5': r'^(?P[0-9A-Fa-f]{32}) (?P.+)$', + 'crc': r'filename=(?P.+)\nsize=(?P\d+)\ncrc32=(?P[0-9A-Fa-f]{8})$', + 'default': r'^(?P[0-9A-Fa-f]+)\s+\*?(?P.+)$'} + + def activate(self): + if not self.config.get('check_checksum'): + self.log_info(_("Checksum validation is disabled in plugin configuration")) + + def init(self): self.algorithms = sorted( getattr(hashlib, "algorithms", ("md5", "sha1", "sha224", "sha256", "sha384", "sha512")), reverse=True) + self.algorithms.extend(["crc32", "adler32"]) - self.formats = self.algorithms + ['sfv', 'crc', 'hash'] - def downloadFinished(self, pyfile): + self.formats = self.algorithms + ["sfv", "crc", "hash"] + + self.retries = {} + + def download_finished(self, pyfile): """ Compute checksum for the downloaded file and compare it with the hash provided by the hoster. pyfile.plugin.check_data should be a dictionary which can contain: - a) if known, the exact filesize in bytes (e.g. "size": 123456789) - b) hexadecimal hash string with algorithm name as key (e.g. "md5": "d76505d0869f9f928a17d42d66326307") + a) if known, the exact filesize in bytes (e.g. 'size': 123456789) + b) hexadecimal hash string with algorithm name as key (e.g. 'md5': "d76505d0869f9f928a17d42d66326307") """ - if hasattr(pyfile.plugin, "check_data") and (isinstance(pyfile.plugin.check_data, dict)): + if hasattr(pyfile.plugin, "check_data") and isinstance(pyfile.plugin.check_data, dict): data = pyfile.plugin.check_data.copy() - elif hasattr(pyfile.plugin, "api_data") and (isinstance(pyfile.plugin.api_data, dict)): + + elif hasattr(pyfile.plugin, "api_data") and isinstance(pyfile.plugin.api_data, dict): data = pyfile.plugin.api_data.copy() + + elif hasattr(pyfile.plugin, "info") and isinstance(pyfile.plugin.info, dict): + data = pyfile.plugin.info.copy() + # @NOTE: Don't check file size until a similary matcher will be implemented + data.pop('size', None) + else: return - self.logDebug(data) + pyfile.setStatus("processing") - if not pyfile.plugin.lastDownload: - self.checkFailed(pyfile, None, "No file downloaded") + if not pyfile.plugin.last_download: + self.check_failed(pyfile, None, "No file downloaded") - local_file = fs_encode(pyfile.plugin.lastDownload) - #download_folder = self.config['general']['download_folder'] - #local_file = fs_encode(save_join(download_folder, pyfile.package().folder, pyfile.name)) + local_file = fs_encode(pyfile.plugin.last_download) + # dl_folder = self.pyload.config.get("general", "download_folder") + # local_file = fsjoin(dl_folder, pyfile.package().folder, pyfile.name) - if not isfile(local_file): - self.checkFailed(pyfile, None, "File does not exist") + if not os.path.isfile(local_file): + self.check_failed(pyfile, None, "File does not exist") - # validate file size + #: Validate file size if "size" in data: api_size = int(data['size']) - file_size = getsize(local_file) + file_size = os.path.getsize(local_file) + if api_size != file_size: - self.logWarning("File %s has incorrect size: %d B (%d expected)" % (pyfile.name, file_size, api_size)) - self.checkFailed(pyfile, local_file, "Incorrect file size") - del data['size'] + self.log_warning(_("File %s has incorrect size: %d B (%d expected)") % + (pyfile.name, file_size, api_size)) + self.check_failed(pyfile, local_file, "Incorrect file size") - # validate checksum - if data and self.config['general']['checksum']: - if "checksum" in data: - data['md5'] = data['checksum'] + data.pop('size', None) + + self.log_debug(data) + #: Validate checksum + if data and self.config.get('check_checksum'): + data['hash'] = data.get('hash', {}) for key in self.algorithms: - if key in data: - checksum = computeChecksum(local_file, key.replace("-", "").lower()) - if checksum: - if checksum == data[key].lower(): - self.logInfo('File integrity of "%s" verified by %s checksum (%s).' % - (pyfile.name, key.upper(), checksum)) - break + if data.get(key) and not key in data['hash']: + data['hash'][key] = data[key] + break + + if len(data['hash']) > 0: + for key in self.algorithms: + if key in data['hash']: + pyfile.setCustomStatus(_("checksum verifying")) + try: + checksum = compute_checksum(local_file, + key.replace("-", "").lower(), + progress_notify=pyfile.setProgress, + abort=lambda: pyfile.abort) + finally: + pyfile.setStatus("processing") + + if checksum is False: + continue + + elif checksum is not None: + if checksum.lower() == data['hash'][key].lower(): + self.log_info(_('File integrity of "%s" verified by %s checksum (%s)') % + (pyfile.name, key.upper(), checksum.lower())) + pyfile.error = _("checksum verified") + break + + else: + self.log_warning(_("%s checksum for file %s does not match (%s != %s)") % + (key.upper(), pyfile.name, checksum, data['hash'][key].lower())) + + self.check_failed(pyfile, local_file, "Checksums do not match") + else: - self.logWarning("%s checksum for file %s does not match (%s != %s)" % - (key.upper(), pyfile.name, checksum, data[key])) - self.checkFailed(pyfile, local_file, "Checksums do not match") - else: - self.logWarning("Unsupported hashing algorithm: %s" % key.upper()) - else: - self.logWarning("Unable to validate checksum for file %s" % pyfile.name) + self.log_warning(_("Unsupported hashing algorithm"), key.upper()) - def checkFailed(self, pyfile, local_file, msg): - check_action = self.getConfig("check_action") + else: + self.log_warning(_('Unable to validate checksum for file: "%s"') % pyfile.name) + + def check_failed(self, pyfile, local_file, msg): + check_action = self.config.get('check_action') if check_action == "retry": - max_tries = self.getConfig("max_tries") - retry_action = self.getConfig("retry_action") - if pyfile.plugin.retries < max_tries: + max_tries = self.config.get('max_tries') + retry_action = self.config.get('retry_action') + if all(_r < max_tries for _id, _r in pyfile.plugin.retries.items()): if local_file: - remove(local_file) - pyfile.plugin.retry(reason=msg, max_tries=max_tries, wait_time=self.getConfig("wait_time")) + os.remove(local_file) + + pyfile.plugin.retry(max_tries, self.config.get('wait_time'), msg) + elif retry_action == "nothing": return + elif check_action == "nothing": return - pyfile.plugin.fail(reason=msg) - def packageFinished(self, pypack): - download_folder = save_join(self.config['general']['download_folder'], pypack.folder, "") + os.remove(local_file) + pyfile.plugin.fail(msg) + + def package_finished(self, pypack): + event_finished = Event() + self.verify_package(pypack, event_finished) + event_finished.wait() #: Postpone `all_downloads_processed` event until we actually finish + + @threaded + def verify_package(self, pypack, event_finished, thread=None): + try: + dl_folder = fsjoin( + self.pyload.config.get( + "general", + "download_folder"), + pypack.folder, + "") + + pdata = pypack.getChildren().items() + files_ids = dict([(fdata['name'], fdata['id']) for fid, fdata in pdata]) + failed_queue = [] + for fid, fdata in pdata: + file_type = os.path.splitext(fdata['name'])[1][1:].lower() + + if file_type not in self.formats: + continue + + hash_file = fsjoin(dl_folder, fdata['name']) + if not os.path.isfile(hash_file): + self.log_warning(_("File not found"), fdata['name']) + continue + + with open(hash_file) as f: + text = f.read() + + failed = [] + for m in re.finditer(self._regexmap.get(file_type, self._regexmap['default']), text, re.M): + data = m.groupdict() + self.log_debug(fdata['name'], data) + + local_file = fsjoin(dl_folder, data['NAME']) + algorithm = self._methodmap.get(file_type, file_type) + + pyfile = None + fid = files_ids.get(data['NAME'], None) + if fid is not None: + pyfile = self.pyload.files.getFile(fid) + pyfile.setCustomStatus(_("checksum verifying")) + thread.addActive(pyfile) + try: + checksum = compute_checksum(local_file, algorithm, + progress_notify=pyfile.setProgress, + abort=lambda: pyfile.abort) + finally: + thread.finishFile(pyfile) - for link in pypack.getChildren().itervalues(): - file_type = splitext(link["name"])[1][1:].lower() - #self.logDebug(link, file_type) + else: + checksum = compute_checksum(local_file, algorithm) - if file_type not in self.formats: - continue + if checksum is False: + continue - hash_file = fs_encode(save_join(download_folder, link["name"])) - if not isfile(hash_file): - self.logWarning("File not found: %s" % link["name"]) - continue + elif checksum is not None: + if checksum.lower() == data['HASH'].lower(): + self.retries.pop(fid, 0) + self.log_info(_('File integrity of "%s" verified by %s checksum (%s)') % + (data['NAME'], algorithm, checksum)) - with open(hash_file) as f: - text = f.read() + if pyfile is not None: + pyfile.error = _("checksum verified") + pyfile.setStatus("finished") + pyfile.release() - for m in re.finditer(self.regexps.get(file_type, self.regexps['default']), text): - data = m.groupdict() - self.logDebug(link["name"], data) + else: + self.log_warning(_("%s checksum for file %s does not match (%s != %s)") % + (algorithm.upper(), data['NAME'], checksum.lower(), data['HASH'].lower())) + + if fid is not None: + failed.append((fid, local_file)) + else: + self.log_warning(_("Unsupported hashing algorithm"), algorithm.upper()) + + if failed: + failed_queue.extend(failed) - local_file = fs_encode(save_join(download_folder, data["name"])) - algorithm = self.methods.get(file_type, file_type) - checksum = computeChecksum(local_file, algorithm) - if checksum == data["hash"]: - self.logInfo('File integrity of "%s" verified by %s checksum (%s).' % - (data["name"], algorithm, checksum)) else: - self.logWarning("%s checksum for file %s does not match (%s != %s)" % - (algorithm, data["name"], checksum, data["hash"])) + self.log_info(_('All files specified by "%s" verified successfully') % fdata['name']) + + if failed_queue: + self.package_check_failed(failed_queue, thread, "Checksums do not match") + + finally: + event_finished.set() + + @threaded + def package_check_failed(self, failed_queue, parent_thread, msg): + parent_thread.join() #: wait for calling thread to finish + time.sleep(1) + + check_action = self.config.get('check_action') + retry_action = self.config.get('retry_action') + + for fid, local_file in failed_queue: + pyfile = self.pyload.files.getFile(fid) + try: + if check_action == "retry": + retry_count = self.retries.get(fid, 0) + max_tries = self.config.get('max_tries') + if retry_count < max_tries: + if local_file: + os.remove(local_file) + + self.retries[fid] = retry_count + 1 + + wait_time = self.config.get('wait_time') + self.log_info(_("Waiting %s...") % format_time(wait_time)) + time.sleep(wait_time) + + pyfile.package().setFinished = False #: Force `package_finished` event again + self.pyload.files.restartFile(fid) + continue + + else: + self.retries.pop(fid, 0) + if retry_action == "nothing": + continue + + else: + self.retries.pop(fid, 0) + if check_action == "nothing": + continue + + os.remove(local_file) + + pyfile.error = msg + pyfile.setStatus("failed") + + finally: + pyfile.release() diff --git a/module/plugins/hooks/ClickAndLoad.py b/module/plugins/hooks/ClickAndLoad.py deleted file mode 100644 index 0fc78abfe0..0000000000 --- a/module/plugins/hooks/ClickAndLoad.py +++ /dev/null @@ -1,90 +0,0 @@ -# -*- 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 . - - @author: RaNaN - @interface-version: 0.2 -""" - -import socket -import thread - -from module.plugins.Hook import Hook - - -class ClickAndLoad(Hook): - __name__ = "ClickAndLoad" - __version__ = "0.22" - __description__ = """Gives abillity to use jd's click and load. depends on webinterface""" - __config__ = [("activated", "bool", "Activated", "True"), - ("extern", "bool", "Allow external link adding", "False")] - __author_name__ = ("RaNaN", "mkaay") - __author_mail__ = ("RaNaN@pyload.de", "mkaay@mkaay.de") - - def coreReady(self): - self.port = int(self.config['webinterface']['port']) - if self.config['webinterface']['activated']: - try: - if self.getConfig("extern"): - ip = "0.0.0.0" - else: - ip = "127.0.0.1" - - thread.start_new_thread(proxy, (self, ip, self.port, 9666)) - except: - self.logError("ClickAndLoad port already in use.") - - -def proxy(self, *settings): - thread.start_new_thread(server, (self,) + settings) - lock = thread.allocate_lock() - lock.acquire() - lock.acquire() - - -def server(self, *settings): - try: - dock_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - dock_socket.bind((settings[0], settings[2])) - dock_socket.listen(5) - while True: - client_socket = dock_socket.accept()[0] - server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - server_socket.connect(("127.0.0.1", settings[1])) - thread.start_new_thread(forward, (client_socket, server_socket)) - thread.start_new_thread(forward, (server_socket, client_socket)) - except socket.error, e: - if hasattr(e, "errno"): - errno = e.errno - else: - errno = e.args[0] - - if errno == 98: - self.logWarning(_("Click'N'Load: Port 9666 already in use")) - return - thread.start_new_thread(server, (self,) + settings) - except: - thread.start_new_thread(server, (self,) + settings) - - -def forward(source, destination): - string = ' ' - while string: - string = source.recv(1024) - if string: - destination.sendall(string) - else: - #source.shutdown(socket.SHUT_RD) - destination.shutdown(socket.SHUT_WR) diff --git a/module/plugins/hooks/ClickNLoad.py b/module/plugins/hooks/ClickNLoad.py new file mode 100644 index 0000000000..10b931a940 --- /dev/null +++ b/module/plugins/hooks/ClickNLoad.py @@ -0,0 +1,193 @@ +# -*- coding: utf-8 -*- + +import socket +import threading +import time + +from ..internal.Addon import Addon +from ..internal.misc import forward, lock, threaded + +try: + import ssl +except ImportError: + pass + + +def resolve_host(host): + try: + IPs = [ + result [4][0] + for result in socket.getaddrinfo(host, None, family=0, type=socket.SOCK_STREAM) + ] + except socket.gaierror: + IPs = [] + + return IPs + + +#@TODO: IPv6 support +class ClickNLoad(Addon): + __name__ = "ClickNLoad" + __type__ = "hook" + __version__ = "0.63" + __status__ = "testing" + + __config__ = [("activated", "bool", "Activated", True), + ("port", "int", "Port", 9666), + ("extern", "bool", "Listen for external connections", True), + ("dest", "queue;collector", "Add packages to", "collector"), + ("hosts_filter", "str", "allowed source hosts (e.g. host:mycomputer.ddns.com;ip:127.0.0.1", "")] + + __description__ = """Click'n'Load support""" + __license__ = "GPLv3" + __authors__ = [("RaNaN", "RaNaN@pyload.de"), + ("Walter Purcaro", "vuolter@gmail.com"), + ("GammaC0de", "nitzo2001[AT]yahoo[DOT]com")] + + def init(self): + self.cnl_ip = "" if self.config.get('extern') else "127.0.0.1" + self.cnl_port = self.config.get('port') + self.web_ip = "127.0.0.1" if any(_ip == self.pyload.config.get('webinterface', 'host') for _ip in ("0.0.0.0", "")) \ + else self.pyload.config.get('webinterface', 'host') + self.web_port = self.pyload.config.get('webinterface', 'port') + + self.server_running = False + self.do_exit = False + self.exit_done = threading.Event() + + def activate(self): + if not self.pyload.config.get('webinterface', 'activated'): + self.log_warning( + _("pyLoad's Web interface is not active, ClickNLoad cannot start")) + return + + self.pyload.scheduler.addJob(5, self.proxy, threaded=False) + + def deactivate(self): + if self.server_running: + self.log_info(_("Shutting down proxy...")) + + self.do_exit = True + + try: + wakeup_socket = socket.socket( + socket.AF_INET, socket.SOCK_STREAM) + wakeup_socket.connect( + ("127.0.0.1" if any( + _ip == self.cnl_ip for _ip in ( + "0.0.0.0", + "")) else self.cnl_ip, + self.cnl_port)) + wakeup_socket.close() + except Exception: + pass + + self.exit_done.wait(10) + if self.exit_done.isSet(): + self.log_debug("Server exited successfully") + else: + self.log_warning( + _("Server was not exited gracefully, shutdown forced")) + + @lock + @threaded + def forward(self, source, destination, queue=False): + if queue: + old_ids = set(pack.pid for pack in self.pyload.api.getCollector()) + + forward(source, destination) + + if queue: + new_ids = set(pack.pid for pack in self.pyload.api.getCollector()) + for id in new_ids - old_ids: + self.pyload.api.pushToQueue(id) + + @threaded + def proxy(self): + self.log_info( + _("Proxy listening on %s:%s") % + (self.cnl_ip or "0.0.0.0", self.cnl_port)) + self._server() + + @threaded + def _server(self): + try: + self.exit_done.clear() + self.server_running = True + + dock_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + dock_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + dock_socket.bind((self.cnl_ip, self.cnl_port)) + dock_socket.listen(5) + + while True: + client_socket, client_addr = dock_socket.accept() + + if not self.do_exit: + host, port = client_addr + hosts_filter = self.config.get("hosts_filter") + if hosts_filter: + allowed = False + for h in hosts_filter.split(";"): + try: + filter_type, filter_val = h.split(":") + except ValueError: + continue + if filter_type == "ip": + allowed_ips = [filter_val] + elif filter_type == "host": + allowed_ips = resolve_host(filter_val) + else: + continue + if host in allowed_ips: + allowed = True + break + if not allowed: + self.log_error(_("Connection from unauthorized host %s ignored") % host) + client_socket.close() + continue + + self.log_debug("Connection from %s:%s" % (host, port)) + + server_socket = socket.socket( + socket.AF_INET, socket.SOCK_STREAM) + + if self.pyload.config.get('webinterface', 'https'): + try: + server_socket = ssl.wrap_socket(server_socket) + + except NameError: + self.log_error( + _("Missing SSL lib"), + _("Please disable HTTPS in pyLoad settings")) + client_socket.close() + continue + + except Exception, e: + self.log_error(_("SSL error: %s") % e.message) + client_socket.close() + continue + + server_socket.connect((self.web_ip, self.web_port)) + + self.forward( + client_socket, + server_socket, + self.config.get('dest') == "queue") + self.forward(server_socket, client_socket) + + else: + break + + dock_socket.close() + self.server_running = False + self.exit_done.set() + + except socket.timeout: + self.log_debug("Connection timed out, retrying...") + return self._server() + + except socket.error, e: + self.log_error(e) + time.sleep(240) + return self._server() diff --git a/module/plugins/hooks/CloudFlareDdos.py b/module/plugins/hooks/CloudFlareDdos.py new file mode 100644 index 0000000000..406a796f69 --- /dev/null +++ b/module/plugins/hooks/CloudFlareDdos.py @@ -0,0 +1,308 @@ +# -*- coding: utf-8 -*- + +import inspect +import re +import urlparse + +from module.network.HTTPRequest import BadHeader + +from ..captcha.ReCaptcha import ReCaptcha +from ..internal.Addon import Addon +from ..internal.misc import parse_html_header + + +def plugin_id(plugin): + return ("<%(plugintype)s %(pluginname)s%(id)s>" % + {'plugintype': plugin.__type__.upper(), + 'pluginname': plugin.__name__, + 'id': "[%s]" % plugin.pyfile.id if plugin.pyfile else ""}) + + +def is_simple_plugin(obj): + return any(k.__name__ in ("SimpleHoster", "SimpleCrypter") + for k in inspect.getmro(type(obj))) + + +def get_plugin_last_header(plugin): + # @NOTE: req can be a HTTPRequest or a Browser object + return plugin.req.http.header if hasattr(plugin.req, "http") else plugin.req.header + + +class CloudFlare(object): + + @staticmethod + def handle_function(addon_plugin, owner_plugin, func_name, orig_func, args): + addon_plugin.log_debug("Calling %s() of %s" % (func_name, plugin_id(owner_plugin))) + + try: + data = orig_func(*args[0], **args[1]) + addon_plugin.log_debug("%s() returned successfully" % func_name) + return data + + except BadHeader, e: + addon_plugin.log_debug("%s(): got BadHeader exception %s" % (func_name, e.code)) + + header = parse_html_header(e.header) + + if "cloudflare" in header.get('server', ""): + if e.code == 403: + data = CloudFlare._solve_cf_security_check(addon_plugin, owner_plugin, e.content) + + elif e.code == 503: + for _i in range(3): + try: + data = CloudFlare._solve_cf_ddos_challenge(addon_plugin, owner_plugin, e.content) + break + + except BadHeader, e: #: Possibly we got another ddos challenge + addon_plugin.log_debug("%s(): got BadHeader exception %s" % (func_name, e.code)) + + header = parse_html_header(e.header) + + if e.code == 503 and "cloudflare" in header.get('server', ""): + continue #: Yes, it's a ddos challenge again.. + + else: + data = None # Tell the exception handler to re-throw the exception + break + + else: + addon_plugin.log_error("%s(): Max solve retries reached" % func_name) + data = None # Tell the exception handler to re-throw the exception + + else: + addon_plugin.log_warning(_("Unknown CloudFlare response code %s") % e.code) + raise + + if data is None: + raise e + + else: + return data + + else: + raise + + @staticmethod + def _solve_cf_ddos_challenge(addon_plugin, owner_plugin, data): + try: + addon_plugin.log_info(_("Detected CloudFlare's DDoS protection page")) + # Cloudflare requires a delay before solving the challenge + wait_time = (int(re.search('submit\(\);\r?\n\s*},\s*([0-9]+)', data).group(1)) + 999) / 1000 + owner_plugin.set_wait(wait_time) + + last_url = owner_plugin.req.lastEffectiveURL + urlp = urlparse.urlparse(last_url) + domain = urlp.netloc + submit_url = "%s://%s/cdn-cgi/l/chk_jschl" % (urlp.scheme, domain) + + get_params = {} + + try: + get_params['jschl_vc'] = re.search(r'name="jschl_vc" value="(\w+)"', data).group(1) + get_params['pass'] = re.search(r'name="pass" value="(.+?)"', data).group(1) + get_params['s'] = re.search(r'name="s" value="(.+?)"', data).group(1) + + # Extract the arithmetic operation + js = re.search(r'setTimeout\(function\(\){\s+(var s,t,o,p,b,r,e,a,k,i,n,g,f.+?\r?\n[\s\S]+?a\.value =.+?)\r?\n', + data).group(1) + js = re.sub(r'a\.value = (.+\.toFixed\(10\);).+', r'\1', js) + + solution_name = re.search(r's,t,o,p,b,r,e,a,k,i,n,g,f,\s*(.+)\s*=', js).group(1) + g = re.search(r'(.*};)\n\s*(t\s*=(.+))\n\s*(;%s.*)' % (solution_name), js, re.M | re.I | re.S).groups() + js = g[0] + g[-1] + js = re.sub(r"[\n\\']", "", js) + + except Exception: + # Something is wrong with the page. + # This may indicate CloudFlare has changed their anti-bot + # technique. + owner_plugin.log_error(_("Unable to parse CloudFlare's DDoS protection page")) + return None # Tell the exception handler to re-throw the exception + + if "toFixed" not in js: + owner_plugin.log_error(_("Unable to parse CloudFlare's DDoS protection page")) + return None # Tell the exception handler to re-throw the exception + + atob = 'var atob = function(str) {return Buffer.from(str, "base64").toString("binary");}' + try: + k = re.search(r'k\s*=\s*\'(.+?)\';', data).group(1) + v = re.search(r'(.*)
' % k, data).group(1) + doc = 'var document= {getElementById: function(x) { return {innerHTML:"%s"};}}' % v + except (AttributeError, IndexError): + doc = '' + js = '%s;%s;var t="%s";%s' % (doc, atob, domain, js) + + # Safely evaluate the Javascript expression + res = owner_plugin.js.eval(js) + + try: + get_params['jschl_answer'] = str(float(res)) + + except ValueError: + owner_plugin.log_error(_("Unable to parse CloudFlare's DDoS protection page")) + return None # Tell the exception handler to re-throw the exception + + owner_plugin.wait() # Do the actual wait + + return owner_plugin.load(submit_url, + get=get_params, + ref=last_url) + + except BadHeader, e: + raise e #: Huston, we have a BadHeader! + + except Exception, e: + addon_plugin.log_error(e) + return None # Tell the exception handler to re-throw the exception + + @staticmethod + def _solve_cf_security_check(addon_plugin, owner_plugin, data): + try: + last_url = owner_plugin.req.lastEffectiveURL + + captcha = ReCaptcha(owner_plugin.pyfile) + + captcha_key = captcha.detect_key(data) + if captcha_key: + addon_plugin.log_info(_("Detected CloudFlare's security check page")) + + response = captcha.challenge(captcha_key, data) + return owner_plugin.load(owner_plugin.fixurl("/cdn-cgi/l/chk_captcha"), + get={'g-recaptcha-response': response}, + ref=last_url) + + else: + addon_plugin.log_warning(_("Got unexpected CloudFlare html page")) + return None # Tell the exception handler to re-throw the exception + + except Exception, e: + addon_plugin.log_error(e) + return None # Tell the exception handler to re-throw the exception + + +class PreloadStub(object): + + def __init__(self, addon_plugin, owner_plugin): + self.addon_plugin = addon_plugin + self.owner_plugin = owner_plugin + self.old_preload = owner_plugin._preload + + def my_preload(self, *args, **kwargs): + data = CloudFlare.handle_function(self.addon_plugin, self.owner_plugin, "_preload", self.old_preload, (args, kwargs)) + if data is not None: + self.owner_plugin.data = data + + def __repr__(self): + return "" % hex(id(self)) + + +class CloudFlareDdos(Addon): + __name__ = "CloudFlareDdos" + __type__ = "hook" + __version__ = "0.17" + __status__ = "testing" + + __config__ = [("activated", "bool", "Activated", False)] + + __description__ = """CloudFlare DDoS protection support""" + __license__ = "GPLv3" + __authors__ = [("GammaC0de", "nitzo2001[AT]yahoo[DOT]com")] + + def activate(self): + self.stubs = {} + self._override_get_url() + + def deactivate(self): + while len(self.stubs): + stub = next(self.stubs.itervalues()) + self._unoverride_preload(stub.owner_plugin) + + self._unoverride_get_url() + + def _unoverride_preload(self, plugin): + if id(plugin) in self.stubs: + self.log_debug("Unoverriding _preload() for %s" % plugin_id(plugin)) + + stub = self.stubs.pop(id(plugin)) + stub.owner_plugin._preload = stub.old_preload + + else: + self.log_warning(_("No _preload() override found for %s, cannot un-override>") % + plugin_id(plugin)) + + def _override_preload(self, plugin): + if id(plugin) not in self.stubs: + stub = PreloadStub(self, plugin) + self.stubs[id(plugin)] = stub + + self.log_debug("Overriding _preload() for %s" % plugin_id(plugin)) + plugin._preload = stub.my_preload + + else: + self.log_warning(_("Already overrided _preload() for %s") % plugin_id(plugin)) + + def _override_get_url(self): + self.log_debug("Overriding get_url()") + + self.old_get_url = self.pyload.requestFactory.getURL + self.pyload.requestFactory.getURL = self.my_get_url + + def _unoverride_get_url(self): + self.log_debug("Unoverriding get_url()") + + self.pyload.requestFactory.getURL = self.old_get_url + + def _find_owner_plugin(self): + """ + Walk the callstack until we find SimpleHoster or SimpleCrypter class + Dirty but works. + """ + f = frame = inspect.currentframe() + try: + while True: + if f is None: + return None + + elif 'self' in f.f_locals and is_simple_plugin(f.f_locals['self']): + return f.f_locals['self'] + + else: + f = f.f_back + + finally: + del frame + + def download_preparing(self, pyfile): + #: Only SimpleHoster and SimpleCrypter based plugins are supported + if not is_simple_plugin(pyfile.plugin): + self.log_debug("Skipping plugin %s" % plugin_id(pyfile.plugin)) + return + + attr = getattr(pyfile.plugin, "_preload", None) + if not attr and not callable(attr): + self.log_error(_("%s is missing _preload() function, cannot override!") % plugin_id(pyfile.plugin)) + return + + self._override_preload(pyfile.plugin) + + def download_processed(self, pyfile): + if id(pyfile.plugin) in self.stubs: + self._unoverride_preload(pyfile.plugin) + + def my_get_url(self, *args, **kwargs): + owner_plugin = self._find_owner_plugin() + if owner_plugin is None: + self.log_warning(_("Owner plugin not found, cannot process")) + return self.old_get_url(*args, **kwargs) + + else: + #@NOTE: Better use owner_plugin.load() instead of get_url() so cookies are saved and so captcha credits + #@NOTE: Also that way we can use 'owner_plugin.req.header' to get the headers, otherwise we cannot get them + res = CloudFlare.handle_function(self, owner_plugin, "get_url", owner_plugin.load, (args, kwargs)) + if kwargs.get('just_header', False): + # @NOTE: SimpleHoster/SimpleCrypter returns a dict while get_url() returns raw headers string, + # make sure we return a string for get_url('just_header'=True) + res = get_plugin_last_header(owner_plugin) + + return res diff --git a/module/plugins/hooks/DeathByCaptcha.py b/module/plugins/hooks/DeathByCaptcha.py index 7de4f4f2ca..14cdd26711 100644 --- a/module/plugins/hooks/DeathByCaptcha.py +++ b/module/plugins/hooks/DeathByCaptcha.py @@ -1,32 +1,17 @@ # -*- 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 . - - @author: mkaay, RaNaN, zoidberg -""" + from __future__ import with_statement -from thread import start_new_thread -from pycurl import FORM_FILE, HTTPHEADER -from time import sleep -from base64 import b64encode +import base64 import re +import time -from module.network.RequestFactory import getRequest +import pycurl from module.network.HTTPRequest import BadHeader -from module.plugins.Hook import Hook -from module.common.json_layer import json_loads +from module.network.RequestFactory import getRequest as get_request + +from ..internal.Addon import Addon +from ..internal.misc import fs_encode, json, threaded class DeathByCaptchaException(Exception): @@ -42,10 +27,10 @@ class DeathByCaptchaException(Exception): def __init__(self, err): self.err = err - def getCode(self): + def get_code(self): return self.err - def getDesc(self): + def get_desc(self): if self.err in self.DBC_ERRORS.keys(): return self.DBC_ERRORS[self.err] else: @@ -58,155 +43,172 @@ def __repr__(self): return "" % self.err -class DeathByCaptcha(Hook): +class DeathByCaptcha(Addon): __name__ = "DeathByCaptcha" - __version__ = "0.03" - __description__ = """send captchas to DeathByCaptcha.com""" + __type__ = "hook" + __version__ = "0.17" + __status__ = "testing" + __config__ = [("activated", "bool", "Activated", False), ("username", "str", "Username", ""), - ("passkey", "password", "Password", ""), - ("force", "bool", "Force DBC even if client is connected", False)] - __author_name__ = ("RaNaN", "zoidberg") - __author_mail__ = ("RaNaN@pyload.org", "zoidberg@mujmail.cz") + ("password", "password", "Password", ""), + ("check_client", "bool", "Don't use if client is connected", True)] - API_URL = "http://api.dbcapi.me/api/" + __description__ = """Send captchas to DeathByCaptcha.com""" + __license__ = "GPLv3" + __authors__ = [("RaNaN", "RaNaN@pyload.net"), + ("zoidberg", "zoidberg@mujmail.cz")] - def setup(self): - self.info = {} + API_URL = "http://api.dbcapi.me/api/" - def call_api(self, api="captcha", post=False, multipart=False): - req = getRequest() - req.c.setopt(HTTPHEADER, ["Accept: application/json", "User-Agent: pyLoad %s" % self.core.version]) + def api_response(self, api="captcha", post=False, multipart=False): + req = get_request() + req.c.setopt( + pycurl.HTTPHEADER, [ + "Accept: application/json", "User-Agent: pyLoad %s" % + self.pyload.version]) if post: if not isinstance(post, dict): post = {} - post.update({"username": self.getConfig("username"), - "password": self.getConfig("passkey")}) + post.update({'username': self.config.get('username'), + 'password': self.config.get('password')}) - response = None + res = None try: - json = req.load("%s%s" % (self.API_URL, api), - post=post, - multipart=multipart) - self.logDebug(json) - response = json_loads(json) + html = self.load("%s%s" % (self.API_URL, api), + post=post, + multipart=multipart, + req=req) + + self.log_debug(html) + res = json.loads(html) - if "error" in response: - raise DeathByCaptchaException(response['error']) - elif "status" not in response: - raise DeathByCaptchaException(str(response)) + if "error" in res: + raise DeathByCaptchaException(res['error']) + elif "status" not in res: + raise DeathByCaptchaException(str(res)) except BadHeader, e: - if 403 == e.code: + if e.code == 403: raise DeathByCaptchaException('not-logged-in') - elif 413 == e.code: + + elif e.code == 413: raise DeathByCaptchaException('invalid-captcha') - elif 503 == e.code: + + elif e.code == 503: raise DeathByCaptchaException('service-overload') + elif e.code in (400, 405): raise DeathByCaptchaException('invalid-request') + else: raise finally: req.close() - return response + return res - def getCredits(self): - response = self.call_api("user", True) + def get_credits(self): + res = self.api_response("user", True) - if 'is_banned' in response and response['is_banned']: + if 'is_banned' in res and res['is_banned']: raise DeathByCaptchaException('banned') - elif 'balance' in response and 'rate' in response: - self.info.update(response) + elif 'balance' in res and 'rate' in res: + self.info.update(res) else: - raise DeathByCaptchaException(response) + raise DeathByCaptchaException(res) - def getStatus(self): - response = self.call_api("status", False) + def get_status(self): + res = self.api_response("status", False) - if 'is_service_overloaded' in response and response['is_service_overloaded']: + if 'is_service_overloaded' in res and res['is_service_overloaded']: raise DeathByCaptchaException('service-overload') def submit(self, captcha, captchaType="file", match=None): - #workaround multipart-post bug in HTTPRequest.py - if re.match("^[A-Za-z0-9]*$", self.getConfig("passkey")): + #@NOTE: Workaround multipart-post bug in HTTPRequest.py + if re.match("^\w*$", self.config.get('password')): multipart = True - data = (FORM_FILE, captcha) + data = (pycurl.FORM_FILE, captcha) else: multipart = False - with open(captcha, 'rb') as f: + with open(fs_encode(captcha), 'rb') as f: data = f.read() - data = "base64:" + b64encode(data) + data = "base64:" + base64.b64encode(data) - response = self.call_api("captcha", {"captchafile": data}, multipart) + res = self.api_response("captcha", {'captchafile': data}, multipart) - if "captcha" not in response: - raise DeathByCaptchaException(response) - ticket = response['captcha'] + if "captcha" not in res: + raise DeathByCaptchaException(res) + ticket = res['captcha'] - for i in range(24): - sleep(5) - response = self.call_api("captcha/%d" % ticket, False) - if response['text'] and response['is_correct']: + for _i in range(24): + time.sleep(5) + res = self.api_response("captcha/%d" % ticket, False) + if res['text'] and res['is_correct']: break else: raise DeathByCaptchaException('timed-out') - result = response['text'] - self.logDebug("result %s : %s" % (ticket, result)) + result = res['text'] + self.log_debug("Result %s : %s" % (ticket, result)) return ticket, result - def newCaptchaTask(self, task): + def captcha_task(self, task): if "service" in task.data: return False if not task.isTextual(): return False - if not self.getConfig("username") or not self.getConfig("passkey"): + if not self.config.get('username') or not self.config.get('password'): return False - if self.core.isClientConnected() and not self.getConfig("force"): + if self.pyload.isClientConnected() and self.config.get('check_client'): return False try: - self.getStatus() - self.getCredits() + self.get_status() + self.get_credits() except DeathByCaptchaException, e: - self.logError(e.getDesc()) + self.log_error(e.message) return False - balance, rate = self.info["balance"], self.info["rate"] - self.logInfo("Account balance: US$%.3f (%d captchas left at %.2f cents each)" % (balance / 100, - balance // rate, rate)) + balance, rate = self.info['balance'], self.info['rate'] + self.log_info(_("Account balance"), + _("US$%.3f (%d captchas left at %.2f cents each)") % (balance / 100, + balance // rate, rate)) if balance > rate: task.handler.append(self) - task.data['service'] = self.__name__ + task.data['service'] = self.classname task.setWaiting(180) - start_new_thread(self.processCaptcha, (task,)) + self._process_captcha(task) - def captchaInvalid(self, task): - if task.data['service'] == self.__name__ and "ticket" in task.data: + def captcha_invalid(self, task): + if task.data['service'] == self.classname and "ticket" in task.data: try: - response = self.call_api("captcha/%d/report" % task.data["ticket"], True) + res = self.api_response( + "captcha/%d/report" % + task.data['ticket'], True) + except DeathByCaptchaException, e: - self.logError(e.getDesc()) + self.log_error(e.message) + except Exception, e: - self.logError(e) + self.log_error(e, trace=True) - def processCaptcha(self, task): - c = task.captchaFile + @threaded + def _process_captcha(self, task): + c = task.captchaParams['file'] try: ticket, result = self.submit(c) except DeathByCaptchaException, e: - task.error = e.getCode() - self.logError(e.getDesc()) + task.error = e.get_code() + self.log_error(e.message) return - task.data["ticket"] = ticket + task.data['ticket'] = ticket task.setResult(result) diff --git a/module/plugins/hooks/DebridItaliaCom.py b/module/plugins/hooks/DebridItaliaCom.py deleted file mode 100644 index 71ebac85c5..0000000000 --- a/module/plugins/hooks/DebridItaliaCom.py +++ /dev/null @@ -1,42 +0,0 @@ -# -*- coding: utf-8 -*- - -############################################################################ -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU Affero 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 Affero General Public License for more details. # -# # -# You should have received a copy of the GNU Affero General Public License # -# along with this program. If not, see . # -############################################################################ - -from module.plugins.internal.MultiHoster import MultiHoster - - -class DebridItaliaCom(MultiHoster): - __name__ = "DebridItaliaCom" - __version__ = "0.07" - __type__ = "hook" - __config__ = [("activated", "bool", "Activated", "False"), - ("hosterListMode", "all;listed;unlisted", "Use for hosters (if supported)", "all"), - ("hosterList", "str", "Hoster list (comma separated)", ""), - ("unloadFailing", "bool", "Revert to standard download if download fails", "False"), - ("interval", "int", "Reload interval in hours (0 to disable)", "24")] - - __description__ = """Debriditalia.com hook plugin""" - __author_name__ = ("stickell") - __author_mail__ = ("l.stickell@yahoo.it") - - def getHoster(self): - return ["netload.in", "hotfile.com", "rapidshare.com", "multiupload.com", - "uploading.com", "megashares.com", "crocko.com", "filepost.com", - "bitshare.com", "share-links.biz", "putlocker.com", "uploaded.to", - "speedload.org", "rapidgator.net", "likeupload.net", "cyberlocker.ch", - "depositfiles.com", "extabit.com", "filefactory.com", "sharefiles.co", - "ryushare.com", "tusfiles.net", "nowvideo.co", "cloudzer.net", "letitbit.net", - "easybytez.com", "uptobox.com", "ddlstorage.com"] diff --git a/module/plugins/hooks/DeleteFinished.py b/module/plugins/hooks/DeleteFinished.py index 3bc98a7b38..3a1207bbcb 100644 --- a/module/plugins/hooks/DeleteFinished.py +++ b/module/plugins/hooks/DeleteFinished.py @@ -1,84 +1,67 @@ # -*- 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. +from module.database import style - You should have received a copy of the GNU General Public License - along with this program; if not, see . +from ..internal.Addon import Addon - @author: Walter Purcaro -""" -from module.database import style -from module.plugins.Hook import Hook +class DeleteFinished(Addon): + __name__ = "DeleteFinished" + __type__ = "hook" + __version__ = "1.19" + __status__ = "testing" + __config__ = [("activated", "bool", "Activated", False), + ("interval", "int", "Check interval in hours", 72), + ("deloffline", "bool", "Delete package with offline links", False)] -class DeleteFinished(Hook): - __name__ = 'DeleteFinished' - __version__ = '1.09' - __description__ = 'Automatically delete all finished packages from queue' - __config__ = [ - ('activated', 'bool', 'Activated', 'False'), - ('interval', 'int', 'Delete every (hours)', '72'), - ('deloffline', 'bool', 'Delete packages with offline links', 'False') - ] - __author_name__ = ('Walter Purcaro') - __author_mail__ = ('vuolter@gmail.com') + __description__ = """Automatically delete all finished packages from queue""" + __license__ = "GPLv3" + __authors__ = [("Walter Purcaro", "vuolter@gmail.com")] - ## overwritten methods ## - def periodical(self): + def periodical_task(self): if not self.info['sleep']: - deloffline = self.getConfig('deloffline') - mode = '0,1,4' if deloffline else '0,4' - msg = 'delete all finished packages in queue list (%s packages with offline links)' - self.logInfo(msg % ('including' if deloffline else 'excluding')) - self.deleteFinished(mode) + deloffline = self.config.get('deloffline') + mode = "0,1,4" if deloffline else "0,4" + msg = _( + 'delete all finished packages in queue list (%s packages with offline links)') + self.log_info( + msg % + (_('including') if deloffline else _('excluding'))) + self.delete_finished(mode) self.info['sleep'] = True - self.addEvent('packageFinished', self.wakeup) + self.add_event('package_finished', self.wakeup) - def pluginConfigChanged(self, plugin, name, value): - if name == 'interval' and value != self.interval: - self.interval = value * 3600 - self.initPeriodical() + def deactivate(self): + self.manager.removeEvent('package_finished', self.wakeup) - def unload(self): - self.removeEvent('packageFinished', self.wakeup) - - def coreReady(self): - self.info = {'sleep': True} - interval = self.getConfig('interval') - self.pluginConfigChanged('DeleteFinished', 'interval', interval) - self.addEvent('packageFinished', self.wakeup) + def activate(self): + self.info['sleep'] = True + self.add_event('package_finished', self.wakeup) + self.periodical.start(self.config.get('interval') * 60 * 60) ## own methods ## @style.queue - def deleteFinished(self, mode): - self.c.execute('DELETE FROM packages WHERE NOT EXISTS(SELECT 1 FROM links WHERE package=packages.id AND status NOT IN (%s))' % mode) - self.c.execute('DELETE FROM links WHERE NOT EXISTS(SELECT 1 FROM packages WHERE id=links.package)') + def delete_finished(self, mode): + self.c.execute( + 'DELETE FROM packages WHERE NOT EXISTS(SELECT 1 FROM links WHERE package=packages.id AND status NOT IN (%s))' % + mode) + self.c.execute( + 'DELETE FROM links WHERE NOT EXISTS(SELECT 1 FROM packages WHERE id=links.package)') def wakeup(self, pypack): - self.removeEvent('packageFinished', self.wakeup) + self.manager.removeEvent('package_finished', self.wakeup) self.info['sleep'] = False ## event managing ## - def addEvent(self, event, func): - """Adds an event listener for event name""" - if event in self.m.events: - if func in self.m.events[event]: - self.logDebug('Function already registered %s' % func) + def add_event(self, event, func): + """ + Adds an event listener for event name + """ + if event in self.manager.events: + if func in self.manager.events[event]: + self.log_debug("Function already registered", func) else: - self.m.events[event].append(func) + self.manager.events[event].append(func) else: - self.m.events[event] = [func] - - def setup(self): - self.m = self.manager - self.removeEvent = self.m.removeEvent + self.manager.events[event] = [func] diff --git a/module/plugins/hooks/DiscordNotifier.py b/module/plugins/hooks/DiscordNotifier.py new file mode 100644 index 0000000000..4479c5e694 --- /dev/null +++ b/module/plugins/hooks/DiscordNotifier.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- + +from module.network.RequestFactory import getRequest as get_request + +from ..internal.Notifier import Notifier + + +class DiscordNotifier(Notifier): + __name__ = "DiscordNotifier" + __type__ = "hook" + __version__ = "0.11" + __status__ = "testing" + + __config__ = [("activated", "bool" , "Activated", False), + ("webhookurl", "string", "The URL of the webhook", ""), + ("captcha", "bool", "Notify captcha request", True), + ("reconnection", "bool", "Notify reconnection request", True), + ("downloadfinished", "bool", "Notify download finished", True), + ("downloadfailed", "bool" ,"Notify download failed", True), + ("alldownloadsfinished", "bool", "Notify all downloads finished", True), + ("alldownloadsprocessed", "bool", "Notify all downloads processed", True), + ("packagefinished", "bool", "Notify package finished", True), + ("packagefailed", "bool", "Notify package failed", True), + ("update", "bool", "Notify pyload update", False), + ("exit", "bool", "Notify pyload shutdown/restart", False), + ("sendinterval", "int", "Interval in seconds between notifications", 1), + ("sendpermin", "int", "Max notifications per minute", 60), + ("ignoreclient", "bool", "Send notifications if client is connected", True)] + + __description__ = "Send push notifications to a Discord channel via a webhook." + __license__ = "GPLv3" + __authors__ = [("Jan-Olaf Becker", "job87@web.de")] + + def get_key(self): + return self.config.get("webhookurl") + + def send(self, event, msg, key): + req = get_request() + self.log_info("Sending message to discord") + self.load(self.get_key(), + post = {'content': event + '\n' + msg}, + req = req) diff --git a/module/plugins/hooks/DownloadScheduler.py b/module/plugins/hooks/DownloadScheduler.py index 4049d71c54..0cade5a13d 100644 --- a/module/plugins/hooks/DownloadScheduler.py +++ b/module/plugins/hooks/DownloadScheduler.py @@ -1,86 +1,96 @@ # -*- 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. - You should have received a copy of the GNU General Public License - along with this program; if not, see . - - @author: zoidberg - Original idea by new.cze -""" - import re -from time import localtime +import time -from module.plugins.Hook import Hook +from ..internal.Addon import Addon -class DownloadScheduler(Hook): +class DownloadScheduler(Addon): __name__ = "DownloadScheduler" - __version__ = "0.21" + __type__ = "hook" + __version__ = "0.31" + __status__ = "testing" + + __config__ = [("activated", "bool", "Activated", False), + ("timetable", "str", "List time periods as hh:mm full or number(kB/s)", "0:00 full, 7:00 250, 10:00 0, 17:00 150"), + ("abort", "bool", "Abort active downloads when start period with speed 0", False)] + __description__ = """Download Scheduler""" - __config__ = [("activated", "bool", "Activated", "False"), - ("timetable", "str", "List time periods as hh:mm full or number(kB/s)", - "0:00 full, 7:00 250, 10:00 0, 17:00 150"), - ("abort", "bool", "Abort active downloads when start period with speed 0", "False")] - __author_name__ = ("zoidberg", "stickell") - __author_mail__ = ("zoidberg@mujmail.cz", "l.stickell@yahoo.it") + __license__ = "GPLv3" + __authors__ = [("zoidberg", "zoidberg@mujmail.cz"), + ("stickell", "l.stickell@yahoo.it"), + ("GammaC0de", "nitzo2001[AT]yahoo[DOT]com")] + + def activate(self): + self.last_timetable = None + self.update_schedule() - def setup(self): - self.cb = None # callback to scheduler job; will be by removed hookmanager when hook unloaded - def coreReady(self): - self.updateSchedule() + def config_changed(self, category, option, value, section): + """Listen for config changes, to trigger a schedule update.""" + if category == self.__name__ and \ + option == 'timetable' and \ + value != self.last_timetable: + self.update_schedule(schedule=value) - def updateSchedule(self, schedule=None): + def update_schedule(self, schedule=None): if schedule is None: - schedule = self.getConfig("timetable") + schedule = self.config.get('timetable') + + self.last_timetable = schedule schedule = re.findall("(\d{1,2}):(\d{2})[\s]*(-?\d+)", schedule.lower().replace("full", "-1").replace("none", "0")) if not schedule: - self.logError("Invalid schedule") + self.log_error(_("Invalid schedule")) return - t0 = localtime() + t0 = time.localtime() now = (t0.tm_hour, t0.tm_min, t0.tm_sec, "X") - schedule = sorted([(int(x[0]), int(x[1]), 0, int(x[2])) for x in schedule] + [now]) + schedule = sorted([(int(x[0]), int(x[1]), 0, int(x[2])) for x in schedule] + [now], + key=lambda a: (a[0], a[1], a[2], a[3] == "X") + ) - self.logDebug("Schedule", schedule) + self.log_debug("Schedule", schedule) for i, v in enumerate(schedule): if v[3] == "X": last, next = schedule[i - 1], schedule[(i + 1) % len(schedule)] - self.logDebug("Now/Last/Next", now, last, next) + self.log_debug("Now/Last/Next", now, last, next) - self.setDownloadSpeed(last[3]) + self.set_download_speed(last[3]) next_time = (((24 + next[0] - now[0]) * 60 + next[1] - now[1]) * 60 + next[2] - now[2]) % 86400 - self.core.scheduler.removeJob(self.cb) - self.cb = self.core.scheduler.addJob(next_time, self.updateSchedule, threaded=False) + self.pyload.scheduler.removeJob(self.cb) + self.cb = self.pyload.scheduler.addJob( + next_time, self.update_schedule, threaded=False) - def setDownloadSpeed(self, speed): + def set_download_speed(self, speed): if speed == 0: - abort = self.getConfig("abort") - self.logInfo("Stopping download server. (Running downloads will %sbe aborted.)" % ('' if abort else 'not ')) - self.core.api.pauseServer() + abort = self.config.get('abort') + self.log_info( + _("Stopping download server. (Running downloads will be aborted.)") + if abort + else _("Stopping download server. (Running downloads will not be aborted.)") + ) + + self.pyload.api.pauseServer() if abort: - self.core.api.stopAllDownloads() + self.pyload.api.stopAllDownloads() + else: - self.core.api.unpauseServer() + self.pyload.api.unpauseServer() if speed > 0: - self.logInfo("Setting download speed to %d kB/s" % speed) - self.core.api.setConfigValue("download", "limit_speed", 1) - self.core.api.setConfigValue("download", "max_speed", speed) + self.log_info(_("Setting download speed to %d kB/s") % speed) + self.pyload.config.set('download', 'limit_speed', 1) + self.pyload.config.set('download', 'max_speed', speed) + else: - self.logInfo("Setting download speed to FULL") - self.core.api.setConfigValue("download", "limit_speed", 0) - self.core.api.setConfigValue("download", "max_speed", -1) + self.log_info(_("Setting download speed to FULL")) + self.pyload.config.set('download', 'limit_speed', 0) + self.pyload.config.set('download', 'max_speed', -1) + + # Make new speed values take effect + self.pyload.requestFactory.updateBucket() diff --git a/module/plugins/hooks/EasybytezCom.py b/module/plugins/hooks/EasybytezCom.py deleted file mode 100644 index cc55da9c0f..0000000000 --- a/module/plugins/hooks/EasybytezCom.py +++ /dev/null @@ -1,34 +0,0 @@ -# -*- coding: utf-8 -*- - -import re - -from module.plugins.internal.MultiHoster import MultiHoster - - -class EasybytezCom(MultiHoster): - __name__ = "EasybytezCom" - __version__ = "0.03" - __type__ = "hook" - __config__ = [("activated", "bool", "Activated", "False"), - ("hosterListMode", "all;listed;unlisted", "Use for hosters (if supported)", "all"), - ("hosterList", "str", "Hoster list (comma separated)", "")] - __description__ = """EasyBytez.com hook plugin""" - __author_name__ = ("zoidberg") - __author_mail__ = ("zoidberg@mujmail.cz") - - def getHoster(self): - self.account = self.core.accountManager.getAccountPlugin(self.__name__) - user = self.account.selectAccount()[0] - - try: - req = self.account.getAccountRequest(user) - page = req.load("http://www.easybytez.com") - - found = re.search(r'\s*Supported sites:(.*)', page) - return found.group(1).split(',') - except Exception, e: - self.logDebug(e) - self.logWarning("Unable to load supported hoster list, using last known") - return ['bitshare.com', 'crocko.com', 'ddlstorage.com', 'depositfiles.com', 'extabit.com', 'hotfile.com', - 'mediafire.com', 'netload.in', 'rapidgator.net', 'rapidshare.com', 'uploading.com', 'uload.to', - 'uploaded.to'] diff --git a/module/plugins/hooks/Ev0InFetcher.py b/module/plugins/hooks/Ev0InFetcher.py deleted file mode 100644 index 912cb5964c..0000000000 --- a/module/plugins/hooks/Ev0InFetcher.py +++ /dev/null @@ -1,89 +0,0 @@ -# -*- 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 . - - @author: mkaay -""" -from time import mktime, time - -from module.lib import feedparser -from module.plugins.Hook import Hook - - -class Ev0InFetcher(Hook): - __name__ = "Ev0InFetcher" - __version__ = "0.21" - __description__ = """checks rss feeds for ev0.in""" - __config__ = [("activated", "bool", "Activated", "False"), - ("interval", "int", "Check interval in minutes", "10"), - ("queue", "bool", "Move new shows directly to Queue", False), - ("shows", "str", "Shows to check for (comma seperated)", ""), - ("quality", "xvid;x264;rmvb", "Video Format", "xvid"), - ("hoster", "str", "Hoster to use (comma seperated)", - "NetloadIn,RapidshareCom,MegauploadCom,HotfileCom")] - __author_name__ = ("mkaay") - __author_mail__ = ("mkaay@mkaay.de") - - def setup(self): - self.interval = self.getConfig("interval") * 60 - - def filterLinks(self, links): - results = self.core.pluginManager.parseUrls(links) - sortedLinks = {} - - for url, hoster in results: - if hoster not in sortedLinks: - sortedLinks[hoster] = [] - sortedLinks[hoster].append(url) - - for h in self.getConfig("hoster").split(","): - try: - return sortedLinks[h.strip()] - except: - continue - return [] - - def periodical(self): - def normalizefiletitle(filename): - filename = filename.replace('.', ' ') - filename = filename.replace('_', ' ') - filename = filename.lower() - return filename - - shows = [s.strip() for s in self.getConfig("shows").split(",")] - - feed = feedparser.parse("http://feeds.feedburner.com/ev0in/%s?format=xml" % self.getConfig("quality")) - - showStorage = {} - for show in shows: - showStorage[show] = int(self.getStorage("show_%s_lastfound" % show, 0)) - - found = False - for item in feed['items']: - for show, lastfound in showStorage.iteritems(): - if show.lower() in normalizefiletitle(item['title']) and lastfound < int(mktime(item.date_parsed)): - links = self.filterLinks(item['description'].split("
")) - packagename = item['title'].encode("utf-8") - self.logInfo("Ev0InFetcher: new episode '%s' (matched '%s')" % (packagename, show)) - self.core.api.addPackage(packagename, links, 1 if self.getConfig("queue") else 0) - self.setStorage("show_%s_lastfound" % show, int(mktime(item.date_parsed))) - found = True - if not found: - #self.logDebug("Ev0InFetcher: no new episodes found") - pass - - for show, lastfound in self.getStorage().iteritems(): - if int(lastfound) > 0 and int(lastfound) + (3600 * 24 * 30) < int(time()): - self.delStorage("show_%s_lastfound" % show) - self.logDebug("Ev0InFetcher: cleaned '%s' record" % show) diff --git a/module/plugins/hooks/EventMapper.py b/module/plugins/hooks/EventMapper.py new file mode 100644 index 0000000000..9ad40056e2 --- /dev/null +++ b/module/plugins/hooks/EventMapper.py @@ -0,0 +1,64 @@ +# -*- coding: utf-8 -*- + +from ..internal.Addon import Addon + + +class EventMapper(Addon): + __name__ = "EventMapper" + __type__ = "hook" + __version__ = "0.02" + __status__ = "testing" + + __config__ = [("activated", "bool", "Activated", True)] + + __description__ = """Map old events to new events""" + __license__ = "GPLv3" + __authors__ = [("Walter Purcaro", "vuolter@gmail.com")] + + def activate(self, *args): + self.manager.dispatchEvent("activate", *args) + + def exit(self, *args): + self.manager.dispatchEvent("exit", *args) + + def config_changed(self, *args): + self.manager.dispatchEvent("config_changed", *args) + + def all_downloads_finished(self, *args): + self.manager.dispatchEvent("all_downloads_finished", *args) + + def all_downloads_processed(self, *args): + self.manager.dispatchEvent("all_downloads_processed", *args) + + def links_added(self, *args): + self.manager.dispatchEvent("links_added", *args) + + def download_preparing(self, *args): + self.manager.dispatchEvent("download_preparing", *args) + + def download_finished(self, *args): + self.manager.dispatchEvent("download_finished", *args) + + def download_failed(self, *args): + self.manager.dispatchEvent("download_failed", *args) + + def package_deleted(self, *args): + self.manager.dispatchEvent("package_deleted", *args) + + def package_finished(self, *args): + self.manager.dispatchEvent("package_finished", *args) + + def before_reconnect(self, *args): + self.manager.dispatchEvent("before_reconnect", *args) + + def after_reconnect(self, *args): + self.manager.dispatchEvent("after_reconnect", *args) + + def captcha_task(self, *args): + self.manager.dispatchEvent("captcha_task", *args) + + def captcha_correct(self, *args): + self.manager.dispatchEvent("captcha_correct", *args) + + def captcha_invalid(self, *args): + self.manager.dispatchEvent("captcha_invalid", *args) diff --git a/module/plugins/hooks/ExpertDecoders.py b/module/plugins/hooks/ExpertDecoders.py index f1b7ea352b..7fd8dd9178 100644 --- a/module/plugins/hooks/ExpertDecoders.py +++ b/module/plugins/hooks/ExpertDecoders.py @@ -1,106 +1,101 @@ # -*- 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 . - - @author: mkaay, RaNaN, zoidberg -""" + from __future__ import with_statement -from thread import start_new_thread -from pycurl import LOW_SPEED_TIME -from uuid import uuid4 -from base64 import b64encode +import base64 +import uuid -from module.network.RequestFactory import getURL, getRequest +import pycurl from module.network.HTTPRequest import BadHeader +from module.network.RequestFactory import getRequest as get_request -from module.plugins.Hook import Hook +from ..internal.Addon import Addon +from ..internal.misc import fs_encode, threaded -class ExpertDecoders(Hook): +class ExpertDecoders(Addon): __name__ = "ExpertDecoders" - __version__ = "0.01" - __description__ = """send captchas to expertdecoders.com""" + __type__ = "hook" + __version__ = "0.14" + __status__ = "testing" + __config__ = [("activated", "bool", "Activated", False), - ("force", "bool", "Force CT even if client is connected", False), - ("passkey", "password", "Access key", ""), ] - __author_name__ = ("RaNaN", "zoidberg") - __author_mail__ = ("RaNaN@pyload.org", "zoidberg@mujmail.cz") + ("passkey", "password", "Access key", ""), + ("check_client", "bool", "Don't use if client is connected", True)] - API_URL = "http://www.fasttypers.org/imagepost.ashx" + __description__ = """Send captchas to expertdecoders.com""" + __license__ = "GPLv3" + __authors__ = [("RaNaN", "RaNaN@pyload.net"), + ("zoidberg", "zoidberg@mujmail.cz")] - def setup(self): - self.info = {} + API_URL = "http://www.fasttypers.org/imagepost.ashx" - def getCredits(self): - response = getURL(self.API_URL, post={"key": self.getConfig("passkey"), "action": "balance"}) + def get_credits(self): + res = self.load( + self.API_URL, + post={ + 'key': self.config.get('passkey'), + 'action': "balance"}) - if response.isdigit(): - self.logInfo(_("%s credits left") % response) - self.info["credits"] = credits = int(response) + if res.isdigit(): + self.log_info(_("%s credits left") % res) + self.info['credits'] = credits = int(res) return credits else: - self.logError(response) + self.log_error(res) return 0 - def processCaptcha(self, task): - task.data["ticket"] = ticket = uuid4() + @threaded + def _process_captcha(self, task): + task.data['ticket'] = ticket = uuid.uuid4() result = None - with open(task.captchaFile, 'rb') as f: + with open(fs_encode(task.captchaParams['file']), 'rb') as f: data = f.read() - data = b64encode(data) - #self.logDebug("%s: %s : %s" % (ticket, task.captchaFile, data)) - req = getRequest() - #raise timeout threshold - req.c.setopt(LOW_SPEED_TIME, 80) + req = get_request() + #: Raise timeout threshold + req.c.setopt(pycurl.LOW_SPEED_TIME, 80) try: - result = req.load(self.API_URL, post={"action": "upload", "key": self.getConfig("passkey"), - "file": data, "gen_task_id": ticket}) + result = self.load(self.API_URL, + post={'action': "upload", + 'key': self.config.get('passkey'), + 'file': base64.b64encode(data), + 'gen_task_id': ticket}, + req=req) finally: req.close() - self.logDebug("result %s : %s" % (ticket, result)) + self.log_debug("Result %s : %s" % (ticket, result)) task.setResult(result) - def newCaptchaTask(self, task): + def captcha_task(self, task): if not task.isTextual(): return False - if not self.getConfig("passkey"): + if not self.config.get('passkey'): return False - if self.core.isClientConnected() and not self.getConfig("force"): + if self.pyload.isClientConnected() and self.config.get('check_client'): return False - if self.getCredits() > 0: + if self.get_credits() > 0: task.handler.append(self) task.setWaiting(100) - start_new_thread(self.processCaptcha, (task,)) + self._process_captcha(task) else: - self.logInfo(_("Your ExpertDecoders Account has not enough credits")) + self.log_info( + _("Your ExpertDecoders Account has not enough credits")) - def captchaInvalid(self, task): + def captcha_invalid(self, task): if "ticket" in task.data: try: - response = getURL(self.API_URL, post={"action": "refund", "key": self.getConfig("passkey"), - "gen_task_id": task.data["ticket"]}) - self.logInfo("Request refund: %s" % response) + res = self.load(self.API_URL, + post={'action': "refund", 'key': self.config.get('passkey'), 'gen_task_id': task.data['ticket']}) + self.log_info(_("Request refund"), res) except BadHeader, e: - self.logError("Could not send refund request.", str(e)) + self.log_error(_("Could not send refund request"), e) diff --git a/module/plugins/hooks/ExternalScripts.py b/module/plugins/hooks/ExternalScripts.py index e557a74056..7a0795c732 100644 --- a/module/plugins/hooks/ExternalScripts.py +++ b/module/plugins/hooks/ExternalScripts.py @@ -1,117 +1,298 @@ # -*- 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. +import os - 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. +from ..internal.Addon import Addon +from ..internal.misc import Expose, Popen, fs_encode - You should have received a copy of the GNU General Public License - along with this program; if not, see . - @author: mkaay - @interface-version: 0.1 -""" +class ExternalScripts(Addon): + __name__ = "ExternalScripts" + __type__ = "hook" + __version__ = "0.76" + __status__ = "testing" -import subprocess -from os import listdir, access, X_OK, makedirs -from os.path import join, exists, basename, abspath + __config__ = [("activated", "bool", "Activated", True), + ("unlock", "bool", "Execute script concurrently", False)] -from module.plugins.Hook import Hook -from module.utils import save_join + __description__ = """Run external scripts""" + __license__ = "GPLv3" + __authors__ = [("Walter Purcaro", "vuolter@gmail.com"), + ("GammaC0de", "nitzo2001[AT}yahoo[DOT]com")] + def init(self): + self.scripts = {} -class ExternalScripts(Hook): - __name__ = "ExternalScripts" - __version__ = "0.23" - __description__ = """Run external scripts""" - __config__ = [("activated", "bool", "Activated", "True")] - __author_name__ = ("mkaay", "RaNaN", "spoob") - __author_mail__ = ("mkaay@mkaay.de", "ranan@pyload.org", "spoob@pyload.org") + self.folders = ["pyload_start", "pyload_restart", "pyload_stop", + "before_reconnect", "after_reconnect", + "download_preparing", "download_failed", + # @TODO: Invert 'download_processed', 'download_finished' order in 0.4.10 + "download_finished", "download_processed", + "archive_extract_failed", "archive_extracted", "archive_processed", + # @TODO: Invert 'package_finished', 'package_processed' order in 0.4.10 + "package_finished", "package_processed", + "package_deleted", "package_failed", "package_extract_failed", "package_extracted", + "all_downloads_processed", "all_downloads_finished", + "all_archives_extracted", "all_archives_processed"] - event_list = ["unrarFinished", "allDownloadsFinished", "allDownloadsProcessed"] + self.event_map = {'archive_extract_failed': "archive_extract_failed", + 'archive_extracted': "archive_extracted", + "archive_processed": "archive_processed", + 'package_extract_failed': "package_extract_failed", + 'package_extracted': "package_extracted", + 'all_archives_extracted': "all_archives_extracted", + 'all_archives_processed': "all_archives_processed", + 'pyload_updated': "pyload_updated"} - def setup(self): - self.scripts = {} + self.periodical.start(60) + self.periodical_task() # @NOTE: Initial scan so dont miss `pyload_start` scripts if any + + def activate(self): + self.pyload_start() + + def make_folders(self): + for folder in self.folders: + dir = os.path.join("scripts", folder) + + if os.path.isdir(dir): + continue + + try: + os.makedirs(dir) + + except OSError, e: + self.log_debug(e, trace=True) + + def periodical_task(self): + self.make_folders() + + for folder in self.folders: + scripts = [] + dirname = os.path.join("scripts", folder) + + if folder not in self.scripts: + self.scripts[folder] = [] + + if os.path.isdir(dirname): + for entry in os.listdir(dirname): + file = os.path.join(dirname, entry) + + if not os.path.isfile(file): + continue + + if file[0] in ("#", "_") or file.endswith( + "~") or file.endswith(".swp"): + continue + + if not os.access(file, os.X_OK): + self.log_warning( + _("Script `%s` is not executable") % entry) + + scripts.append(file) + + new_scripts = [ + _s for _s in scripts if _s not in self.scripts[folder]] + + if new_scripts: + script_names = map(os.path.basename, new_scripts) + self.log_info(_("Activated scripts in folder `%s`: %s") + % (folder, ", ".join(script_names))) + + removed_scripts = [ + _s for _s in self.scripts[folder] if _s not in scripts] + + if removed_scripts: + script_names = map(os.path.basename, removed_scripts) + self.log_info(_("Deactivated scripts in folder `%s`: %s") + % (folder, ", ".join(script_names))) + + self.scripts[folder] = scripts + + def call_cmd(self, command, *args, **kwargs): + call = map(fs_encode, [command] + list(args)) + + self.log_debug( + "EXECUTE " + " ".join('"' + _arg + '"' if ' ' in _arg else _arg for _arg in call)) - folders = ['download_preparing', 'download_finished', 'package_finished', - 'before_reconnect', 'after_reconnect', 'unrar_finished', - 'all_dls_finished', 'all_dls_processed'] + p = Popen(call, bufsize=-1) # @NOTE: output goes to pyload - for folder in folders: - self.scripts[folder] = [] + return p - self.initPluginType(folder, join(pypath, 'scripts', folder)) - self.initPluginType(folder, join('scripts', folder)) + @Expose + def call_script(self, folder, *args, **kwargs): + scripts = self.scripts.get(folder) - for script_type, names in self.scripts.iteritems(): - if names: - self.logInfo((_("Installed scripts for %s: ") % script_type ) + ", ".join([basename(x) for x in names])) + if folder not in self.scripts: + self.log_debug("Folder `%s` not found" % folder) + return - def initPluginType(self, folder, path): - if not exists(path): + if not scripts: + self.log_debug("No script found under folder `%s`" % folder) + return + + self.log_info(_("Executing scripts in folder `%s`...") % folder) + + for file in scripts: try: - makedirs(path) - except: - self.logDebug("Script folder %s not created" % folder) - return + p = self.call_cmd(file, *args) - for f in listdir(path): - if f.startswith("#") or f.startswith(".") or f.startswith("_") or f.endswith("~") or f.endswith(".swp"): - continue + except Exception, e: + self.log_error(_("Runtime error: %s") % file, + e or _("Unknown error")) + + else: + lock = kwargs.get('lock', None) + if lock is True or lock is None and not self.config.get( + 'unlock'): + p.communicate() + + def pyload_updated(self, etag): + """plugins were updated by UpdateManager""" + self.call_script("pyload_updated", etag) + + def pyload_start(self): + """pyload was just started""" + self.call_script('pyload_start') + + def exit(self): + """deprecated method, use pyload_stop or pyload_restart instead""" + event = "restart" if self.pyload.do_restart else "stop" + self.call_script("pyload_" + event, lock=True) + + def before_reconnect(self, ip): + """called before reconnecting""" + self.call_script("before_reconnect", ip) + + def after_reconnect(self, ip, oldip): + """called after reconnecting""" + self.call_script("after_reconnect", ip, oldip) + + def download_preparing(self, pyfile): + """a download was just queued and will be prepared now""" + args = [pyfile.id, pyfile.name, None, pyfile.pluginname, pyfile.url] + self.call_script("download_preparing", *args) + + def download_failed(self, pyfile): + """download has failed""" + file = pyfile.plugin.last_download + args = [pyfile.id, pyfile.name, file, pyfile.pluginname, pyfile.url] + self.call_script("download_failed", *args) + + def download_finished(self, pyfile): + """download successfully finished""" + file = pyfile.plugin.last_download + args = [pyfile.id, pyfile.name, file, pyfile.pluginname, pyfile.url, pyfile.package().name] + self.call_script("download_finished", *args) + + def download_processed(self, pyfile): + """download was precessed""" + file = pyfile.plugin.last_download + args = [pyfile.id, pyfile.name, file, pyfile.pluginname, pyfile.url] + self.call_script("download_processed", *args) + + def archive_extract_failed(self, pyfile, archive): + """archive extraction failed""" + args = [ + pyfile.id, + pyfile.name, + archive.filename, + archive.out, + archive.files] + self.call_script("archive_extract_failed", *args) + + def archive_extracted(self, pyfile, archive): + """archive was successfully extracted""" + args = [ + pyfile.id, + pyfile.name, + archive.filename, + archive.out, + archive.files] + self.call_script("archive_extracted", *args) + + def archive_processed(self, pypack): + """package was either extracted (successfully or not) or ignored because not an archive""" + dl_folder = self.pyload.config.get("general", "download_folder") + + if self.pyload.config.get("general", "folder_per_package"): + dl_folder = os.path.join(dl_folder, pypack.folder) + + args = [pypack.id, pypack.name, dl_folder, pypack.password] + self.call_script("archive_processed", *args) + + def package_finished(self, pypack): + """package finished successfully""" + dl_folder = self.pyload.config.get("general", "download_folder") + + if self.pyload.config.get("general", "folder_per_package"): + dl_folder = os.path.join(dl_folder, pypack.folder) + + args = [pypack.id, pypack.name, dl_folder, pypack.password] + self.call_script("package_finished", *args) + + def package_processed(self, pypack): + """package was processed""" + dl_folder = self.pyload.config.get("general", "download_folder") + + if self.pyload.config.get("general", "folder_per_package"): + dl_folder = os.path.join(dl_folder, pypack.folder) + + args = [pypack.id, pypack.name, dl_folder, pypack.password] + self.call_script("package_processed", *args) + + def package_deleted(self, pid): + """package wad deleted from the queue""" + dl_folder = self.pyload.config.get("general", "download_folder") + pdata = self.pyload.api.getPackageInfo(pid) + + if self.pyload.config.get("general", "folder_per_package"): + dl_folder = os.path.join(dl_folder, pdata.folder) + + args = [pdata.pid, pdata.name, dl_folder, pdata.password] + self.call_script("package_deleted", *args) + + def package_failed(self, pypack): + """package failed somehow""" + dl_folder = self.pyload.config.get("general", "download_folder") - if not access(join(path, f), X_OK): - self.logWarning(_("Script not executable:") + " %s/%s" % (folder, f)) + if self.pyload.config.get("general", "folder_per_package"): + dl_folder = os.path.join(dl_folder, pypack.folder) - self.scripts[folder].append(join(path, f)) + args = [pypack.id, pypack.name, dl_folder, pypack.password] + self.call_script("package_failed", *args) - def callScript(self, script, *args): - try: - cmd = [script] + [str(x) if not isinstance(x, basestring) else x for x in args] - self.logDebug("Executing %(script)s: %(cmd)s" % {"script": abspath(script), "cmd": " ".join(cmd)}) - #output goes to pyload - subprocess.Popen(cmd, bufsize=-1) - except Exception, e: - self.logError(_("Error in %(script)s: %(error)s") % {"script": basename(script), "error": str(e)}) + def package_extract_failed(self, pypack): + """package extraction failed""" + dl_folder = self.pyload.config.get("general", "download_folder") - def downloadPreparing(self, pyfile): - for script in self.scripts['download_preparing']: - self.callScript(script, pyfile.pluginname, pyfile.url, pyfile.id) + if self.pyload.config.get("general", "folder_per_package"): + dl_folder = os.path.join(dl_folder, pypack.folder) - def downloadFinished(self, pyfile): - for script in self.scripts['download_finished']: - self.callScript(script, pyfile.pluginname, pyfile.url, pyfile.name, - save_join(self.config['general']['download_folder'], - pyfile.package().folder, pyfile.name), pyfile.id) + args = [pypack.id, pypack.name, dl_folder, pypack.password] + self.call_script("package_extract_failed", *args) - def packageFinished(self, pypack): - for script in self.scripts['package_finished']: - folder = self.config['general']['download_folder'] - folder = save_join(folder, pypack.folder) + def package_extracted(self, pypack): + """package was successfully extracted""" + dl_folder = self.pyload.config.get("general", "download_folder") - self.callScript(script, pypack.name, folder, pypack.password, pypack.id) + if self.pyload.config.get("general", "folder_per_package"): + dl_folder = os.path.join(dl_folder, pypack.folder) - def beforeReconnecting(self, ip): - for script in self.scripts['before_reconnect']: - self.callScript(script, ip) + args = [pypack.id, pypack.name, dl_folder] + self.call_script("package_extracted", *args) - def afterReconnecting(self, ip): - for script in self.scripts['after_reconnect']: - self.callScript(script, ip) + def all_downloads_finished(self): + """every download in queue is finished successfully""" + self.call_script("all_downloads_finished") - def unrarFinished(self, folder, fname): - for script in self.scripts["unrar_finished"]: - self.callScript(script, folder, fname) + def all_downloads_processed(self): + self.call_script("all_downloads_processed") + """every download was handled (successfully or not), pyload would idle afterwards""" - def allDownloadsFinished(self): - for script in self.scripts["all_dls_finished"]: - self.callScript(script) + def all_archives_extracted(self): + """all archives were extracted""" + self.call_script("all_archives_extracted") - def allDownloadsProcessed(self): - for script in self.scripts["all_dls_processed"]: - self.callScript(script) + def all_archives_processed(self): + """every archive was handled (successfully or not)""" + self.call_script("all_archives_processed") diff --git a/module/plugins/hooks/ExtractArchive.py b/module/plugins/hooks/ExtractArchive.py index be023301c9..3980858645 100644 --- a/module/plugins/hooks/ExtractArchive.py +++ b/module/plugins/hooks/ExtractArchive.py @@ -1,318 +1,594 @@ -#!/usr/bin/env python # -*- coding: utf-8 -*- -import sys +from __future__ import with_statement + import os -from os import remove, chmod, makedirs -from os.path import exists, basename, isfile, isdir, join -from traceback import print_exc -from copy import copy +import sys + +from ..internal.Addon import Addon +from ..internal.Extractor import ArchiveError, CRCError, PasswordError +from ..internal.misc import Expose, fs_encode, exists, fsjoin, safename, threaded, uniqify # monkey patch bug in python 2.6 and lower -# see http://bugs.python.org/issue6122 -# http://bugs.python.org/issue1236 +# http://bugs.python.org/issue6122 , http://bugs.python.org/issue1236 , # http://bugs.python.org/issue1731717 if sys.version_info < (2, 7) and os.name != "nt": - from subprocess import Popen import errno + import subprocess def _eintr_retry_call(func, *args): while True: try: return func(*args) + except OSError, e: if e.errno == errno.EINTR: continue raise - # unsued timeout option for older python version + #: Unsued timeout option for older python version def wait(self, timeout=0): - """Wait for child process to terminate. Returns returncode - attribute.""" + """ + Wait for child process to terminate. Returns returncode + attribute. + """ if self.returncode is None: try: pid, sts = _eintr_retry_call(os.waitpid, self.pid, 0) + except OSError, e: if e.errno != errno.ECHILD: raise - # This happens if SIGCLD is set to be ignored or waiting - # for child processes has otherwise been disabled for our - # process. This child is dead, we can't get the status. + #: This happens if SIGCLD is set to be ignored or waiting + #: For child processes has otherwise been disabled for our + #: process. This child is dead, we can't get the status. sts = 0 self._handle_exitstatus(sts) return self.returncode - Popen.wait = wait + subprocess.Popen.wait = wait + +try: + import send2trash +except ImportError: + pass -if os.name != "nt": - from os import chown - 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 +class ArchiveQueue(object): + def __init__(self, plugin, storage): + self.plugin = plugin + self.storage = storage + self.length = 0 + def __len__(self): + return self.length -class ExtractArchive(Hook): - """ - Provides: unrarFinished (folder, filename) - """ + def get(self): + return self.plugin.db.retrieve(self.storage, default=[]) + + def set(self, value): + self.length = len(value) + return self.plugin.db.store(self.storage, value) + + def delete(self): + self.length = 0 + return self.plugin.db.delete(self.storage) + + def add(self, item): + queue = self.get() + if item not in queue: + return self.set(queue + [item]) + else: + return True + + def remove(self, item): + queue = self.get() + try: + queue.remove(item) + + except ValueError: + pass + + if not queue: + return self.delete() + + return self.set(queue) + + +class ExtractArchive(Addon): __name__ = "ExtractArchive" - __version__ = "0.16" - __description__ = "Extract different kind of archives" - __config__ = [("activated", "bool", "Activated", True), - ("fullpath", "bool", "Extract full path", True), - ("overwrite", "bool", "Overwrite files", True), - ("passwordfile", "file", "password file", "unrar_passwords.txt"), - ("deletearchive", "bool", "Delete archives when done", False), + __type__ = "hook" + __version__ = "1.72" + __status__ = "testing" + + __config__ = [("activated", "bool", "Activated", False), + ("fullpath", "bool", "Extract with full paths", True), + ("overwrite", "bool", "Overwrite files", False), + ("keepbroken", "bool", "Try to extract broken archives", False), + ("repair", "bool", "Repair broken archives (RAR required)", False), + ("usepasswordfile", "bool", "Use password file", True), + ("passwordfile", "file", "Password file", "passwords.txt"), + ("delete", "bool", "Delete archive after extraction", True), + ("deltotrash", "bool", "Move to trash instead delete", True), ("subfolder", "bool", "Create subfolder for each package", False), - ("destination", "folder", "Extract files to", ""), - ("excludefiles", "str", "Exclude files from unpacking (seperated by ;)", ""), - ("recursive", "bool", "Extract archives in archvies", True), - ("queue", "bool", "Wait for all downloads to be finished", True), - ("renice", "int", "CPU Priority", 0)] - __author_name__ = ("pyload Team", "AndroKev") - __author_mail__ = ("adminpyload.org", "@pyloadforum") - - event_list = ["allDownloadsProcessed"] - - def setup(self): - self.plugins = [] + ("destination", "folder", "Extract files to folder", ""), + ("extensions", "str", "Extract archives ending with extension", "001,7z,bz2,bzip2,gz,gzip,lha,lzh,lzma,rar,tar,taz,tbz,tbz2,tgz,xar,xz,z,zip"), + ("excludefiles", "str", "Don't extract the following files", "*.nfo,*.DS_Store,index.dat,thumb.db"), + ("recursive", "bool", "Extract archives in archives", True), + ("waitall", "bool", "Run after all downloads was processed", False), + ("priority", "int", "Process priority", 0)] + + __description__ = """Extract different kind of archives""" + __license__ = "GPLv3" + __authors__ = [("Walter Purcaro", "vuolter@gmail.com"), + ("Immenz", "immenz@gmx.net"), + ("GammaC0de", "nitzo2001[AT]yahoo[DOT]com")] + + def init(self): + self.event_map = {'allDownloadsProcessed': "all_downloads_processed", + 'packageDeleted': "package_deleted"} + + self.queue = ArchiveQueue(self, "Queue") + + self.extracting = False + self.extracted = 0 + self.last_package = False + self.extractors = [] self.passwords = [] - names = [] + self.repair = False - for p in ("UnRar", "UnZip"): + def activate(self): + for p in ("HjSplit", "UnRar", "SevenZip", "UnZip", "UnTar"): try: - module = self.core.pluginManager.loadModule("internal", p) - klass = getattr(module, p) - if klass.checkDeps(): - names.append(p) - self.plugins.append(klass) + klass = self.pyload.pluginManager.loadClass("internal", p) + if klass.find(): + self.extractors.append(klass) + if klass.REPAIR: + self.repair = self.config.get('repair') except OSError, e: if e.errno == 2: - self.logInfo(_("No %s installed") % p) + self.log_warning(_("No %s installed") % p) else: - self.logWarning(_("Could not activate %s") % p, str(e)) - if self.core.debug: - print_exc() + self.log_warning(_("Could not activate: %s") % p, e) except Exception, e: - self.logWarning(_("Could not activate %s") % p, str(e)) - if self.core.debug: - print_exc() + self.log_warning(_("Could not activate: %s") % p, e) - if names: - self.logInfo(_("Activated") + " " + " ".join(names)) + if self.extractors: + self.log_debug(*["Found %s %s" % (Extractor.__name__, Extractor.VERSION) + for Extractor in self.extractors]) + self.extract_queued() #: Resume unfinished extractions else: - self.logInfo(_("No Extract plugins activated")) + self.log_info(_("No Extract plugins activated")) + + @threaded + def extract_queued(self, thread): + # @NOTE: doing the check here for safety (called by coreReady) + if self.extracting: + return - # queue with package ids - self.queue = [] + self.extracting = True - @Expose - def extractPackage(self, id): - """ Extract package with given id""" - self.manager.startThread(self.extract, [id]) - - def packageFinished(self, pypack): - if self.getConfig("queue"): - self.logInfo(_("Package %s queued for later extracting") % pypack.name) - self.queue.append(pypack.id) - else: - self.manager.startThread(self.extract, [pypack.id]) + packages = self.queue.get() + while packages: + if self.extract(packages, thread): + self.extracted += 1 - @threaded - def allDownloadsProcessed(self, thread): - local = copy(self.queue) - del self.queue[:] - self.extract(local, thread) + if self.last_package and len(self.queue) == 0: #: last_package is set by all_downloads_processed() + self.last_package = False + if self.extracted: + self.extracted = 0 + self.manager.dispatchEvent("all_archives_extracted") + self.manager.dispatchEvent("all_archives_processed") + + packages = self.queue.get() #: Check for packages added during extraction + + self.extracting = False - def extract(self, ids, thread=None): - # reload from txt file - self.reloadPasswords() + #: Deprecated method, use `extract_package` instead + @Expose + def extractPackage(self, *args, **kwargs): + """ + See `extract_package` + """ + return self.extract_package(*args, **kwargs) + + @Expose + def extract_package(self, *ids): + """ + Extract packages with given id + """ + for id in ids: + self.queue.add(id) + if not self.config.get('waitall') and not self.extracting: + self.extract_queued() + + def package_deleted(self, pid): + self.queue.remove(pid) + + def package_finished(self, pypack): + self.queue.add(pypack.id) + if not self.config.get('waitall') and not self.extracting: + self.extract_queued() + + def all_downloads_processed(self): + self.last_package = True + if self.config.get('waitall') and not self.extracting: + self.extract_queued() - # dl folder - dl = self.config['general']['download_folder'] + @Expose + def extract(self, package_ids, thread=None): # @TODO: Use pypack, not pid to improve method usability + if not package_ids: + return False extracted = [] + failed = [] + + toList = lambda string: string.replace(' ', '').replace(',','|').replace(';', '|').split('|') + + destination = self.config.get('destination') + subfolder = self.config.get('subfolder') + fullpath = self.config.get('fullpath') + overwrite = self.config.get('overwrite') + priority = self.config.get('priority') + recursive = self.config.get('recursive') + keepbroken = self.config.get('keepbroken') + + extensions = [x.lstrip('.').lower() + for x in toList(self.config.get('extensions'))] + excludefiles = toList(self.config.get('excludefiles')) - #iterate packages -> plugins -> targets - for pid in ids: - p = self.core.files.getPackage(pid) - self.logInfo(_("Check package %s") % p.name) - if not p: + if extensions: + self.log_debug("Use for extensions: .%s" % "|.".join(extensions)) + + #: Reload from txt file + self.reload_passwords() + + dl_folder = self.pyload.config.get("general", "download_folder") + + #: Iterate packages -> extractors -> targets + for package_id in package_ids: + pypack = self.pyload.files.getPackage(package_id) + + if not pypack: + self.queue.remove(package_id) continue - # determine output folder - out = save_join(dl, p.folder, "") - # force trailing slash + self.log_info(_("Check package: %s") % pypack.name) + + pack_dl_folder = fsjoin( + dl_folder, + pypack.folder, + "") #: Force trailing slash - if self.getConfig("destination") and self.getConfig("destination").lower() != "none": + #: Determine output folder + extract_folder = fsjoin( + pack_dl_folder, + destination, + "") #: Force trailing slash - out = save_join(dl, p.folder, self.getConfig("destination"), "") - #relative to package folder if destination is relative, otherwise absolute path overwrites them + if subfolder: + extract_folder = fsjoin(extract_folder, + pypack.folder or safename(pypack.name.replace("http://", ""))) - if self.getConfig("subfolder"): - out = join(out, fs_encode(p.folder)) + if not exists(extract_folder): + os.makedirs(extract_folder) - if not exists(out): - makedirs(out) + if subfolder: + self.set_permissions(extract_folder) - files_ids = [(save_join(dl, p.folder, x["name"]), x["id"]) for x in p.getChildren().itervalues()] matched = False + success = True + files_ids = dict((fdata['name'], (fdata['id'], (fsjoin(pack_dl_folder, fdata['name'])), extract_folder)) + for fdata in pypack.getChildren().values()).values() #: Remove duplicates - # check as long there are unseen files + #: Check as long there are unseen files while files_ids: new_files_ids = [] - for plugin in self.plugins: - targets = plugin.getTargets(files_ids) + if extensions: #: Include only specified archive types + files_ids = filter(lambda file_id: any((Extractor.archivetype(file_id[1]) in extensions + for Extractor in self.extractors)), files_ids) + + #: Sort by filename to ensure (or at least try) that a multi-volume archive is targeted by its first part + #: This is important because, for example, UnRar ignores preceding parts in listing mode + files_ids.sort(key=lambda file_id: file_id[1]) + + for Extractor in self.extractors: + targets = Extractor.get_targets(files_ids) if targets: - self.logDebug("Targets for %s: %s" % (plugin.__name__, targets)) + self.log_debug("Targets for %s: %s" % (Extractor.__name__, targets)) matched = True - for target, fid in targets: - if target in extracted: - self.logDebug(basename(target), "skipped") - continue - extracted.append(target) # prevent extracting same file twice - - klass = plugin(self, target, out, self.getConfig("fullpath"), self.getConfig("overwrite"), self.getConfig("excludefiles"), - self.getConfig("renice")) - klass.init() - - self.logInfo(basename(target), _("Extract to %s") % out) - new_files = self.startExtracting(klass, fid, p.password.strip().splitlines(), thread) - self.logDebug("Extracted: %s" % new_files) - self.setPermissions(new_files) - - for file in new_files: - if not exists(file): - self.logDebug("new file %s does not exists" % file) + + for fid, fname, fout in targets: + name = os.path.basename(fname) + + if not exists(fname): + self.log_debug(name, "File not found") continue - if self.getConfig("recursive") and isfile(file): - new_files_ids.append((file, fid)) # append as new target - files_ids = new_files_ids # also check extracted files + self.log_info(name, _("Extract to: %s") % fout) + try: + pyfile = self.pyload.files.getFile(fid) + archive = Extractor(pyfile, + fname, + fout, + fullpath, + overwrite, + excludefiles, + priority, + keepbroken) + + thread.addActive(pyfile) + archive.init() + + #: Save for removal from file processing list, which happens after deletion. + #: So archive.chunks() would just return an empty list. + chunks = archive.chunks() + + try: + new_files = self._extract(pyfile, archive, pypack.password) + + finally: + pyfile.setProgress(100) + thread.finishFile(pyfile) + + except Exception, e: + self.log_error(name, e) + success = False + continue - if not matched: - self.logInfo(_("No files found to extract")) + #: Remove processed file and related multi-parts from list + files_ids = [(_fid, _fname, _fout) + for _fid, _fname, _fout in files_ids + if _fname not in chunks] - def startExtracting(self, plugin, fid, passwords, thread): - pyfile = self.core.files.getFile(fid) - if not pyfile: - return [] + self.log_debug("Extracted files: %s" % new_files) - pyfile.setCustomStatus(_("extracting")) - thread.addActive(pyfile) # keep this file until everything is done + new_folders = [] + for _f in new_files: + _d = os.path.dirname(_f) + while extract_folder in _d: + if _d not in new_folders: + new_folders.append(_d) + _d = os.path.dirname(_d) - try: - progress = lambda x: pyfile.setProgress(x) - success = False + for foldername in new_folders: + self.set_permissions(foldername) + + for filename in new_files: + self.set_permissions(filename) + + for filename in new_files: + if not exists(filename): + self.log_debug("New file %s does not exists" % filename) + continue + + if recursive and os.path.isfile(filename): + new_files_ids.append((fid, filename, os.path.dirname(filename))) #: Append as new target + + self.manager.dispatchEvent("archive_extracted", pyfile, archive) + + files_ids = new_files_ids #: Also check extracted files + + if matched: + if success: + #: Delete empty pack folder if extract_folder resides outside download folder + if self.config.get('delete') and self.pyload.config.get('general', 'folder_per_package'): + if not extract_folder.startswith(pack_dl_folder): + if len(os.listdir(pack_dl_folder)) == 0: + try: + os.rmdir(pack_dl_folder) + self.log_debug("Successfully deleted pack folder %s" % pack_dl_folder) + + except OSError: + self.log_warning("Unable to delete pack folder %s" % pack_dl_folder) + + else: + self.log_warning("Not deleting pack folder %s, folder not empty" % pack_dl_folder) + + extracted.append(package_id) + self.manager.dispatchEvent("package_extracted", pypack) + + else: + failed.append(package_id) + self.manager.dispatchEvent("package_extract_failed", pypack) - if not plugin.checkArchive(): - plugin.extract(progress) - success = True else: - self.logInfo(basename(plugin.file), _("Password protected")) - self.logDebug("Passwords: %s" % str(passwords)) - - pwlist = copy(self.getPasswords()) - #remove already supplied pws from list (only local) - for pw in passwords: - if pw in pwlist: - pwlist.remove(pw) - - for pw in passwords + pwlist: - try: - self.logDebug("Try password: %s" % pw) - if plugin.checkPassword(pw): - plugin.extract(progress, pw) - self.addPassword(pw) - success = True + self.log_info(_("No files found to extract")) + + if not matched or not success and subfolder: + try: + os.rmdir(extract_folder) + + except OSError: + pass + + self.queue.remove(package_id) + + self.manager.dispatchEvent("archive_processed", pypack) + + return True if extracted else False + + def _extract(self, pyfile, archive, password): + name = os.path.basename(archive.filename) + + pyfile.setStatus("processing") + + encrypted = False + try: + self.log_debug("Password: %s" % (password or "none provided")) + passwords = uniqify([password] + self.get_passwords(False)) if \ + self.config.get('usepasswordfile') else [password] + + for pw in passwords: + try: + pyfile.setCustomStatus(_("archive testing")) + pyfile.setProgress(0) + self.log_debug("Verifying using password: %s" % (pw or "None")) + archive.verify(pw) + pyfile.setProgress(100) + + except PasswordError: + if not encrypted: + self.log_info(name, _("Password protected")) + encrypted = True + self.log_debug("Password was wrong") + + except CRCError, e: + self.log_debug(name, e) + self.log_info(name, _("CRC Error")) + + if not self.repair: + raise CRCError("Archive damaged") + + else: + self.log_warning(name, _("Repairing...")) + pyfile.setCustomStatus(_("archive repairing")) + pyfile.setProgress(0) + repaired = archive.repair() + pyfile.setProgress(100) + + if not repaired and not self.config.get('keepbroken'): + raise CRCError("Archive damaged") + + else: + self.add_password(pw) + password = pw break - except WrongPassword: - self.logDebug("Password was wrong") - - if not success: - self.logError(basename(plugin.file), _("Wrong password")) - return [] - - if self.core.debug: - self.logDebug("Would delete: %s" % ", ".join(plugin.getDeleteFiles())) - - if self.getConfig("deletearchive"): - files = plugin.getDeleteFiles() - self.logInfo(_("Deleting %s files") % len(files)) - for f in files: - if exists(f): - remove(f) + + except ArchiveError, e: + raise ArchiveError(e) + + else: + self.add_password(pw) + password = pw + self.log_debug("Password Correct") + break + + else: + if encrypted: + self.log_error(_("None of the known passwords worked")) + raise PasswordError + + pyfile.setCustomStatus(_("archive extracting")) + + pyfile.setProgress(0) + self.log_debug("Extracting using password: %s" % (password or "None")) + archive.extract(password) + pyfile.setProgress(100) + + pyfile.setStatus("processing") + extracted_files = archive.files or archive.list(password) + delfiles = archive.chunks() + self.log_debug("Would delete: " + ", ".join(delfiles)) + + if self.config.get('delete'): + self.log_info(_("Deleting %s files") % len(delfiles)) + + deltotrash = self.config.get('deltotrash') + for f in delfiles: + file = fs_encode(f) + if not exists(file): + continue + + if not deltotrash: + os.remove(file) + else: - self.logDebug("%s does not exists" % f) + try: + send2trash.send2trash(file) + + except NameError: + self.log_warning(_("Unable to move %s to trash") % os.path.basename(f), + _("Send2Trash lib not found")) + + except Exception, e: + self.log_warning(_("Unable to move %s to trash") % os.path.basename(f), + e.message) + + else: + self.log_info(_("Moved %s to trash") % os.path.basename(f)) + + self.log_info(name, _("Extracting finished")) + + return extracted_files - self.logInfo(basename(plugin.file), _("Extracting finished")) - self.manager.dispatchEvent("unrarFinished", plugin.out, plugin.file) + except PasswordError: + self.log_error(name, _("Wrong password" if password else "No password found")) - return plugin.getExtractedFiles() + except CRCError, e: + self.log_error(name, _("CRC mismatch"), e) except ArchiveError, e: - self.logError(basename(plugin.file), _("Archive Error"), str(e)) - except CRCError: - self.logError(basename(plugin.file), _("CRC Mismatch")) + self.log_error(name, _("Archive error"), e) + except Exception, e: - if self.core.debug: - print_exc() - self.logError(basename(plugin.file), _("Unknown Error"), str(e)) + self.log_error(name, _("Unknown error"), e) + + self.manager.dispatchEvent("archive_extract_failed", pyfile, archive) + + raise Exception(_("Extract failed")) - return [] + #: Deprecated method, use `get_passwords` instead + @Expose + def getPasswords(self, *args, **kwargs): + """ + See `get_passwords` + """ + return self.get_passwords(*args, **kwargs) @Expose - def getPasswords(self): - """ List of saved passwords """ + def get_passwords(self, reload=True): + """ + List of saved passwords + """ + if reload: + self.reload_passwords() + return self.passwords - def reloadPasswords(self): - pwfile = self.getConfig("passwordfile") - if not exists(pwfile): - open(pwfile, "wb").close() + def reload_passwords(self): + try: + passwords = [] - passwords = [] - f = open(pwfile, "rb") - for pw in f.read().splitlines(): - passwords.append(pw) - f.close() + file = fs_encode(self.config.get('passwordfile')) + with open(file) as f: + for pw in f.read().splitlines(): + passwords.append(pw) - self.passwords = passwords + except IOError, e: + if e.errno == 2: + with open(file, "wb") as f: + pass + else: + self.log_error(e) + + else: + self.passwords = passwords + + #: Deprecated method, use `add_password` instead @Expose - def addPassword(self, pw): - """ Adds a password to saved list""" - pwfile = self.getConfig("passwordfile") - - if pw in self.passwords: - self.passwords.remove(pw) - self.passwords.insert(0, pw) - - f = open(pwfile, "wb") - for pw in self.passwords: - f.write(pw + "\n") - f.close() - - def setPermissions(self, files): - for f in files: - if not exists(f): - continue - try: - if self.config["permission"]["change_file"]: - if isfile(f): - chmod(f, int(self.config["permission"]["file"], 8)) - elif isdir(f): - chmod(f, int(self.config["permission"]["folder"], 8)) - - if self.config["permission"]["change_dl"] and os.name != "nt": - uid = getpwnam(self.config["permission"]["user"])[2] - gid = getgrnam(self.config["permission"]["group"])[2] - chown(f, uid, gid) - except Exception, e: - self.logWarning(_("Setting User and Group failed"), e) + def addPassword(self, *args, **kwargs): + """ + See `add_password` + """ + return self.add_password(*args, **kwargs) + + @Expose + def add_password(self, password): + """ + Adds a password to saved list + """ + try: + self.passwords = uniqify([password] + self.passwords) + + file = fs_encode(self.config.get('passwordfile')) + with open(file, "wb") as f: + for pw in self.passwords: + f.write(pw + '\n') + + except IOError, e: + self.log_error(e) diff --git a/module/plugins/hooks/FastixRu.py b/module/plugins/hooks/FastixRu.py deleted file mode 100644 index 25c9a1a674..0000000000 --- a/module/plugins/hooks/FastixRu.py +++ /dev/null @@ -1,27 +0,0 @@ -# -*- coding: utf-8 -*- - -# should be working - -from module.network.RequestFactory import getURL -from module.plugins.internal.MultiHoster import MultiHoster -from module.common.json_layer import json_loads - - -class FastixRu(MultiHoster): - __name__ = "FastixRu" - __version__ = "0.02" - __type__ = "hook" - __config__ = [("activated", "bool", "Activated", "False"), - ("hosterListMode", "all;listed;unlisted", "Use for hosters (if supported)", "all"), - ("unloadFailing", "bool", "Revert to standard download if download fails", "False"), - ("interval", "int", "Reload interval in hours (0 to disable)", "24")] - __description__ = """Fastix hook plugin""" - __author_name__ = ("Massimo, Rosamilia") - __author_mail__ = ("max@spiritix.eu") - - def getHoster(self): - page = getURL( - "http://fastix.ru/api_v2/?apikey=5182964c3f8f9a7f0b00000a_kelmFB4n1IrnCDYuIFn2y&sub=allowed_sources") - host_list = json_loads(page) - host_list = host_list['allow'] - return host_list diff --git a/module/plugins/hooks/HotFolder.py b/module/plugins/hooks/HotFolder.py index e44c1e1720..21c8c8d869 100644 --- a/module/plugins/hooks/HotFolder.py +++ b/module/plugins/hooks/HotFolder.py @@ -1,83 +1,89 @@ # -*- 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 . - - @author: RaNaN - @interface-version: 0.2 -""" - -from os import makedirs -from os import listdir -from os.path import exists -from os.path import join -from os.path import isfile -from shutil import move +from __future__ import with_statement + +import os +import shutil import time -from module.plugins.Hook import Hook +from ..internal.Addon import Addon +from ..internal.misc import fs_encode, fsjoin -class HotFolder(Hook): +class HotFolder(Addon): __name__ = "HotFolder" - __version__ = "0.11" - __description__ = """observe folder and file for changes and add container and links""" - __config__ = [("activated", "bool", "Activated", "False"), - ("folder", "str", "Folder to observe", "container"), - ("watch_file", "bool", "Observe link file", "False"), - ("keep", "bool", "Keep added containers", "True"), - ("file", "str", "Link file", "links.txt")] - __threaded__ = [] - __author_name__ = ("RaNaN") - __author_mail__ = ("RaNaN@pyload.de") - - def setup(self): - self.interval = 10 - - def periodical(self): - - if not exists(join(self.getConfig("folder"), "finished")): - makedirs(join(self.getConfig("folder"), "finished")) - - if self.getConfig("watch_file"): - - if not exists(self.getConfig("file")): - f = open(self.getConfig("file"), "wb") - f.close() - - f = open(self.getConfig("file"), "rb") - content = f.read().strip() - f.close() - f = open(self.getConfig("file"), "wb") - f.close() - if content: - name = "%s_%s.txt" % (self.getConfig("file"), time.strftime("%H-%M-%S_%d%b%Y")) - - f = open(join(self.getConfig("folder"), "finished", name), "wb") - f.write(content) - f.close() - - self.core.api.addPackage(f.name, [f.name], 1) - - for f in listdir(self.getConfig("folder")): - path = join(self.getConfig("folder"), f) - - if not isfile(path) or f.endswith("~") or f.startswith("#") or f.startswith("."): - continue - - newpath = join(self.getConfig("folder"), "finished", f if self.getConfig("keep") else "tmp_" + f) - move(path, newpath) - - self.logInfo(_("Added %s from HotFolder") % f) - self.core.api.addPackage(f, [newpath], 1) + __type__ = "hook" + __version__ = "0.27" + __status__ = "testing" + + __config__ = [("activated", "bool", "Activated", False), + ("folder", "folder", "Folder to watch", "watchdir"), + ("watchfile", "bool", "Watch link file", False), + ("delete", "bool", "Delete added containers", False), + ("file", "file", "Link file", "links.txt"), + ("interval", "int", "File / folder check interval in seconds (minimum 20)", 60), + ("enable_extension_filter", "bool", "Extension filter", False), + ("extension_filter", "str", "Extensions to look for (comma separated)", "dlc"), + ("add_to", "Collector;Queue", "Add files to", "Queue")] + + __description__ = """Observe folder and file for changes and add container and links""" + __license__ = "GPLv3" + __authors__ = [("RaNaN", "RaNaN@pyload.de"), + ("GammaC0de", "nitzo2001[AT]yahoo[DOT]com")] + + def activate(self): + self.extensions = None + if self.config.get("enable_extension_filter"): + extension_filter = self.config.get("extension_filter") + self.extensions = [s.strip() for s in extension_filter.split(",")] + self.log_info(_("Watching only for extensions [%s]") % + ",".join(["'%s'" % ext for ext in self.extensions])) + + interval = max(self.config.get('interval'), 20) + self.periodical.start(interval, threaded=True) + + def periodical_task(self): + folder = fs_encode(self.config.get('folder')) + file = fs_encode(self.config.get('file')) + add_to = 0 if self.config.get("add_to") == "Collector" else 1 + + try: + if not os.path.isdir(os.path.join(folder, "finished")): + os.makedirs(os.path.join(folder, "finished")) + + if self.config.get('watchfile'): + with open(file, "a+") as f: + f.seek(0) + content = f.read().strip() + + if content: + f = open(file, "wb") + f.close() + + name = "%s_%s.txt" % (file, time.strftime("%H-%M-%S_%d%b%Y")) + + with open(fsjoin(folder, "finished", name), "wb") as f: + f.write(content) + + self.pyload.api.addPackage(f.name, [f.name], add_to) + + for f in os.listdir(folder): + path = os.path.join(folder, f) + + if not os.path.isfile(path) or f.endswith("~") or f.startswith("#") or f.startswith("."): + continue + + if self.extensions is not None: + extension = os.path.splitext(f)[1] + # Note that extension contains the leading dot + if len(extension) == 0 or extension[1:] not in self.extensions: + continue + + newpath = os.path.join(folder, "finished", "tmp_" + f if self.config.get('delete') else f) + shutil.move(path, newpath) + + self.log_info(_("Added %s from HotFolder") % f) + self.pyload.api.addPackage(f, [newpath], add_to) + + except (IOError, OSError), e: + self.log_error(e, trace=True) diff --git a/module/plugins/hooks/IRC.py b/module/plugins/hooks/IRC.py new file mode 100644 index 0000000000..bc21fc3bb0 --- /dev/null +++ b/module/plugins/hooks/IRC.py @@ -0,0 +1,501 @@ +# -*- coding: utf-8 -*- + +import re +import select +import socket +import time +import traceback +from threading import Thread + +import pycurl +import ssl +from module.Api import FileDoesNotExists, PackageDoesNotExists + +from ..internal.misc import format_size +from ..internal.Notifier import Notifier + + +class IRC(Thread, Notifier): + __name__ = "IRC" + __type__ = "hook" + __version__ = "0.28" + __status__ = "testing" + + __config__ = [("activated", "bool", "Activated", False), + ("host", "str", "IRC-Server Address", "Enter your server here!"), + ("port", "int", "IRC-Server Port", 6667), + ("ident", "str", "Clients ident", "pyload-irc"), + ("realname", "str", "Realname", "pyload-irc"), + ("ssl", "bool", "Use SSL", False), + ("nick", "str", "Nickname the Client will take", "pyLoad-IRC"), + ("owner", "str", "Nickname the Client will accept commands from", "Enter your nick here!"), + ("info_file", "bool", "Inform about every file finished", False), + ("info_pack", "bool", "Inform about every package finished", True), + ("captcha", "bool", "Send captcha requests", True), + ("maxline", "int", "Maximum line per message", 6)] + + __description__ = """Connect to irc and let owner perform different tasks""" + __license__ = "GPLv3" + __authors__ = [("Jeix", "Jeix@hasnomail.com")] + + def __init__(self, *args, **kwargs): + Thread.__init__(self) + Notifier.__init__(self, *args, **kwargs) + self.setDaemon(True) + + def activate(self): + self.abort = False + self.more = [] + self.new_package = {} + + self.start() + + def package_finished(self, pypack): + try: + if self.config.get('info_pack'): + self.response(_("Package finished: %s") % pypack.name) + + except Exception: + pass + + def download_finished(self, pyfile): + try: + if self.config.get('info_file'): + self.response(_("Download finished: %s @ %s ") % (pyfile.name, pyfile.pluginname)) + + except Exception: + pass + + def captcha_task(self, task): + if self.config.get('captcha') and task.isTextual(): + task.handler.append(self) + task.setWaiting(60) + + html = self.load("http://www.freeimagehosting.net/upl.php", + post={'file': (pycurl.FORM_FILE, task.captchaParams['file'])}) + + url = re.search(r"src='([^']+)'", html).group(1) + self.response(_("New Captcha Request: %s") % url) + self.response(_("Answer with 'c %s text on the captcha'") % task.id) + + def run(self): + #: Connect to IRC etc. + self.sock = socket.socket() + host = self.config.get('host') + self.sock.connect((host, self.config.get('port'))) + + if self.config.get('ssl'): + self.sock = ssl.wrap_socket(self.sock, cert_reqs=ssl.CERT_NONE) # @TODO: support certificate + + nick = self.config.get('nick') + self.sock.send("NICK %s\r\n" % nick) + self.sock.send("USER %s %s bla :%s\r\n" % (nick, host, nick)) + for t in self.config.get('owner').split(): + if t.strip().startswith("#"): + self.sock.send("JOIN %s\r\n" % t.strip()) + self.log_info(_("Connected to"), host) + self.log_info(_("Switching to listening mode!")) + try: + self.main_loop() + + except IRCError, ex: + self.sock.send("QUIT :byebye\r\n") + if self.pyload.debug: + traceback.print_exc() + self.sock.close() + + def main_loop(self): + readbuffer = "" + while True: + time.sleep(1) + fdset = select.select([self.sock], [], [], 0) + if self.sock not in fdset[0]: + continue + + if self.abort: + raise IRCError("quit") + + readbuffer += self.sock.recv(1024) + temp = readbuffer.split("\n") + readbuffer = temp.pop() + + for line in temp: + line = line.rstrip() + first = line.split() + + if first[0] == "PING": + self.sock.send("PONG :%s\r\n" % first[1]) + + if first[0] == "ERROR": + raise IRCError(line) + + msg = line.split(None, 3) + if len(msg) < 4: + continue + + msg = { + 'origin': msg[0][1:], + 'action': msg[1], + 'target': msg[2], + 'text': msg[3][1:] + } + + self.handle_events(msg) + + def handle_events(self, msg): + if not msg['origin'].split("!", 1)[0] in self.config.get('owner').split(): + return + + if msg['target'].split("!", 1)[0] != self.config.get('nick'): + return + + if msg['action'] != "PRIVMSG": + return + + #: HANDLE CTCP ANTI FLOOD/BOT PROTECTION + if msg['text'] == "\x01VERSION\x01": + self.log_debug("Sending CTCP VERSION") + self.sock.send("NOTICE %s :%s\r\n" % (msg['origin'], "pyLoad! IRC Interface")) + return + + elif msg['text'] == "\x01TIME\x01": + self.log_debug("Sending CTCP TIME") + self.sock.send("NOTICE %s :%d\r\n" % (msg['origin'], time.time())) + return + + elif msg['text'] == "\x01LAG\x01": + self.log_debug("Received CTCP LAG") #: don't know how to answer + return + + trigger = "pass" + args = None + + try: + temp = msg['text'].split() + trigger = temp[0] + if len(temp) > 1: + args = temp[1:] + + except Exception: + pass + + handler = getattr(self, "event_%s" % trigger, self.event_pass) + try: + res = handler(args) + for line in res: + self.response(line, msg['origin']) + + except Exception, e: + self.log_error(e, trace=True) + + def response(self, msg, origin=""): + if origin == "": + for t in self.config.get('owner').split(): + self.sock.send("PRIVMSG %s :%s\r\n" % (t.strip(), msg)) + else: + self.sock.send("PRIVMSG %s :%s\r\n" % (origin.split("!", 1)[0], msg)) + + # Events + def event_pass(self, args): + return [] + + def event_status(self, args): + downloads = self.pyload.api.statusDownloads() + if not downloads: + return ["INFO: There are no active downloads currently."] + + temp_progress = "" + lines = ["ID - Name - Status - Speed - ETA - Progress"] + for data in downloads: + + if data.status == 5: + temp_progress = data.format_wait + else: + temp_progress = "%d%% (%s)" % (data.percent, data.format_size) + + lines.append("#%d - %s - %s - %s - %s - %s" % (data.fid, data.name, data.statusmsg, + "%s/s" % format_size(data.speed), + "%s" % data.format_eta, temp_progress)) + return lines + + def event_queue(self, args): + pdata = self.pyload.api.getQueueData() + + if not pdata: + return ["INFO: There are no packages in queue."] + + lines = [] + for pack in pdata: + lines.append('PACKAGE #%s: "%s" with %d links.' % (pack.pid, pack.name, len(pack.links))) + + return lines + + def event_collector(self, args): + pdata = self.pyload.api.getCollectorData() + if not pdata: + return ["INFO: No packages in collector!"] + + lines = [] + for pack in pdata: + lines.append('PACKAGE #%s: "%s" with %d links.' % (pack.pid, pack.name, len(pack.links))) + + return lines + + def event_info(self, args): + if not args: + return ["ERROR: Use info like this: info "] + + info = None + try: + info = self.pyload.api.getFileData(int(args[0])) + + except FileDoesNotExists: + return ["ERROR: Link doesn't exists."] + + return ['LINK #%s: %s (%s) [%s][%s]' % (info.fid, info.name, info.format_size, info.statusmsg, info.plugin)] + + def event_packinfo(self, args): + if not args: + return ["ERROR: Use packinfo like this: packinfo "] + + lines = [] + idorname = args[0] + + pack = self._getPackageByNameOrId(idorname) + if not pack: + return ["ERROR: Package doesn't exists."] + + self.more = [] + lines.append('PACKAGE #%s: "%s" with %d links' % (pack.pid, pack.name, len(pack.links))) + for pyfile in pack.links: + self.more.append('LINK #%s: %s (%s) [%s][%s]' % (pyfile.fid, pyfile.name, pyfile.format_size, + pyfile.statusmsg, pyfile.plugin)) + + maxline = self.config.get('maxline') + if len(self.more) < maxline: + lines.extend(self.more) + self.more = [] + else: + lines.extend(self.more[:maxline]) + self.more = self.more[maxline:] + lines.append("%d more links do display." % len(self.more)) + + return lines + + def event_more(self, args): + if not self.more: + return ["No more information to display."] + + maxline = self.config.get('maxline') + lines = self.more[:maxline] + self.more = self.more[maxline:] + lines.append("%d more lines do display." % len(self.more)) + + return lines + + def event_unpause(self, args): + self.pyload.api.unpauseServer() + return ["INFO: Starting downloads."] + + def event_pause(self, args): + self.pyload.api.pauseServer() + return ["INFO: No new downloads will be started."] + + def event_togglepause(self, args): + if self.pyload.api.togglePause(): + return ["INFO: Starting downloads."] + else: + return ["INFO: No new downloads will be started."] + + def event_add(self, args): + if len(args) < 2: + return ['ERROR: Add links like this: "add links". ', + "This will add the link to to the package / the package with id !"] + + idorname = args[0].strip() + links = [x.strip() for x in args[1:]] + + pack = self._getPackageByNameOrId(idorname) + if not pack: + #: Create new package + id = self.pyload.api.addPackage(idorname, links, 1) + return ["INFO: Created new Package %s [#%d] with %d links." % (idorname, id, len(links))] + + self.pyload.api.addFiles(pack.pid, links) + return ["INFO: Added %d links to Package %s [#%d]" % (len(links), pack.name, pack.pid)] + + def event_del(self, args): + if len(args) < 2: + return ["ERROR: Use del command like this: del -p|-l [...] (-p indicates that the ids are from packages, -l indicates that the ids are from links)"] + + if args[0] == "-p": + ret = self.pyload.api.deletePackages(map(int, args[1:])) + return ["INFO: Deleted %d packages!" % len(args[1:])] + + elif args[0] == "-l": + ret = self.pyload.api.deleteFiles(map(int, args[1:])) + return ["INFO: Deleted %d links!" % len(args[1:])] + + else: + return ["ERROR: Use del command like this: del <-p|-l> [...] (-p indicates that the ids are from packages, -l indicates that the ids are from links)"] + + def event_push(self, args): + if not args: + return ["ERROR: Push package to queue like this: push "] + + id = int(args[0]) + try: + self.pyload.api.getPackageInfo(id) + except PackageDoesNotExists: + return ["ERROR: Package #%d does not exist." % id] + + self.pyload.api.pushToQueue(id) + return ["INFO: Pushed package #%d to queue." % id] + + def event_pull(self, args): + if not args: + return ["ERROR: Pull package from queue like this: pull ."] + + id = int(args[0]) + if not self.pyload.api.getPackageData(id): + return ["ERROR: Package #%d does not exist." % id] + + self.pyload.api.pullFromQueue(id) + return ["INFO: Pulled package #%d from queue to collector." % id] + + def event_c(self, args): + """ + Captcha answer + """ + if not args: + return ["ERROR: Captcha ID missing."] + + task = self.pyload.captchaManager.getTaskByID(args[0]) + if not task: + return ["ERROR: Captcha Task with ID %s does not exists." % args[0]] + + task.setResult(" ".join(args[1:])) + return ["INFO: Result %s saved." % " ".join(args[1:])] + + def event_freeSpace(self, args): + b = format_size(int(self.pyload.api.freeSpace())) + return ["INFO: Free space is %s." % (b)] + + def event_restart(self, args): + self.pyload.api.restart() + return ["INFO: Done."] + + def event_restartFile(self, args): + if not args: + return ['ERROR: missing argument'] + id = int(args[0]) + if not self.pyload.api.getFileData(id): + return ["ERROR: File #%d does not exist." % id] + self.pyload.api.restartFile(id) + return ["INFO: Restart file #%d." % id] + + def event_restartPackage(self, args): + if not args: + return ['ERROR: missing argument'] + idorname = args[0] + pack = self._getPackageByNameOrId(idorname) + if not pack: + return ["ERROR: Package #%s does not exist." % idorname] + self.pyload.api.restartPackage(pack.pid) + return ["INFO: Restart package %s (#%d)." % (pack.name, pack.pid)] + + def event_deleteFinished(self, args): + return ["INFO: Deleted package ids: %s." % self.pyload.api.deleteFinished()] + + def event_getLog(self, args): + """Returns most recent log entries.""" + self.more = [] + lines = [] + log = self.pyload.api.getLog() + + for line in log: + if line: + if line[-1] == '\n': + line = line[:-1] + self.more.append("LOG: %s" % line) + + maxline = self.config.get('maxline') + if args and args[0] == 'last': + if len(args) < 2: + self.more = self.more[-maxline:] + else: + self.more = self.more[-(int(args[1])):] + + if len(self.more) < maxline: + lines.extend(self.more) + self.more = [] + else: + lines.extend(self.more[:maxline]) + self.more = self.more[maxline:] + lines.append("%d more logs do display." % len(self.more)) + + return lines + + def event_help(self, args): + lines = ["The following commands are available:", + "add [...] Adds link to package. (creates new package if it does not exist)", + "collector Shows all packages in collector", + "del -p|-l [...] Deletes all packages|links with the ids specified", + "deleteFinished Deletes all finished files and completly finished packages", + "freeSpace Available free space at download directory in bytes", + "getLog [last [nb]] Returns most recent log entries", + "help Shows this help message", + "info Shows info of the link with id ", + "more Shows more info when the result was truncated", + "packinfo Shows info of the package with id ", + "pause Stops the download (but not abort active downloads)", + "pull Pull package from queue", + "push Push package to queue", + "queue Shows all packages in the queue", + "restart Restart pyload core", + "restartFailed Restarts all failed failes", + "restartFile Resets file status, so it will be downloaded again", + "restartPackage Restarts a package, resets every containing files", + "status Show general download status", + "togglepause Toggle pause state", + "unpause Starts all downloads"] + return lines + + # End events + + def _getPackageByNameOrId(self, idorname): + """Return the first packageData found or None.""" + pack = None + if idorname.isdigit(): + try: + id = int(idorname) + pack = self.pyload.api.getPackageData(id) + except PackageDoesNotExists: + pack = self._getPackageByName(idorname) + else: + pack = self._getPackageByName(idorname) + return pack + + def _getPackageByName(self, name): + """Return the first packageData found or None.""" + + pq = self.pyload.api.getQueueData() + for pack in pq: + if pack.name == name: + self.log_debug('pack.name', pack.name, 'pack.pid', pack.pid) + return pack + + pc = self.pyload.api.getCollector() + for pack in pc: + if pack.name == name: + self.log_debug('pack.name', pack.name, 'pack.pid', pack.pid) + return pack + return None + +class IRCError(Exception): + + def __init__(self, value): + self.value = value + + def __str__(self): + return repr(self.value) diff --git a/module/plugins/hooks/IRCInterface.py b/module/plugins/hooks/IRCInterface.py deleted file mode 100644 index 8dadf08ed3..0000000000 --- a/module/plugins/hooks/IRCInterface.py +++ /dev/null @@ -1,422 +0,0 @@ -# -*- 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 . - - @author: RaNaN - @author: jeix - @interface-version: 0.2 -""" - -from select import select -import socket -from threading import Thread -import time -from time import sleep -from traceback import print_exc -import re -from pycurl import FORM_FILE - -from module.plugins.Hook import Hook -from module.network.RequestFactory import getURL -from module.utils import formatSize -from module.Api import PackageDoesNotExists, FileDoesNotExists - - -class IRCInterface(Thread, Hook): - __name__ = "IRCInterface" - __version__ = "0.11" - __description__ = """connect to irc and let owner perform different tasks""" - __config__ = [("activated", "bool", "Activated", "False"), - ("host", "str", "IRC-Server Address", "Enter your server here!"), - ("port", "int", "IRC-Server Port", "6667"), - ("ident", "str", "Clients ident", "pyload-irc"), - ("realname", "str", "Realname", "pyload-irc"), - ("nick", "str", "Nickname the Client will take", "pyLoad-IRC"), - ("owner", "str", "Nickname the Client will accept commands from", "Enter your nick here!"), - ("info_file", "bool", "Inform about every file finished", "False"), - ("info_pack", "bool", "Inform about every package finished", "True"), - ("captcha", "bool", "Send captcha requests", "True")] - __author_name__ = ("Jeix") - __author_mail__ = ("Jeix@hasnomail.com") - - def __init__(self, core, manager): - Thread.__init__(self) - Hook.__init__(self, core, manager) - self.setDaemon(True) - # self.sm = core.server_methods - self.api = core.api # todo, only use api - - def coreReady(self): - self.new_package = {} - - self.abort = False - - self.links_added = 0 - self.more = [] - - self.start() - - def packageFinished(self, pypack): - try: - if self.getConfig("info_pack"): - self.response(_("Package finished: %s") % pypack.name) - except: - pass - - def downloadFinished(self, pyfile): - try: - if self.getConfig("info_file"): - self.response( - _("Download finished: %(name)s @ %(plugin)s ") % {"name": pyfile.name, "plugin": pyfile.pluginname}) - except: - pass - - def newCaptchaTask(self, task): - if self.getConfig("captcha") and task.isTextual(): - task.handler.append(self) - task.setWaiting(60) - - page = getURL("http://www.freeimagehosting.net/upload.php", - post={"attached": (FORM_FILE, task.captchaFile)}, multipart=True) - - url = re.search(r"\[img\]([^\[]+)\[/img\]\[/url\]", page).group(1) - self.response(_("New Captcha Request: %s") % url) - self.response(_("Answer with 'c %s text on the captcha'") % task.id) - - def run(self): - # connect to IRC etc. - self.sock = socket.socket() - host = self.getConfig("host") - self.sock.connect((host, self.getConfig("port"))) - nick = self.getConfig("nick") - self.sock.send("NICK %s\r\n" % nick) - self.sock.send("USER %s %s bla :%s\r\n" % (nick, host, nick)) - for t in self.getConfig("owner").split(): - if t.strip().startswith("#"): - self.sock.send("JOIN %s\r\n" % t.strip()) - self.logInfo("pyLoad IRC: Connected to %s!" % host) - self.logInfo("pyLoad IRC: Switching to listening mode!") - try: - self.main_loop() - - except IRCError, ex: - self.sock.send("QUIT :byebye\r\n") - print_exc() - self.sock.close() - - def main_loop(self): - readbuffer = "" - while True: - sleep(1) - fdset = select([self.sock], [], [], 0) - if self.sock not in fdset[0]: - continue - - if self.abort: - raise IRCError("quit") - - readbuffer += self.sock.recv(1024) - temp = readbuffer.split("\n") - readbuffer = temp.pop() - - for line in temp: - line = line.rstrip() - first = line.split() - - if first[0] == "PING": - self.sock.send("PONG %s\r\n" % first[1]) - - if first[0] == "ERROR": - raise IRCError(line) - - msg = line.split(None, 3) - if len(msg) < 4: - continue - - msg = { - "origin": msg[0][1:], - "action": msg[1], - "target": msg[2], - "text": msg[3][1:] - } - - self.handle_events(msg) - - def handle_events(self, msg): - if not msg["origin"].split("!", 1)[0] in self.getConfig("owner").split(): - return - - if msg["target"].split("!", 1)[0] != self.getConfig("nick"): - return - - if msg["action"] != "PRIVMSG": - return - - # HANDLE CTCP ANTI FLOOD/BOT PROTECTION - if msg["text"] == "\x01VERSION\x01": - self.logDebug("Sending CTCP VERSION.") - self.sock.send("NOTICE %s :%s\r\n" % (msg['origin'], "pyLoad! IRC Interface")) - return - elif msg["text"] == "\x01TIME\x01": - self.logDebug("Sending CTCP TIME.") - self.sock.send("NOTICE %s :%d\r\n" % (msg['origin'], time.time())) - return - elif msg["text"] == "\x01LAG\x01": - self.logDebug("Received CTCP LAG.") # don't know how to answer - return - - trigger = "pass" - args = None - - try: - temp = msg["text"].split() - trigger = temp[0] - if len(temp) > 1: - args = temp[1:] - except: - pass - - handler = getattr(self, "event_%s" % trigger, self.event_pass) - try: - res = handler(args) - for line in res: - self.response(line, msg["origin"]) - except Exception, e: - self.logError("pyLoad IRC: " + repr(e)) - - def response(self, msg, origin=""): - if origin == "": - for t in self.getConfig("owner").split(): - self.sock.send("PRIVMSG %s :%s\r\n" % (t.strip(), msg)) - else: - self.sock.send("PRIVMSG %s :%s\r\n" % (origin.split("!", 1)[0], msg)) - - #### Events - - def event_pass(self, args): - return [] - - def event_status(self, args): - downloads = self.api.statusDownloads() - if not downloads: - return ["INFO: There are no active downloads currently."] - - temp_progress = "" - lines = ["ID - Name - Status - Speed - ETA - Progress"] - for data in downloads: - - if data.status == 5: - temp_progress = data.format_wait - else: - temp_progress = "%d%% (%s)" % (data.percent, data.format_size) - - lines.append("#%d - %s - %s - %s - %s - %s" % - ( - data.fid, - data.name, - data.statusmsg, - "%s/s" % formatSize(data.speed), - "%s" % data.format_eta, - temp_progress - )) - return lines - - def event_queue(self, args): - ps = self.api.getQueueData() - - if not ps: - return ["INFO: There are no packages in queue."] - - lines = [] - for pack in ps: - lines.append('PACKAGE #%s: "%s" with %d links.' % (pack.pid, pack.name, len(pack.links))) - - return lines - - def event_collector(self, args): - ps = self.api.getCollectorData() - if not ps: - return ["INFO: No packages in collector!"] - - lines = [] - for pack in ps: - lines.append('PACKAGE #%s: "%s" with %d links.' % (pack.pid, pack.name, len(pack.links))) - - return lines - - def event_info(self, args): - if not args: - return ['ERROR: Use info like this: info '] - - info = None - try: - info = self.api.getFileData(int(args[0])) - - except FileDoesNotExists: - return ["ERROR: Link doesn't exists."] - - return ['LINK #%s: %s (%s) [%s][%s]' % (info.fid, info.name, info.format_size, info.statusmsg, info.plugin)] - - def event_packinfo(self, args): - if not args: - return ['ERROR: Use packinfo like this: packinfo '] - - lines = [] - pack = None - try: - pack = self.api.getPackageData(int(args[0])) - - except PackageDoesNotExists: - return ["ERROR: Package doesn't exists."] - - id = args[0] - - self.more = [] - - lines.append('PACKAGE #%s: "%s" with %d links' % (id, pack.name, len(pack.links))) - for pyfile in pack.links: - self.more.append('LINK #%s: %s (%s) [%s][%s]' % (pyfile.fid, pyfile.name, pyfile.format_size, - pyfile.statusmsg, pyfile.plugin)) - - if len(self.more) < 6: - lines.extend(self.more) - self.more = [] - else: - lines.extend(self.more[:6]) - self.more = self.more[6:] - lines.append("%d more links do display." % len(self.more)) - - return lines - - def event_more(self, args): - if not self.more: - return ["No more information to display."] - - lines = self.more[:6] - self.more = self.more[6:] - lines.append("%d more links do display." % len(self.more)) - - return lines - - def event_start(self, args): - - self.api.unpauseServer() - return ["INFO: Starting downloads."] - - def event_stop(self, args): - - self.api.pauseServer() - return ["INFO: No new downloads will be started."] - - def event_add(self, args): - if len(args) < 2: - return ['ERROR: Add links like this: "add links". ', - 'This will add the link to to the package / the package with id !'] - - pack = args[0].strip() - links = [x.strip() for x in args[1:]] - - count_added = 0 - count_failed = 0 - try: - id = int(pack) - pack = self.api.getPackageData(id) - if not pack: - return ["ERROR: Package doesn't exists."] - - #TODO add links - - return ["INFO: Added %d links to Package %s [#%d]" % (len(links), pack["name"], id)] - - except: - # create new package - id = self.api.addPackage(pack, links, 1) - return ["INFO: Created new Package %s [#%d] with %d links." % (pack, id, len(links))] - - def event_del(self, args): - if len(args) < 2: - return ["ERROR: Use del command like this: del -p|-l [...] (-p indicates that the ids are from packages, -l indicates that the ids are from links)"] - - if args[0] == "-p": - ret = self.api.deletePackages(map(int, args[1:])) - return ["INFO: Deleted %d packages!" % len(args[1:])] - - elif args[0] == "-l": - ret = self.api.delLinks(map(int, args[1:])) - return ["INFO: Deleted %d links!" % len(args[1:])] - - else: - return ["ERROR: Use del command like this: del <-p|-l> [...] (-p indicates that the ids are from packages, -l indicates that the ids are from links)"] - - def event_push(self, args): - if not args: - return ["ERROR: Push package to queue like this: push "] - - id = int(args[0]) - try: - info = self.api.getPackageInfo(id) - except PackageDoesNotExists: - return ["ERROR: Package #%d does not exist." % id] - - self.api.pushToQueue(id) - return ["INFO: Pushed package #%d to queue." % id] - - def event_pull(self, args): - if not args: - return ["ERROR: Pull package from queue like this: pull ."] - - id = int(args[0]) - if not self.api.getPackageData(id): - return ["ERROR: Package #%d does not exist." % id] - - self.api.pullFromQueue(id) - return ["INFO: Pulled package #%d from queue to collector." % id] - - def event_c(self, args): - """ captcha answer """ - if not args: - return ["ERROR: Captcha ID missing."] - - task = self.core.captchaManager.getTaskByID(args[0]) - if not task: - return ["ERROR: Captcha Task with ID %s does not exists." % args[0]] - - task.setResult(" ".join(args[1:])) - return ["INFO: Result %s saved." % " ".join(args[1:])] - - def event_help(self, args): - lines = ["The following commands are available:", - "add [...] Adds link to package. (creates new package if it does not exist)", - "queue Shows all packages in the queue", - "collector Shows all packages in collector", - "del -p|-l [...] Deletes all packages|links with the ids specified", - "info Shows info of the link with id ", - "packinfo Shows info of the package with id ", - "more Shows more info when the result was truncated", - "start Starts all downloads", - "stop Stops the download (but not abort active downloads)", - "push Push package to queue", - "pull Pull package from queue", - "status Show general download status", - "help Shows this help message"] - return lines - - -class IRCError(Exception): - def __init__(self, value): - self.value = value - - def __str__(self): - return repr(self.value) diff --git a/module/plugins/hooks/ImageTyperz.py b/module/plugins/hooks/ImageTyperz.py index c9e43b8ae2..11ddbb4c45 100644 --- a/module/plugins/hooks/ImageTyperz.py +++ b/module/plugins/hooks/ImageTyperz.py @@ -1,35 +1,23 @@ # -*- 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 . - - @author: mkaay, RaNaN, zoidberg -""" + from __future__ import with_statement -from thread import start_new_thread -from pycurl import FORM_FILE, LOW_SPEED_TIME + +import base64 import re -from base64 import b64encode -from module.network.RequestFactory import getURL, getRequest -from module.plugins.Hook import Hook +import pycurl +from module.network.RequestFactory import getRequest as get_request + +from ..internal.Addon import Addon +from ..internal.misc import threaded class ImageTyperzException(Exception): + def __init__(self, err): self.err = err - def getCode(self): + def get_code(self): return self.err def __str__(self): @@ -39,113 +27,125 @@ def __repr__(self): return "" % self.err -class ImageTyperz(Hook): +class ImageTyperz(Addon): __name__ = "ImageTyperz" - __version__ = "0.04" - __description__ = """send captchas to ImageTyperz.com""" + __type__ = "hook" + __version__ = "0.15" + __status__ = "testing" + __config__ = [("activated", "bool", "Activated", False), ("username", "str", "Username", ""), - ("passkey", "password", "Password", ""), - ("force", "bool", "Force IT even if client is connected", False)] - __author_name__ = ("RaNaN", "zoidberg") - __author_mail__ = ("RaNaN@pyload.org", "zoidberg@mujmail.cz") + ("password", "password", "Password", ""), + ("check_client", "bool", "Don't use if client is connected", True)] + + __description__ = """Send captchas to ImageTyperz.com""" + __license__ = "GPLv3" + __authors__ = [("RaNaN", "RaNaN@pyload.net"), + ("zoidberg", "zoidberg@mujmail.cz")] SUBMIT_URL = "http://captchatypers.com/Forms/UploadFileAndGetTextNEW.ashx" RESPOND_URL = "http://captchatypers.com/Forms/SetBadImage.ashx" GETCREDITS_URL = "http://captchatypers.com/Forms/RequestBalance.ashx" - def setup(self): - self.info = {} + def get_credits(self): + res = self.load(self.GETCREDITS_URL, + post={'action': "REQUESTBALANCE", + 'username': self.config.get('username'), + 'password': self.config.get('password')}) - def getCredits(self): - response = getURL(self.GETCREDITS_URL, post={"action": "REQUESTBALANCE", "username": self.getConfig("username"), - "password": self.getConfig("passkey")}) - - if response.startswith('ERROR'): - raise ImageTyperzException(response) + if res.startswith('ERROR'): + raise ImageTyperzException(res) try: - balance = float(response) - except: - raise ImageTyperzException("invalid response") + balance = float(res) + + except Exception: + raise ImageTyperzException("Invalid response") - self.logInfo("Account balance: $%s left" % response) + self.log_info(_("Account balance: $%s left") % res) return balance def submit(self, captcha, captchaType="file", match=None): - req = getRequest() - #raise timeout threshold - req.c.setopt(LOW_SPEED_TIME, 80) + req = get_request() + #: Raise timeout threshold + req.c.setopt(pycurl.LOW_SPEED_TIME, 80) try: - #workaround multipart-post bug in HTTPRequest.py - if re.match("^[A-Za-z0-9]*$", self.getConfig("passkey")): + #@NOTE: Workaround multipart-post bug in HTTPRequest.py + if re.match("^\w*$", self.config.get('password')): multipart = True - data = (FORM_FILE, captcha) + data = (pycurl.FORM_FILE, captcha) else: multipart = False with open(captcha, 'rb') as f: data = f.read() - data = b64encode(data) - - response = req.load(self.SUBMIT_URL, post={"action": "UPLOADCAPTCHA", - "username": self.getConfig("username"), - "password": self.getConfig("passkey"), "file": data}, - multipart=multipart) + data = base64.b64encode(data) + + res = self.load(self.SUBMIT_URL, + post={'action': "UPLOADCAPTCHA", + 'username': self.config.get('username'), + 'password': self.config.get('password'), 'file': data}, + multipart=multipart, + req=req) finally: req.close() - if response.startswith("ERROR"): - raise ImageTyperzException(response) + if res.startswith("ERROR"): + raise ImageTyperzException(res) else: - data = response.split('|') + data = res.split('|') if len(data) == 2: ticket, result = data else: - raise ImageTyperzException("Unknown response %s" % response) + raise ImageTyperzException("Unknown response: %s" % res) return ticket, result - def newCaptchaTask(self, task): + def captcha_task(self, task): if "service" in task.data: return False if not task.isTextual(): return False - if not self.getConfig("username") or not self.getConfig("passkey"): + if not self.config.get('username') or not self.config.get('password'): return False - if self.core.isClientConnected() and not self.getConfig("force"): + if self.pyload.isClientConnected() and self.config.get('check_client'): return False - if self.getCredits() > 0: + if self.get_credits() > 0: task.handler.append(self) - task.data['service'] = self.__name__ + task.data['service'] = self.classname task.setWaiting(100) - start_new_thread(self.processCaptcha, (task,)) + self._process_captcha(task) else: - self.logInfo("Your %s account has not enough credits" % self.__name__) - - def captchaInvalid(self, task): - if task.data['service'] == self.__name__ and "ticket" in task.data: - response = getURL(self.RESPOND_URL, post={"action": "SETBADIMAGE", "username": self.getConfig("username"), - "password": self.getConfig("passkey"), - "imageid": task.data["ticket"]}) - - if response == "SUCCESS": - self.logInfo("Bad captcha solution received, requested refund") + self.log_info(_("Your account has not enough credits")) + + def captcha_invalid(self, task): + if task.data['service'] == self.classname and "ticket" in task.data: + res = self.load(self.RESPOND_URL, + post={'action': "SETBADIMAGE", + 'username': self.config.get('username'), + 'password': self.config.get('password'), + 'imageid': task.data['ticket']}) + + if res == "SUCCESS": + self.log_info( + _("Bad captcha solution received, requested refund")) else: - self.logError("Bad captcha solution received, refund request failed", response) + self.log_error( + _("Bad captcha solution received, refund request failed"), res) - def processCaptcha(self, task): - c = task.captchaFile + @threaded + def _process_captcha(self, task): + c = task.captchaParams['file'] try: ticket, result = self.submit(c) except ImageTyperzException, e: - task.error = e.getCode() + task.error = e.get_code() return - task.data["ticket"] = ticket + task.data['ticket'] = ticket task.setResult(result) diff --git a/module/plugins/hooks/JustPremium.py b/module/plugins/hooks/JustPremium.py new file mode 100644 index 0000000000..2f570a480f --- /dev/null +++ b/module/plugins/hooks/JustPremium.py @@ -0,0 +1,53 @@ +# -*- coding: utf-8 -*- + +import re + +from ..internal.Addon import Addon + + +class JustPremium(Addon): + __name__ = "JustPremium" + __type__ = "hook" + __version__ = "0.27" + __status__ = "testing" + + __config__ = [("activated", "bool", "Activated", False), + ("excluded", "str", "Exclude hosters (comma separated)", ""), + ("included", "str", "Include hosters (comma separated)", "")] + + __description__ = """Remove not-premium links from added urls""" + __license__ = "GPLv3" + __authors__ = [("mazleu", "mazleica@gmail.com"), + ("Walter Purcaro", "vuolter@gmail.com"), + ("immenz", "immenz@gmx.net")] + + def init(self): + self.event_map = {'linksAdded': "links_added"} + + def links_added(self, links, pid): + hosterdict = self.pyload.pluginManager.hosterPlugins + linkdict = self.pyload.api.checkURLs(links) + + premiumplugins = set(account.type for account in self.pyload.api.getAccounts(False) + if account.valid and account.premium) + multihosters = set(hoster for hoster in self.pyload.pluginManager.hosterPlugins + if 'new_name' in hosterdict[hoster] + and hosterdict[hoster]['new_name'] in premiumplugins) + + excluded = map(lambda domain: "".join(part.capitalize() for part in re.split(r'(\.|\d+)', domain) if part != '.'), + self.config.get('excluded').replace(' ', '').replace(',', '|').replace(';', '|').split('|')) + included = map(lambda domain: "".join(part.capitalize() for part in re.split(r'(\.|\d+)', domain) if part != '.'), + self.config.get('included').replace(' ', '').replace(',', '|').replace(';', '|').split('|')) + + hosterlist = (premiumplugins | multihosters).union( + excluded).difference(included) + + #: Found at least one hoster with account or multihoster + if not any(True for pluginname in linkdict if pluginname in hosterlist): + return + + for pluginname in set(linkdict.keys()) - hosterlist: + self.log_info(_("Remove links of plugin: %s") % pluginname) + for link in linkdict[pluginname]: + self.log_debug("Remove link: %s" % link) + links.remove(link) diff --git a/module/plugins/hooks/LinkFilter.py b/module/plugins/hooks/LinkFilter.py new file mode 100644 index 0000000000..76f5bfc3eb --- /dev/null +++ b/module/plugins/hooks/LinkFilter.py @@ -0,0 +1,72 @@ + +from ..internal.Addon import Addon + +class LinkFilter(Addon): + __name__ = "LinkFilter" + __type__ = "hook" + __version__ = "0.16" + __status__ = "testing" + + __config__ = [("activated", "bool", "Activated", False), + ("filter", "str", "Filter links containing (comma separated)", ""), + ("list_type", "listed;unlisted", "Allow only links that are", "unlisted"), + ("filter_all", "bool", "Filter all link plugin types (also crypters, containers...)", False)] + + __description__ = "Filters all added hoster links" + __license__ = "GPLv3" + __authors__ = [("segelkma", None)] + + def activate(self): + self.manager.addEvent('linksAdded', self.filter_links) + + def deactivate(self): + self.manager.removeEvent('linksAdded', self.filter_links) + + def filter_links(self, links, pid): + filters = self.config.get('filter').replace(' ', '') + if filters == "": + return + filters = filters.split(',') + if self.config.get('list_type', "unlisted") == "listed": + self.whitelist(links, filters) + else: + self.blacklist(links, filters) + + def whitelist(self, links, filters): + plugindict = dict(self.pyload.pluginManager.parseUrls(links)) + linkcount = len(links) + links[:] = [link for link in links if + any(link.find(_filter) != -1 for _filter in filters) or + not self.is_hoster_link(link) and plugindict[link] != "BasePlugin"] + linkcount -= len(links) + + if linkcount > 0: + linkstring = '' if self.config.get('filter_all') else 'hoster ' + linkstring += 'link' if linkcount == 1 else 'links' + self.log_warning( + _('Whitelist filter removed %s %s not containing (%s)') % + (linkcount, linkstring, ', '.join(filters))) + + def blacklist(self, links, filters): + for _filter in filters: + linkcount = len(links) + links[:] = [link for link in links if + link.find(_filter) == -1 or + not self.is_hoster_link(link)] + linkcount -= len(links) + + if linkcount > 0: + linkstring = '' if self.config.get('filter_all') else 'hoster ' + linkstring += 'link' if linkcount == 1 else 'links' + self.log_warning( + 'Blacklist filter removed %s %s containing %s' % + (linkcount, linkstring, _filter)) + + def is_hoster_link(self, link): + #declare all links as hoster links so the filter will work on all links + if self.config.get('filter_all'): + return True + for item in self.pyload.pluginManager.hosterPlugins.items(): + if item[1]['re'].match(link): + return True + return False diff --git a/module/plugins/hooks/LinkdecrypterCom.py b/module/plugins/hooks/LinkdecrypterCom.py deleted file mode 100644 index 8f656eb99d..0000000000 --- a/module/plugins/hooks/LinkdecrypterCom.py +++ /dev/null @@ -1,66 +0,0 @@ -# -*- 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 . - - @author: zoidberg -""" - -import re - -from module.plugins.Hook import Hook -from module.network.RequestFactory import getURL -from module.utils import remove_chars - - -class LinkdecrypterCom(Hook): - __name__ = "LinkdecrypterCom" - __version__ = "0.19" - __description__ = """linkdecrypter.com - regexp loader""" - __config__ = [("activated", "bool", "Activated", "False")] - __author_name__ = ("zoidberg") - - def coreReady(self): - try: - self.loadPatterns() - except Exception, e: - self.logError(e) - - def loadPatterns(self): - page = getURL("http://linkdecrypter.com/") - m = re.search(r'Supported\(\d+\): ([^+<]*)', page) - if not m: - self.logError(_("Crypter list not found")) - return - - builtin = [name.lower() for name in self.core.pluginManager.crypterPlugins.keys()] - builtin.extend(["downloadserienjunkiesorg"]) - - crypter_pattern = re.compile("(\w[\w.-]+)") - online = [] - for crypter in m.group(1).split(', '): - m = re.match(crypter_pattern, crypter) - if m and remove_chars(m.group(1), "-.") not in builtin: - online.append(m.group(1).replace(".", "\\.")) - - if not online: - self.logError(_("Crypter list is empty")) - return - - regexp = r"https?://([^.]+\.)*?(%s)/.*" % "|".join(online) - - dict = self.core.pluginManager.crypterPlugins[self.__name__] - dict["pattern"] = regexp - dict["re"] = re.compile(regexp) - - self.logDebug("REGEXP: " + regexp) diff --git a/module/plugins/hooks/LogMarker.py b/module/plugins/hooks/LogMarker.py new file mode 100644 index 0000000000..60f7f63189 --- /dev/null +++ b/module/plugins/hooks/LogMarker.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- + +import datetime + +from ..internal.Addon import Addon +from ..internal.misc import seconds_to_nexthour + + +class LogMarker(Addon): + __name__ = "LogMarker" + __type__ = "hook" + __version__ = "0.08" + __status__ = "testing" + + __config__ = [("activated", "bool", "Activated", False), + ("mark_hour", "bool", "Mark hours", True), + ("mark_day", "bool", "Mark days", True)] + + __description__ = """Print a mark in the log""" + __license__ = "GPLv3" + __authors__ = [("Walter Purcaro", "vuolter@gmail.com")] + + def activated(self): + self.periodical.start( + 1 * 60 * 60 - 1, + delay=seconds_to_nexthour( + strict=True) - 1) + + def periodical_task(self): + if self.config.get('mark_day') and datetime.datetime.today().hour == 0: + self.log_info("------------------------------------------------") + self.log_info( + _("------------------- DAY MARK -------------------")) + self.log_info("------------------------------------------------") + + elif self.config.get('mark_hour'): + self.log_info("------------------------------------------------") + self.log_info( + _("------------------- HOUR MARK ------------------")) + self.log_info("------------------------------------------------") diff --git a/module/plugins/hooks/MergeFiles.py b/module/plugins/hooks/MergeFiles.py index 869b5b6f84..eba389fc98 100644 --- a/module/plugins/hooks/MergeFiles.py +++ b/module/plugins/hooks/MergeFiles.py @@ -1,90 +1,76 @@ # -*- 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 . - - @author: and9000 -""" +from __future__ import with_statement import os import re -import traceback -from os.path import join -from module.utils import save_join, fs_encode -from module.plugins.Hook import Hook +from ..internal.Addon import Addon +from ..internal.misc import fsjoin, threaded -BUFFER_SIZE = 4096 - -class MergeFiles(Hook): +class MergeFiles(Addon): __name__ = "MergeFiles" - __version__ = "0.12" - __description__ = "Merges parts splitted with hjsplit" - __config__ = [("activated", "bool", "Activated", "False")] - __threaded__ = ["packageFinished"] - __author_name__ = ("and9000") - __author_mail__ = ("me@has-no-mail.com") - - def setup(self): - # nothing to do - pass - - def packageFinished(self, pack): + __type__ = "hook" + __version__ = "0.24" + __status__ = "testing" + + __config__ = [("activated", "bool", "Activated", False)] + + __description__ = """Merges parts splitted with hjsplit""" + __license__ = "GPLv3" + __authors__ = [("and9000", "me@has-no-mail.com")] + + BUFFER_SIZE = 4096 + + @threaded + def package_finished(self, pack): files = {} fid_dict = {} - for fid, data in pack.getChildren().iteritems(): - if re.search("\.[0-9]{3}$", data["name"]): - if data["name"][:-4] not in files: - files[data["name"][:-4]] = [] - files[data["name"][:-4]].append(data["name"]) - files[data["name"][:-4]].sort() - fid_dict[data["name"]] = fid - - download_folder = self.config['general']['download_folder'] - - if self.config['general']['folder_per_package']: - download_folder = save_join(download_folder, pack.folder) - - for name, file_list in files.iteritems(): - self.logInfo("Starting merging of %s" % name) - final_file = open(join(download_folder, fs_encode(name)), "wb") - - for splitted_file in file_list: - self.logDebug("Merging part %s" % splitted_file) - pyfile = self.core.files.getFile(fid_dict[splitted_file]) - pyfile.setStatus("processing") - try: - s_file = open(os.path.join(download_folder, splitted_file), "rb") - size_written = 0 - s_file_size = int(os.path.getsize(os.path.join(download_folder, splitted_file))) - while True: - f_buffer = s_file.read(BUFFER_SIZE) - if f_buffer: - final_file.write(f_buffer) - size_written += BUFFER_SIZE - pyfile.setProgress((size_written * 100) / s_file_size) - else: - break - s_file.close() - self.logDebug("Finished merging part %s" % splitted_file) - except Exception, e: - print traceback.print_exc() - finally: - pyfile.setProgress(100) - pyfile.setStatus("finished") - pyfile.release() - - final_file.close() - self.logInfo("Finished merging of %s" % name) + for fid, data in pack.getChildren().items(): + if re.search("\.\d{3}$", data['name']): + if data['name'][:-4] not in files: + files[data['name'][:-4]] = [] + files[data['name'][:-4]].append(data['name']) + files[data['name'][:-4]].sort() + fid_dict[data['name']] = fid + + dl_folder = self.pyload.config.get("general", "download_folder") + + if self.pyload.config.get("general", "folder_per_package"): + dl_folder = fsjoin(dl_folder, pack.folder) + + for name, file_list in files.items(): + self.log_info(_("Starting merging of"), name) + + with open(fsjoin(dl_folder, name), "wb") as final_file: + for splitted_file in file_list: + self.log_debug("Merging part", splitted_file) + + pyfile = self.pyload.files.getFile(fid_dict[splitted_file]) + + pyfile.setStatus("processing") + + try: + with open(fsjoin(dl_folder, splitted_file), "rb") as s_file: + size_written = 0 + s_file_size = int(os.path.getsize(os.path.join(dl_folder, splitted_file))) + while True: + f_buffer = s_file.read(self.BUFFER_SIZE) + if f_buffer: + final_file.write(f_buffer) + size_written += self.BUFFER_SIZE + pyfile.setProgress((size_written * 100) / s_file_size) + else: + break + self.log_debug("Finished merging part", splitted_file) + + except Exception, e: + self.log_error(e, trace=True) + + finally: + pyfile.setProgress(100) + pyfile.setStatus("finished") + pyfile.release() + + self.log_info(_("Finished merging of"), name) diff --git a/module/plugins/hooks/MultiDebridCom.py b/module/plugins/hooks/MultiDebridCom.py deleted file mode 100644 index c951386487..0000000000 --- a/module/plugins/hooks/MultiDebridCom.py +++ /dev/null @@ -1,42 +0,0 @@ -# -*- coding: utf-8 -*- - -############################################################################ -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU Affero 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 Affero General Public License for more details. # -# # -# You should have received a copy of the GNU Affero General Public License # -# along with this program. If not, see . # -############################################################################ - -from module.plugins.internal.MultiHoster import MultiHoster -from module.network.RequestFactory import getURL -from module.common.json_layer import json_loads - - -class MultiDebridCom(MultiHoster): - __name__ = "MultiDebridCom" - __version__ = "0.01" - __type__ = "hook" - __config__ = [("activated", "bool", "Activated", "False"), - ("hosterListMode", "all;listed;unlisted", "Use for hosters (if supported)", "all"), - ("hosterList", "str", "Hoster list (comma separated)", ""), - ("unloadFailing", "bool", "Revert to standard download if download fails", "False"), - ("interval", "int", "Reload interval in hours (0 to disable)", "24")] - - __description__ = """Multi-debrid.com hook plugin""" - __author_name__ = ("stickell") - __author_mail__ = ("l.stickell@yahoo.it") - - def getHoster(self): - json_data = getURL('http://multi-debrid.com/api.php?hosts', decode=True) - self.logDebug('JSON data: ' + json_data) - json_data = json_loads(json_data) - - return json_data['hosts'] diff --git a/module/plugins/hooks/MultiHome.py b/module/plugins/hooks/MultiHome.py index 473e6dcb1c..13852a0dc2 100644 --- a/module/plugins/hooks/MultiHome.py +++ b/module/plugins/hooks/MultiHome.py @@ -1,87 +1,94 @@ # -*- 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. +import time - 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. +from ..internal.Addon import Addon - You should have received a copy of the GNU General Public License - along with this program; if not, see . - - @author: mkaay -""" -from time import time +class Interface(object): + + def __init__(self, address): + self.address = address + self.history = {} + + def last_plugin_access(self, plugin_name, account): + if (plugin_name, account) in self.history: + return self.history[(plugin_name, account)] + else: + return 0 -from module.plugins.Hook import Hook + def use_for(self, plugin_name, account): + self.history[(plugin_name, account)] = time.time() + + def __repr__(self): + return "" % self.address -class MultiHome(Hook): +class MultiHome(Addon): __name__ = "MultiHome" - __version__ = "0.11" - __description__ = """ip address changer""" - __config__ = [("activated", "bool", "Activated", "False"), + __type__ = "hook" + __version__ = "0.21" + __status__ = "testing" + + __config__ = [("activated", "bool", "Activated", False), ("interfaces", "str", "Interfaces", "None")] - __author_name__ = ("mkaay") - __author_mail__ = ("mkaay@mkaay.de") - def setup(self): - self.register = {} + __description__ = """IP address changer""" + __license__ = "GPLv3" + __authors__ = [("mkaay", "mkaay@mkaay.de"), + ("GammaC0de", "nitzo2001{AT]yahoo[DOT]com")] + + def init(self): self.interfaces = [] - self.parseInterfaces(self.getConfig("interfaces").split(";")) + self.old_get_request = None + + self.parse_interfaces(self.config.get('interfaces').split(";")) + if not self.interfaces: - self.parseInterfaces([self.config["download"]["interface"]]) - self.setConfig("interfaces", self.toConfig()) + self.parse_interfaces( + [self.pyload.config.get('download', 'interface')]) + self.config.set('interfaces', self.to_config()) - def toConfig(self): - return ";".join([i.adress for i in self.interfaces]) + def to_config(self): + return ";".join(i.address for i in self.interfaces) - def parseInterfaces(self, interfaces): + def parse_interfaces(self, interfaces): for interface in interfaces: if not interface or str(interface).lower() == "none": continue self.interfaces.append(Interface(interface)) - def coreReady(self): - requestFactory = self.core.requestFactory - oldGetRequest = requestFactory.getRequest + def activate(self): + self.old_get_request = self.pyload.requestFactory.getRequest - def getRequest(pluginName, account=None): - iface = self.bestInterface(pluginName, account) - if iface: - iface.useFor(pluginName, account) - requestFactory.iface = lambda: iface.adress - self.logDebug("Multihome: using address: " + iface.adress) - return oldGetRequest(pluginName, account) + new_get_request = self.build_get_request() + self.pyload.requestFactory.getRequest = lambda *args: new_get_request( + *args) - requestFactory.getRequest = getRequest - - def bestInterface(self, pluginName, account): + def best_interface(self, plugin_name, account): best = None + for interface in self.interfaces: - if not best or interface.lastPluginAccess(pluginName, account) < best.lastPluginAccess(pluginName, account): + if not best or interface.last_plugin_access( + plugin_name, account) < best.last_plugin_access(plugin_name, account): best = interface + return best + def get_request(self, plugin_name, account=None): + iface = self.best_interface(plugin_name, account) + if iface is None: + self.log_warning(_("Best interface not found")) + return self.old_get_request(plugin_name, account) -class Interface(object): - def __init__(self, adress): - self.adress = adress - self.history = {} + iface.use_for(plugin_name, account) + self.pyload.requestFactory.iface = lambda: iface.address + self.log_debug("Using address", iface.address) - def lastPluginAccess(self, pluginName, account): - if (pluginName, account) in self.history: - return self.history[(pluginName, account)] - return 0 + return self.old_get_request(plugin_name, account) - def useFor(self, pluginName, account): - self.history[(pluginName, account)] = time() + def build_get_request(self): + def resfunc(*args): + return self.get_request(*args) - def __repr__(self): - return "" % self.adress + return resfunc diff --git a/module/plugins/hooks/MultishareCz.py b/module/plugins/hooks/MultishareCz.py deleted file mode 100644 index fc35bb7855..0000000000 --- a/module/plugins/hooks/MultishareCz.py +++ /dev/null @@ -1,24 +0,0 @@ -# -*- coding: utf-8 -*- - -import re - -from module.network.RequestFactory import getURL -from module.plugins.internal.MultiHoster import MultiHoster - - -class MultishareCz(MultiHoster): - __name__ = "MultishareCz" - __version__ = "0.04" - __type__ = "hook" - __config__ = [("activated", "bool", "Activated", "False"), - ("hosterListMode", "all;listed;unlisted", "Use for hosters (if supported)", "all"), - ("hosterList", "str", "Hoster list (comma separated)", "uloz.to")] - __description__ = """MultiShare.cz hook plugin""" - __author_name__ = ("zoidberg") - __author_mail__ = ("zoidberg@mujmail.cz") - - HOSTER_PATTERN = r']*?alt="([^"]+)">\s*[^>]*?alt="OK"' - - def getHoster(self): - page = getURL("http://www.multishare.cz/monitoring/") - return re.findall(self.HOSTER_PATTERN, page) diff --git a/module/plugins/hooks/Premium4Me.py b/module/plugins/hooks/Premium4Me.py deleted file mode 100644 index 4bcc79b25b..0000000000 --- a/module/plugins/hooks/Premium4Me.py +++ /dev/null @@ -1,32 +0,0 @@ -# -*- coding: utf-8 -*- - -from module.network.RequestFactory import getURL -from module.plugins.internal.MultiHoster import MultiHoster - - -class Premium4Me(MultiHoster): - __name__ = "Premium4Me" - __version__ = "0.03" - __type__ = "hook" - - __config__ = [("activated", "bool", "Activated", "False"), - ("hosterListMode", "all;listed;unlisted", "Use for downloads from supported hosters:", "all"), - ("hosterList", "str", "Hoster list (comma separated)", "")] - __description__ = """Premium.to hook plugin""" - __author_name__ = ("RaNaN", "zoidberg", "stickell") - __author_mail__ = ("RaNaN@pyload.org", "zoidberg@mujmail.cz", "l.stickell@yahoo.it") - - def getHoster(self): - page = getURL("http://premium.to/api/hosters.php?authcode=%s" % self.account.authcode) - return [x.strip() for x in page.replace("\"", "").split(";")] - - def coreReady(self): - self.account = self.core.accountManager.getAccountPlugin("Premium4Me") - - user = self.account.selectAccount()[0] - - if not user: - self.logError(_("Please add your premium.to account first and restart pyLoad")) - return - - return MultiHoster.coreReady(self) diff --git a/module/plugins/hooks/PremiumizeMe.py b/module/plugins/hooks/PremiumizeMe.py deleted file mode 100644 index 07630420c3..0000000000 --- a/module/plugins/hooks/PremiumizeMe.py +++ /dev/null @@ -1,52 +0,0 @@ -from module.plugins.internal.MultiHoster import MultiHoster - -from module.common.json_layer import json_loads -from module.network.RequestFactory import getURL - - -class PremiumizeMe(MultiHoster): - __name__ = "PremiumizeMe" - __version__ = "0.12" - __type__ = "hook" - __description__ = """Premiumize.Me hook plugin""" - - __config__ = [("activated", "bool", "Activated", "False"), - ("hosterListMode", "all;listed;unlisted", "Use for hosters (if supported):", "all"), - ("hosterList", "str", "Hoster list (comma separated)", ""), - ("unloadFailing", "bool", "Revert to stanard download if download fails", "False"), - ("interval", "int", "Reload interval in hours (0 to disable)", "24")] - - __author_name__ = ("Florian Franzen") - __author_mail__ = ("FlorianFranzen@gmail.com") - - def getHoster(self): - # If no accounts are available there will be no hosters available - if not self.account or not self.account.canUse(): - return [] - - # Get account data - (user, data) = self.account.selectAccount() - - # Get supported hosters list from premiumize.me using the - # json API v1 (see https://secure.premiumize.me/?show=api) - answer = getURL("https://api.premiumize.me/pm-api/v1.php?method=hosterlist¶ms[login]=%s¶ms[pass]=%s" % ( - user, data['password'])) - data = json_loads(answer) - - # If account is not valid thera are no hosters available - if data['status'] != 200: - return [] - - # Extract hosters from json file - return data['result']['hosterlist'] - - def coreReady(self): - # Get account plugin and check if there is a valid account available - self.account = self.core.accountManager.getAccountPlugin("PremiumizeMe") - if not self.account.canUse(): - self.account = None - self.logError(_("Please add a valid premiumize.me account first and restart pyLoad.")) - return - - # Run the overwriten core ready which actually enables the multihoster hook - return MultiHoster.coreReady(self) diff --git a/module/plugins/hooks/PushBullet.py b/module/plugins/hooks/PushBullet.py new file mode 100644 index 0000000000..46975f4cfd --- /dev/null +++ b/module/plugins/hooks/PushBullet.py @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- + +import pycurl +from module.network.RequestFactory import getRequest as get_request + +from ..internal.Notifier import Notifier + + +class PushBullet(Notifier): + __name__ = "PushBullet" + __type__ = "hook" + __version__ = "0.06" + __status__ = "testing" + + __config__ = [("activated", "bool", "Activated", False), + ("tokenkey", "str", "Access Token", ""), + ("captcha", "bool", "Notify captcha request", True), + ("reconnection", "bool", "Notify reconnection request", False), + ("downloadfinished", "bool", "Notify download finished", True), + ("downloadfailed", "bool", "Notify download failed", True), + ("alldownloadsfinished", "bool", "Notify all downloads finished", True), + ("alldownloadsprocessed", "bool", "Notify all downloads processed", True), + ("packagefinished", "bool", "Notify package finished", True), + ("packagefailed", "bool", "Notify package failed", True), + ("update", "bool", "Notify pyLoad update", False), + ("exit", "bool", "Notify pyLoad shutdown/restart", False), + ("sendinterval", "int", + "Interval in seconds between notifications", 1), + ("sendpermin", "int", "Max notifications per minute", 60), + ("ignoreclient", "bool", "Send notifications if client is connected", True)] + + __description__ = """Send push notifications to your phone using pushbullet.com""" + __license__ = "GPLv3" + __authors__ = [("Malkavi", "")] + + def get_key(self): + return self.config.get('tokenkey') + + def send(self, event, msg, key): + req = get_request() + req.c.setopt(pycurl.HTTPHEADER, ["Access-Token: %s" % str(key)]) + + self.load("https://api.pushbullet.com/v2/pushes", + post={'type': 'note', + 'title': event, + 'message': msg}, + req=req) diff --git a/module/plugins/hooks/PushOver.py b/module/plugins/hooks/PushOver.py new file mode 100644 index 0000000000..d4e78518e4 --- /dev/null +++ b/module/plugins/hooks/PushOver.py @@ -0,0 +1,44 @@ +# -*- coding: utf-8 -*- + + +from ..internal.Notifier import Notifier + + +class PushOver(Notifier): + __name__ = "PushOver" + __type__ = "hook" + __version__ = "0.08" + __status__ = "testing" + + __config__ = [("activated", "bool", "Activated", False), + ("tokenkey", "str", "Token key", ""), + ("userkey", "str", "User key", ""), + ("captcha", "bool", "Notify captcha request", True), + ("reconnection", "bool", "Notify reconnection request", False), + ("downloadfinished", "bool", "Notify download finished", True), + ("downloadfailed", "bool", "Notify download failed", True), + ("alldownloadsfinished", "bool", "Notify all downloads finished", True), + ("alldownloadsprocessed", "bool", "Notify all downloads processed", True), + ("packagefinished", "bool", "Notify package finished", True), + ("packagefailed", "bool", "Notify package failed", True), + ("update", "bool", "Notify pyLoad update", False), + ("exit", "bool", "Notify pyLoad shutdown/restart", False), + ("sendinterval", "int", + "Interval in seconds between notifications", 1), + ("sendpermin", "int", "Max notifications per minute", 60), + ("ignoreclient", "bool", "Send notifications if client is connected", True)] + + __description__ = """Send push notifications to your phone using pushover.net""" + __license__ = "GPLv3" + __authors__ = [("Malkavi", "")] + + def get_key(self): + return self.config.get('tokenkey'), self.config.get('userkey') + + def send(self, event, msg, key): + token, user = key + self.load("https://api.pushover.net/1/messages.json", + post={'token': token, + 'user': user, + 'title': event, + 'message': msg or event}) # @NOTE: msg can not be None or empty diff --git a/module/plugins/hooks/RPNetBiz.py b/module/plugins/hooks/RPNetBiz.py deleted file mode 100644 index 69976ffc99..0000000000 --- a/module/plugins/hooks/RPNetBiz.py +++ /dev/null @@ -1,47 +0,0 @@ -from module.plugins.internal.MultiHoster import MultiHoster -from module.common.json_layer import json_loads -from module.network.RequestFactory import getURL - - -class RPNetBiz(MultiHoster): - __name__ = "RPNetBiz" - __version__ = "0.1" - __type__ = "hook" - __description__ = """RPNet.Biz hook plugin""" - __config__ = [("activated", "bool", "Activated", "False"), - ("hosterListMode", "all;listed;unlisted", "Use for hosters (if supported):", "all"), - ("hosterList", "str", "Hoster list (comma separated)", ""), - ("unloadFailing", "bool", "Revert to stanard download if download fails", "False"), - ("interval", "int", "Reload interval in hours (0 to disable)", "24")] - __author_name__ = ("Dman") - __author_mail__ = ("dmanugm@gmail.com") - - def getHoster(self): - # No hosts supported if no account - if not self.account or not self.account.canUse(): - return [] - - # Get account data - (user, data) = self.account.selectAccount() - - response = getURL("https://premium.rpnet.biz/client_api.php", - get={"username": user, "password": data['password'], "action": "showHosterList"}) - hoster_list = json_loads(response) - - # If account is not valid thera are no hosters available - if 'error' in hoster_list: - return [] - - # Extract hosters from json file - return hoster_list['hosters'] - - def coreReady(self): - # Get account plugin and check if there is a valid account available - self.account = self.core.accountManager.getAccountPlugin("RPNetBiz") - if not self.account.canUse(): - self.account = None - self.logError(_("Please enter your %s account or deactivate this plugin") % "rpnet") - return - - # Run the overwriten core ready which actually enables the multihoster hook - return MultiHoster.coreReady(self) diff --git a/module/plugins/hooks/RealdebridCom.py b/module/plugins/hooks/RealdebridCom.py deleted file mode 100644 index 41e9884953..0000000000 --- a/module/plugins/hooks/RealdebridCom.py +++ /dev/null @@ -1,26 +0,0 @@ -# -*- coding: utf-8 -*- - -from module.network.RequestFactory import getURL -from module.plugins.internal.MultiHoster import MultiHoster - - -class RealdebridCom(MultiHoster): - __name__ = "RealdebridCom" - __version__ = "0.43" - __type__ = "hook" - - __config__ = [("activated", "bool", "Activated", "False"), - ("https", "bool", "Enable HTTPS", "False"), - ("hosterListMode", "all;listed;unlisted", "Use for hosters (if supported):", "all"), - ("hosterList", "str", "Hoster list (comma separated)", ""), - ("unloadFailing", "bool", "Revert to stanard download if download fails", "False"), - ("interval", "int", "Reload interval in hours (0 to disable)", "24")] - __description__ = """Real-Debrid.com hook plugin""" - __author_name__ = ("Devirex, Hazzard") - __author_mail__ = ("naibaf_11@yahoo.de") - - def getHoster(self): - https = "https" if self.getConfig("https") else "http" - page = getURL(https + "://real-debrid.com/api/hosters.php").replace("\"", "").strip() - - return [x.strip() for x in page.split(",") if x.strip()] diff --git a/module/plugins/hooks/RehostTo.py b/module/plugins/hooks/RehostTo.py deleted file mode 100644 index 6e24988c83..0000000000 --- a/module/plugins/hooks/RehostTo.py +++ /dev/null @@ -1,39 +0,0 @@ -# -*- coding: utf-8 -*- - -from module.network.RequestFactory import getURL -from module.plugins.internal.MultiHoster import MultiHoster - - -class RehostTo(MultiHoster): - __name__ = "RehostTo" - __version__ = "0.43" - __type__ = "hook" - - __config__ = [("activated", "bool", "Activated", "False"), - ("hosterListMode", "all;listed;unlisted", "Use for hosters (if supported)", "all"), - ("hosterList", "str", "Hoster list (comma separated)", ""), - ("unloadFailing", "bool", "Revert to stanard download if download fails", "False"), - ("interval", "int", "Reload interval in hours (0 to disable)", "24")] - - __description__ = """rehost.to hook plugin""" - __author_name__ = ("RaNaN") - __author_mail__ = ("RaNaN@pyload.org") - - def getHoster(self): - page = getURL("http://rehost.to/api.php?cmd=get_supported_och_dl&long_ses=%s" % self.long_ses) - return [x.strip() for x in page.replace("\"", "").split(",")] - - def coreReady(self): - self.account = self.core.accountManager.getAccountPlugin("RehostTo") - - user = self.account.selectAccount()[0] - - if not user: - self.logError("Rehost.to: " + _("Please add your rehost.to account first and restart pyLoad")) - return - - data = self.account.getAccountInfo(user) - self.ses = data["ses"] - self.long_ses = data["long_ses"] - - return MultiHoster.coreReady(self) diff --git a/module/plugins/hooks/ReloadCc.py b/module/plugins/hooks/ReloadCc.py deleted file mode 100644 index d07923624f..0000000000 --- a/module/plugins/hooks/ReloadCc.py +++ /dev/null @@ -1,65 +0,0 @@ -from module.plugins.internal.MultiHoster import MultiHoster - -from module.common.json_layer import json_loads -from module.network.RequestFactory import getURL - - -class ReloadCc(MultiHoster): - __name__ = "ReloadCc" - __version__ = "0.3" - __type__ = "hook" - __description__ = """Reload.cc hook plugin""" - - __config__ = [("activated", "bool", "Activated", "False"), - ("hosterListMode", "all;listed;unlisted", "Use for hosters (if supported):", "all"), - ("hosterList", "str", "Hoster list (comma separated)", "")] - - __author_name__ = ("Reload Team") - __author_mail__ = ("hello@reload.cc") - - interval = 0 # Disable periodic calls - - def getHoster(self): - # If no accounts are available there will be no hosters available - if not self.account or not self.account.canUse(): - print "ReloadCc: No accounts available" - return [] - - # Get account data - (user, data) = self.account.selectAccount() - - # Get supported hosters list from reload.cc using the json API v1 - query_params = dict( - via='pyload', - v=1, - get_supported='true', - get_traffic='true', - user=user - ) - - try: - query_params.update(dict(hash=self.account.infos[user]['pwdhash'])) - except Exception: - query_params.update(dict(pwd=data['password'])) - - answer = getURL("http://api.reload.cc/login", get=query_params) - data = json_loads(answer) - - # If account is not valid thera are no hosters available - if data['status'] != "ok": - print "ReloadCc: Status is not ok: %s" % data['status'] - return [] - - # Extract hosters from json file - return data['msg']['supportedHosters'] - - def coreReady(self): - # Get account plugin and check if there is a valid account available - self.account = self.core.accountManager.getAccountPlugin("ReloadCc") - if not self.account.canUse(): - self.account = None - self.logError("Please add a valid reload.cc account first and restart pyLoad.") - return - - # Run the overwriten core ready which actually enables the multihoster hook - return MultiHoster.coreReady(self) diff --git a/module/plugins/hooks/RestartFailed.py b/module/plugins/hooks/RestartFailed.py index 3bf6fe3650..fd3ab8eb1e 100644 --- a/module/plugins/hooks/RestartFailed.py +++ b/module/plugins/hooks/RestartFailed.py @@ -1,32 +1,24 @@ # -*- coding: utf-8 -*- -from module.plugins.Hook import Hook +from ..internal.Addon import Addon -class RestartFailed(Hook): +class RestartFailed(Addon): __name__ = "RestartFailed" - __version__ = "1.52" - __description__ = "restartedFailed Packages after defined time" - __config__ = [("activated", "bool", "Activated", "False"), - ("interval", "int", "Interval in Minutes", "15")] + __type__ = "hook" + __version__ = "1.65" + __status__ = "testing" - __author_name__ = ("bambie") - __author_mail__ = ("bambie@gulli.com") + __config__ = [("activated", "bool", "Activated", False), + ("interval", "int", "Check interval in minutes", 90)] - interval = 300 + __description__ = """Restart all the failed downloads in queue""" + __license__ = "GPLv3" + __authors__ = [("Walter Purcaro", "vuolter@gmail.com")] - def setup(self): - self.info = {"running": False} + def periodical_task(self): + self.log_info(_("Restarting all failed downloads...")) + self.pyload.api.restartFailed() - def coreReady(self): - self.info["running"] = True - self.logInfo("loaded") - self.interval = self.getConfig("interval") * 60 - self.logDebug("interval is set to %s" % self.interval) - - def periodical(self): - self.logDebug("periodical called") - if self.getConfig("interval") * 60 != self.interval: - self.interval = self.getConfig("interval") * 60 - self.logDebug("interval is set to %s" % self.interval) - self.core.api.restartFailed() + def activate(self): + self.periodical.start(self.config.get('interval') * 60) diff --git a/module/plugins/hooks/SimplydebridCom.py b/module/plugins/hooks/SimplydebridCom.py deleted file mode 100644 index 3272df567d..0000000000 --- a/module/plugins/hooks/SimplydebridCom.py +++ /dev/null @@ -1,20 +0,0 @@ -# -*- coding: utf-8 -*- - -from module.network.RequestFactory import getURL -from module.plugins.internal.MultiHoster import MultiHoster - - -class SimplydebridCom(MultiHoster): - __name__ = "SimplydebridCom" - __version__ = "0.01" - __type__ = "hook" - __config__ = [("activated", "bool", "Activated", "False"), - ("hosterListMode", "all;listed;unlisted", "Use for hosters (if supported)", "all"), - ("hosterList", "str", "Hoster list (comma separated)", "")] - __description__ = """Simply-Debrid.com hook plugin""" - __author_name__ = ("Kagenoshin") - __author_mail__ = ("kagenoshin@gmx.ch") - - def getHoster(self): - page = getURL("http://simply-debrid.com/api.php?list=1") - return [x.strip() for x in page.rstrip(';').replace("\"", "").split(";")] diff --git a/module/plugins/hooks/SkipRev.py b/module/plugins/hooks/SkipRev.py new file mode 100644 index 0000000000..b2646c4ffa --- /dev/null +++ b/module/plugins/hooks/SkipRev.py @@ -0,0 +1,101 @@ +# -*- coding: utf-8 -*- + +import re + +from module.PyFile import PyFile + +from ..internal.Addon import Addon + + +class SkipRev(Addon): + __name__ = "SkipRev" + __type__ = "hook" + __version__ = "0.39" + __status__ = "testing" + + __config__ = [("activated", "bool", "Activated", False), + ("mode", "Auto;Manual", "Choose recovery archives to skip", "Auto"), + ("revtokeep", "int", "Number of recovery archives to keep for package", 0)] + + __description__ = """Skip recovery archives (.rev)""" + __license__ = "GPLv3" + __authors__ = [("Walter Purcaro", "vuolter@gmail.com")] + + def _name(self, pyfile): + return pyfile.plugin.get_info(pyfile.url)['name'] + + def _create_pyFile(self, data): + pylink = self.pyload.api._convertPyFile(data) + return PyFile(self.pyload.files, + pylink.fid, + pylink.url, + pylink.name, + pylink.size, + pylink.status, + pylink.error, + pylink.plugin, + pylink.packageID, + pylink.order) + + def download_preparing(self, pyfile): + name = self._name(pyfile) + + if pyfile.statusname == "unskipped" or not name.endswith( + ".rev") or not ".part" in name: + return + + revtokeep = - \ + 1 if self.config.get( + 'mode') == "Auto" else self.config.get('revtokeep') + + if revtokeep: + status_list = ( + 1, 4, 8, 9, 14) if revtokeep < 0 else ( + 1, 3, 4, 8, 9, 14) + pyname = re.compile( + r'%s\.part\d+\.rev$' % + name.rsplit( + '.', + 2)[0].replace( + '.', + '\.')) + + queued = [True for fid, fdata in pyfile.package().getChildren().items() + if fdata['status'] not in status_list and pyname.match(fdata['name'])].count(True) + + if not queued or queued < revtokeep: #: Keep one rev at least in auto mode + return + + pyfile.setCustomStatus("SkipRev", "skipped") + + def download_failed(self, pyfile): + if pyfile.name.rsplit('.', 1)[-1].strip() not in ("rar", "rev"): + return + + revtokeep = - \ + 1 if self.config.get( + 'mode') == "Auto" else self.config.get('revtokeep') + + if not revtokeep: + return + + pyname = re.compile( + r'%s\.part\d+\.rev$' % + pyfile.name.rsplit( + '.', + 2)[0].replace( + '.', + '\.')) + + for fid, fdata in pyfile.package().getChildren().items(): + if fdata['status'] == 4 and pyname.match(fdata['name']): + pyfile_new = self._create_pyFile(fdata) + + if revtokeep > -1 or pyfile.name.endswith(".rev"): + pyfile_new.setStatus("queued") + else: + pyfile_new.setCustomStatus(_("unskipped"), "queued") + + self.pyload.files.save() + pyfile_new.release() + return diff --git a/module/plugins/hooks/TORRENT.py b/module/plugins/hooks/TORRENT.py new file mode 100644 index 0000000000..ace21509bb --- /dev/null +++ b/module/plugins/hooks/TORRENT.py @@ -0,0 +1,76 @@ +# -*- coding: utf-8 -*- + +import re + +from ..internal.Addon import Addon + + +class TORRENT(Addon): + __name__ = "TORRENT" + __type__ = "hook" + __version__ = "0.07" + __status__ = "testing" + + __config__ = [("activated", "bool", "Activated", False), + ("torrent_plugin", "None;c:AlldebridComTorrent;c:DebridlinkFrTorrent;h:LinksnappyComTorrent;c:RealdebridComTorrent;c:TorboxAppTorrent;h:ZbigzCom", "Associate torrents / magnets with plugin", "None")] + + __description__ = """Associate torrents / magnets with plugin""" + __license__ = "GPLv3" + __authors__ = [("GammaC0de", "nitzo2001@yahoo.com")] + + def activate(self): + self.pyload.hookManager.addEvent("plugin_updated", self.plugins_updated) + self.torrent_plugin = self.config.get("torrent_plugin") + self._associate(self.torrent_plugin) + self._report_status() + + def deactivate(self): + self.pyload.hookManager.removeEvent("plugin_updated", self.plugins_updated) + self._remove_association(self.torrent_plugin) + self.torrent_plugin = "None" + self._report_status() + + def plugins_updated(self, updated_plugins): + if self.torrent_plugin != "None": + self._remove_association(self.torrent_plugin) + self._associate(self.torrent_plugin) + + def config_changed(self, *args): + if args[3] == "plugin" and args[0] == "TORRENT" and args[1] == "torrent_plugin" and args[2] != self.torrent_plugin: + self._remove_association(self.torrent_plugin) + self.torrent_plugin = args[2] + self._associate(self.torrent_plugin) + self._report_status() + + def _report_status(self): + if self.torrent_plugin == "None": + self.log_warning(_("torrents / magnets are not associated with any plugin")) + else: + self.log_info(_("Using %s to handle torrents / magnets") % self.torrent_plugin.split(":")[1]) + + def _associate(self, plugin): + if plugin != "None": + plugin_type, plugin_name = plugin.split(':') + plugin_type = "crypter" if plugin_type == "c" else "hoster" + + hdict = self.pyload.pluginManager.plugins['container']['TORRENT'] + hdict['pattern'] = r'(?!(?:file|https?)://).+\.(?:torrent|magnet)' + hdict['re'] = re.compile(hdict['pattern']) + + hdict = self.pyload.pluginManager.plugins[plugin_type][plugin_name] + hdict['pattern'] = r'(?:file|https?)://.+\.torrent|magnet:\?.+' + hdict['re'] = re.compile(hdict['pattern']) + + def _remove_association(self, plugin): + if plugin != "None": + plugin_type, plugin_name = plugin.split(':') + plugin_type = "crypter" if plugin_type == "c" else "hoster" + + hdict = self.pyload.pluginManager.plugins[plugin_type][plugin_name] + hdict['pattern'] = r'^unmatchable$' + hdict['re'] = re.compile(hdict['pattern']) + + hdict = self.pyload.pluginManager.plugins['container']['TORRENT'] + hdict['pattern'] = r'(?:file|https?)://.+\.torrent|magnet:\?.+|(?!(?:file|https?)://).+\.(?:torrent|magnet)' + hdict['re'] = re.compile(hdict['pattern']) + diff --git a/module/plugins/hooks/TransmissionRPC.py b/module/plugins/hooks/TransmissionRPC.py new file mode 100644 index 0000000000..605e1f7dee --- /dev/null +++ b/module/plugins/hooks/TransmissionRPC.py @@ -0,0 +1,78 @@ +# -*- coding: utf-8 -*- + +import random +import re + +import pycurl +from module.network.HTTPRequest import BadHeader +from module.network.RequestFactory import getRequest as get_request + +from ..internal.Addon import Addon +from ..internal.misc import json + + +class TransmissionRPC(Addon): + __name__ = "TransmissionRPC" + __type__ = "hook" + __version__ = "0.19" + __status__ = "testing" + + __pattern__ = r'https?://.+\.torrent|magnet:\?.+' + __config__ = [("activated", "bool", "Activated", False), + ("rpc_url", "str", "Transmission RPC URL", "http://127.0.0.1:9091/transmission/rpc")] + + __description__ = """Send torrent and magnet URLs to Transmission Bittorent daemon via RPC""" + __license__ = "GPLv3" + __authors__ = [("GammaC0de", None)] + + def init(self): + self.event_map = {'linksAdded': "links_added"} + + def links_added(self, links, pid): + _re_link = re.compile(self.__pattern__) + urls = [link for link in links if _re_link.match(link)] + for url in urls: + self.log_debug("Sending link: %s" % url) + self.send_to_transmission(url) + links.remove(url) + + def send_to_transmission(self, url): + transmission_rpc_url = self.config.get('rpc_url') + client_request_id = self.classname + \ + "".join(random.choice('0123456789ABCDEF') for _i in range(4)) + req = get_request() + + try: + response = self.load(transmission_rpc_url, + post=json.dumps({'arguments': {'filename': url}, + 'method': 'torrent-add', + 'tag': client_request_id}), + req=req) + + except Exception, e: + if isinstance(e, BadHeader) and e.code == 409: + headers = dict( + re.findall( + r'(?P.+?): (?P.+?)\r?\n', + req.header)) + session_id = headers['X-Transmission-Session-Id'] + req.c.setopt( + pycurl.HTTPHEADER, [ + "X-Transmission-Session-Id: %s" % + session_id]) + try: + response = self.load(transmission_rpc_url, + post=json.dumps({'arguments': {'filename': url}, + 'method': 'torrent-add', + 'tag': client_request_id}), + req=req) + + res = json.loads(response) + if "result" in res: + self.log_debug("Result: %s" % res['result']) + + except Exception, e: + self.log_error(e) + + else: + self.log_error(e) diff --git a/module/plugins/hooks/UnSkipOnFail.py b/module/plugins/hooks/UnSkipOnFail.py index 455832b095..09b7747d37 100644 --- a/module/plugins/hooks/UnSkipOnFail.py +++ b/module/plugins/hooks/UnSkipOnFail.py @@ -1,97 +1,84 @@ # -*- 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 . - - @author: hgg -""" -from os.path import basename - -from module.utils import fs_encode -from module.plugins.Hook import Hook from module.PyFile import PyFile +from ..internal.Addon import Addon -class UnSkipOnFail(Hook): - __name__ = 'UnSkipOnFail' - __version__ = '0.01' - __description__ = 'When a download fails, restart "skipped" duplicates.' - __config__ = [('activated', 'bool', 'Activated', True), ] - __author_name__ = ('hagg',) - __author_mail__ = ('') - - def downloadFailed(self, pyfile): - pyfile_name = basename(pyfile.name) - pid = pyfile.package().id - msg = 'look for skipped duplicates for %s (pid:%s)...' - self.logInfo(msg % (pyfile_name, pid)) - dups = self.findDuplicates(pyfile) - for link in dups: - # check if link is "skipped"(=4) - if link.status == 4: - lpid = link.packageID - self.logInfo('restart "%s" (pid:%s)...' % (pyfile_name, lpid)) - self.setLinkStatus(link, "queued") - - def findDuplicates(self, pyfile): - """ Search all packages for duplicate links to "pyfile". + +class UnSkipOnFail(Addon): + __name__ = "UnSkipOnFail" + __type__ = "hook" + __version__ = "0.14" + __status__ = "testing" + + __config__ = [("activated", "bool", "Activated", True)] + + __description__ = """Restart skipped duplicates when download fails""" + __license__ = "GPLv3" + __authors__ = [("Walter Purcaro", "vuolter@gmail.com")] + + def download_failed(self, pyfile): + msg = _("Looking for skipped duplicates of: %s (pid:%s)") + self.log_info(msg % (pyfile.name, pyfile.package().id)) + + link = self.find_duplicate(pyfile) + if link: + self.log_info( + _("Queue found duplicate: %s (pid:%s)") % + (link.name, link.packageID)) + + #: Change status of "link" to "new_status". + #: "link" has to be a valid FileData object, + #: "new_status" has to be a valid status name + #: (i.e. "queued" for this Plugin) + #: It creates a temporary PyFile object using + #: "link" data, changes its status, and tells + #: The pyload.files-manager to save its data. + pyfile_new = self._create_pyFile(link) + + pyfile_new.setCustomStatus(_("unskipped"), "queued") + + self.pyload.files.save() + pyfile_new.release() + + else: + self.log_info(_("No duplicates found")) + + def find_duplicate(self, pyfile): + """Search all packages for duplicate links to "pyfile". Duplicates are links that would overwrite "pyfile". To test on duplicity the package-folder and link-name - of twolinks are compared (basename(link.name)). + of twolinks are compared (link.name). So this method returns a list of all links with equal package-folders and filenames as "pyfile", but except the data for "pyfile" iotselöf. It does MOT check the link's status. """ - dups = [] - pyfile_name = fs_encode(basename(pyfile.name)) - # get packages (w/o files, as most file data is useless here) - queue = self.core.api.getQueue() - for package in queue: - # check if package-folder equals pyfile's package folder - if fs_encode(package.folder) == fs_encode(pyfile.package().folder): - # now get packaged data w/ files/links - pdata = self.core.api.getPackageData(package.pid) - if pdata.links: - for link in pdata.links: - link_name = fs_encode(basename(link.name)) - # check if link name collides with pdata's name - if link_name == pyfile_name: - # at last check if it is not pyfile itself - if link.fid != pyfile.id: - dups.append(link) - return dups - - def setLinkStatus(self, link, new_status): - """ Change status of "link" to "new_status". - "link" has to be a valid FileData object, - "new_status" has to be a valid status name - (i.e. "queued" for this Plugin) - It creates a temporary PyFile object using - "link" data, changes its status, and tells - the core.files-manager to save its data. - """ - pyfile = PyFile(self.core.files, - link.fid, - link.url, - link.name, - link.size, - link.status, - link.error, - link.plugin, - link.packageID, - link.order) - pyfile.setStatus(new_status) - self.core.files.save() - pyfile.release() + for pinfo in self.pyload.api.getQueue(): + #: Check if package-folder equals pyfile's package folder + if pinfo.folder != pyfile.package().folder: + continue + + #: Now get packaged data w/ files/links + pdata = self.pyload.api.getPackageData(pinfo.pid) + for link in pdata.links: + #: Check if link == "skipped" + if link.status != 4: + continue + + #: Check if link name collides with pdata's name + #: and at last check if it is not pyfile itself + if link.name == pyfile.name and link.fid != pyfile.id: + return link + + def _create_pyFile(self, pylink): + return PyFile(self.pyload.files, + pylink.fid, + pylink.url, + pylink.name, + pylink.size, + pylink.status, + pylink.error, + pylink.plugin, + pylink.packageID, + pylink.order) diff --git a/module/plugins/hooks/UnrestrictLi.py b/module/plugins/hooks/UnrestrictLi.py deleted file mode 100644 index 0810a22d5b..0000000000 --- a/module/plugins/hooks/UnrestrictLi.py +++ /dev/null @@ -1,44 +0,0 @@ -# -*- coding: utf-8 -*- - -############################################################################ -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU Affero 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 Affero General Public License for more details. # -# # -# You should have received a copy of the GNU Affero General Public License # -# along with this program. If not, see . # -############################################################################ - -from module.plugins.internal.MultiHoster import MultiHoster -from module.network.RequestFactory import getURL -from module.common.json_layer import json_loads - - -class UnrestrictLi(MultiHoster): - __name__ = "UnrestrictLi" - __version__ = "0.02" - __type__ = "hook" - __config__ = [("activated", "bool", "Activated", "False"), - ("hosterListMode", "all;listed;unlisted", "Use for hosters (if supported)", "all"), - ("hosterList", "str", "Hoster list (comma separated)", ""), - ("unloadFailing", "bool", "Revert to standard download if download fails", "False"), - ("interval", "int", "Reload interval in hours (0 to disable)", "24"), - ("history", "bool", "Delete History", "False")] - - __description__ = """Unrestrict.li hook plugin""" - __author_name__ = ("stickell") - __author_mail__ = ("l.stickell@yahoo.it") - - def getHoster(self): - json_data = getURL('http://unrestrict.li/api/jdownloader/hosts.php?format=json') - json_data = json_loads(json_data) - - host_list = [element['host'] for element in json_data['result']] - - return host_list diff --git a/module/plugins/hooks/UpdateManager.py b/module/plugins/hooks/UpdateManager.py index 62031e6a47..71d499881f 100644 --- a/module/plugins/hooks/UpdateManager.py +++ b/module/plugins/hooks/UpdateManager.py @@ -1,202 +1,388 @@ # -*- 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. +from __future__ import with_statement - 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 . - - @author: RaNaN -""" - -import sys +import operator +import os import re -from os import stat -from os.path import join, exists -from time import time +import sys +import time -from module.ConfigParser import IGNORE -from module.network.RequestFactory import getURL -from module.plugins.Hook import threaded, Expose, Hook +from ..internal.Addon import Addon +from ..internal.misc import Expose, encode, exists, fsjoin, threaded -class UpdateManager(Hook): +class UpdateManager(Addon): __name__ = "UpdateManager" - __version__ = "0.15" - __description__ = """checks for updates""" - __config__ = [("activated", "bool", "Activated", "True"), - ("interval", "int", "Check interval in minutes", "480"), - ("debug", "bool", "Check for plugin changes when in debug mode", False)] - __author_name__ = ("RaNaN") - __author_mail__ = ("ranan@pyload.org") - - URL = "http://get.pyload.org/check2/%s/" - MIN_TIME = 3 * 60 * 60 # 3h minimum check interval - - @property - def debug(self): - return self.core.debug and self.getConfig("debug") - - def setup(self): - if self.debug: - self.logDebug("Monitoring file changes") - self.interval = 4 - self.last_check = 0 # timestamp of updatecheck - self.old_periodical = self.periodical - self.periodical = self.checkChanges - self.mtimes = {} # recordes times + __type__ = "hook" + __version__ = "1.21" + __status__ = "testing" + + __config__ = [("activated", "bool", "Activated", True), + ("checkinterval", "int", "Check interval in hours", 6), + ("autorestart", "bool", "Auto-restart pyLoad when required", True), + ("checkonstart", "bool", "Check for updates on startup", True), + ("checkperiod", "bool", "Check for updates periodically", True), + ("reloadplugins", "bool", "Monitor plugin code changes in debug mode", True), + ("nodebugupdate", "bool", "Don't update plugins in debug mode", False)] + + __description__ = """Check for updates""" + __license__ = "GPLv3" + __authors__ = [("Walter Purcaro", "vuolter@gmail.com")] + + _VERSION = re.compile(r'^\s*__version__\s*=\s*("|\')([\d.]+)\1', re.M) + + # SERVER_URL = "http://updatemanager.pyload.org" + # SERVER_URL = "http://updatemanager-spyload.rhcloud.com" + SERVER_URL = "https://raw.githubusercontent.com/pyload/updates/master/plugins.txt" + CHECK_INTERVAL = 3 * 60 * 60 #: 3 hours + + def activate(self): + if self.checkonstart: + self.pyload.api.pauseServer() + self.update() + + if self.do_restart is False: + self.pyload.api.unpauseServer() + + self.periodical.start(10) + + def init(self): + self.info.update({ + 'pyload': False, + 'plugins': False, + 'last_check': time.time()}) + self.mtimes = {} #: Store modification time for each plugin + self.event_map = {'allDownloadsProcessed': "all_downloads_processed"} + + if self.config.get('checkonstart'): + self.pyload.api.pauseServer() + self.checkonstart = True else: - self.interval = max(self.getConfig("interval") * 60, self.MIN_TIME) + self.checkonstart = False - self.updated = False - self.reloaded = True - self.version = "None" + self.do_restart = False - self.info = {"pyload": False, "plugins": False} + def all_downloads_processed(self): + if self.do_restart is True: + self.pyload.api.restart() - @threaded - def periodical(self): + def periodical_task(self): + if self.pyload.debug: + if self.config.get('reloadplugins'): + self.autoreload_plugins() - updates = self.checkForUpdate() - if updates: - self.checkPlugins(updates) + if self.config.get('nodebugupdate'): + return - if self.updated and not self.reloaded: - self.info["plugins"] = True - self.logInfo(_("*** Plugins have been updated, please restart pyLoad ***")) - elif self.updated and self.reloaded: - self.logInfo(_("Plugins updated and reloaded")) - self.updated = False - elif self.version == "None": - self.logInfo(_("No plugin updates available")) + if self.config.get('checkperiod') and \ + time.time() - max(self.CHECK_INTERVAL, self.config.get('checkinterval') * 60 * 60) > self.info['last_check']: + self.update() + if self.do_restart is True: + if self.pyload.threadManager.pause and not self.pyload.api.statusDownloads(): + self.pyload.api.restart() + + #: Deprecated method, use `autoreload_plugins` instead @Expose - def recheckForUpdates(self): - """recheck if updates are available""" - self.periodical() + def autoreloadPlugins(self, *args, **kwargs): + """ + See `autoreload_plugins` + """ + return self.autoreload_plugins(*args, **kwargs) - def checkForUpdate(self): - """checks if an update is available, return result""" + @Expose + def autoreload_plugins(self): + """ + Reload and reindex all modified plugins + """ + reloads = [] + modules = filter( + lambda m: m and (m.__name__.startswith("module.plugins.") or + m.__name__.startswith("userplugins.")) and + m.__name__.count(".") >= 2, sys.modules.values() + ) + for m in modules: + root, plugin_type, plugin_name = m.__name__.rsplit(".", 2) + plugin_id = (plugin_type, plugin_name) + if plugin_type in self.pyload.pluginManager.plugins: + f = m.__file__.replace(".pyc", ".py") + if not os.path.isfile(f): + continue - try: - if self.version == "None": # No updated known - version_check = getURL(self.URL % self.core.api.getServerVersion()).splitlines() - self.version = version_check[0] + mtime = os.path.getmtime(f) - # Still no updates, plugins will be checked - if self.version == "None": - self.logInfo(_("No Updates for pyLoad")) - return version_check[1:] + if plugin_id not in self.mtimes: + self.mtimes[plugin_id] = mtime - self.info["pyload"] = True - self.logInfo(_("*** New pyLoad Version %s available ***") % self.version) - self.logInfo(_("*** Get it here: http://pyload.org/download ***")) + elif self.mtimes[plugin_id] < mtime: + reloads.append(plugin_id) + self.mtimes[plugin_id] = mtime - except: - self.logWarning(_("Not able to connect server for updates")) + return True if self.pyload.pluginManager.reloadPlugins(reloads) else False - return None # Nothing will be done + def server_response(self, line=None): + try: + html = self.load(self.SERVER_URL, + get={'v': self.pyload.api.getServerVersion()}) - def checkPlugins(self, updates): - """ checks for plugins updates""" + except Exception: + self.log_warning(_("Unable to connect to the server to retrieve updates")) - # plugins were already updated - if self.info["plugins"]: + else: + res = html.splitlines() + + if line is not None: + try: + res = res[line] + except IndexError: + res = None + + return res + + @Expose + @threaded + def update(self): + """ + Check for updates + """ + if self._update() != 2 or not self.config.get('autorestart'): return - reloads = [] + if not self.pyload.api.statusDownloads(): + self.pyload.api.restart() + else: + self.log_warning(_("pyLoad restart scheduled"), + _("Downloads are active, pyLoad restart postponed once the download is done")) + self.pyload.api.pauseServer() + self.do_restart = True - vre = re.compile(r'__version__.*=.*("|\')([0-9.]+)') - url = updates[0] - schema = updates[1].split("|") - updates = updates[2:] + def _update(self): + newversion = self.server_response(0) - for plugin in updates: - info = dict(zip(schema, plugin.split("|"))) - filename = info["name"] - prefix = info["type"] - version = info["version"] + self.info['pyload'] = False + self.info['last_check'] = time.time() - if filename.endswith(".pyc"): - name = filename[:filename.find("_")] - else: - name = filename.replace(".py", "") + if not newversion: + exitcode = 0 + + elif newversion == self.pyload.api.getServerVersion(): + self.log_info(_("pyLoad is up to date!")) + exitcode = self.update_plugins() + + elif re.search(r'^\d+(?:\.\d+){0,3}[a-z]?$', newversion): + self.log_info(_("*** New pyLoad %s available ***") % newversion) + self.log_info(_("*** Get it here: https://github.com/pyload/pyload/releases ***")) + self.info['pyload'] = True + exitcode = 3 + + else: + exitcode = 0 + + #: Exit codes: + #: -1 = No plugin updated, new pyLoad version available + #: 0 = No plugin updated + #: 1 = Plugins updated + #: 2 = Plugins updated, but restart required + return exitcode - #TODO: obsolete in 0.5.0 - if prefix.endswith("s"): - type = prefix[:-1] + @Expose + def update_plugins(self): + server_data = self.server_response() + + if not server_data or server_data[0] != self.pyload.api.getServerVersion(): + return 0 + + updated = self._update_plugins(server_data) + + if updated: + self.log_info(_("*** Plugins updated ***")) + + if self.pyload.pluginManager.reloadPlugins(updated): + exitcode = 1 else: - type = prefix + self.log_warning(_("You have to restart pyLoad to use the updated plugins")) + self.info['plugins'] = True + exitcode = 2 + + paused = self.pyload.threadManager.pause + self.pyload.api.pauseServer() + self.manager.dispatchEvent("plugin_updated", updated) + if not paused: + self.pyload.api.unpauseServer() + else: + self.log_info(_("All plugins are up to date!")) + exitcode = 0 - plugins = getattr(self.core.pluginManager, "%sPlugins" % type) + #: Exit codes: + #: 0 = No plugin updated + #: 1 = Plugins updated + #: 2 = Plugins updated, but restart required + return exitcode - if name in plugins: - if float(plugins[name]["v"]) >= float(version): - continue + def parse_updates(self, server_data): + schema = server_data[2].split('|') - if name in IGNORE or (type, name) in IGNORE: + if "BLACKLIST" in server_data: + blacklist = server_data[server_data.index('BLACKLIST') + 1:] + updatelist = server_data[3:server_data.index('BLACKLIST')] + else: + blacklist = [] + updatelist = server_data[3:] + + for l in updatelist, blacklist: + nl = [] + for line in l: + d = dict(zip(schema, line.split('|'))) + d['name'] = d['name'].rsplit('.py', 1)[0] + nl.append(d) + l[:] = nl + + updatelist = sorted(updatelist, + key=operator.itemgetter("type", "name")) + blacklist = sorted(blacklist, key=operator.itemgetter("type", "name")) + + return updatelist, blacklist + + def _update_plugins(self, server_data): + """ + Check for plugin updates + """ + updated = [] + + updatelist, blacklist = self.parse_updates(server_data) + + url = server_data[1] + req = self.pyload.requestFactory.getRequest(self.classname) + + if blacklist: + #@NOTE: Protect UpdateManager from self-removing + if os.name == "nt": + #@NOTE: Windows filesystem is case insensitive, make sure we do not delete legitimate plugins + whitelisted_plugins = [ + (plugin['type'], plugin['name'].upper()) for plugin in updatelist] + blacklisted_plugins = [(plugin['type'], plugin['name']) for plugin in blacklist + if not (plugin['name'] == self.classname and plugin['type'] == self.__type__) + and (plugin['type'], plugin['name'].upper()) not in whitelisted_plugins] + else: + blacklisted_plugins = [(plugin['type'], plugin['name']) for plugin in blacklist + if not (plugin['name'] == self.classname and plugin['type'] == self.__type__)] + + c = 1 + l = len(blacklisted_plugins) + for idx, plugin in enumerate(updatelist): + if c > l: + break + plugin_name = plugin['name'] + plugin_type = plugin['type'] + for t, n in blacklisted_plugins: + if n != plugin_name or t != plugin_type: + continue + updatelist.pop(idx) + c += 1 + break + + for t, n in self.remove_plugins(blacklisted_plugins): + self.log_info(_("Removed blacklisted plugin: %(type)s %(name)s") % + {'type': t.upper(), + 'name': n,}) + + for plugin in updatelist: + plugin_name = plugin['name'] + plugin_type = plugin['type'] + plugin_version = plugin['version'] + + plugins = getattr(self.pyload.pluginManager, + "%sPlugins" % plugin_type.rstrip('s')) # @TODO: Remove rstrip in 0.4.10 + + oldver = float(plugins[plugin_name]['v']) if plugin_name in plugins else None + try: + newver = float(plugin_version) + except ValueError: + self.log_error(_("Error updating plugin: %s %s") % (plugin_type.rstrip('s').upper(), plugin_name), + _("Bad version number on the server")) continue - self.logInfo(_("New version of %(type)s|%(name)s : %(version).2f") % { - "type": type, - "name": name, - "version": float(version) - }) + if not oldver: + msg = "New plugin: %(type)s %(name)s (v%(newver).2f)" + elif newver > oldver: + msg = "New version of plugin: %(type)s %(name)s (v%(oldver).2f -> v%(newver).2f)" + else: + continue + self.log_info(_(msg) % {'type': plugin_type.rstrip('s').upper(), # @TODO: Remove rstrip in 0.4.10 + 'name': plugin_name, + 'oldver': oldver, + 'newver': newver}) try: - content = getURL(url % info) + content = self.load(url % plugin + ".py", decode=False, req=req) + + if req.code == 404: + raise Exception(_("URL not found")) + + m = self._VERSION.search(content) + if m and m.group(2) == plugin_version: + with open(fsjoin("userplugins", plugin_type, plugin_name + ".py"), "wb") as f: + f.write(encode(content)) + + updated.append((plugin_type, plugin_name)) + else: + raise Exception(_("Version mismatch")) + except Exception, e: - self.logWarning(_("Error when updating %s") % filename, str(e)) - continue + self.log_error(_("Error updating plugin: %s %s") % + (plugin_type.rstrip('s').upper(), plugin_name), + e) # @TODO: Remove rstrip in 0.4.10 - m = vre.search(content) - if not m or m.group(2) != version: - self.logWarning(_("Error when updating %s") % name, _("Version mismatch")) - continue + return updated + + #: Deprecated method, use `remove_plugins` instead + @Expose + def removePlugins(self, *args, **kwargs): + """ + See `remove_plugins` + """ + return self.remove_plugins(*args, **kwargs) + + @Expose + def remove_plugins(self, plugin_ids): + """ + Delete plugins from disk + """ + if not plugin_ids: + return - f = open(join("userplugins", prefix, filename), "wb") - f.write(content) - f.close() - self.updated = True + removed = set() - reloads.append((prefix, name)) + self.log_debug("Requested deletion of plugins: %s" % plugin_ids) - self.reloaded = self.core.pluginManager.reloadPlugins(reloads) + for plugin_type, plugin_name in plugin_ids: + rootplugins = os.path.join(pypath, "module", "plugins") - def checkChanges(self): + for basedir in ("userplugins", rootplugins): + py_filename = fsjoin(basedir, plugin_type, plugin_name + ".py") + pyc_filename = py_filename + "c" - if self.last_check + max(self.getConfig("interval") * 60, self.MIN_TIME) < time(): - self.old_periodical() - self.last_check = time() + if plugin_type == "hook": + try: + self.manager.deactivateHook(plugin_name) - modules = filter( - lambda m: m and (m.__name__.startswith("module.plugins.") or m.__name__.startswith( - "userplugins.")) and m.__name__.count(".") >= 2, sys.modules.itervalues()) + except Exception, e: + self.log_debug(e, trace=True) - reloads = [] + for filename in (py_filename, pyc_filename): + if not exists(filename): + continue - 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 exists(f): - continue + try: + os.remove(filename) - mtime = stat(f).st_mtime + except OSError, e: + self.log_warning(_("Error removing `%s`") % filename, e) - if id not in self.mtimes: - self.mtimes[id] = mtime - elif self.mtimes[id] < mtime: - reloads.append(id) - self.mtimes[id] = mtime + else: + plugin_id = (plugin_type, plugin_name) + removed.add(plugin_id) - self.core.pluginManager.reloadPlugins(reloads) + #: Return a list of the plugins successfully removed + return list(removed) diff --git a/module/plugins/hooks/UserAgentSwitcher.py b/module/plugins/hooks/UserAgentSwitcher.py new file mode 100644 index 0000000000..6305057a7f --- /dev/null +++ b/module/plugins/hooks/UserAgentSwitcher.py @@ -0,0 +1,45 @@ +# -*- coding: utf-8 -*- + +import pycurl +from module.network.HTTPRequest import HTTPRequest +from module.network.Browser import Browser + +from ..internal.Addon import Addon +from ..internal.misc import transcode + + +class UserAgentSwitcher(Addon): + __name__ = "UserAgentSwitcher" + __type__ = "hook" + __version__ = "0.18" + __status__ = "testing" + + __config__ = [("activated", "bool", "Activated", True), + ("connecttimeout", "int", + "Max timeout for link connection in seconds", 60), + ("maxredirs", "int", "Maximum number of redirects to follow", 10), + ("useragent", "str", "Custom user-agent string", "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:71.0) Gecko/20100101 Firefox/71.0")] + + __description__ = """Custom user-agent""" + __license__ = "GPLv3" + __authors__ = [("Walter Purcaro", "vuolter@gmail.com")] + + def download_preparing(self, pyfile): + if not isinstance(pyfile.plugin.req, HTTPRequest) and not isinstance(pyfile.plugin.req, Browser): + return + + connecttimeout = self.config.get('connecttimeout') + maxredirs = self.config.get('maxredirs') + useragent = self.config.get('useragent') + + if connecttimeout: + self.log_debug("Setting connection timeout to %s seconds" % connecttimeout) + pyfile.plugin.req.http.c.setopt(pycurl.CONNECTTIMEOUT, connecttimeout) + + if maxredirs: + self.log_debug("Setting maximum redirections to %s" % maxredirs) + pyfile.plugin.req.http.c.setopt(pycurl.MAXREDIRS, maxredirs) + + if useragent: + self.log_debug("Use custom user-agent string `%s`" % useragent) + pyfile.plugin.req.http.c.setopt(pycurl.USERAGENT, transcode(useragent, 'utf-8', 'latin1')) diff --git a/module/plugins/hooks/WindowsPhoneNotify.py b/module/plugins/hooks/WindowsPhoneNotify.py new file mode 100644 index 0000000000..3cbfd79b2d --- /dev/null +++ b/module/plugins/hooks/WindowsPhoneNotify.py @@ -0,0 +1,61 @@ +# -*- coding: utf-8 -*- + + +import httplib + +from ..internal.Notifier import Notifier + + +class WindowsPhoneNotify(Notifier): + __name__ = "WindowsPhoneNotify" + __type__ = "hook" + __version__ = "0.19" + __status__ = "testing" + + __config__ = [("activated", "bool", "Activated", False), + ("pushid", "str", "Push ID", ""), + ("pushurl", "str", "Push url", ""), + ("captcha", "bool", "Notify captcha request", True), + ("reconnection", "bool", "Notify reconnection request", False), + ("downloadfinished", "bool", "Notify download finished", True), + ("downloadfailed", "bool", "Notify download failed", True), + ("alldownloadsfinished", "bool", "Notify all downloads finished", True), + ("alldownloadsprocessed", "bool", "Notify all downloads processed", True), + ("packagefinished", "bool", "Notify package finished", True), + ("packagefailed", "bool", "Notify package failed", True), + ("update", "bool", "Notify pyLoad update", False), + ("exit", "bool", "Notify pyLoad shutdown/restart", False), + ("sendinterval", "int", + "Interval in seconds between notifications", 1), + ("sendpermin", "int", "Max notifications per minute", 60), + ("ignoreclient", "bool", "Send notifications if client is connected", True)] + + __description__ = """Send push notifications to Windows Phone""" + __license__ = "GPLv3" + __authors__ = [("Andy Voigt", "phone-support@hotmail.de"), + ("Walter Purcaro", "vuolter@gmail.com")] + + def get_key(self): + return self.config.get('pushid'), self.config.get('pushurl') + + def format_request(self, msg): + return (" " + " pyLoad %s " + " " % msg) + + def send(self, event, msg, key): + id, url = key + request = self.format_request( + "%s: %s" % + (event, msg) if msg else event) + webservice = httplib.HTTP(url) + + webservice.putrequest("POST", id) + webservice.putheader("Host", url) + webservice.putheader("Content-type", "text/xml") + webservice.putheader("X-NotificationClass", "2") + webservice.putheader("X-WindowsPhone-Target", "toast") + webservice.putheader("Content-length", "%d" % len(request)) + webservice.endheaders() + webservice.send(request) + webservice.close() diff --git a/module/plugins/hooks/WindowsPhoneToastNotify.py b/module/plugins/hooks/WindowsPhoneToastNotify.py deleted file mode 100644 index d110f7896b..0000000000 --- a/module/plugins/hooks/WindowsPhoneToastNotify.py +++ /dev/null @@ -1,68 +0,0 @@ -# -*- 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 . - - @author: RaNaN, Godofdream, zoidberg -""" -import time, httplib -from module.plugins.Hook import Hook - -class WindowsPhoneToastNotify(Hook): - __name__ = "WindowsPhoneToastNotify" - __version__ = "0.02" - __description__ = """Send push notifications to Windows Phone.""" - __author_name__ = ("Andy Voigt") - __author_mail__ = ("phone-support@hotmail.de") - __config__ = [("activated", "bool", "Activated", False), - ("force", "bool", "Force even if client is connected", False), - ("pushId", "str", "pushId", ""), - ("pushUrl","str","pushUrl", ""), - ("pushTimeout","int","Timeout between notifications in seconds","0")] - - def setup(self): - self.info = {} - - def getXmlData(self): - myxml = (" " - " Pyload Mobile Captcha waiting! " - " ") - return myxml - - def doRequest(self): - URL = self.getConfig("pushUrl") - request = self.getXmlData() - webservice = httplib.HTTP(URL) - webservice.putrequest("POST", self.getConfig("pushId")) - webservice.putheader("Host", URL) - webservice.putheader("Content-type", "text/xml") - webservice.putheader("X-NotificationClass", "2") - webservice.putheader("X-WindowsPhone-Target", "toast") - webservice.putheader("Content-length", "%d" % len(request)) - webservice.endheaders() - webservice.send(request) - webservice.close() - self.setStorage("LAST_NOTIFY", time.time()) - - def newCaptchaTask(self, task): - if not self.getConfig("pushId") or not self.getConfig("pushUrl"): - return False - - if self.core.isClientConnected() and not self.getConfig("force"): - return False - - if (time.time() - float(self.getStorage("LAST_NOTIFY", 0))) < self.getConf("pushTimeout"): - return False - - self.doRequest() - diff --git a/module/plugins/hooks/XFileSharing.py b/module/plugins/hooks/XFileSharing.py new file mode 100644 index 0000000000..40a5b3aeb7 --- /dev/null +++ b/module/plugins/hooks/XFileSharing.py @@ -0,0 +1,128 @@ +# -*- coding: utf-8 -*- + +import inspect +import re + +from ..internal.Addon import Addon + + +class XFileSharing(Addon): + __name__ = "XFileSharing" + __type__ = "hook" + __version__ = "0.56" + __status__ = "testing" + + __config__ = [("activated", "bool", "Activated", False), + ("use_hoster_list", "bool", "Load listed hosters only", False), + ("use_crypter_list", "bool", "Load listed crypters only", False), + ("use_builtin_list", "bool", "Load built-in plugin list", True), + ("hoster_list", "str", "Hoster list (comma separated)", ""), + ("crypter_list", "str", "Crypter list (comma separated)", "")] + + __description__ = """Load XFileSharing hosters and crypters which don't need a own plugin""" + __license__ = "GPLv3" + __authors__ = [("Walter Purcaro", "vuolter@gmail.com")] + + _regexmap = {'hoster': (r'(?:https?://(?:www\.)?)(?!(?:www\.)?(?:%s))(?P(?:[\d.]+|[\w\-^_]{3,63}(?:\.[a-zA-Z]{2,})+)(?:\:\d+)?)/(?:embed-)?\w{12}(?:\W|$)', + r'https?://(?:[^/]+\.)?(?P%s)/(?:embed-)?\w+'), + 'crypter': (r'(?:https?://(?:www\.)?)(?!(?:www\.)?(?:%s))(?P(?:[\d.]+|[\w\-^_]{3,63}(?:\.[a-zA-Z]{2,})+)(?:\:\d+)?)/(?:user|folder)s?/\w+', + r'https?://(?:[^/]+\.)?(?P%s)/(?:user|folder)s?/\w+')} + + BUILTIN_HOSTERS = [ # WORKING HOSTERS: + "ani-stream.com", "backin.net", "cloudshares.net", "cloudsix.me", + "eyesfile.ca", "file4safe.com", "fileband.com", "filedwon.com", + "fileparadox.in", "filevice.com", "hostingbulk.com", "junkyvideo.com", + "ravishare.com", "salefiles.com", "sendmyway.com", "sharebeast.com", + "sharesix.com", "thefile.me", "verzend.be", "worldbytez.com", + "xvidstage.com", + # NOT TESTED: + "101shared.com", "4upfiles.com", "filemaze.ws", "filenuke.com", + "linkzhost.com", "mightyupload.com", "rockdizfile.com", "sharerepo.com", + "shareswift.com", "uploadbaz.com", "uploadc.com", "vidbull.com", + "zalaa.com", "zomgupload.com", + # NOT WORKING: + "amonshare.com", "banicrazy.info", "boosterking.com", "host4desi.com", + "laoupload.com", "rd-fs.com"] + BUILTIN_CRYPTERS = ["junocloud.me", "rapidfileshare.net"] + + def activate(self): + for type, plugin in (("hoster", "XFileSharing"), + ("crypter", "XFileSharingFolder")): + self._load(type, plugin) + + def deactivate(self): + for type, plugin in (("hoster", "XFileSharing"), + ("crypter", "XFileSharingFolder")): + self._unload(type, plugin) + + def get_pattern(self, type, plugin): + if self.config.get('use_%s_list' % type): + plugin_list = self.config.get('%s_list' % type) + plugin_list = plugin_list.replace(' ', '').replace('\\', '') + plugin_list = plugin_list.replace('|', ',').replace(';', ',') + plugin_list = plugin_list.lower().split(',') + + plugin_set = set(plugin_list) + + if self.config.get('use_builtin_list'): + builtin_list = getattr(self, "BUILTIN_%sS" % type.upper()) + plugin_set.update(builtin_list) + + plugin_set.difference_update(('', u'')) + + if not plugin_set: + self.log_info(_("No %s to handle") % type) + return + + match_list = '|'.join(sorted(plugin_set)).replace('.', '\.') + pattern = self._regexmap[type][1] % match_list + + self.log_info(_("Handle %d %s%s: %s") % + (len(plugin_set), + type, + "" if len(plugin_set) == 1 else "s", + match_list.replace('\.', '.').replace('|', ', '))) + else: + plugin_list = [] + isXFS = lambda klass: any(k.__name__.startswith("XFS") + for k in inspect.getmro(klass)) + + for p in self.pyload.pluginManager.plugins[type].values(): + try: + klass = self.pyload.pluginManager.loadClass(type, p[ + 'name']) + + except AttributeError, e: + self.log_debug(e, trace=True) + continue + + if hasattr(klass, "PLUGIN_DOMAIN") and klass.PLUGIN_DOMAIN and isXFS( + klass): + plugin_list.append(klass.PLUGIN_DOMAIN) + + if plugin_list: + unmatch_list = '|'.join(sorted(plugin_list)).replace('.', '\.') + pattern = self._regexmap[type][0] % unmatch_list + else: + pattern = self._regexmap[type][0] + + self.log_info(_("Auto-discover new %ss") % type) + + return pattern + + def _load(self, type, plugin): + dict = self.pyload.pluginManager.plugins[type][plugin] + pattern = self.get_pattern(type, plugin) + + if not pattern: + return + + dict['pattern'] = pattern + dict['re'] = re.compile(pattern) + + self.log_debug("Pattern for %ss: %s" % (type, pattern)) + + def _unload(self, type, plugin): + dict = self.pyload.pluginManager.plugins[type][plugin] + dict['pattern'] = r'^unmatchable$' + dict['re'] = re.compile(dict['pattern']) diff --git a/module/plugins/hooks/XFileSharingPro.py b/module/plugins/hooks/XFileSharingPro.py deleted file mode 100644 index fe2df840d9..0000000000 --- a/module/plugins/hooks/XFileSharingPro.py +++ /dev/null @@ -1,74 +0,0 @@ -# -*- coding: utf-8 -*- - -import re - -from module.plugins.Hook import Hook - - -class XFileSharingPro(Hook): - __name__ = "XFileSharingPro" - __version__ = "0.06" - __type__ = "hook" - __config__ = [("activated", "bool", "Activated", "True"), - ("loadDefault", "bool", "Include default (built-in) hoster list", "True"), - ("includeList", "str", "Include hosters (comma separated)", ""), - ("excludeList", "str", "Exclude hosters (comma separated)", "")] - __description__ = """Hoster URL pattern loader for the generic XFileSharingPro plugin""" - __author_name__ = ("zoidberg") - __author_mail__ = ("zoidberg@mujmail.cz") - - def coreReady(self): - self.loadPattern() - - def loadPattern(self): - hosterList = self.getConfigSet('includeList') - excludeList = self.getConfigSet('excludeList') - - if self.getConfig('loadDefault'): - hosterList |= set(( - #WORKING HOSTERS: - "aieshare.com", "asixfiles.com", "banashare.com", "cyberlocker.ch", "eyesfile.co", "eyesfile.com", - "fileband.com", "filedwon.com", "filedownloads.org", "hipfile.com", "kingsupload.com", "mlfat4arab.com", - "netuploaded.com", "odsiebie.pl", "q4share.com", "ravishare.com", "uptobox.com", "verzend.be", - "xvidstage.com", - #NOT TESTED: - "bebasupload.com", "boosterking.com", "divxme.com", "filevelocity.com", "glumbouploads.com", - "grupload.com", "heftyfile.com", "host4desi.com", "laoupload.com", "linkzhost.com", "movreel.com", - "rockdizfile.com", "limfile.com", "share76.com", "sharebeast.com", "sharehut.com", "sharerun.com", - "shareswift.com", "sharingonline.com", "6ybh-upload.com", "skipfile.com", "spaadyshare.com", - "space4file.com", "uploadbaz.com", "uploadc.com", "uploaddot.com", "uploadfloor.com", "uploadic.com", - "uploadville.com", "vidbull.com", "zalaa.com", "zomgupload.com", "kupload.org", "movbay.org", - "multishare.org", "omegave.org", "toucansharing.org", "uflinq.org", "banicrazy.info", "flowhot.info", - "upbrasil.info", "shareyourfilez.biz", "bzlink.us", "cloudcache.cc", "fileserver.cc", "farshare.to", - "filemaze.ws", "filehost.ws", "filestock.ru", "moidisk.ru", "4up.im", "100shared.com", - #WRONG FILE NAME: - "sendmyway.com", "upchi.co.il", - #NOT WORKING: - "amonshare.com", "imageporter.com", "file4safe.com", - #DOWN OR BROKEN: - "ddlanime.com", "fileforth.com", "loombo.com", "goldfile.eu", "putshare.com" - )) - - hosterList -= (excludeList) - hosterList -= set(('', u'')) - - if not hosterList: - self.unload() - return - - regexp = r"http://(?:[^/]*\.)?(%s)/\w{12}" % ("|".join(sorted(hosterList)).replace('.', '\.')) - #self.logDebug(regexp) - - dict = self.core.pluginManager.hosterPlugins['XFileSharingPro'] - dict["pattern"] = regexp - dict["re"] = re.compile(regexp) - self.logDebug("Pattern loaded - handling %d hosters" % len(hosterList)) - - def getConfigSet(self, option): - s = self.getConfig(option).lower().replace('|', ',').replace(';', ',') - return set([x.strip() for x in s.split(',')]) - - def unload(self): - dict = self.core.pluginManager.hosterPlugins['XFileSharingPro'] - dict["pattern"] = r"^unmatchable$" - dict["re"] = re.compile(r"^unmatchable$") diff --git a/module/plugins/hooks/XMPP.py b/module/plugins/hooks/XMPP.py new file mode 100644 index 0000000000..85fc858468 --- /dev/null +++ b/module/plugins/hooks/XMPP.py @@ -0,0 +1,341 @@ +# -*- coding: utf-8 -*- + +import sleekxmpp +from sleekxmpp.xmlstream.matcher import MatchXPath +from sleekxmpp.xmlstream.handler import Callback +import ssl + +from .IRC import IRC + +from module.plugins.internal.Addon import Addon + +class XMPP(IRC): + __name__ = "XMPP" + __type__ = "hook" + __version__ = "0.22" + __status__ = "testing" + + __config__ = [("activated", "bool", "Activated", False), + ("jid", "str", "Jabber ID", "user@exmaple-jabber-server.org"), + ("pw", "str", "Password", ""), + ("tls", "bool", "Use TLS", True), + ("use_ssl", "bool", "Use old SSL", False), + ("owners", "str", "List of JIDs accepting commands from", "me@icq-gateway.org;some@msn-gateway.org"), + ("captcha", "bool", "Send captcha requests", True), + ("info_file", "bool", "Inform about every file finished", False), + ("info_pack", "bool", "Inform about every package finished", True), + ("all_download", "bool", "Inform about all download finished", False), + ("package_failed", "bool", "Notify package failed", False), + ("download_failed", "bool", "Notify download failed", True), + ("download_start", "bool", "Notify download start", True), + ("maxline", "int", "Maximum line per message", 6)] + + __description__ = """Connect to jabber and let owner perform different tasks""" + __license__ = "GPLv3" + __authors__ = [("RaNaN", "RaNaN@pyload.net")] + + SHORTCUT_COMMANDS = { + 'a': 'add', + 'c': 'collector', + 'f': 'freeSpace', + 'h': 'help', + 'i': 'info', + 'l': 'getLog', + 'm': 'more', + 'p': 'packinfo', + 'q': 'queue', + 'r': 'restart', + 'rf': 'restartFile', + 'rp': 'restartPackage', + 's': 'status', + } + + def __init__(self, *args, **kwargs): + IRC.__init__(self, *args, **kwargs) + + def activate(self): + self.log_debug('activate') + self.new_package = {} + self.jid = sleekxmpp.jid.JID(self.config.get('jid')) + self.jid.resource = 'PyLoadNotifyBot' + self.log_debug(self.jid) + + xmpp = NotifyBot( + self.jid, + self.config.get('pw'), + self.config.get('tls'), + self.config.get('use_ssl'), + self.log_info, + self.log_debug + ) + self.log_debug('activate xmpp') + xmpp.register_plugin('xep_0030') # Service Discovery + xmpp.register_plugin('xep_0004') # Data Forms + xmpp.register_plugin('xep_0060') # PubSub + xmpp.register_plugin('xep_0199') # XMPP Ping + xmpp.ssl_version = ssl.PROTOCOL_TLSv1_2 + + # The message event is triggered whenever a message + # stanza is received. Be aware that that includes + # MUC messages and error messages. + xmpp.add_event_handler("message", self.message) + xmpp.add_event_handler("connected", self.connected) + xmpp.add_event_handler("disconnected", self.disconnected) + xmpp.add_event_handler("failed_auth", self.failed_auth) + xmpp.add_event_handler("changed_status", self.changed_status) + xmpp.add_event_handler("presence_error", self.presence_error) + xmpp.add_event_handler("presence_unavailable", + self.presence_unavailable) + + xmpp.register_handler(Callback('Stream Error', + MatchXPath("{%s}error" % xmpp.stream_ns), + self.stream_error)) + + self.xmpp = xmpp + self.start() + + def run(self): + self.log_debug('def run') + try: + if self.xmpp.connect(): + self.xmpp.process(block=False) + self.log_info('Done') + # self.periodical.start(30) + + else: + self.log_error("Unable to connect.") + + except Exception as e: + self.log_error(e, trace=True) + + ############################################################################ + ## xmpp handler + + def changed_status(self, stanza=None): + self.log_debug('changed_status', stanza, stanza.get_type()) + + def connected(self, event=None): + self.log_info("Client was connected", event) + + def disconnected(self, event=None): + self.log_info("Client was disconnected", event) + + def presence_error(self, stanza=None): + self.log_debug('presence_error', stanza) + + def presence_unavailable(self, stanza=None): + self.log_debug('presence_unavailable', stanza) + + def failed_auth(self, event=None): + self.log_info('Failed to authenticate') + + def stream_error(self, err=None): + self.log_debug("Stream Error", err) + # self.periodical.stop() + + def message(self, stanza): + """ + Message handler for the component. + """ + self.log_debug('message', stanza) + + subject = stanza['subject'] + body = stanza['body'] + t = stanza['type'] + self.log_debug("Message from %s received." % stanza.get_from()) + self.log_debug("Body: %s Subject: %s Type: %s" % (body, subject, t)) + + if t == "headline": + #: 'headline' messages should never be replied to + return True + + if subject: + subject = u"Re: " + subject + + to_jid = stanza['from'] + from_jid = stanza['to'] + to_name = to_jid.jid + + names = self.config.get('owners').split(";") + + self.log_debug('names', names) + self.log_debug('to_name', to_name) + self.log_debug('to_jid', to_jid) + + if not (to_name in names or to_jid.node + "@" + to_jid.domain in names): + return True + + messages = [] + command = "pass" + args = None + + try: + temp = body.split() + command = temp[0] + if len(temp) > 1: + args = temp[1:] + + except Exception: + pass + + self.log_debug('command', command) + command = self.shortcut_command(command) + self.log_debug('shortcut_command', command) + + handler = getattr(self, "event_%s" % command, self.event_pass) + ret = False + try: + self.log_debug('args', args, type(args)) + + res = handler(args) + self.log_debug('res', res) + + if res: + msg_reply = '\n'.join(res) + # add shortcut in help + if command == 'help': + msg_reply += '\nShortcut:\n' + for cmd_short,cmd_long in self.SHORTCUT_COMMANDS.items(): + msg_reply += cmd_short+': '+cmd_long+', ' + + else: + msg_reply = "ERROR: command invalide, enter: help" + + self.log_debug('Send reponse', msg_reply) + ret = stanza.reply(msg_reply).send() + + except Exception as e: + self.log_error(e, trace=True) + stanza.reply('ERROR: '+str(e)).send() + + return ret + + ## end xmpp handler + ############################################################################ + + def shortcut_command(self, shortcommand): + command = shortcommand + if shortcommand and shortcommand in self.SHORTCUT_COMMANDS: + command = self.SHORTCUT_COMMANDS[shortcommand] + + return command + + def response(self, msg, origin=""): + self.log_debug('def response', msg, origin) + return self.announce(msg) + + def announce(self, message): + """ + Send message to all owners + """ + self.log_debug('Announce, message:', message) + for user in self.config.get('owners').split(";"): + self.log_debug("Send message to", user) + to_jid = sleekxmpp.jid.JID(user) + self.xmpp.sendMessage(mfrom=self.jid, mto=to_jid, mtype='chat', mbody=str(message)) + + def exit(self): + self.xmpp.disconnect() + + def before_reconnect(self, ip): + self.log_debug('after_reconnect') + self.xmpp.disconnect() + # self.periodical.stop() + + def after_reconnect(self, ip, oldip): + self.log_debug('after_reconnect') + self.xmpp.connect() + # self.periodical.start(600) + + def download_failed(self, pyfile): + self.log_debug('download_failed', pyfile, pyfile.error) + try: + if self.config.get('download_failed'): + self.announce(_("Download failed: %s (#%d) in #%d @ %s: %s") % (pyfile.name, + pyfile.id, + pyfile.packageid, + pyfile.pluginname, + pyfile.error)) + + except Exception as e: + self.log_error(e, trace=True) + + def package_failed(self, pypack): + self.log_debug('package_failed', pypack) + try: + if self.config.get('package_failed'): + self.announce(_("Package failed: %s (%d).") % (pypack.name, pypack.id)) + + except Exception as e: + self.log_error(e, trace=True) + + def package_finished(self, pypack): + self.log_debug('package_finished') + try: + if self.config.get('info_pack'): + self.announce(_("Package finished: %s (%d).") % (pypack.name, pypack.id)) + + except Exception as e: + self.log_error(e, trace=True) + + def download_finished(self, pyfile): + self.log_debug('download_finished') + try: + if self.config.get('info_file'): + self.announce(_("Download finished: %s (#%d) in #%d @ %s") % (pyfile.name, + pyfile.id, + pyfile.packageid, + pyfile.pluginname)) + + except Exception as e: + self.log_error(e, trace=True) + + def all_downloads_processed(self, arg=None): + self.log_debug('all_downloads_processed', arg) + try: + if self.config.get('all_download'): + self.announce(_("All download finished.")) + + except Exception: + pass + + def download_start(self, pyfile, url, filename): + self.log_debug('download_start', pyfile, url, filename) + try: + if self.config.get('download_start'): + self.announce(_("Download start: %s (#%d) in (#%d) @ %s.") % (pyfile.name, + pyfile.id, + pyfile.packageid, + pyfile.pluginname)) + except Exception: + pass + +class NotifyBot(sleekxmpp.ClientXMPP): + """ + A simple SleekXMPP bot that will echo messages it + receives, along with a short thank you message. + """ + + def __init__(self, jid, password, use_tls, use_ssl, log_info, log_debug): + self.log_debug = log_debug + self.log_info = log_info + + sleekxmpp.ClientXMPP.__init__(self, jid, password, use_tls=use_tls, use_ssl=use_ssl) + self.add_event_handler("session_start", self.start) + + def start(self, event): + """ + Process the session_start event. + + Typical actions for the session_start event are + requesting the roster and broadcasting an initial + presence stanza. + + Arguments: + event -- An empty dictionary. The session_start + event does not provide any additional + data. + """ + self.log_debug('start') + self.send_presence() + self.get_roster(timeout=60) diff --git a/module/plugins/hooks/XMPPInterface.py b/module/plugins/hooks/XMPPInterface.py deleted file mode 100644 index adffc04e39..0000000000 --- a/module/plugins/hooks/XMPPInterface.py +++ /dev/null @@ -1,251 +0,0 @@ -# -*- 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 . - - @author: RaNaN - @interface-version: 0.2 -""" - -from pyxmpp import streamtls -from pyxmpp.all import JID, Message -from pyxmpp.jabber.client import JabberClient -from pyxmpp.interface import implements -from pyxmpp.interfaces import * - -from module.plugins.hooks.IRCInterface import IRCInterface - - -class XMPPInterface(IRCInterface, JabberClient): - __name__ = "XMPPInterface" - __version__ = "0.11" - __description__ = """connect to jabber and let owner perform different tasks""" - __config__ = [("activated", "bool", "Activated", "False"), - ("jid", "str", "Jabber ID", "user@exmaple-jabber-server.org"), - ("pw", "str", "Password", ""), - ("tls", "bool", "Use TLS", False), - ("owners", "str", "List of JIDs accepting commands from", "me@icq-gateway.org;some@msn-gateway.org"), - ("info_file", "bool", "Inform about every file finished", "False"), - ("info_pack", "bool", "Inform about every package finished", "True"), - ("captcha", "bool", "Send captcha requests", "True")] - __author_name__ = ("RaNaN") - __author_mail__ = ("RaNaN@pyload.org") - - implements(IMessageHandlersProvider) - - def __init__(self, core, manager): - IRCInterface.__init__(self, core, manager) - - self.jid = JID(self.getConfig("jid")) - password = self.getConfig("pw") - - # if bare JID is provided add a resource -- it is required - if not self.jid.resource: - self.jid = JID(self.jid.node, self.jid.domain, "pyLoad") - - if self.getConfig("tls"): - tls_settings = streamtls.TLSSettings(require=True, verify_peer=False) - auth = ("sasl:PLAIN", "sasl:DIGEST-MD5") - else: - tls_settings = None - auth = ("sasl:DIGEST-MD5", "digest") - - # setup client with provided connection information - # and identity data - JabberClient.__init__(self, self.jid, password, - disco_name="pyLoad XMPP Client", disco_type="bot", - tls_settings=tls_settings, auth_methods=auth) - - self.interface_providers = [ - VersionHandler(self), - self, - ] - - def coreReady(self): - self.new_package = {} - - self.start() - - def packageFinished(self, pypack): - try: - if self.getConfig("info_pack"): - self.announce(_("Package finished: %s") % pypack.name) - except: - pass - - def downloadFinished(self, pyfile): - try: - if self.getConfig("info_file"): - self.announce( - _("Download finished: %(name)s @ %(plugin)s") % {"name": pyfile.name, "plugin": pyfile.pluginname}) - except: - pass - - def run(self): - # connect to IRC etc. - self.connect() - try: - self.loop() - except Exception, ex: - self.logError("pyLoad XMPP: %s" % str(ex)) - - def stream_state_changed(self, state, arg): - """This one is called when the state of stream connecting the component - to a server changes. This will usually be used to let the user - know what is going on.""" - self.logDebug("pyLoad XMPP: *** State changed: %s %r ***" % (state, arg)) - - def disconnected(self): - self.logDebug("pyLoad XMPP: Client was disconnected") - - def stream_closed(self, stream): - self.logDebug("pyLoad XMPP: Stream was closed | %s" % stream) - - def stream_error(self, err): - self.logDebug("pyLoad XMPP: Stream Error: %s" % err) - - def get_message_handlers(self): - """Return list of (message_type, message_handler) tuples. - - The handlers returned will be called when matching message is received - in a client session.""" - return [ - ("normal", self.message), - ] - - def message(self, stanza): - """Message handler for the component.""" - subject = stanza.get_subject() - body = stanza.get_body() - t = stanza.get_type() - self.logDebug(u'pyLoad XMPP: Message from %s received.' % (unicode(stanza.get_from(), ))) - self.logDebug(u'pyLoad XMPP: Body: %s Subject: %s Type: %s' % (body, subject, t)) - - if t == "headline": - # 'headline' messages should never be replied to - return True - if subject: - subject = u"Re: " + subject - - to_jid = stanza.get_from() - from_jid = stanza.get_to() - - #j = JID() - to_name = to_jid.as_utf8() - from_name = from_jid.as_utf8() - - names = self.getConfig("owners").split(";") - - if to_name in names or to_jid.node + "@" + to_jid.domain in names: - messages = [] - - trigger = "pass" - args = None - - try: - temp = body.split() - trigger = temp[0] - if len(temp) > 1: - args = temp[1:] - except: - pass - - handler = getattr(self, "event_%s" % trigger, self.event_pass) - try: - res = handler(args) - for line in res: - m = Message( - to_jid=to_jid, - from_jid=from_jid, - stanza_type=stanza.get_type(), - subject=subject, - body=line) - - messages.append(m) - except Exception, e: - self.logError("pyLoad XMPP: " + repr(e)) - - return messages - - else: - return True - - def response(self, msg, origin=""): - return self.announce(msg) - - def announce(self, message): - """ send message to all owners""" - for user in self.getConfig("owners").split(";"): - self.logDebug("pyLoad XMPP: Send message to %s" % user) - - to_jid = JID(user) - - m = Message(from_jid=self.jid, - to_jid=to_jid, - stanza_type="chat", - body=message) - - stream = self.get_stream() - if not stream: - self.connect() - stream = self.get_stream() - - stream.send(m) - - def beforeReconnecting(self, ip): - self.disconnect() - - def afterReconnecting(self, ip): - self.connect() - - -class VersionHandler(object): - """Provides handler for a version query. - - This class will answer version query and announce 'jabber:iq:version' namespace - in the client's disco#info results.""" - - implements(IIqHandlersProvider, IFeaturesProvider) - - def __init__(self, client): - """Just remember who created this.""" - self.client = client - - def get_features(self): - """Return namespace which should the client include in its reply to a - disco#info query.""" - return ["jabber:iq:version"] - - def get_iq_get_handlers(self): - """Return list of tuples (element_name, namespace, handler) describing - handlers of stanzas""" - return [ - ("query", "jabber:iq:version", self.get_version), - ] - - def get_iq_set_handlers(self): - """Return empty list, as this class provides no stanza handler.""" - return [] - - def get_version(self, iq): - """Handler for jabber:iq:version queries. - - jabber:iq:version queries are not supported directly by PyXMPP, so the - XML node is accessed directly through the libxml2 API. This should be - used very carefully!""" - iq = iq.make_result_response() - q = iq.new_query("jabber:iq:version") - q.newTextChild(q.ns(), "name", "Echo component") - q.newTextChild(q.ns(), "version", "1.0") - return iq diff --git a/module/plugins/hooks/ZeveraCom.py b/module/plugins/hooks/ZeveraCom.py deleted file mode 100644 index fb84886d1f..0000000000 --- a/module/plugins/hooks/ZeveraCom.py +++ /dev/null @@ -1,20 +0,0 @@ -# -*- coding: utf-8 -*- - -from module.network.RequestFactory import getURL -from module.plugins.internal.MultiHoster import MultiHoster - - -class ZeveraCom(MultiHoster): - __name__ = "ZeveraCom" - __version__ = "0.02" - __type__ = "hook" - __config__ = [("activated", "bool", "Activated", "False"), - ("hosterListMode", "all;listed;unlisted", "Use for hosters (if supported)", "all"), - ("hosterList", "str", "Hoster list (comma separated)", "")] - __description__ = """Real-Debrid.com hook plugin""" - __author_name__ = ("zoidberg") - __author_mail__ = ("zoidberg@mujmail.cz") - - def getHoster(self): - page = getURL("http://www.zevera.com/jDownloader.ashx?cmd=gethosters") - return [x.strip() for x in page.replace("\"", "").split(",")] diff --git a/module/plugins/hoster/ARD.py b/module/plugins/hoster/ARD.py deleted file mode 100644 index 12cb6c95a8..0000000000 --- a/module/plugins/hoster/ARD.py +++ /dev/null @@ -1,83 +0,0 @@ -import subprocess -import re -import os.path -import os - -from module.utils import save_join, save_path -from module.plugins.Hoster import Hoster - -# Requires rtmpdump -# by Roland Beermann - - -class RTMP: - # TODO: Port to some RTMP-library like rtmpy or similar - # TODO?: Integrate properly into the API of pyLoad - - command = "rtmpdump" - - @classmethod - def download_rtmp_stream(cls, url, output_file, playpath=None): - opts = [ - "-r", url, - "-o", output_file, - ] - if playpath: - opts.append("--playpath") - opts.append(playpath) - - cls._invoke_rtmpdump(opts) - - @classmethod - def _invoke_rtmpdump(cls, opts): - args = [ - cls.command - ] - args.extend(opts) - - return subprocess.check_call(args) - - -class ARD(Hoster): - __name__ = "ARD Mediathek" - __version__ = "0.12" - __pattern__ = r"http://www\.ardmediathek\.de/.*" - __config__ = [] - - def process(self, pyfile): - site = self.load(pyfile.url) - - avail_videos = re.findall( - r'mediaCollection.addMediaStream\(0, ([0-9]*), "([^\"]*)", "([^\"]*)", "[^\"]*"\);', site) - avail_videos.sort(key=lambda videodesc: int(videodesc[0]), - reverse=True) # The higher the number, the better the quality - - quality, url, playpath = avail_videos[0] - - pyfile.name = re.search(r"

([^<]*)

", site).group(1) - - if url.startswith("http"): - # Best quality is available over HTTP. Very rare. - self.download(url) - else: - pyfile.setStatus("downloading") - - download_folder = self.config['general']['download_folder'] - - location = save_join(download_folder, pyfile.package().folder) - - if not os.path.exists(location): - os.makedirs(location, int(self.config["permission"]["folder"], 8)) - - if self.config["permission"]["change_dl"] and os.name != "nt": - try: - uid = getpwnam(self.config["permission"]["user"])[2] - gid = getgrnam(self.config["permission"]["group"])[2] - - chown(location, uid, gid) - except Exception, e: - self.logWarning(_("Setting User and Group failed: %s") % str(e)) - - output_file = save_join(location, save_path(pyfile.name)) + os.path.splitext(playpath)[1] - - RTMP.download_rtmp_stream(url, playpath=playpath, output_file=output_file) diff --git a/module/plugins/hoster/AccioDebridCom.py b/module/plugins/hoster/AccioDebridCom.py new file mode 100644 index 0000000000..25bc3d48ac --- /dev/null +++ b/module/plugins/hoster/AccioDebridCom.py @@ -0,0 +1,70 @@ +# -*- coding: utf-8 -*- + +import pycurl +from module.network.HTTPRequest import BadHeader + +from ..internal.misc import json +from ..internal.MultiHoster import MultiHoster + + +def args(**kwargs): + return kwargs + + +class AccioDebridCom(MultiHoster): + __name__ = "AccioDebridCom" + __type__ = "hoster" + __version__ = "0.02" + __status__ = "testing" + + __pattern__ = r'http://((?:www\d+\.|s\d+\.)?accio-debrid\.com|\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})/download/file/[\w^_]+' + __config__ = [("activated", "bool", "Activated", True), + ("use_premium", "bool", "Use premium account if available", True), + ("fallback", "bool", "Fallback to free download if premium fails", False), + ("chk_filesize", "bool", "Check file size", True), + ("max_wait", "int", "Reconnect if waiting time is greater than minutes", 10), + ("revert_failed", "bool", "Revert to standard download if fails", True)] + + __description__ = """Accio-debrid.com multi-hoster plugin""" + __license__ = "GPLv3" + __authors__ = [("PlugPlus", "accio.debrid@gmail.com")] + + API_URL = "https://accio-debrid.com/apiv2/" + + def api_response(self, action, get={}, post={}): + get['action'] = action + + # Better use pyLoad User-Agent so we don't get blocked + self.req.http.c.setopt(pycurl.USERAGENT, "pyLoad/%s" % self.pyload.version) + + json_data = self.load(self.API_URL, get=get, post=post) + + return json.loads(json_data) + + def handle_premium(self, pyfile): + try: + res = self.api_response("getLink", + get=args(token=self.account.info['data']['cache_info'][self.account.user]['token']), + post=args(link=pyfile.url)) + + except BadHeader, e: + if e.code == 405: + self.fail(_("Banned IP")) + + else: + raise + + if res['response_code'] == "ok": + self.link = res['debridLink'] + + elif res['response_code'] == "UNKNOWN_ACCOUNT_TOKEN": + self.account.relogin() + self.retry() + + elif res['response_code'] == "UNALLOWED_IP": + self.fail(_("Banned IP")) + + else: + self.log_error(res['response_text']) + self.fail(res['response_text']) + diff --git a/module/plugins/hoster/AlfafileNet.py b/module/plugins/hoster/AlfafileNet.py new file mode 100644 index 0000000000..9a38ee24a8 --- /dev/null +++ b/module/plugins/hoster/AlfafileNet.py @@ -0,0 +1,96 @@ +# -*- coding: utf-8 -*- + +import re + +from ..captcha.ReCaptcha import ReCaptcha +from ..captcha.SolveMedia import SolveMedia +from ..internal.misc import json, seconds_to_midnight +from ..internal.SimpleHoster import SimpleHoster + + +class AlfafileNet(SimpleHoster): + __name__ = "AlfafileNet" + __type__ = "hoster" + __version__ = "0.07" + __status__ = "testing" + + __pattern__ = r'https?://(?:www\.)?(alfafile\.net)/file/(?P\w+)' + __config__ = [("activated", "bool", "Activated", True), + ("use_premium", "bool", "Use premium account if available", True), + ("fallback", "bool", "Fallback to free download if premium fails", True), + ("chk_filesize", "bool", "Check file size", True), + ("max_wait", "int", "Reconnect if waiting time is greater than minutes", 10)] + + __description__ = """alfafile.net hoster plugin""" + __license__ = "GPLv3" + __authors__ = [("GammaC0de", "nitzo2001[AT]yahoo[DOT]com")] + + URL_REPLACEMENTS = [(__pattern__ + ".*", r'https://alfafile.net/file/\g')] + COOKIES = [("alfafile.net", "lang", "en")] + + NAME_PATTERN = r'(?P[\d.,]+) (?P[\w^_]+)<' + + LINK_PATTERN = r'
Download' + + DL_LIMIT_PATTERN = r'Try again in (.+?)<' + PREMIUM_ONLY_PATTERN = r'In order to buy premium access' + + def handle_free(self, pyfile): + json_data = self.load(self.fixurl("/download/start_timer/" + self.info['pattern']['ID'])) + json_data = json.loads(json_data) + + if json_data['show_timer']: + self.wait(json_data['timer']) + + redirect_url = self.fixurl(json_data['redirect_url']) + self.data = self.load(redirect_url) + + solvemedia = SolveMedia(self.pyfile) + captcha_key = solvemedia.detect_key() + if captcha_key: + self.captcha = solvemedia + response, challenge = solvemedia.challenge(captcha_key) + + self.data = self.load(redirect_url, + post={'adcopy_response': response, + 'adcopy_challenge': challenge}) + + else: + recaptcha = ReCaptcha(self.pyfile) + captcha_key = recaptcha.detect_key() + if captcha_key: + self.captcha = recaptcha + response = recaptcha.challenge(captcha_key) + + self.data = self.load(redirect_url, + post={'g-recaptcha-response': response}) + + else: + self.error(_("Captcha pattern not found")) + + if "Invalid captcha" in self.data: + self.retry_captcha() + else: + self.captcha.correct() + + m = re.search(self.LINK_PATTERN, self.data) + if m is not None: + self.link = m.group(1) + + else: + self.data = json_data['html'] + self.check_errors() + + def check_errors(self): + SimpleHoster.check_errors(self) + + if re.search(r"You can't download not more than \d+ file at a time", self.data): + self.retry(wait=20 * 60, + msg=_("Too many max simultaneous downloads"), + msgfail=_("Too many max simultaneous downloads")) + + if "You have reached your daily downloads limit" in self.data: + self.retry(wait=seconds_to_midnight(), + msg=_("Daily download limit reached"), + msgfail=_("Daily download limit reached")) diff --git a/module/plugins/hoster/AlldebridCom.py b/module/plugins/hoster/AlldebridCom.py index 82f4531a69..a1d8190c50 100644 --- a/module/plugins/hoster/AlldebridCom.py +++ b/module/plugins/hoster/AlldebridCom.py @@ -1,83 +1,119 @@ # -*- coding: utf-8 -*- import re -from urllib import unquote -from random import randrange -from module.plugins.Hoster import Hoster -from module.common.json_layer import json_loads -from module.utils import parseFileSize +import time +from ..internal.misc import json +from ..internal.MultiHoster import MultiHoster -class AlldebridCom(Hoster): + +class AlldebridCom(MultiHoster): __name__ = "AlldebridCom" - __version__ = "0.34" __type__ = "hoster" + __version__ = "0.67" + __status__ = "testing" + + __pattern__ = r'https?://(?:\w+\.)?(?:alldebrid\.com|debrid\.it|alld\.io)/(?:dl|f)/[\w^_]+' + __config__ = [("activated", "bool", "Activated", True), + ("use_premium", "bool", "Use premium account if available", True), + ("fallback", "bool", "Fallback to free download if premium fails", False), + ("chk_filesize", "bool", "Check file size", True), + ("max_wait", "int", "Reconnect if waiting time is greater than minutes", 10), + ("revert_failed", "bool", "Revert to standard download if fails", True), + ("stream_quality", "Lowest;LD 144p;LD 240p;SD 380p;HQ 480p;HD 720p;HD 1080p;Highest", "Quality to download from stream hosters", "Highest")] + + __description__ = """Alldebrid.com multi-hoster plugin""" + __license__ = "GPLv3" + __authors__ = [("Andy Voigt", "spamsales@online.de"), + ("GammaC0de", "nitzo2001[AT]yahoo[DOT]com")] + + DISPOSITION = False + URL_REPLACEMENTS = [(r'https?://(?:www\.)?mega(?:\.co)?\.nz/#N!(?P[\w^_]+)!(?P[\w\-,=]+)###n=(?P[\w^_]+)', + lambda m:"https://mega.nz/#!%s!%s~~%s" % (m.group("ID"), m.group("KEY"), m.group("OWNER"))), + (r'https?://(?:www\.)?mega(?:\.co)?\.nz/.*', lambda m:m.group(0).replace('_', '/'))] + + # See https://docs.alldebrid.com/ + API_URL = "https://api.alldebrid.com/v4.1/" + + def api_response(self, method, get={}, post={}, multipart=False): + get.update({'agent': "pyLoad", + 'version': self.pyload.version}) + json_data = json.loads(self.load(self.API_URL + method, get=get, post=post, multipart=multipart)) + if json_data['status'] == "success": + return json_data['data'] + else: + return json_data - __pattern__ = r"https?://.*alldebrid\..*" - __description__ = """Alldebrid.com hoster plugin""" - __author_name__ = ("Andy, Voigt") - __author_mail__ = ("spamsales@online.de") - - def getFilename(self, url): - try: - name = unquote(url.rsplit("/", 1)[1]) - except IndexError: - name = "Unknown_Filename..." - if name.endswith("..."): # incomplete filename, append random stuff - name += "%s.tmp" % randrange(100, 999) - return name + def sleep(self, sec): + for _i in range(sec): + if self.pyfile.abort: + break + time.sleep(1) def setup(self): - self.chunkLimit = 16 - self.resumeDownload = True - - def process(self, pyfile): - if re.match(self.__pattern__, pyfile.url): - new_url = pyfile.url - elif not self.account: - self.logError(_("Please enter your %s account or deactivate this plugin") % "AllDebrid") - self.fail("No AllDebrid account provided") - else: - self.logDebug("Old URL: %s" % pyfile.url) - password = self.getPassword().splitlines() - password = "" if not password else password[0] + self.chunk_limit = 16 - url = "http://www.alldebrid.com/service.php?link=%s&json=true&pw=%s" % (pyfile.url, password) - page = self.load(url) - data = json_loads(page) + def handle_premium(self, pyfile): + api_data = self.api_response("link/unlock", + get={'link': pyfile.url, + 'password': self.get_password(), + 'apikey': self.account.info['login']['password']}) - self.logDebug("Json data: %s" % str(data)) + if api_data.get("error", False): + if api_data['error']['code'] == 'LINK_DOWN': + self.offline() - if data["error"]: - if data["error"] == "This link isn't available on the hoster website.": - self.offline() - else: - self.logWarning(data["error"]) - self.tempOffline() else: - if self.pyfile.name and not self.pyfile.name.endswith('.tmp'): - self.pyfile.name = data["filename"] - self.pyfile.size = parseFileSize(data["filesize"]) - new_url = data["link"] + self.log_error(api_data['error']['message']) + self.temp_offline() - if self.getConfig("https"): - new_url = new_url.replace("http://", "https://") else: - new_url = new_url.replace("https://", "http://") - - if new_url != pyfile.url: - self.logDebug("New URL: %s" % new_url) - - if pyfile.name.startswith("http") or pyfile.name.startswith("Unknown"): - #only use when name wasnt already set - pyfile.name = self.getFilename(new_url) - - self.download(new_url, disposition=True) - - check = self.checkDownload({"error": "An error occured while processing your request", - "empty": re.compile(r"^$")}) - - if check == "error": - self.retry(reason="An error occured while generating link.", wait_time=60) - elif check == "empty": - self.retry(reason="Downloaded File was empty.", wait_time=60) + if api_data['link'] == "" and "streams" in api_data: + unlock_id = api_data['id'] + streams = dict([ + (_s['quality'], {'ext': _s['ext'], 'filesize': _s['filesize'], 'id': _s['id']}) + for _s in api_data['streams'] + if type(_s['quality']) == int]) + qualities = sorted(streams.keys()) + self.log_debug("AVAILABLE STREAMS: %s" % qualities) + desired_quality = self.config.get("stream_quality") + if desired_quality == "Lowest": + chosen_quality = qualities[0] + elif desired_quality == "Highest": + chosen_quality = qualities[-1] + else: + desired_quality = int(re.search(r"\d+", desired_quality).group(0)) + chosen_quality = min(qualities, key=lambda x: abs(x - desired_quality)) + self.log_debug("CHOSEN STREAM: %s" % chosen_quality) + + stream_id = streams[chosen_quality]['id'] + stream_name = api_data['filename'] + "." + streams[chosen_quality]['ext'] + stream_size = streams[chosen_quality]['filesize'] + api_data = self.api_response("link/streaming", + get={'apikey': self.account.info['login']['password'], + 'id': unlock_id, + 'stream': stream_id}) + if api_data.get("error", False): + self.log_error(api_data['error']['message']) + self.temp_offline() + + delayed_id = api_data.get('delayed') + if delayed_id: + pyfile.setCustomStatus("delayed stream") + while True: + api_data = self.api_response("link/delayed", + get={'apikey': self.account.info['login']['password'], + 'id': delayed_id}) + if 'link' in api_data: + pyfile.name = stream_name + pyfile.size = stream_size + self.chunk_limit = api_data.get("max_chunks", 16) + self.link = api_data['link'] + return + + self.sleep(5) + + pyfile.name = api_data['filename'] + pyfile.size = api_data['filesize'] + self.chunk_limit = api_data.get("max_chunks", 16) + self.link = api_data['link'] diff --git a/module/plugins/hoster/AndroidfilehostCom.py b/module/plugins/hoster/AndroidfilehostCom.py new file mode 100644 index 0000000000..a2b56e0e7c --- /dev/null +++ b/module/plugins/hoster/AndroidfilehostCom.py @@ -0,0 +1,65 @@ +# -*- coding: utf-8 -* + +import pycurl +import re + +from ..internal.SimpleHoster import SimpleHoster + + +class AndroidfilehostCom(SimpleHoster): + __name__ = "AndroidfilehostCom" + __type__ = "hoster" + __version__ = "0.07" + __status__ = "testing" + + __pattern__ = r'https?://(?:www\.)?androidfilehost\.com/\?fid=\d+' + __config__ = [("activated", "bool", "Activated", True), + ("use_premium", "bool", "Use premium account if available", True), + ("fallback", "bool", "Fallback to free download if premium fails", True), + ("chk_filesize", "bool", "Check file size", True), + ("max_wait", "int", "Reconnect if waiting time is greater than minutes", 10)] + + __description__ = """Androidfilehost.com hoster plugin""" + __license__ = "GPLv3" + __authors__ = [("zapp-brannigan", "fuerst.reinje@web.de")] + + NAME_PATTERN = r'name="filename" id="filename" value="(?P.*?)" />' + SIZE_PATTERN = r'(?P[\d.,]+)(?P[\w^_]+)
Size
' + HASHSUM_PATTERN = r'(?P.*?)
(?PMD5)
' + + OFFLINE_PATTERN = r'404 not found' + TEMP_OFFLINE_PATTERN = r'[^\w](503\s|[Mm]aint(e|ai)nance|[Tt]emp([.-]|orarily))' + + WAIT_PATTERN = r'users must wait (\d+) secs' + + def setup(self): + self.multiDL = True + self.resume_download = True + self.chunk_limit = 1 + + def handle_free(self, pyfile): + wait = re.search(self.WAIT_PATTERN, self.data) + if wait is not None : + self.log_debug("Waiting time: %s seconds" % wait.group(1)) + + fid = re.search(r'id="fid" value="(\d+)" />', self.data).group(1) + self.log_debug("FID: %s" % fid) + + self.req.http.c.setopt(pycurl.HTTPHEADER, ["X-MOD-SBB-CTYPE: xhr"]) + + html = self.load("https://androidfilehost.com/libs/otf/mirrors.otf.php", + post={'submit': 'submit', + 'action': 'getdownloadmirrors', + 'fid': fid}) + self.req.http.c.setopt(pycurl.HTTPHEADER, ["X-MOD-SBB-CTYPE:"]) + + + self.link = re.findall('"url":"(.*?)"', html)[0].replace("\\", "") + mirror_host = self.link.split("/")[2] + + self.log_debug("Mirror Host: %s" % mirror_host) + + html = self.load("https://androidfilehost.com/libs/otf/stats.otf.php", + get={'fid': fid, + 'w': 'download', + 'mirror': mirror_host}) diff --git a/module/plugins/hoster/AnonfilesCom.py b/module/plugins/hoster/AnonfilesCom.py new file mode 100644 index 0000000000..d2597f3607 --- /dev/null +++ b/module/plugins/hoster/AnonfilesCom.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- + +from ..internal.SimpleHoster import SimpleHoster + + +class AnonfilesCom(SimpleHoster): + __name__ = "AnonfilesCom" + __type__ = "hoster" + __version__ = "0.02" + __status__ = "testing" + + __pattern__ = r'https?://(?:www\.)?anonfiles?\.com/(?P\w+)' + __config__ = [("activated", "bool", "Activated", True), + ("use_premium", "bool", "Use premium account if available", True), + ("fallback", "bool", "Fallback to free download if premium fails", True), + ("chk_filesize", "bool", "Check file size", True), + ("max_wait", "int", "Reconnect if waiting time is greater than minutes", 10)] + + __description__ = """Anonfiles.com hoster plugin""" + __license__ = "GPLv3" + __authors__ = [("GammaC0de", "nitzo2001[AT]yahoo[DOT]com")] + + NAME_PATTERN = r'class="text-center text-wordwrap">(?P.+?)<' + SIZE_PATTERN = r'Download\s*\((?P[\d.,]+) (?P[\w^_]+)\)' + + LINK_PATTERN = r'href="(https://cdn-\d+.anonfiles.com/.+?)"' + + URL_REPLACEMENTS = [(__pattern__ + ".*", "https://anonfiles.com/\g")] + + def setup(self): + self.multiDL = True + self.resume_download = True + self.chunk_limit = -1 + diff --git a/module/plugins/hoster/ArchiveOrg.py b/module/plugins/hoster/ArchiveOrg.py new file mode 100644 index 0000000000..17cf803e28 --- /dev/null +++ b/module/plugins/hoster/ArchiveOrg.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- + +from ..internal.SimpleHoster import SimpleHoster + + +class ArchiveOrg(SimpleHoster): + __name__ = "ArchiveOrg" + __type__ = "hoster" + __version__ = "0.02" + __status__ = "testing" + + __pattern__ = r"https?://(?:www\.)?archive\.org/download/.+" + __config__ = [ + ("activated", "bool", "Activated", True), + ("use_premium", "bool", "Use premium account if available", True), + ("fallback", "bool", "Fallback to free download if premium fails", True), + ("chk_filesize", "bool", "Check file size", True), + ("max_wait", "int", "Reconnect if waiting time is greater than minutes", 10), + ] + + __description__ = """Archive.org hoster plugin""" + __license__ = "GPLv3" + __authors__ = [("GammaC0de", "nitzo2001[AT]yahoo[DOT]com")] + + def setup(self): + self.multiDl = True + self.resume_download = True + self.chunk_limit = -1 diff --git a/module/plugins/hoster/BasePlugin.py b/module/plugins/hoster/BasePlugin.py index 2bdfda7c48..69ce2b3e87 100644 --- a/module/plugins/hoster/BasePlugin.py +++ b/module/plugins/hoster/BasePlugin.py @@ -1,113 +1,24 @@ -#!/usr/bin/env python # -*- coding: utf-8 -*- -from urlparse import urlparse -from re import search -from urllib import unquote -from module.network.HTTPRequest import BadHeader -from module.plugins.Hoster import Hoster -from module.utils import html_unescape, remove_chars +from .Http import Http -class BasePlugin(Hoster): +class BasePlugin(Http): __name__ = "BasePlugin" __type__ = "hoster" - __pattern__ = r"^unmatchable$" - __version__ = "0.19" - __description__ = """Base Plugin when any other didnt fit""" - __author_name__ = ("RaNaN") - __author_mail__ = ("RaNaN@pyload.org") + __version__ = "0.52" + __status__ = "testing" - def setup(self): - self.chunkLimit = -1 - self.resumeDownload = True - - def process(self, pyfile): - """main function""" - - #debug part, for api exerciser - if pyfile.url.startswith("DEBUG_API"): - self.multiDL = False - return - - # self.__name__ = "NetloadIn" - # pyfile.name = "test" - # self.html = self.load("http://localhost:9000/short") - # self.download("http://localhost:9000/short") - # self.api = self.load("http://localhost:9000/short") - # self.decryptCaptcha("http://localhost:9000/captcha") - # - # if pyfile.url == "79": - # self.core.api.addPackage("test", [str(i) for i in range(80)], 1) - # - # return - if pyfile.url.startswith("http"): - - try: - self.downloadFile(pyfile) - except BadHeader, e: - if e.code in (401, 403): - self.logDebug("Auth required") - - account = self.core.accountManager.getAccountPlugin('Http') - servers = [x['login'] for x in account.getAllAccounts()] - server = urlparse(pyfile.url).netloc - - if server in servers: - self.logDebug("Logging on to %s" % server) - self.req.addAuth(account.accounts[server]["password"]) - else: - for pwd in pyfile.package().password.splitlines(): - if ":" in pwd: - self.req.addAuth(pwd.strip()) - break - else: - self.fail(_("Authorization required (username:password)")) + __pattern__ = r'^unmatchable$' + __config__ = [("activated", "bool", "Activated", True)] - self.downloadFile(pyfile) - else: - raise + __description__ = """Default hoster plugin when any other didnt fit""" + __license__ = "GPLv3" + __authors__ = [("Walter Purcaro", "vuolter@gmail.com")] - else: - self.fail("No Plugin matched and not a downloadable url.") - - def downloadFile(self, pyfile): - url = pyfile.url - - for i in range(5): - header = self.load(url, just_header=True) - - # self.load does not raise a BadHeader on 404 responses, do it here - if 'code' in header and header['code'] == 404: - raise BadHeader(404) - - if 'location' in header: - self.logDebug("Location: " + header['location']) - base = search(r'https?://[^/]+', url).group(0) - if header['location'].startswith("http"): - url = unquote(header['location']) - elif header['location'].startswith("/"): - url = base + unquote(header['location']) - else: - url = '%s/%s' % (base, unquote(header['location'])) - else: - break - - name = html_unescape(unquote(urlparse(url).path.split("/")[-1])) - - if 'content-disposition' in header: - self.logDebug("Content-Disposition: " + header['content-disposition']) - m = search("filename(?P=|\*=(?P.+)'')(?P.*)", header['content-disposition']) - if m: - disp = m.groupdict() - self.logDebug(disp) - if not disp['enc']: - disp['enc'] = 'utf-8' - name = remove_chars(disp['name'], "\"';").strip() - name = unicode(unquote(name), disp['enc']) + def setup(self): + self.chunk_limit = -1 + self.resume_download = True - if not name: - name = url - pyfile.name = name - self.logDebug("Filename: %s" % pyfile.name) - self.download(url, disposition=True) + if not self.pyfile.url.startswith("http"): + self.fail(_("No plugin matched")) diff --git a/module/plugins/hoster/BasketbuildCom.py b/module/plugins/hoster/BasketbuildCom.py new file mode 100644 index 0000000000..deeeef0cc5 --- /dev/null +++ b/module/plugins/hoster/BasketbuildCom.py @@ -0,0 +1,64 @@ +# -*- coding: utf-8 -* +# +# Test links: +# https://s.basketbuild.com/filedl/devs?dev=pacman&dl=pacman/falcon/RC-3/pac_falcon-RC-3-20141103.zip +# https://s.basketbuild.com/filedl/gapps?dl=gapps-gb-20110828-signed.zip + +import re + +from ..internal.SimpleHoster import SimpleHoster + + +class BasketbuildCom(SimpleHoster): + __name__ = "BasketbuildCom" + __type__ = "hoster" + __version__ = "0.08" + __status__ = "testing" + + __pattern__ = r'https?://(?:www\.)?(?:\w\.)?basketbuild\.com/filedl/.+' + __config__ = [("activated", "bool", "Activated", True), + ("use_premium", "bool", "Use premium account if available", True), + ("fallback", "bool", + "Fallback to free download if premium fails", True), + ("chk_filesize", "bool", "Check file size", True), + ("max_wait", "int", "Reconnect if waiting time is greater than minutes", 10)] + + __description__ = """Basketbuild.com hoster plugin""" + __license__ = "GPLv3" + __authors__ = [("zapp-brannigan", "fuerst.reinje@web.de")] + + NAME_PATTERN = r'File Name: (?P.+?)
' + SIZE_PATTERN = r'File Size:
(?P[\d.,]+) (?P[\w^_]+)' + OFFLINE_PATTERN = r'404 - Page Not Found' + + def setup(self): + self.multiDL = True + self.resume_download = True + self.chunk_limit = 1 + + def handle_free(self, pyfile): + try: + link1 = re.search(r'href="(.+dlgate/.+)"', self.data).group(1) + self.data = self.load(link1) + + except AttributeError: + self.error(_("Hop #1 not found")) + + else: + self.log_debug("Next hop: %s" % link1) + + try: + wait = re.search(r'var sec = (\d+)', self.data).group(1) + self.log_debug("Wait %s seconds" % wait) + self.wait(wait) + + except AttributeError: + self.log_debug("No wait time found") + + try: + self.link = re.search( + r'id="dlLink">\s*. - - @author: zoidberg -""" import re -from time import time -from module.plugins.internal.SimpleHoster import SimpleHoster, create_getInfo -from module.common.json_layer import json_loads +from ..internal.misc import json +from ..internal.SimpleHoster import SimpleHoster class BayfilesCom(SimpleHoster): __name__ = "BayfilesCom" __type__ = "hoster" - __pattern__ = r"https?://(?:www\.)?bayfiles\.(com|net)/file/(?P[a-zA-Z0-9]+/[a-zA-Z0-9]+/[^/]+)" - __version__ = "0.06" - __description__ = """Bayfiles.com plugin - free only""" - __author_name__ = ("zoidberg", "Walter Purcaro") - __author_mail__ = ("zoidberg@mujmail.cz", "vuolter@gmail.com") - - FILE_INFO_PATTERN = r'

[^<]*(?P[0-9., ]+)(?P[kKMG])i?B

' - FILE_OFFLINE_PATTERN = r'(

The requested file could not be found.

|404 Not Found)' - - WAIT_PATTERN = r'>Your IP [0-9.]* has recently downloaded a file\. Upgrade to premium or wait (\d+) minutes\.<' - VARS_PATTERN = r'var vfid = (\d+);\s*var delay = (\d+);' - LINK_PATTERN = r"javascript:window.location.href = '([^']+)';" - PREMIUM_LINK_PATTERN = r'(?:
\w+)" + __config__ = [("enabled", "bool", "Activated", True), + ("use_premium", "bool", "Use premium account if available", True), + ("fallback", "bool", "Fallback to free download if premium fails", True), + ("chk_filesize", "bool", "Check file size", True), + ("max_wait", "int", "Reconnect if waiting time is greater than minutes", 10)] - if not "token" in response or not response['token']: - self.fail('No token') + __description__ = """Bayfiles.com hoster plugin""" + __license__ = "GPLv3" + __authors__ = [("GammaC0de", "nitzo2001[AT]yahoo[DOT]com")] - self.setWait(int(delay)) - self.wait() + URL_REPLACEMENTS = [(r"^http://", "https://")] - self.html = self.load('https://bayfiles.com/ajax_download', get={ - "token": response['token'], - "action": "getLink", - "vfid": vfid}) + LINK_PATTERN = r'href="(https://cdn-\d+\.bayfiles\.com/.+?)"' - # Get final link and download - found = re.search(self.LINK_PATTERN, self.html) - if not found: - self.parseError("Free link") - self.startDownload(found.group(1)) + def api_info(self, url): + info = {} + file_id = re.match(self.__pattern__, url).group("ID") + json_data = self.load("https://api.bayfiles.com/v2/file/%s/info" % file_id) + api_data = json.loads(json_data) - def handlePremium(self): - found = re.search(self.PREMIUM_LINK_PATTERN, self.html) - if not found: - self.parseError("Premium link") - self.startDownload(found.group(1)) + if api_data["status"] is True: + info["status"] = 2 + info["name"] = api_data["data"]["file"]["metadata"]["name"] + info["size"] = api_data["data"]["file"]["metadata"]["size"]["bytes"] - def startDownload(self, url): - self.logDebug("%s URL: %s" % ("Premium" if self.premium else "Free", url)) - self.download(url) - # check download - check = self.checkDownload({ - "waitforfreeslots": re.compile(r"BayFiles"), - "notfound": re.compile(r"404 Not Found") - }) - if check == "waitforfreeslots": - self.retry(30, 60 * 5, "Wait for free slot") - elif check == "notfound": - self.retry(30, 60 * 5, "404 Not found") + else: + if api_data["error"]["type"] in ("FILE_NOT_FOUND", "ERROR_FILE_BANNED"): + info["status"] = 1 + else: + info["error"] = api_data["error"]["message"] + info["status"] = 8 -getInfo = create_getInfo(BayfilesCom) + return info diff --git a/module/plugins/hoster/BezvadataCz.py b/module/plugins/hoster/BezvadataCz.py index d923a0633b..8486fef1c5 100644 --- a/module/plugins/hoster/BezvadataCz.py +++ b/module/plugins/hoster/BezvadataCz.py @@ -1,100 +1,79 @@ # -*- 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 . - - @author: zoidberg -""" import re -from module.plugins.internal.SimpleHoster import SimpleHoster, create_getInfo + +from ..internal.SimpleHoster import SimpleHoster class BezvadataCz(SimpleHoster): __name__ = "BezvadataCz" __type__ = "hoster" - __pattern__ = r"http://(\w*\.)*bezvadata.cz/stahnout/.*" - __version__ = "0.24" - __description__ = """BezvaData.cz""" - __author_name__ = ("zoidberg") - __author_mail__ = ("zoidberg@mujmail.cz") + __version__ = "0.35" + __status__ = "testing" - FILE_NAME_PATTERN = r'

Soubor: (?P[^<]+)

' - FILE_SIZE_PATTERN = r'
  • Velikost: (?P[^<]+)
  • ' - FILE_OFFLINE_PATTERN = r'BezvaData \| Soubor nenalezen' - - def setup(self): - self.multiDL = self.resumeDownload = True - - def handleFree(self): - #download button - found = re.search(r'
    ', self.html) - if not found: - self.parseError("page2 URL") - url = "http://bezvadata.cz%s" % found.group(1) - self.logDebug("DL URL %s" % url) + __description__ = """BezvaData.cz hoster plugin""" + __license__ = "GPLv3" + __authors__ = [("zoidberg", "zoidberg@mujmail.cz")] - #countdown - found = re.search(r'id="countdown">(\d\d):(\d\d)<', self.html) - wait_time = (int(found.group(1)) * 60 + int(found.group(2)) + 1) if found else 120 - self.setWait(wait_time, False) - self.wait() + NAME_PATTERN = r'

    Soubor: (?P.+?)

    ' + SIZE_PATTERN = r'
  • Velikost: (?P.+?)
  • ' + OFFLINE_PATTERN = r'BezvaData \| Soubor nenalezen' - self.download(url) - - def checkErrors(self): - if 'images/button-download-disable.png' in self.html: - self.longWait(300, 24) # parallel dl limit - elif '
    ', self.data) + if m is None: + self.error(_("Page 2 URL not found")) + url = "http://bezvadata.cz%s" % m.group(1) + self.log_debug("DL URL %s" % url) + + #: countdown + m = re.search(r'id="countdown">(\d\d):(\d\d)<', self.data) + wait_time = (int(m.group(1)) * 60 + int(m.group(2))) if m else 120 + self.wait(wait_time, False) + + self.link = url + + def check_errors(self): + if 'images/button-download-disable.png' in self.data: + #: Parallel dl limit + self.retry(5 * 60, 24, _("Download limit reached")) + elif '
    Filename:(?P.*?)
    ' - FILE_SIZE_PATTERN = r'Size:(?P.*?)
    ' - HOSTER_NAME = "billionuploads.com" - - -getInfo = create_getInfo(BillionuploadsCom) diff --git a/module/plugins/hoster/BitshareCom.py b/module/plugins/hoster/BitshareCom.py deleted file mode 100644 index 2b3f7777fe..0000000000 --- a/module/plugins/hoster/BitshareCom.py +++ /dev/null @@ -1,146 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import with_statement - -import re - -from module.plugins.internal.SimpleHoster import SimpleHoster, create_getInfo -from module.plugins.internal.CaptchaService import ReCaptcha - - -class BitshareCom(SimpleHoster): - __name__ = "BitshareCom" - __type__ = "hoster" - __pattern__ = r"http://(www\.)?bitshare\.com/(files/(?P[a-zA-Z0-9]+)(/(?P.*?)\.html)?|\?f=(?P[a-zA-Z0-9]+))" - __version__ = "0.49" - __description__ = """Bitshare.Com File Download Hoster""" - __author_name__ = ("paulking", "fragonib") - __author_mail__ = (None, "fragonib[AT]yahoo[DOT]es") - - HOSTER_DOMAIN = "bitshare.com" - FILE_OFFLINE_PATTERN = r'(>We are sorry, but the requested file was not found in our database|>Error - File not available<|The file was deleted either by the uploader, inactivity or due to copyright claim)' - FILE_INFO_PATTERN = r'Downloading (?P.+) - (?P[\d.]+) (?P\w+)' - FILE_AJAXID_PATTERN = r'var ajaxdl = "(.*?)";' - CAPTCHA_KEY_PATTERN = r"http://api\.recaptcha\.net/challenge\?k=(.*?) " - TRAFFIC_USED_UP = r"Your Traffic is used up for today. Upgrade to premium to continue!" - - def setup(self): - self.req.cj.setCookie(self.HOSTER_DOMAIN, "language_selection", "EN") - self.multiDL = self.premium - self.chunkLimit = 1 - - def process(self, pyfile): - if self.premium: - self.account.relogin(self.user) - - self.pyfile = pyfile - - # File id - m = re.match(self.__pattern__, self.pyfile.url) - self.file_id = max(m.group('id1'), m.group('id2')) - self.logDebug("File id is [%s]" % self.file_id) - - # Load main page - self.html = self.load(self.pyfile.url, ref=False, decode=True) - - # Check offline - if re.search(self.FILE_OFFLINE_PATTERN, self.html): - self.offline() - - # Check Traffic used up - if re.search(self.TRAFFIC_USED_UP, self.html): - self.logInfo("Your Traffic is used up for today. Wait 1800 seconds or reconnect!") - self.logDebug("Waiting %d seconds." % 1800) - self.setWait(1800, True) - self.wantReconnect = True - self.wait() - self.retry() - - # File name - m = re.search(self.__pattern__, self.pyfile.url) - name1 = m.group('name') if m else None - m = re.search(self.FILE_INFO_PATTERN, self.html) - name2 = m.group('N') if m else None - self.pyfile.name = max(name1, name2) - - # Ajax file id - self.ajaxid = re.search(self.FILE_AJAXID_PATTERN, self.html).group(1) - self.logDebug("File ajax id is [%s]" % self.ajaxid) - - # This may either download our file or forward us to an error page - url = self.getDownloadUrl() - self.logDebug("Downloading file with url [%s]" % url) - self.download(url) - - def getDownloadUrl(self): - # Return location if direct download is active - if self.premium: - header = self.load(self.pyfile.url, cookies=True, just_header=True) - if 'location' in header: - return header['location'] - - # Get download info - self.logDebug("Getting download info") - response = self.load("http://bitshare.com/files-ajax/" + self.file_id + "/request.html", - post={"request": "generateID", "ajaxid": self.ajaxid}) - self.handleErrors(response, ':') - parts = response.split(":") - filetype = parts[0] - wait = int(parts[1]) - captcha = int(parts[2]) - self.logDebug("Download info [type: '%s', waiting: %d, captcha: %d]" % (filetype, wait, captcha)) - - # Waiting - if wait > 0: - self.logDebug("Waiting %d seconds." % wait) - if wait < 120: - self.setWait(wait, False) - self.wait() - else: - self.setWait(wait - 55, True) - self.wait() - self.retry() - - # Resolve captcha - if captcha == 1: - self.logDebug("File is captcha protected") - id = re.search(self.CAPTCHA_KEY_PATTERN, self.html).group(1) - # Try up to 3 times - for i in range(3): - self.logDebug("Resolving ReCaptcha with key [%s], round %d" % (id, i + 1)) - recaptcha = ReCaptcha(self) - challenge, code = recaptcha.challenge(id) - response = self.load("http://bitshare.com/files-ajax/" + self.file_id + "/request.html", - post={"request": "validateCaptcha", "ajaxid": self.ajaxid, - "recaptcha_challenge_field": challenge, "recaptcha_response_field": code}) - if self.handleCaptchaErrors(response): - break - - # Get download URL - self.logDebug("Getting download url") - response = self.load("http://bitshare.com/files-ajax/" + self.file_id + "/request.html", - post={"request": "getDownloadURL", "ajaxid": self.ajaxid}) - self.handleErrors(response, '#') - url = response.split("#")[-1] - - return url - - def handleErrors(self, response, separator): - self.logDebug("Checking response [%s]" % response) - if "ERROR:Session timed out" in response: - self.retry() - elif "ERROR" in response: - msg = response.split(separator)[-1] - self.fail(msg) - - def handleCaptchaErrors(self, response): - self.logDebug("Result of captcha resolving [%s]" % response) - if "SUCCESS" in response: - self.correctCaptcha() - return True - elif "ERROR:SESSION ERROR" in response: - self.retry() - self.logDebug("Wrong captcha") - self.invalidCaptcha() - - -getInfo = create_getInfo(BitshareCom) diff --git a/module/plugins/hoster/BoltsharingCom.py b/module/plugins/hoster/BoltsharingCom.py deleted file mode 100644 index cc8b1c7e61..0000000000 --- a/module/plugins/hoster/BoltsharingCom.py +++ /dev/null @@ -1,16 +0,0 @@ -# -*- coding: utf-8 -*- - -from module.plugins.internal.DeadHoster import DeadHoster, create_getInfo - - -class BoltsharingCom(DeadHoster): - __name__ = "BoltsharingCom" - __type__ = "hoster" - __pattern__ = r"http://(?:\w*\.)*?boltsharing.com/\w{12}" - __version__ = "0.02" - __description__ = """Boltsharing.com hoster plugin""" - __author_name__ = ("zoidberg") - __author_mail__ = ("zoidberg@mujmail.cz") - - -getInfo = create_getInfo(BoltsharingCom) diff --git a/module/plugins/hoster/CatShareNet.py b/module/plugins/hoster/CatShareNet.py deleted file mode 100644 index 66d46c2e87..0000000000 --- a/module/plugins/hoster/CatShareNet.py +++ /dev/null @@ -1,39 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -import re -from module.plugins.internal.SimpleHoster import SimpleHoster, create_getInfo -from module.plugins.internal.CaptchaService import ReCaptcha - - -class CatShareNet(SimpleHoster): - __name__ = "CatShareNet" - __type__ = "hoster" - __pattern__ = r"http://(www\.)?catshare.net/\w{16}.*" - __version__ = "0.01" - __description__ = """CatShare.net Download Hoster""" - __author_name__ = ("z00nx") - __author_mail__ = ("z00nx0@gmail.com") - - FILE_INFO_PATTERN = r'

    ]+>(?P.*)

    \s+

    ]+>(?P.*)

    ' - FILE_OFFLINE_PATTERN = r'Podany plik zosta' - SECONDS_PATTERN = 'var\s+count\s+=\s+(\d+);' - RECAPTCHA_KEY = "6Lfln9kSAAAAANZ9JtHSOgxUPB9qfDFeLUI_QMEy" - - def handleFree(self): - found = re.search(self.SECONDS_PATTERN, self.html) - seconds = int(found.group(1)) - self.logDebug("Seconds found", seconds) - self.setWait(seconds + 1) - self.wait() - recaptcha = ReCaptcha(self) - challenge, code = recaptcha.challenge(self.RECAPTCHA_KEY) - post_data = {"recaptcha_challenge_field": challenge, "recaptcha_response_field": code} - self.download(self.pyfile.url, post=post_data) - check = self.checkDownload({"html": re.compile("\A\w{12})' + __config__ = [("activated", "bool", "Activated", True), + ("use_premium", "bool", "Use premium account if available", True), + ("fallback", "bool", "Fallback to free download if premium fails", True), + ("chk_filesize", "bool", "Check file size", True), + ("max_wait", "int", "Reconnect if waiting time is greater than minutes", 10)] + + __description__ = """Clicknupload.com hoster plugin""" + __license__ = "GPLv3" + __authors__ = [("tbsn", "tbsnpy_github@gmx.de"), + ("GammaC0de", "nitzo2001[AT]yahoo[DOT]com")] + + PLUGIN_DOMAIN = "clicknupload.red" + + URL_REPLACEMENTS = [(__pattern__ + '.*', "https://clicknupload.red/\g")] + + LINK_PATTERN = r"onClick=\"window.open\('(.+?)'\);" + + NAME_PATTERN = r'name="fname" value="(?P.+?)">' + SIZE_PATTERN = r">size\s*(?P[\d.,]+) (?P[\w^_]+)" + + OFFLINE_PATTERN = r"File Not Found" + ERROR_PATTERN = "" + + WAIT_PATTERN = r'(\d+)' diff --git a/module/plugins/hoster/CloudMailRu.py b/module/plugins/hoster/CloudMailRu.py new file mode 100644 index 0000000000..85b0dd19ac --- /dev/null +++ b/module/plugins/hoster/CloudMailRu.py @@ -0,0 +1,50 @@ +# -*- coding: utf-8 -*- + +import base64 +import re +import urllib + +from ..internal.Hoster import Hoster +from ..internal.misc import json + + +class CloudMailRu(Hoster): + __name__ = "CloudMailRu" + __type__ = "hoster" + __version__ = "0.05" + __status__ = "testing" + + __pattern__ = r'https?://cloud\.mail\.ru/dl\?q=(?P.+)' + __config__ = [("activated", "bool", "Activated", True), + ("use_premium", "bool", "Use premium account if available", True), + ("fallback", "bool","Fallback to free download if premium fails", True), + ("chk_filesize", "bool", "Check file size", True), + ("max_wait", "int", "Reconnect if waiting time is greater than minutes", 10)] + + __description__ = """Cloud.mail.ru hoster plugin""" + __license__ = "GPLv3" + __authors__ = [("GammaC0de", "nitzo2001[AT]yahoo[DOT]com")] + + OFFLINE_PATTERN = r'"error":\s*"not_exists"' + + def get_info(self, url="", html=""): + info = super(CloudMailRu, self).get_info(url, html) + + qs = re.match(self.__pattern__, url).group('QS') + file_info = json.loads(base64.b64decode(qs)) + + info.update({ + 'name': urllib.unquote_plus(file_info['n']).encode('latin1').decode('utf8'), + 'size': file_info['s'], + 'u': file_info['u'] + }) + + return info + + def setup(self): + self.chunk_limit = -1 + self.resume_download = True + self.multiDL = True + + def process(self, pyfile): + self.download(self.info['u'], disposition=False) diff --git a/module/plugins/hoster/CloudzerNet.py b/module/plugins/hoster/CloudzerNet.py deleted file mode 100644 index e95f907923..0000000000 --- a/module/plugins/hoster/CloudzerNet.py +++ /dev/null @@ -1,73 +0,0 @@ -# -*- coding: utf-8 -*- -import re -from module.plugins.internal.SimpleHoster import SimpleHoster -from module.common.json_layer import json_loads -from module.plugins.internal.CaptchaService import ReCaptcha -from module.network.RequestFactory import getURL -from module.utils import parseFileSize - - -def getInfo(urls): - for url in urls: - header = getURL(url, just_header=True) - if 'Location: http://cloudzer.net/404' in header: - file_info = (url, 0, 1, url) - else: - if url.endswith('/'): - api_data = getURL(url + 'status') - else: - api_data = getURL(url + '/status') - name, size = api_data.splitlines() - size = parseFileSize(size) - file_info = (name, size, 2, url) - yield file_info - - -class CloudzerNet(SimpleHoster): - __name__ = "CloudzerNet" - __type__ = "hoster" - __pattern__ = r"http://(www\.)?(cloudzer\.net/file/|clz\.to/(file/)?)(?P\w+).*" - __version__ = "0.03" - __description__ = """Cloudzer.net hoster plugin""" - __author_name__ = ("gs", "z00nx", "stickell") - __author_mail__ = ("I-_-I-_-I@web.de", "z00nx0@gmail.com", "l.stickell@yahoo.it") - - FILE_SIZE_PATTERN = '(?P[^<]+)' - WAIT_PATTERN = '' - FILE_OFFLINE_PATTERN = r'Please check the URL for typing errors, respectively' - CAPTCHA_KEY = '6Lcqz78SAAAAAPgsTYF3UlGf2QFQCNuPMenuyHF3' - - def handleFree(self): - found = re.search(self.WAIT_PATTERN, self.html) - seconds = int(found.group(1)) - self.logDebug("Found wait", seconds) - self.setWait(seconds + 1) - self.wait() - response = self.load('http://cloudzer.net/io/ticket/slot/%s' % self.file_info['ID'], post=' ', cookies=True) - self.logDebug("Download slot request response", response) - response = json_loads(response) - if response["succ"] is not True: - self.fail("Unable to get a download slot") - - recaptcha = ReCaptcha(self) - challenge, response = recaptcha.challenge(self.CAPTCHA_KEY) - post_data = {"recaptcha_challenge_field": challenge, "recaptcha_response_field": response} - response = json_loads(self.load('http://cloudzer.net/io/ticket/captcha/%s' % self.file_info['ID'], - post=post_data, cookies=True)) - self.logDebug("Captcha check response", response) - self.logDebug("First check") - - if "err" in response: - if response["err"] == "captcha": - self.logDebug("Wrong captcha") - self.invalidCaptcha() - self.retry() - elif "Sie haben die max" in response["err"] or "You have reached the max" in response["err"]: - self.logDebug("Download limit reached, waiting an hour") - self.setWait(3600, True) - self.wait() - if "type" in response: - if response["type"] == "download": - url = response["url"] - self.logDebug("Download link", url) - self.download(url, disposition=True) diff --git a/module/plugins/hoster/CloudzillaTo.py b/module/plugins/hoster/CloudzillaTo.py new file mode 100644 index 0000000000..61c82d5757 --- /dev/null +++ b/module/plugins/hoster/CloudzillaTo.py @@ -0,0 +1,66 @@ +# -*- coding: utf-8 -*- + +import re + +from ..internal.SimpleHoster import SimpleHoster + + +class CloudzillaTo(SimpleHoster): + __name__ = "CloudzillaTo" + __type__ = "hoster" + __version__ = "0.13" + __status__ = "testing" + + __pattern__ = r'http://(?:www\.)?cloudzilla\.to/share/file/(?P[\w^_]+)' + __config__ = [("activated", "bool", "Activated", True), + ("use_premium", "bool", "Use premium account if available", True), + ("fallback", "bool", + "Fallback to free download if premium fails", True), + ("chk_filesize", "bool", "Check file size", True), + ("max_wait", "int", "Reconnect if waiting time is greater than minutes", 10)] + + __description__ = """Cloudzilla.to hoster plugin""" + __license__ = "GPLv3" + __authors__ = [("Walter Purcaro", "vuolter@gmail.com")] + + INFO_PATTERN = r'title="(?P.+?)">\1 \((?P[\d.]+) (?P[\w^_]+)' + OFFLINE_PATTERN = r'>File not found...<' + + PASSWORD_PATTERN = r'
    ' + + def check_errors(self): + if re.search(self.PASSWORD_PATTERN, self.data): + pw = self.get_password() + if pw: + self.data = self.load(self.pyfile.url, get={'key': pw}) + else: + self.fail(_("Missing password")) + + if re.search(self.PASSWORD_PATTERN, self.data): + self.retry(msg="Wrong password") + else: + return SimpleHoster.check_errors(self) + + def handle_free(self, pyfile): + self.data = self.load("http://www.cloudzilla.to/generateticket/", + post={'file_id': self.info['pattern']['ID'], 'key': self.get_password()}) + + ticket = dict(re.findall(r'<(.+?)>([^<>]+?) 5) + + self.link = "http://%(server)s/download/%(file_id)s/%(ticket_id)s" % {'server': ticket['server'], + 'file_id': self.info['pattern']['ID'], + 'ticket_id': ticket['ticket_id']} + + def handle_premium(self, pyfile): + return self.handle_free(pyfile) diff --git a/module/plugins/hoster/CosmoboxOrg.py b/module/plugins/hoster/CosmoboxOrg.py new file mode 100644 index 0000000000..4200250626 --- /dev/null +++ b/module/plugins/hoster/CosmoboxOrg.py @@ -0,0 +1,58 @@ +# -*- coding: utf-8 -*- + +import re + +from ..internal.XFSHoster import XFSHoster +from ..internal.misc import parse_time + +class CosmoboxOrg(XFSHoster): + __name__ = "CosmoboxOrg" + __type__ = "hoster" + __version__ = "0.03" + __status__ = "testing" + + __pattern__ = r'https?://cosmobox\.org/\w{12}' + __config__ = [("activated", "bool", "Activated", True), + ("chk_filesize", "bool", "Check file size", True), + ("max_wait", "int", + "Reconnect if waiting time is greater than minutes", 10)] + + __description__ = """Cosmobox.org hoster plugin""" + __license__ = "GPLv3" + __authors__ = [("OzzieIsaacs", "Ozzie.Fernandez.Isaacs@googlemail.com")] + + PLUGIN_DOMAIN = "cosmobox.org" + + NAME_PATTERN = r"You're downloading: (?P.+?)<" + SIZE_PATTERN = r'(?P[\d.,]+) (?P[\w^_]+)' + WAIT_PATTERN = r'(\d+)' + + URL_REPLACEMENTS = [(r'^http://', "https://")] + + def handle_free(self, pyfile): + action, inputs = self.parse_html_form(input_names={'op': re.compile(r'^download')}) + if inputs is None: + self.fail("Free download form not found") + + inputs['method_free'] = "Free+Download" + + self.data = self.load("https://cosmobox.org/download", + post=inputs, + ref=self.pyfile.url, + redirect=False) + + m = re.search(r'role="alert">You have reached your download limit', self.data) + if m is not None: + wait_time = 3*60*60 #: wait 3 hours + self.wait(wait_time) + + else: + m = re.search(self.WAIT_PATTERN, self.data) + if m is not None: + waitmsg = m.group(1).strip() + wait_time = parse_time(waitmsg) + self.wait(wait_time) + + action, inputs = self.parse_html_form(input_names={'op': re.compile(r'^download')}) + self.handle_captcha(inputs) + self.data = self.download("https://cosmobox.org/download", post=inputs) diff --git a/module/plugins/hoster/CramitIn.py b/module/plugins/hoster/CramitIn.py index 68df322f56..8ca6e04958 100644 --- a/module/plugins/hoster/CramitIn.py +++ b/module/plugins/hoster/CramitIn.py @@ -1,22 +1,27 @@ # -*- coding: utf-8 -*- -from module.plugins.hoster.XFileSharingPro import XFileSharingPro, create_getInfo +from ..internal.XFSHoster import XFSHoster -class CramitIn(XFileSharingPro): + +class CramitIn(XFSHoster): __name__ = "CramitIn" __type__ = "hoster" - __pattern__ = r"http://(?:\w*\.)*cramit.in/\w{12}" - __version__ = "0.04" - __description__ = """Cramit.in hoster plugin""" - __author_name__ = ("zoidberg") - __author_mail__ = ("zoidberg@mujmail.cz") + __version__ = "0.13" + __status__ = "testing" - FILE_INFO_PATTERN = r'\s*(?P.*?).*?\s*\((?P.*?)\)' - DIRECT_LINK_PATTERN = r'href="(http://cramit.in/file_download/.*?)"' - HOSTER_NAME = "cramit.in" + __pattern__ = r'http://(?:www\.)?cramit\.in/\w{12}' + __config__ = [("activated", "bool", "Activated", True), + ("use_premium", "bool", "Use premium account if available", True), + ("fallback", "bool", + "Fallback to free download if premium fails", True), + ("chk_filesize", "bool", "Check file size", True), + ("max_wait", "int", "Reconnect if waiting time is greater than minutes", 10)] - def setup(self): - self.resumeDownload = self.multiDL = self.premium + __description__ = """Cramit.in hoster plugin""" + __license__ = "GPLv3" + __authors__ = [("zoidberg", "zoidberg@mujmail.cz")] + PLUGIN_DOMAIN = "cramit.in" -getInfo = create_getInfo(CramitIn) + INFO_PATTERN = r'\s*(?P.*?).*?\s*\((?P.*?)\)' + LINK_PATTERN = r'href="(http://cramit\.in/file_download/.*?)"' diff --git a/module/plugins/hoster/CrockoCom.py b/module/plugins/hoster/CrockoCom.py deleted file mode 100644 index 5bd1a945cc..0000000000 --- a/module/plugins/hoster/CrockoCom.py +++ /dev/null @@ -1,73 +0,0 @@ -# -*- coding: utf-8 -*- - -import re - -from module.plugins.internal.SimpleHoster import SimpleHoster, create_getInfo -from module.plugins.internal.CaptchaService import ReCaptcha - - -class CrockoCom(SimpleHoster): - __name__ = "CrockoCom" - __type__ = "hoster" - __pattern__ = r"http://(www\.)?(crocko|easy-share).com/\w+" - __version__ = "0.16" - __description__ = """Crocko Download Hoster""" - __author_name__ = ("zoidberg") - __author_mail__ = ("zoidberg@mujmail.cz") - - FILE_NAME_PATTERN = r'Download:\s*(?P.*)' - FILE_SIZE_PATTERN = r'(?P[^<]+)' - FILE_OFFLINE_PATTERN = r"

    Sorry,
    the page you're looking for
    isn't here.

    |File not found" - DOWNLOAD_URL_PATTERN = r"window.location ='([^']+)';" - CAPTCHA_URL_PATTERN = re.compile(r"u='(/file_contents/captcha/\w+)';\s*w='(\d+)';") - CAPTCHA_KEY_PATTERN = re.compile(r'Recaptcha.create\("([^"]+)"') - - FORM_PATTERN = r'
    (.*?)
    ' - FORM_INPUT_PATTERN = r']* name="?([^" ]+)"? value="?([^" ]+)"?[^>]*>' - - FILE_NAME_REPLACEMENTS = [(r'<[^>]*>', '')] - - def handleFree(self): - if "You need Premium membership to download this file." in self.html: - self.fail("You need Premium membership to download this file.") - - for _ in xrange(5): - found = re.search(self.CAPTCHA_URL_PATTERN, self.html) - if found: - url, wait_time = 'http://crocko.com' + found.group(1), found.group(2) - self.setWait(wait_time) - self.wait() - self.html = self.load(url) - else: - break - - found = re.search(self.CAPTCHA_KEY_PATTERN, self.html) - if not found: - self.parseError('Captcha KEY') - captcha_key = found.group(1) - - found = re.search(self.FORM_PATTERN, self.html, re.DOTALL) - if not found: - self.parseError('ACTION') - action, form = found.groups() - inputs = dict(re.findall(self.FORM_INPUT_PATTERN, form)) - - recaptcha = ReCaptcha(self) - - for _ in xrange(5): - inputs['recaptcha_challenge_field'], inputs['recaptcha_response_field'] = recaptcha.challenge(captcha_key) - self.download(action, post=inputs) - - check = self.checkDownload({ - "captcha_err": self.CAPTCHA_KEY_PATTERN - }) - - if check == "captcha_err": - self.invalidCaptcha() - else: - break - else: - self.fail('No valid captcha solution received') - - -getInfo = create_getInfo(CrockoCom) diff --git a/module/plugins/hoster/CyberlockerCh.py b/module/plugins/hoster/CyberlockerCh.py deleted file mode 100644 index 19a4473b3a..0000000000 --- a/module/plugins/hoster/CyberlockerCh.py +++ /dev/null @@ -1,17 +0,0 @@ -# -*- coding: utf-8 -*- -from module.plugins.hoster.XFileSharingPro import XFileSharingPro, create_getInfo - - -class CyberlockerCh(XFileSharingPro): - __name__ = "CyberlockerCh" - __type__ = "hoster" - __pattern__ = r"http://(www\.)?cyberlocker\.ch/\w{12}" - __version__ = "0.01" - __description__ = """Cyberlocker.ch hoster plugin""" - __author_name__ = ("stickell") - __author_mail__ = ("l.stickell@yahoo.it") - - HOSTER_NAME = "cyberlocker.ch" - - -getInfo = create_getInfo(CyberlockerCh) diff --git a/module/plugins/hoster/CzshareCom.py b/module/plugins/hoster/CzshareCom.py index 58573d3292..9da7b6b08d 100644 --- a/module/plugins/hoster/CzshareCom.py +++ b/module/plugins/hoster/CzshareCom.py @@ -1,158 +1,170 @@ # -*- 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 . - - @author: zoidberg -""" - -# Test links (random.bin): -# http://czshare.com/5278880/random.bin import re -from module.plugins.internal.SimpleHoster import SimpleHoster, create_getInfo, PluginParseError -from module.utils import parseFileSize + +from ..internal.misc import parse_size +from ..internal.SimpleHoster import SimpleHoster class CzshareCom(SimpleHoster): __name__ = "CzshareCom" __type__ = "hoster" - __pattern__ = r"http://(\w*\.)*czshare\.(com|cz)/(\d+/|download.php\?).*" - __version__ = "0.93" - __description__ = """CZshare.com""" - __author_name__ = ("zoidberg") - - FILE_NAME_PATTERN = r'
    \s*

    \s*Cel. n.zev: ]*>(?P[^<]+)' - FILE_SIZE_PATTERN = r'

    (?:\s*

    [^\n]*

    )*\s*Velikost:\s*(?P[0-9., ]+)(?P[kKMG])i?B\s*
    ' - FILE_OFFLINE_PATTERN = r'
    \s*

    ' - - FILE_SIZE_REPLACEMENTS = [(' ', '')] - FILE_URL_REPLACEMENTS = [(r'http://[^/]*/download.php\?.*?id=(\w+).*', r'http://czshare.com/\1/x/')] - SH_CHECK_TRAFFIC = True - - FREE_URL_PATTERN = r'[^>]*alt="([^"]+)" />' - FREE_FORM_PATTERN = r'
    \s*(.*?)
    ' - PREMIUM_FORM_PATTERN = r'
    (.*?)
    ' - FORM_INPUT_PATTERN = r']* name="([^"]+)" value="([^"]+)"[^>]*/>' - MULTIDL_PATTERN = r"

    Z[^<]*PROFI.

    " - USER_CREDIT_PATTERN = r'
    \s*kredit: ([0-9., ]+)([kKMG]i?B)\s*
    ' - - def checkTrafficLeft(self): - # check if user logged in - found = re.search(self.USER_CREDIT_PATTERN, self.html) - if not found: - self.account.relogin(self.user) - self.html = self.load(self.pyfile.url, cookies=True, decode=True) - found = re.search(self.USER_CREDIT_PATTERN, self.html) - if not found: - return False - - # check user credit + __version__ = "1.11" + __status__ = "testing" + + __pattern__ = r'https?://(?:www\.)?(czshare|sdilej)\.(com|cz)/(\d+/|download\.php\?).+' + __config__ = [("activated", "bool", "Activated", True), + ("use_premium", "bool", "Use premium account if available", True), + ("fallback", "bool", + "Fallback to free download if premium fails", True), + ("chk_filesize", "bool", "Check file size", True), + ("max_wait", "int", "Reconnect if waiting time is greater than minutes", 10)] + + __description__ = """CZshare.com hoster plugin, now Sdilej.cz""" + __license__ = "GPLv3" + __authors__ = [("zoidberg", "zoidberg@mujmail.cz"), + ("ondrej", "git@ondrej.it"), ] + + NAME_PATTERN = r'
    \s*

    \s*Cel. n.zev: (?P.+?)' + SIZE_PATTERN = r'

    (?:\s*

    [^\n]*

    )*\s*Velikost:\s*(?P[\d .,]+)(?P[\w^_]+)\s*
    ' + OFFLINE_PATTERN = r'
    \s*

    ' + + SIZE_REPLACEMENTS = [(' ', '')] + URL_REPLACEMENTS = [ + (r'http://[^/]*/download.php\?.*?id=(\w+).*', + r'http://sdilej.cz/\1/x/')] + + CHECK_TRAFFIC = True + + FREE_URL_PATTERN = r'[^>]*alt="(.+?)" />' + FREE_FORM_PATTERN = r'
    \s*(.*?)
    ' + PREMIUM_FORM_PATTERN = r'
    (.*?)
    ' + FORM_INPUT_PATTERN = r']* name="(.+?)" value="(.+?)"[^>]*/>' + MULTIDL_PATTERN = r'

    Z.*?PROFI.

    ' + USER_CREDIT_PATTERN = r'
    \s*kredit: ([\d .,]+)(\w+)\s*
    ' + + def out_of_traffic(self): + #: Check if user logged in + m = re.search(self.USER_CREDIT_PATTERN, self.data) + if m is None: + self.account.relogin() + self.data = self.load(self.pyfile.url) + m = re.search(self.USER_CREDIT_PATTERN, self.data) + if m is None: + return True + + #: Check user credit try: - credit = parseFileSize(found.group(1).replace(' ', ''), found.group(2)) - self.logInfo("Premium download for %i KiB of Credit" % (self.pyfile.size / 1024)) - self.logInfo("User %s has %i KiB left" % (self.user, credit / 1024)) + credit = parse_size(m.group(1).replace(' ', ''), m.group(2)) + self.log_info( + _("Premium download for %i KiB of Credit") % + (self.pyfile.size / 1024)) + self.log_info( + _("User %s has %i KiB left") % + (self.account.user, credit / 1024)) if credit < self.pyfile.size: - self.logInfo("Not enough credit to download file %s" % self.pyfile.name) - return False + self.log_info( + _("Not enough credit to download file: %s") % + self.pyfile.name) + return True + except Exception, e: - # let's continue and see what happens... - self.logError('Parse error (CREDIT): %s' % e) + #: let's continue and see what happens... + self.log_error(e, trace=True) - return True + return False - def handlePremium(self): - # parse download link + def handle_premium(self, pyfile): try: - form = re.search(self.PREMIUM_FORM_PATTERN, self.html, re.DOTALL).group(1) + form = re.search( + self.PREMIUM_FORM_PATTERN, + self.data, + re.S).group(1) inputs = dict(re.findall(self.FORM_INPUT_PATTERN, form)) + except Exception, e: - self.logError("Parse error (FORM): %s" % e) - self.resetAccount() - - # download the file, destination is determined by pyLoad - self.download("http://czshare.com/profi_down.php", post=inputs, disposition=True) - self.checkDownloadedFile() - - def handleFree(self): - # get free url - found = re.search(self.FREE_URL_PATTERN, self.html) - if found is None: - raise PluginParseError('Free URL') - parsed_url = "http://czshare.com" + found.group(1) - self.logDebug("PARSED_URL:" + parsed_url) - - # get download ticket and parse html - self.html = self.load(parsed_url, cookies=True, decode=True) - if re.search(self.MULTIDL_PATTERN, self.html): - self.longWait(300, 12) + self.log_error(e, trace=True) + self.restart(premium=False) + + #: Download the file, destination is determined by pyLoad + self.download( + "http://sdilej.cz/profi_down.php", + post=inputs, + disposition=True) + + def handle_free(self, pyfile): + #: Get free url + m = re.search(self.FREE_URL_PATTERN, self.data) + if m is None: + self.error(_("FREE_URL_PATTERN not found")) + + parsed_url = "http://sdilej.cz" + m.group(1) + + self.log_debug("PARSED_URL:" + parsed_url) + + #: Get download ticket and parse html + self.data = self.load(parsed_url) + if re.search(self.MULTIDL_PATTERN, self.data): + self.retry(5 * 60, 12, _("Download limit reached")) try: - form = re.search(self.FREE_FORM_PATTERN, self.html, re.DOTALL).group(1) + form = re.search(self.FREE_FORM_PATTERN, self.data, re.S).group(1) inputs = dict(re.findall(self.FORM_INPUT_PATTERN, form)) - self.pyfile.size = int(inputs['size']) + pyfile.size = int(inputs['size']) + except Exception, e: - self.logError(e) - raise PluginParseError('Form') - - # get and decrypt captcha - captcha_url = 'http://czshare.com/captcha.php' - for i in range(5): - inputs['captchastring2'] = self.decryptCaptcha(captcha_url) - self.html = self.load(parsed_url, cookies=True, post=inputs, decode=True) - if u"
  • Zadaný ověřovací kód nesouhlasí!
  • " in self.html: - self.invalidCaptcha() - elif re.search(self.MULTIDL_PATTERN, self.html): - self.longWait(300, 12) - else: - self.correctCaptcha() - break + self.log_error(e, trace=True) + self.error(_("Form")) + + #: Get and decrypt captcha + captcha_url = 'http://sdilej.cz/captcha.php' + inputs['captchastring2'] = self.captcha.decrypt(captcha_url) + self.data = self.load(parsed_url, post=inputs) + + if u"
  • Zadaný ověřovací kód nesouhlasí!
  • " in self.data: + self.retry_captcha() + + elif re.search(self.MULTIDL_PATTERN, self.data): + self.retry(5 * 60, 12, _("Download limit reached")) + else: - self.fail("No valid captcha code entered") + self.captcha.correct() + + m = re.search("countdown_number = (\d+);", self.data) + self.set_wait(int(m.group(1)) if m else 50) - found = re.search("countdown_number = (\d+);", self.html) - self.setWait(int(found.group(1)) if found else 50) + #: Download the file, destination is determined by pyLoad + self.log_debug("WAIT URL", self.req.lastEffectiveURL) - # download the file, destination is determined by pyLoad - self.logDebug("WAIT URL", self.req.lastEffectiveURL) - found = re.search("free_wait.php\?server=(.*?)&(.*)", self.req.lastEffectiveURL) - if not found: - raise PluginParseError('Download URL') + m = re.search( + "free_wait.php\?server=(.*?)&(.*)", + self.req.lastEffectiveURL) + if m is None: + self.error(_("Download URL not found")) - url = "http://%s/download.php?%s" % (found.group(1), found.group(2)) + self.link = "http://%s/download.php?%s" % (m.group(1), m.group(2)) self.wait() - self.download(url) - self.checkDownloadedFile() - - def checkDownloadedFile(self): - # check download - check = self.checkDownload({ - "tempoffline": re.compile(r"^Soubor je do.*asn.* nedostupn.*$"), - "credit": re.compile(r"^Nem.*te dostate.*n.* kredit.$"), - "multi_dl": re.compile(self.MULTIDL_PATTERN), - "captcha_err": "
  • Zadaný ověřovací kód nesouhlasí!
  • " + + def check_download(self): + #: Check download + check = self.scan_download({ + "temp offline": re.compile(r'^Soubor je do.*asn.* nedostupn.*$'), + 'credit': re.compile(r'^Nem.*te dostate.*n.* kredit.$'), + "multi-dl": re.compile(self.MULTIDL_PATTERN), + 'captcha': "
  • Zadaný ověřovací kód nesouhlasí!
  • " }) - if check == "tempoffline": - self.fail("File not available - try later") - if check == "credit": - self.resetAccount() - elif check == "multi_dl": - self.longWait(300, 12) - elif check == "captcha_err": - self.invalidCaptcha() - self.retry() + if check == "temp offline": + self.fail(_("File not available - try later")) + + elif check == "credit": + self.restart(premium=False) + + elif check == "multi-dl": + self.retry(5 * 60, 12, _("Download limit reached")) + elif check == "captcha": + self.retry_captcha() -getInfo = create_getInfo(CzshareCom) + return SimpleHoster.check_download(self) diff --git a/module/plugins/hoster/DailymotionCom.py b/module/plugins/hoster/DailymotionCom.py index 0822b0c52e..bf23ab1e60 100644 --- a/module/plugins/hoster/DailymotionCom.py +++ b/module/plugins/hoster/DailymotionCom.py @@ -1,126 +1,106 @@ # -*- coding: utf-8 -*- -############################################################################ -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero 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 Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . -# -# @author: Walter Purcaro -############################################################################ - - +import random import re -from module.common.json_layer import json_loads -from module.network.RequestFactory import getURL -from module.plugins.Hoster import Hoster +from module.network.RequestFactory import getURL as get_url from module.PyFile import statusMap +from ..internal.Hoster import Hoster +from ..internal.misc import json + + +def get_info(urls): + result = [] + m = re.compile(DailymotionCom.__pattern__) -def getInfo(urls): - result = [] #: [ .. (name, size, status, url) .. ] - regex = re.compile(DailymotionCom.__pattern__) - apiurl = "https://api.dailymotion.com/video/" - request = {"fields": "access_error,status,title"} for url in urls: - id = regex.search(url).group("ID") - page = getURL(apiurl + id, get=request) - info = json_loads(page) + id = m.match(url).group("ID") + html = get_url("https://api.dailymotion.com/video/%s" % id, + get={"fields": "access_error,status,title"}) + info = json.loads(html) - if "title" in info: - name = info["title"] + ".mp4" - else: - name = url + name = info["title"] + ".mp4" if "title" in info else url if "error" in info or info["access_error"]: status = "offline" + else: status = info["status"] + if status in ("ready", "published"): status = "online" + elif status in ("waiting", "processing"): status = "temp. offline" + else: status = "offline" result.append((name, 0, statusMap[status], url)) + return result class DailymotionCom(Hoster): __name__ = "DailymotionCom" __type__ = "hoster" - __pattern__ = r"https?://(?:www\.)?dailymotion\.com/.*?video/(?P[\w^_]+)" - __version__ = "0.2" - __config__ = [("quality", "Lowest;LD 144p;LD 240p;SD 384p;HQ 480p;HD 720p;HD 1080p;Highest", "Quality", "HD 720p")] - __description__ = """Dailymotion Video Download Hoster""" - __author_name__ = ("Walter Purcaro") - __author_mail__ = ("vuolter@gmail.com") + __version__ = "0.32" + __status__ = "testing" + + __pattern__ = r"https?://(?:www\.)?(?:dailymotion\.com/.*video|dai\.ly)/(?P[\w^_]+)" + __config__ = [("enabled", "bool", "Activated", True), + ("quality", "Lowest;LD 144p;LD 240p;SD 380p;HQ 480p;HD 720p;HD 1080p;Highest", "Quality", "Highest")] + + __description__ = """Dailymotion.com downloader plugin""" + __license__ = "GPLv3" + __authors__ = [("Walter Purcaro", "vuolter@gmail.com"), + ("Synology PAT", "pat@synology.com"), + ("GammaC0de", "nitzo2001[AT}yahoo[DOT]com")] + + STREAM_PATTERN = r"\"(?Phttps?:\\/\\/www.dailymotion.com\\/cdn\\/H264-(?P\d+)x(?P\d+).*?)\"" def setup(self): - self.resumeDownload = self.multiDL = True - - def getStreams(self): - streams = [] - for result in re.finditer(r"\"(?Phttp:\\/\\/www.dailymotion.com\\/cdn\\/H264-(?P.*?)\\.*?)\"", - self.html): - url = result.group("URL") - qf = result.group("QF") - link = url.replace("\\", "") - quality = tuple(int(x) for x in qf.split("x")) - streams.append((quality, link)) - return sorted(streams, key=lambda x: x[0][::-1]) - - def getQuality(self): - q = self.getConfig("quality") - if q == "Lowest": - quality = 0 - elif q == "Highest": - quality = -1 - else: - quality = int(q.rsplit(" ")[1][:-1]) - return quality - - def getLink(self, streams, quality): - if quality > 0: - for x, s in reversed([item for item in enumerate(streams)]): - qf = s[0][1] - if qf <= quality: - idx = x - break - else: - idx = 0 - else: - idx = quality + self.resume_download = True + self.multiDl = True - s = streams[idx] - self.logInfo("Download video quality %sx%s" % s[0]) - return s[1] + def get_info(self, url="", html=""): + info = super(DailymotionCom, self).get_info(url, html) - def checkInfo(self, pyfile): - pyfile.name, pyfile.size, pyfile.status, pyfile.url = getInfo([pyfile.url])[0] - if pyfile.status == 1: - self.offline() - elif pyfile.status == 6: - self.tempOffline() + name, size, status, url = get_info([url])[0] - def process(self, pyfile): - self.checkInfo(pyfile) + info.update({"name": name, "status": status}) - id = re.match(self.__pattern__, pyfile.url).group("ID") - self.html = self.load("http://www.dailymotion.com/embed/video/" + id, decode=True) + return info - streams = self.getStreams() - quality = self.getQuality() - link = self.getLink(streams, quality) + def process(self, pyfile): + desired_quality = self.config.get("quality") + + self.data = self.load("https://www.dailymotion.com/player/metadata/video/%s" % self.info["pattern"]["ID"]) + json_data = json.loads(self.data) + m3u8_url = next(iter(json_data["qualities"].values()))[0]["url"] + m3u8_data = self.load(m3u8_url) + + streams = {} + for m in re.finditer(r"#EXT-X-STREAM-INF:(.+)", m3u8_data): + stream = dict([ + (x.group(1), x.group(2) or x.group(3)) + for x in re.finditer(r'([\w-]+)=(?:(?=")"([^"]+)|(?!")([^,]+))', m.group(1)) + ]) + quality = int(stream["NAME"]) + dl_url = stream["PROGRESSIVE-URI"] + streams[quality] = streams.get(quality, []) + [dl_url] + + if not streams: + self.fail(_("Failed to get any streams.")) + + qualities = sorted(streams.keys()) + if desired_quality == "Lowest": + quality = qualities[0] + elif desired_quality == "Highest": + quality = qualities[-1] + else: + desired_quality = int(re.search(r"\d+", desired_quality).group(0)) + quality = min(qualities, key=lambda x: abs(x - desired_quality)) - self.download(link) + self.download(random.choice(streams[quality])) diff --git a/module/plugins/hoster/DailyuploadsNet.py b/module/plugins/hoster/DailyuploadsNet.py new file mode 100644 index 0000000000..7106a6abaa --- /dev/null +++ b/module/plugins/hoster/DailyuploadsNet.py @@ -0,0 +1,45 @@ +# -*- coding: utf-8 -*- + +import re + +from ..internal.SimpleHoster import SimpleHoster + + +class DailyuploadsNet(SimpleHoster): + __name__ = "DailyuploadsNet" + __type__ = "hoster" + __version__ = "0.01" + __status__ = "testing" + + __pattern__ = r'https?://(?:www\.)?dailyuploads\.net/\w+' + __config__ = [("activated", "bool", "Activated", True), + ("use_premium", "bool", "Use premium account if available", True), + ("fallback", "bool", "Fallback to free download if premium fails", True), + ("chk_filesize", "bool", "Check file size", True), + ("max_wait", "int", "Reconnect if waiting time is greater than minutes", 10)] + + __description__ = """Sendit.cloud hoster plugin""" + __license__ = "GPLv3" + __authors__ = [("GammaC0de", "nitzo2001[AT]yahoo[DOT]com")] + + NAME_PATTERN = r'Filename:(?P.+?)' + SIZE_PATTERN = r'Size:.+?\((?P[\d.,]+) (?Pbytes)\)' + + OFFLINE_PATTERN = r'>File Not Found' + + def setup(self): + self.multiDL = True + self.resume_download = True + self.chunk_limit = 1 + + def handle_free(self, pyfile): + url, inputs = self.parse_html_form('name="F1"') + if inputs is not None: + inputs['referer'] = pyfile.url + self.data = self.load(pyfile.url, post=inputs) + + m = re.search(self.LINK_FREE_PATTERN, self.data) + if m is not None: + self.link = m.group(1) \ No newline at end of file diff --git a/module/plugins/hoster/DataHu.py b/module/plugins/hoster/DataHu.py index 7abd93d1ff..2d7b38b108 100644 --- a/module/plugins/hoster/DataHu.py +++ b/module/plugins/hoster/DataHu.py @@ -1,53 +1,35 @@ # -*- coding: utf-8 -*- - -############################################################################ -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU Affero 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 Affero General Public License for more details. # -# # -# You should have received a copy of the GNU Affero General Public License # -# along with this program. If not, see . # -############################################################################ - -# Test links (random.bin): +# +# Test links: # http://data.hu/get/6381232/random.bin -import re -from module.plugins.internal.SimpleHoster import SimpleHoster, create_getInfo +from ..internal.SimpleHoster import SimpleHoster class DataHu(SimpleHoster): __name__ = "DataHu" __type__ = "hoster" - __pattern__ = r"http://(www\.)?data.hu/get/\w+" - __version__ = "0.01" - __description__ = """Data.hu Download Hoster""" - __author_name__ = ("crash", "stickell") - __author_mail__ = ("l.stickell@yahoo.it") - - FILE_INFO_PATTERN = ur'(?P<N>.*) \((?P<S>[^)]+)\) let\xf6lt\xe9se' - FILE_OFFLINE_PATTERN = ur'Az adott f\xe1jl nem l\xe9tezik' - DIRECT_LINK_PATTERN = r'
    ' - - def handleFree(self): - self.resumeDownload = True - self.html = self.load(self.pyfile.url, decode=True) - - m = re.search(self.DIRECT_LINK_PATTERN, self.html) - if m: - url = m.group(1) - self.logDebug('Direct link: ' + url) - else: - self.parseError('Unable to get direct link') - - self.download(url, disposition=True) - - -getInfo = create_getInfo(DataHu) + __version__ = "0.08" + __status__ = "testing" + + __pattern__ = r'http://(?:www\.)?data\.hu/get/\w+' + __config__ = [("activated", "bool", "Activated", True), + ("use_premium", "bool", "Use premium account if available", True), + ("fallback", "bool", + "Fallback to free download if premium fails", True), + ("chk_filesize", "bool", "Check file size", True), + ("max_wait", "int", "Reconnect if waiting time is greater than minutes", 10)] + + __description__ = """Data.hu hoster plugin""" + __license__ = "GPLv3" + __authors__ = [("crash", None), + ("stickell", "l.stickell@yahoo.it")] + + INFO_PATTERN = ur'(?P<N>.*) \((?P<S>[^)]+)\) let\xf6lt\xe9se' + OFFLINE_PATTERN = ur'Az adott f\xe1jl nem l\xe9tezik' + LINK_FREE_PATTERN = r'
    ' + + def setup(self): + self.resume_download = True + self.multiDL = self.premium diff --git a/module/plugins/hoster/DataportCz.py b/module/plugins/hoster/DataportCz.py index 1e24388d7f..1084b80a76 100644 --- a/module/plugins/hoster/DataportCz.py +++ b/module/plugins/hoster/DataportCz.py @@ -1,68 +1,58 @@ # -*- 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 . - - @author: zoidberg -""" - -from module.plugins.internal.SimpleHoster import SimpleHoster, create_getInfo, PluginParseError +from ..internal.SimpleHoster import SimpleHoster class DataportCz(SimpleHoster): __name__ = "DataportCz" __type__ = "hoster" - __pattern__ = r"http://(?:.*?\.)?dataport.cz/file/(.*)" - __version__ = "0.37" - __description__ = """Dataport.cz plugin - free only""" - __author_name__ = ("zoidberg") - - FILE_NAME_PATTERN = r'(?P[^<]+)' - FILE_SIZE_PATTERN = r'Velikost\s*(?P[^<]+)' - FILE_OFFLINE_PATTERN = r'

    Soubor nebyl nalezen

    ' - FILE_URL_REPLACEMENTS = [(__pattern__, r'http://www.dataport.cz/file/\1')] - - CAPTCHA_URL_PATTERN = r'
    \s*(?P.+?)' + SIZE_PATTERN = r'Velikost\s*(?P[^<]+)' + OFFLINE_PATTERN = r'

    Soubor nebyl nalezen

    ' + + CAPTCHA_PATTERN = r'
    \s*(\d+)
    ' - def handleFree(self): - captchas = {"1": "jkeG", "2": "hMJQ", "3": "vmEK", "4": "ePQM", "5": "blBd"} - - for i in range(60): - action, inputs = self.parseHtmlForm('free_download_form') - self.logDebug(action, inputs) - if not action or not inputs: - raise PluginParseError('free_download_form') - - if "captchaId" in inputs and inputs["captchaId"] in captchas: - inputs['captchaCode'] = captchas[inputs["captchaId"]] - else: - raise PluginParseError('captcha') - - self.html = self.download("http://www.dataport.cz%s" % action, post=inputs) - - check = self.checkDownload({"captcha": 'alert("\u0160patn\u011b opsan\u00fd k\u00f3d z obr\u00e1zu");', - "slot": 'alert("Je n\u00e1m l\u00edto, ale moment\u00e1ln\u011b nejsou'}) - if check == "captcha": - raise PluginParseError('invalid captcha') - elif check == "slot": - self.logDebug("No free slots - wait 60s and retry") - self.setWait(60, False) - self.wait() - self.html = self.load(self.pyfile.url, decode=True) - continue - else: - break - - -create_getInfo(DataportCz) \ No newline at end of file + def handle_free(self, pyfile): + captchas = { + '1': "jkeG", + '2': "hMJQ", + '3': "vmEK", + '4': "ePQM", + '5': "blBd"} + + action, inputs = self.parse_html_form('free_download_form') + self.log_debug(action, inputs) + if not action or not inputs: + self.error(_("free_download_form")) + + if "captchaId" in inputs and inputs['captchaId'] in captchas: + inputs['captchaCode'] = captchas[inputs['captchaId']] + else: + self.error(_("Captcha not found")) + + self.download("http://www.dataport.cz%s" % action, post=inputs) + + check = self.scan_download({'captcha': 'alert("\u0160patn\u011b opsan\u00fd k\u00f3d z obr\u00e1zu");', + 'slot': 'alert("Je n\u00e1m l\u00edto, ale moment\u00e1ln\u011b nejsou'}) + if check == "captcha": + self.retry_captcha() + + elif check == "slot": + self.log_debug("No free slots - wait 60s and retry") + self.retry(wait=60) diff --git a/module/plugins/hoster/DateiTo.py b/module/plugins/hoster/DateiTo.py deleted file mode 100644 index d7760d9402..0000000000 --- a/module/plugins/hoster/DateiTo.py +++ /dev/null @@ -1,97 +0,0 @@ -# -*- 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 . - - @author: zoidberg -""" - -import re -from module.plugins.internal.SimpleHoster import SimpleHoster, create_getInfo -from module.plugins.internal.CaptchaService import ReCaptcha - - -class DateiTo(SimpleHoster): - __name__ = "DateiTo" - __type__ = "hoster" - __pattern__ = r"http://(?:www\.)?datei\.to/datei/(?P\w+)\.html" - __version__ = "0.02" - __description__ = """Datei.to plugin - free only""" - __author_name__ = ("zoidberg") - __author_mail__ = ("zoidberg@mujmail.cz") - - FILE_NAME_PATTERN = r'Dateiname:\s*(?P.*?)\s*(?P.*?)Datei wurde nicht gefunden<|>Bitte wähle deine Datei aus... <' - PARALELL_PATTERN = r'>Du lädst bereits eine Datei herunter<' - - WAIT_PATTERN = r'countdown\({seconds: (\d+)' - DATA_PATTERN = r'url: "(.*?)", data: "(.*?)",' - RECAPTCHA_KEY_PATTERN = r'Recaptcha.create\("(.*?)"' - - def handleFree(self): - url = 'http://datei.to/ajax/download.php' - data = {'P': 'I', 'ID': self.file_info['ID']} - - recaptcha = ReCaptcha(self) - - for i in range(10): - self.logDebug("URL", url, "POST", data) - self.html = self.load(url, post=data) - self.checkErrors() - - if url.endswith('download.php') and 'P' in data: - if data['P'] == 'I': - self.doWait() - - elif data['P'] == 'IV': - break - - found = re.search(self.DATA_PATTERN, self.html) - if not found: - self.parseError('data') - url = 'http://datei.to/' + found.group(1) - data = dict(x.split('=') for x in found.group(2).split('&')) - - if url.endswith('recaptcha.php'): - found = re.search(self.RECAPTCHA_KEY_PATTERN, self.html) - recaptcha_key = found.group(1) if found else "6LdBbL8SAAAAAI0vKUo58XRwDd5Tu_Ze1DA7qTao" - - data['recaptcha_challenge_field'], data['recaptcha_response_field'] = recaptcha.challenge(recaptcha_key) - - else: - self.fail('Too bad...') - - download_url = self.html - self.logDebug('Download URL', download_url) - self.download(download_url) - - def checkErrors(self): - found = re.search(self.PARALELL_PATTERN, self.html) - if found: - found = re.search(self.WAIT_PATTERN, self.html) - wait_time = int(found.group(1)) if found else 30 - self.setWait(wait_time + 1, False) - self.wait(300) - self.retry() - - def doWait(self): - found = re.search(self.WAIT_PATTERN, self.html) - wait_time = int(found.group(1)) if found else 30 - self.setWait(wait_time + 1, False) - - self.load('http://datei.to/ajax/download.php', post={'P': 'Ads'}) - self.wait() - - -getInfo = create_getInfo(DateiTo) diff --git a/module/plugins/hoster/DatoidCz.py b/module/plugins/hoster/DatoidCz.py new file mode 100644 index 0000000000..71a8952e23 --- /dev/null +++ b/module/plugins/hoster/DatoidCz.py @@ -0,0 +1,53 @@ +# -*- coding: utf-8 -*- + +import time +import urlparse + +from ..internal.misc import json +from ..internal.SimpleHoster import SimpleHoster + + +class DatoidCz(SimpleHoster): + __name__ = "DatoidCz" + __type__ = "hoster" + __version__ = "0.02" + __status__ = "testing" + + __pattern__ = r'https?://(?:www\.)?datoid\.(?:cz|sk|pl)/(?!slozka)\w{6}' + __config__ = [("activated", "bool", "Activated", True), + ("use_premium", "bool", "Use premium account if available", True), + ("fallback", "bool", + "Fallback to free download if premium fails", True), + ("chk_filesize", "bool", "Check file size", True), + ("max_wait", "int", "Reconnect if waiting time is greater than minutes", 10)] + + __description__ = """Datoid.cz hoster plugin""" + __license__ = "GPLv3" + __authors__ = [("GammaC0de", "nitzo2001[AT]yahoo[DOT]com")] + + NAME_PATTERN = ur'Název souboru: (?P.+)' + SIZE_PATTERN = r'Velikost: (?P[\d.,]+) (?P[\w^_]+)' + OFFLINE_PATTERN = r'Tento soubor neexistuje' + + URL_REPLACEMENTS = [(r'datoid.sk', r'datoid.cz'), + (r'datoid.pl', r'datoid.cz')] + + def handle_free(self, pyfile): + url = self.req.lastEffectiveURL + urlp = urlparse.urlparse(url) + + json_data = json.loads(self.load(urlparse.urljoin( + url, "/f/" + urlp.path + str(int(time.time() * 1000))))) + self.log_debug(json_data) + + if "error" in json_data: + self.fail(json_data['error']) + + self.link = json_data['redirect'] + + def handle_premium(self, pyfile): + url = self.req.lastEffectiveURL + urlp = urlparse.urlparse(url) + + self.link = urlparse.urljoin( + url, "/f/" + urlp.path + str(int(time.time() * 1000))) diff --git a/module/plugins/hoster/DdlstorageCom.py b/module/plugins/hoster/DdlstorageCom.py deleted file mode 100644 index 82072aadba..0000000000 --- a/module/plugins/hoster/DdlstorageCom.py +++ /dev/null @@ -1,84 +0,0 @@ -# -*- coding: utf-8 -*- -import re -from hashlib import md5 - -from module.plugins.hoster.XFileSharingPro import XFileSharingPro -from module.network.RequestFactory import getURL -from module.plugins.Plugin import chunks -from module.common.json_layer import json_loads - - -def getInfo(urls): - # DDLStorage API Documentation: - # http://www.ddlstorage.com/cgi-bin/api_req.cgi?req_type=doc - ids = dict() - for url in urls: - m = re.search(DdlstorageCom.__pattern__, url) - ids[m.group('ID')] = url - - for chunk in chunks(ids.keys(), 5): - api = getURL('http://www.ddlstorage.com/cgi-bin/api_req.cgi', - post={'req_type': 'file_info_free', - 'client_id': 53472, - 'file_code': ','.join(chunk), - 'sign': md5('file_info_free%d%s%s' % (53472, ','.join(chunk), - '25JcpU2dPOKg8E2OEoRqMSRu068r0Cv3')).hexdigest()}) - api = api.replace('
    ', '').replace('
    ', '') - api = json_loads(api) - - result = list() - for el in api: - if el['status'] == 'online': - result.append((el['file_name'], int(el['file_size']), 2, ids[el['file_code']])) - else: - result.append((ids[el['file_code']], 0, 1, ids[el['file_code']])) - yield result - - -class DdlstorageCom(XFileSharingPro): - __name__ = "DdlstorageCom" - __type__ = "hoster" - __pattern__ = r"http://(?:\w*\.)*?ddlstorage.com/(?P\w{12})" - __version__ = "1.00" - __description__ = """DDLStorage.com hoster plugin""" - __author_name__ = ("zoidberg", "stickell") - __author_mail__ = ("zoidberg@mujmail.cz", "l.stickell@yahoo.it") - - FILE_INFO_PATTERN = r'

    ]*>(?P.+) \((?P[^)]+)\)

    ' - HOSTER_NAME = "ddlstorage.com" - - def prepare(self): - self.getAPIData() - super(DdlstorageCom, self).prepare() - - def getAPIData(self): - file_id = re.search(self.__pattern__, self.pyfile.url).group('ID') - data = {'client_id': 53472, - 'file_code': file_id} - if self.user: - passwd = self.account.getAccountData(self.user)["password"] - data['req_type'] = 'file_info_reg' - data['user_login'] = self.user - data['user_password'] = md5(passwd).hexdigest() - data['sign'] = md5('file_info_reg%d%s%s%s%s' % (data['client_id'], data['user_login'], - data['user_password'], data['file_code'], - '25JcpU2dPOKg8E2OEoRqMSRu068r0Cv3')).hexdigest() - else: - data['req_type'] = 'file_info_free' - data['sign'] = md5('file_info_free%d%s%s' % (data['client_id'], data['file_code'], - '25JcpU2dPOKg8E2OEoRqMSRu068r0Cv3')).hexdigest() - - self.api_data = self.load('http://www.ddlstorage.com/cgi-bin/api_req.cgi', post=data) - self.api_data = self.api_data.replace('
    ', '').replace('
    ', '') - self.logDebug('API Data: ' + self.api_data) - self.api_data = json_loads(self.api_data)[0] - - if self.api_data['status'] == 'offline': - self.offline() - - if 'file_name' in self.api_data: - self.pyfile.name = self.api_data['file_name'] - if 'file_size' in self.api_data: - self.pyfile.size = self.api_data['size'] = self.api_data['file_size'] - if 'file_md5_base64' in self.api_data: - self.api_data['md5_ddlstorage'] = self.api_data['file_md5_base64'] diff --git a/module/plugins/hoster/DdownloadCom.py b/module/plugins/hoster/DdownloadCom.py new file mode 100644 index 0000000000..3ec25d94fd --- /dev/null +++ b/module/plugins/hoster/DdownloadCom.py @@ -0,0 +1,70 @@ +# -*- coding: utf-8 -*- + +import pycurl + +from ..internal.misc import json +from ..internal.XFSHoster import XFSHoster + + +class DdownloadCom(XFSHoster): + __name__ = "DdownloadCom" + __type__ = "hoster" + __version__ = "0.12" + __status__ = "testing" + + __pattern__ = r'https?://(?:www\.)?(?:ddl\.to|ddownload\.com)/(?P\w{12})' + __config__ = [("activated", "bool", "Activated", True), + ("use_premium", "bool", "Use premium account if available", True), + ("fallback", "bool", "Fallback to free download if premium fails", True), + ("chk_filesize", "bool", "Check file size", True), + ("max_wait", "int", "Reconnect if waiting time is greater than minutes", 10)] + + __description__ = """Ddownload.com hoster plugin""" + __license__ = "GPLv3" + __authors__ = [("GammaC0de", "nitzo2001[AT]yahoo[DOT]com")] + + PLUGIN_DOMAIN = "ddownload.com" + + URL_REPLACEMENTS = [(__pattern__ + '.*', r"https://ddownload.com/\g")] + + NAME_PATTERN = r'
    \s*

    (?P.+?)

    ' + SIZE_PATTERN = r'(?P[\d.,]+) (?P[\w^_]+)' + + OFFLINE_PATTERN = r'>File Not Found<' + DL_LIMIT_PATTERN = r'You have to wait (.+?) till next download' + + API_KEY = "37699zuaj90n9hxado2m7" + API_URL = "https://api-v2.ddownload.com/api/" + + #: See https://ddownload.com/api + def api_response(self, method, **kwargs): + kwargs.update({'key': self.API_KEY}) + json_data = self.load(self.API_URL + method, get=kwargs) + return json.loads(json_data) + + # def api_info(self, url): + # info = {} + # api_data = self.api_response("file/info", file_code=re.match(cls.__pattern__, url).group('ID')) + # + # if api_data['status'] == 200: + # if api_data['result'][0]['status'] == 200: + # info['status'] = 2 + # info['name'] = api_data['result'][0]['name'] + # info['size'] = api_data['result'][0]['size'] + # + # else: + # info['status'] = 8 + # + # return info + + def set_useragent(self): + self.req.http.c.setopt(pycurl.USERAGENT, "pyLoad/%s" % self.pyload.version) + + def setup(self): + super(DdownloadCom, self).setup() + self.set_useragent() + + def load_account(self): + self.set_useragent() + super(DdownloadCom, self).load_account() + diff --git a/module/plugins/hoster/DebridItaliaCom.py b/module/plugins/hoster/DebridItaliaCom.py index 08470b9846..60649f446e 100644 --- a/module/plugins/hoster/DebridItaliaCom.py +++ b/module/plugins/hoster/DebridItaliaCom.py @@ -1,61 +1,51 @@ # -*- coding: utf-8 -*- -############################################################################ -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU Affero 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 Affero General Public License for more details. # -# # -# You should have received a copy of the GNU Affero General Public License # -# along with this program. If not, see . # -############################################################################ - import re -from module.plugins.Hoster import Hoster +from ..internal.MultiHoster import MultiHoster -class DebridItaliaCom(Hoster): +class DebridItaliaCom(MultiHoster): __name__ = "DebridItaliaCom" - __version__ = "0.05" __type__ = "hoster" - __pattern__ = r"https?://.*debriditalia\.com" - __description__ = """Debriditalia.com hoster plugin""" - __author_name__ = ("stickell") - __author_mail__ = ("l.stickell@yahoo.it") - - def setup(self): - self.chunkLimit = -1 - self.resumeDownload = True - - def process(self, pyfile): - if re.match(self.__pattern__, pyfile.url): - new_url = pyfile.url - elif not self.account: - self.logError(_("Please enter your %s account or deactivate this plugin") % "DebridItalia") - self.fail("No DebridItalia account provided") - else: - self.logDebug("Old URL: %s" % pyfile.url) - url = "http://debriditalia.com/linkgen2.php?xjxfun=convertiLink&xjxargs[]=S" % pyfile.url - page = self.load(url) - self.logDebug("XML data: %s" % page) - - if 'File not available' in page: - self.fail('File not available') - else: - new_url = re.search(r'
    (?P[^<]+)', page).group('direct') + __version__ = "0.26" + __status__ = "testing" + + __pattern__ = r'https?://(?:www\.|s\d+\.)?debriditalia\.com/dl/\d+' + __config__ = [("activated", "bool", "Activated", True), + ("use_premium", "bool", "Use premium account if available", True), + ("fallback", "bool", "Fallback to free download if premium fails", False), + ("chk_filesize", "bool", "Check file size", True), + ("max_wait", "int", "Reconnect if waiting time is greater than minutes", 10), + ("revert_failed", "bool", "Revert to standard download if fails", True)] + + __description__ = """Debriditalia.com multi-hoster plugin""" + __license__ = "GPLv3" + __authors__ = [("stickell", "l.stickell@yahoo.it"), + ("Walter Purcaro", "vuolter@gmail.com"), + ("GammaC0de", "nitzo2001[AT]yahoo[DOT]com")] + + API_URL = "https://debriditalia.com/api.php" + + def api_response(self, method, **kwargs): + kwargs[method] = "" + return self.load(self.API_URL, get=kwargs) + + def handle_premium(self, pyfile): + self.data = self.api_response("generate", + link=pyfile.url.replace("https://", "http://"), + u=self.account.user, + p=self.account.info['login']['password']) + + m = re.search(r'ERROR:(.*)', self.data) + if m is None: + self.link = self.data - if new_url != pyfile.url: - self.logDebug("New URL: %s" % new_url) - - self.download(new_url, disposition=True) + else: + error = m.group(1).strip() - check = self.checkDownload({"empty": re.compile(r"^$")}) + if error in ("not_available", "not_supported"): + self.offline() - if check == "empty": - self.retry(5, 120, 'Empty file downloaded') + else: + self.fail(error) diff --git a/module/plugins/hoster/DebridlinkFr.py b/module/plugins/hoster/DebridlinkFr.py new file mode 100644 index 0000000000..1db60302fc --- /dev/null +++ b/module/plugins/hoster/DebridlinkFr.py @@ -0,0 +1,77 @@ +# -*- coding: utf-8 -*- + +import pycurl +from module.network.HTTPRequest import BadHeader + +from ..internal.misc import json +from ..internal.MultiHoster import MultiHoster + + +def error_description(error_code): + err_message = {'notLink': "Check the 'link' parameter (Empty or bad)", + 'notDebrid': "Maybe the filehoster is down or the link is not online", + 'badFileUrl': "The link format is not valid", + 'hostNotValid': "The filehoster is not supported", + 'notFreeHost': "This filehoster is not available for the free member", + 'disabledHost': "The filehoster are disabled", + 'noGetFilename': "Unable to retrieve the file name", + 'maxLink': "Limitation of number links per day reached", + 'maxLinkHost': "Limitation of number links per day for this host reached", + 'notAddTorrent': "Unable to add the torrent, check url", + 'torrentTooBig': "The torrent is too big or have too many files", + 'maxTorrent': "Limitation of torrents per day reached"}.get(error_code) + + return err_message or "Unknown error: '%s'" % error_code + + +class DebridlinkFr(MultiHoster): + __name__ = "DebridlinkFr" + __type__ = "hoster" + __version__ = "0.07" + __status__ = "testing" + + __pattern__ = r'https?://.*\.debrid\.link/.*' + __config__ = [("activated", "bool", "Activated", True), + ("use_premium", "bool", "Use premium account if available", True), + ("fallback", "bool", "Fallback to free download if premium fails", False), + ("chk_filesize", "bool", "Check file size", True), + ("max_wait", "int", "Reconnect if waiting time is greater than minutes", 10), + ("revert_failed", "bool", "Revert to standard download if fails", True)] + + __description__ = """Debrid-slink.fr multi-hoster plugin""" + __license__ = "GPLv3" + __authors__ = [("GammaC0de", "nitzo2001[AT]yahoo[DOT]com")] + + #: See https://debrid-link.fr/api_doc/v2 + API_URL = "https://debrid-link.fr/api/" + + def api_request(self, method, get={}, post={}): + api_token = self.account.info['data'].get('api_token', None) + if api_token: + self.req.http.c.setopt(pycurl.HTTPHEADER, ["Authorization: Bearer " + api_token]) + self.req.http.c.setopt(pycurl.USERAGENT, "pyLoad/%s" % self.pyload.version) + try: + json_data = self.load(self.API_URL + method, get=get, post=post) + except BadHeader, e: + json_data = e.content + + return json.loads(json_data) + + def handle_premium(self, pyfile): + api_data = self.api_request("v2/downloader/add", + post={'url':pyfile.url}) + + if api_data['success']: + self.link = api_data['value']['downloadUrl'] + pyfile.name = api_data['value'].get('name', pyfile.name) + self.resume_download = api_data['value'].get('resume', self.resume_download) + self.chunk_limit = api_data['value'].get('chunk',self.chunk_limit) + + else: + err_code = api_data['error'] + if err_code == "fileNotFound": + self.offline() + + else: + self.fail(api_data.get('error_description', error_description(api_data["error"]))) + diff --git a/module/plugins/hoster/DebridplanetCom.py b/module/plugins/hoster/DebridplanetCom.py new file mode 100644 index 0000000000..c22c9cf644 --- /dev/null +++ b/module/plugins/hoster/DebridplanetCom.py @@ -0,0 +1,53 @@ +# -*- coding: utf-8 -*- + +import pycurl + +from ..internal.misc import json +from ..internal.MultiHoster import MultiHoster + + +class DebridplanetCom(MultiHoster): + __name__ = "DebridplanetCom" + __type__ = "downloader" + __version__ = "0.01" + __status__ = "testing" + + __pattern__ = r"^unmatchable$" + __config__ = [("activated", "bool", "Activated", True), + ("use_premium", "bool", "Use premium account if available", True), + ("fallback", "bool", "Fallback to free download if premium fails", False), + ("chk_filesize", "bool", "Check file size", True), + ("max_wait", "int", "Reconnect if waiting time is greater than minutes", 10), + ("revert_failed", "bool", "Revert to standard download if fails", True)] + + __description__ = """Debridplanet.com multi-hoster plugin""" + __license__ = "GPLv3" + __authors__ = [("GammaC0de", "nitzo2001[AT]yahoo[DOT]com")] + + API_URL = "https://debridplanet.com/v1/" + + def api_request(self, method, **kwargs): + token = self.account.info["data"].get("token") + if token is not None: + self.req.http.c.setopt( + pycurl.HTTPHEADER, ["Authorization: Bearer " + token] + ) + json_data = self.load("%s%s.php" % (self.API_URL, method), + post=json.dumps(kwargs)) + return json.loads(json_data) + + def handle_premium(self, pyfile): + if self.account.relogin(): #: Insure valid API token + api_data = self.api_request("gen_link", listurl=[pyfile.url]) + if len(api_data) > 0: + if api_data[0]["success"]: + file_info = api_data[0]["data"] + pyfile.name = file_info["filename"] + pyfile.size = file_info["filesize"] + self.resume_download = file_info["resumable"] + self.link = file_info["link"] + + else: + err_msg = api_data[0]["message"] + self.log_error(err_msg) + self.fail(err_msg) diff --git a/module/plugins/hoster/DepositfilesCom.py b/module/plugins/hoster/DepositfilesCom.py index 99c0d13a19..1e793410b5 100644 --- a/module/plugins/hoster/DepositfilesCom.py +++ b/module/plugins/hoster/DepositfilesCom.py @@ -1,115 +1,110 @@ -#!/usr/bin/env python # -*- coding: utf-8 -*- import re -from urllib import unquote -from module.plugins.internal.SimpleHoster import SimpleHoster, create_getInfo -from module.plugins.internal.CaptchaService import ReCaptcha +import urllib + +from ..captcha.ReCaptcha import ReCaptcha +from ..captcha.SolveMedia import SolveMedia +from ..internal.SimpleHoster import SimpleHoster class DepositfilesCom(SimpleHoster): __name__ = "DepositfilesCom" __type__ = "hoster" - __pattern__ = r"https?://[\w\.]*?(depositfiles\.com|dfiles\.eu)(/\w{1,3})?/files/[\w]+" - __version__ = "0.45" - __description__ = """Depositfiles.com Download Hoster""" - __author_name__ = ("spoob", "zoidberg") - __author_mail__ = ("spoob@pyload.org", "zoidberg@mujmail.cz") - - FILE_SIZE_PATTERN = r': (?P[0-9.]+) (?P[kKMG])i?B' - FILE_NAME_PATTERN = r'', self.data) + if m is not None: + jscript = self.load("http://hostuje.net/" + m.group(1)) + m = re.search(r"\('(\w+\.php\?i=\w+)'\);", jscript) + if m is not None: + self.load("http://hostuje.net/" + m.group(1)) + else: + self.error(_("Unexpected javascript format")) + else: + self.error(_("Script not found")) + + action, inputs = self.parse_html_form( + pyfile.url.replace( + ".", "\.").replace( + "?", "\?")) + if not action: + self.error(_("Form not found")) + + self.download(action, post=inputs) diff --git a/module/plugins/hoster/HotfileCom.py b/module/plugins/hoster/HotfileCom.py deleted file mode 100644 index c7dd1d1a1f..0000000000 --- a/module/plugins/hoster/HotfileCom.py +++ /dev/null @@ -1,16 +0,0 @@ -# -*- coding: utf-8 -*- - -from module.plugins.internal.DeadHoster import DeadHoster, create_getInfo - - -class HotfileCom(DeadHoster): - __name__ = "HotfileCom" - __type__ = "hoster" - __pattern__ = r"https?://(www.)?hotfile\.com/dl/\d+/[0-9a-zA-Z]+/" - __version__ = "0.37" - __description__ = """Hotfile.com Download Hoster""" - __author_name__ = ("sitacuisses", "spoob", "mkaay", "JoKoT3") - __author_mail__ = ("sitacuisses@yhoo.de", "spoob@pyload.org", "mkaay@mkaay.de", "jokot3@gmail.com") - - -getInfo = create_getInfo(HotfileCom) diff --git a/module/plugins/hoster/HotlinkCc.py b/module/plugins/hoster/HotlinkCc.py new file mode 100644 index 0000000000..8ed9e2b9cc --- /dev/null +++ b/module/plugins/hoster/HotlinkCc.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- + +import re + +from ..internal.SimpleHoster import SimpleHoster + + +class HotlinkCc(SimpleHoster): + __name__ = "HotlinkCc" + __type__ = "hoster" + __version__ = "0.01" + __status__ = "testing" + + __pattern__ = r'https?://hotlink\.cc/\w+' + __config__ = [("activated", "bool", "Activated", True), + ("use_premium", "bool", "Use premium account if available", True), + ("fallback", "bool", "Fallback to free download if premium fails", True), + ("chk_filesize", "bool", "Check file size", True), + ("max_wait", "int", "Reconnect if waiting time is greater than minutes", 10)] + + __description__ = """Hotlink.cc hoster plugin""" + __license__ = "GPLv3" + __authors__ = [("GammaC0de", "nitzo2001[AT]yahoo[DOT]com")] + + NAME_PATTERN = r'\s*(?P.+?)<' + SIZE_PATTERN = r'Filesize (?P[\d.,]+) (?P[\w^_]+)<' + + WAIT_PATTERN = r'(\d+)<' + DL_LIMIT_PATTERN = r'You have to wait (.+?) untill the next download' + + OFFLINE_PATTERN = r'>File Not Found)?((?:[\w\s]*(?:[Ee]rror|ERROR)\s*\:?)?\s*\d{3})(?:\Z|\s+)'), + 'Html file': re.compile(r'\A\s*. # -############################################################################ - -# Test links (random.bin): -# http://hugefiles.net/prthf9ya4w6s - -from module.plugins.hoster.XFileSharingPro import XFileSharingPro, create_getInfo - - -class HugefilesNet(XFileSharingPro): + +from ..internal.XFSHoster import XFSHoster + + +class HugefilesNet(XFSHoster): __name__ = "HugefilesNet" __type__ = "hoster" - __pattern__ = r"http://(www\.)?hugefiles\.net/\w{12}" - __version__ = "0.01" - __description__ = """Hugefiles.net hoster plugin""" - __author_name__ = ("stickell") - __author_mail__ = ("l.stickell@yahoo.it") + __version__ = "0.12" + __status__ = "testing" - HOSTER_NAME = "hugefiles.net" + __pattern__ = r'http://(?:www\.)?hugefiles\.net/\w{12}' + __config__ = [("activated", "bool", "Activated", True), + ("use_premium", "bool", "Use premium account if available", True), + ("fallback", "bool", + "Fallback to free download if premium fails", True), + ("chk_filesize", "bool", "Check file size", True), + ("max_wait", "int", "Reconnect if waiting time is greater than minutes", 10)] - FILE_SIZE_PATTERN = r'File Size:\s*]*>(?P[^<]+)
    ' + __description__ = """Hugefiles.net hoster plugin""" + __license__ = "GPLv3" + __authors__ = [("stickell", "l.stickell@yahoo.it"), + ("GammaC0de", "nitzo2001[AT]yahoo[DOT]com")] + PLUGIN_DOMAIN = "hugefiles.net" -getInfo = create_getInfo(HugefilesNet) + SIZE_PATTERN = r' \((?P[\d.,]+) (?P[\w^_]+)\)' diff --git a/module/plugins/hoster/HundredEightyUploadCom.py b/module/plugins/hoster/HundredEightyUploadCom.py index 3cf32d338d..bd352b18af 100644 --- a/module/plugins/hoster/HundredEightyUploadCom.py +++ b/module/plugins/hoster/HundredEightyUploadCom.py @@ -1,39 +1,26 @@ # -*- coding: utf-8 -*- -############################################################################ -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU Affero 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 Affero General Public License for more details. # -# # -# You should have received a copy of the GNU Affero General Public License # -# along with this program. If not, see . # -############################################################################ +from ..internal.XFSHoster import XFSHoster -# Test links (random.bin): -# http://180upload.com/js9qdm6kjnrs -from module.plugins.hoster.XFileSharingPro import XFileSharingPro, create_getInfo - - -class HundredEightyUploadCom(XFileSharingPro): +class HundredEightyUploadCom(XFSHoster): __name__ = "HundredEightyUploadCom" __type__ = "hoster" - __pattern__ = r"http://(?:\w*\.)?180upload\.com/(\w+).*" - __version__ = "0.01" - __description__ = """180upload.com hoster plugin""" - __author_name__ = ("stickell") - __author_mail__ = ("l.stickell@yahoo.it") + __version__ = "0.11" + __status__ = "testing" - FILE_NAME_PATTERN = r'Filename:(?P.+)-->' - FILE_SIZE_PATTERN = r'Size:(?P[\d.]+) (?P[A-Z]+)\s*' + __pattern__ = r'http://(?:www\.)?180upload\.com/\w{12}' + __config__ = [("activated", "bool", "Activated", True), + ("use_premium", "bool", "Use premium account if available", True), + ("fallback", "bool", + "Fallback to free download if premium fails", True), + ("chk_filesize", "bool", "Check file size", True), + ("max_wait", "int", "Reconnect if waiting time is greater than minutes", 10)] - HOSTER_NAME = "180upload.com" + __description__ = """180upload.com hoster plugin""" + __license__ = "GPLv3" + __authors__ = [("stickell", "l.stickell@yahoo.it")] + PLUGIN_DOMAIN = "180upload.com" -getInfo = create_getInfo(HundredEightyUploadCom) + OFFLINE_PATTERN = r'>File Not Found' diff --git a/module/plugins/hoster/IFileWs.py b/module/plugins/hoster/IFileWs.py deleted file mode 100644 index 160fe641c5..0000000000 --- a/module/plugins/hoster/IFileWs.py +++ /dev/null @@ -1,20 +0,0 @@ -# -*- coding: utf-8 -*- -from module.plugins.hoster.XFileSharingPro import XFileSharingPro, create_getInfo - - -class IFileWs(XFileSharingPro): - __name__ = "IFileWs" - __type__ = "hoster" - __pattern__ = r"http://(www\.)?ifile\.ws/\w+(/.+)?" - __version__ = "0.01" - __description__ = """Ifile.ws hoster plugin""" - __author_name__ = ("z00nx") - __author_mail__ = ("z00nx0@gmail.com") - - FILE_INFO_PATTERN = '(?P[^<]+)

    \s+\[(?P[^]]+)\]' - FILE_OFFLINE_PATTERN = 'File Not Found|The file was removed by administrator' - HOSTER_NAME = "ifile.ws" - LONG_WAIT_PATTERN = "(?P\d(?=\s+minutes)).*(?P\d+(?=\s+seconds))" - - -getInfo = create_getInfo(IFileWs) diff --git a/module/plugins/hoster/IcyFilesCom.py b/module/plugins/hoster/IcyFilesCom.py deleted file mode 100644 index d0b101717f..0000000000 --- a/module/plugins/hoster/IcyFilesCom.py +++ /dev/null @@ -1,32 +0,0 @@ -# -*- 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 . - - @author: godofdream -""" - -from module.plugins.internal.DeadHoster import DeadHoster, create_getInfo - - -class IcyFilesCom(DeadHoster): - __name__ = "IcyFilesCom" - __type__ = "hoster" - __pattern__ = r"http://(?:www\.)?icyfiles\.com/(.*)" - __version__ = "0.06" - __description__ = """IcyFiles.com plugin - free only""" - __author_name__ = ("godofdream") - __author_mail__ = ("soilfiction@gmail.com") - - -getInfo = create_getInfo(IcyFilesCom) diff --git a/module/plugins/hoster/IfileIt.py b/module/plugins/hoster/IfileIt.py deleted file mode 100644 index 0e3bdd2277..0000000000 --- a/module/plugins/hoster/IfileIt.py +++ /dev/null @@ -1,75 +0,0 @@ -# -*- 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 . - - @author: zoidberg -""" - -import re -from module.plugins.internal.SimpleHoster import SimpleHoster, create_getInfo -from module.common.json_layer import json_loads -from module.plugins.internal.CaptchaService import ReCaptcha - - -class IfileIt(SimpleHoster): - __name__ = "IfileIt" - __type__ = "hoster" - __pattern__ = r"^unmatchable$" - __version__ = "0.27" - __description__ = """Ifile.it""" - __author_name__ = ("zoidberg") - - #EVAL_PATTERN = r'(eval\(function\(p,a,c,k,e,d\).*)' - #DEC_PATTERN = r"requestBtn_clickEvent[^}]*url:\s*([^,]+)" - DOWNLOAD_LINK_PATTERN = r' If it doesn\'t, ' - RECAPTCHA_KEY_PATTERN = r"var __recaptcha_public\s*=\s*'([^']+)';" - FILE_INFO_PATTERN = r']*>\s* \s*\s*\s*' - TEMP_OFFLINE_PATTERN = r'Downloading of this file is temporarily disabled' - - def handleFree(self): - ukey = re.search(self.__pattern__, self.pyfile.url).group(1) - json_url = 'http://ifile.it/new_download-request.json' - post_data = {"ukey": ukey, "ab": "0"} - - json_response = json_loads(self.load(json_url, post=post_data)) - self.logDebug(json_response) - if json_response['status'] == 3: - self.offline() - - if json_response["captcha"]: - captcha_key = re.search(self.RECAPTCHA_KEY_PATTERN, self.html).group(1) - recaptcha = ReCaptcha(self) - post_data["ctype"] = "recaptcha" - - for i in range(5): - post_data["recaptcha_challenge"], post_data["recaptcha_response"] = recaptcha.challenge(captcha_key) - json_response = json_loads(self.load(json_url, post=post_data)) - self.logDebug(json_response) - - if json_response["retry"]: - self.invalidCaptcha() - else: - self.correctCaptcha() - break - else: - self.fail("Incorrect captcha") - - if not "ticket_url" in json_response: - self.parseError("Download URL") - - self.download(json_response["ticket_url"]) - - -getInfo = create_getInfo(IfileIt) diff --git a/module/plugins/hoster/IfolderRu.py b/module/plugins/hoster/IfolderRu.py index 14e568f8fe..b599aab7a8 100644 --- a/module/plugins/hoster/IfolderRu.py +++ b/module/plugins/hoster/IfolderRu.py @@ -1,88 +1,64 @@ # -*- 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 . - - @author: zoidberg -""" import re -from module.plugins.internal.SimpleHoster import SimpleHoster, create_getInfo + +from ..internal.SimpleHoster import SimpleHoster class IfolderRu(SimpleHoster): __name__ = "IfolderRu" __type__ = "hoster" - __pattern__ = r"http://(?:[^.]*\.)?(?:ifolder\.ru|rusfolder\.(?:com|net|ru))/(?:files/)?(?P\d+).*" - __version__ = "0.38" - __description__ = """rusfolder.com / ifolder.ru""" - __author_name__ = ("zoidberg") - __author_mail__ = ("zoidberg@mujmail.cz") - - FILE_SIZE_REPLACEMENTS = [(u'Кб', 'KB'), (u'Мб', 'MB'), (u'Гб', 'GB')] - FILE_NAME_PATTERN = ur'(?:
    )?Название:(?:)? (?P[^<]+)<(?:/div|br)>' - FILE_SIZE_PATTERN = ur'(?:
    )?Размер:(?:)? (?P[^<]+)<(?:/div|br)>' - FILE_OFFLINE_PATTERN = ur'

    Файл номер [^<]* (не найден|удален) !!!

    ' - - SESSION_ID_PATTERN = r'
    ]+)>' - INTS_SESSION_PATTERN = r'\(\'ints_session\'\);\s*if\(tag\)\{tag.value = "([^"]+)";\}' - HIDDEN_INPUT_PATTERN = r"var v = .*?name='([^']+)' value='1'" - DOWNLOAD_LINK_PATTERN = r'неверный код,
    введите еще раз

    ' + __version__ = "0.44" + __status__ = "testing" - def setup(self): - self.resumeDownload = self.multiDL = True if self.account else False - self.chunkLimit = 1 + __pattern__ = r'http://(?:www)?(files\.)?(ifolder\.ru|metalarea\.org|rusfolder\.(com|net|ru))/(files/)?(?P\d+)' + __config__ = [("activated", "bool", "Activated", True), + ("use_premium", "bool", "Use premium account if available", True), + ("fallback", "bool", + "Fallback to free download if premium fails", True), + ("chk_filesize", "bool", "Check file size", True), + ("max_wait", "int", "Reconnect if waiting time is greater than minutes", 10)] + + __description__ = """Ifolder.ru hoster plugin""" + __license__ = "GPLv3" + __authors__ = [("zoidberg", "zoidberg@mujmail.cz")] - def process(self, pyfile): - file_id = re.search(self.__pattern__, pyfile.url).group('ID') - self.html = self.load("http://rusfolder.com/%s" % file_id, cookies=True, decode=True) - self.getFileInfo() + SIZE_REPLACEMENTS = [(u'Кб', 'KB'), (u'Мб', 'MB'), (u'Гб', 'GB')] - url = re.search(r"location\.href = '(http://ints\..*?=)'", self.html).group(1) - self.html = self.load(url, cookies=True, decode=True) + NAME_PATTERN = ur'(?:
    )?Название:(?:)? (?P.+?)<(?:/div|br)>' + SIZE_PATTERN = ur'(?:
    )?Размер:(?:)? (?P.+?)<(?:/div|br)>' + OFFLINE_PATTERN = ur'

    Файл номер .*? (не найден|удален) !!!

    ' - url, session_id = re.search(self.SESSION_ID_PATTERN, self.html).groups() - self.html = self.load(url, cookies=True, decode=True) + SESSION_ID_PATTERN = r'неверный код,
    введите еще раз
    ' + + def setup(self): + self.resume_download = self.multiDL = bool(self.account) + self.chunk_limit = 1 + + def handle_free(self, pyfile): + url = "http://rusfolder.com/%s" % self.info['pattern']['ID'] + self.data = self.load( + "http://rusfolder.com/%s" % + self.info['pattern']['ID']) + self.get_fileInfo() + session_id = re.search(self.SESSION_ID_PATTERN, self.data).groups() captcha_url = "http://ints.rusfolder.com/random/images/?session=%s" % session_id - for i in range(5): - self.html = self.load(url, cookies=True) - action, inputs = self.parseHtmlForm('ID="Form1"') - inputs['ints_session'] = re.search(self.INTS_SESSION_PATTERN, self.html).group(1) - inputs[re.search(self.HIDDEN_INPUT_PATTERN, self.html).group(1)] = '1' - inputs['confirmed_number'] = self.decryptCaptcha(captcha_url, cookies=True) - inputs['action'] = '1' - self.logDebug(inputs) - - self.html = self.load(url, decode=True, cookies=True, post=inputs) - if self.WRONG_CAPTCHA_PATTERN in self.html: - self.invalidCaptcha() - else: - break - else: - self.fail("Invalid captcha") - - download_url = re.search(self.DOWNLOAD_LINK_PATTERN, self.html).group(1) - self.correctCaptcha() - self.logDebug("Download URL: %s" % download_url) - self.download(download_url) - - -getInfo = create_getInfo(IfolderRu) + + action, inputs = self.parse_html_form('id="download-step-one-form"') + inputs['confirmed_number'] = self.captcha.decrypt( + captcha_url, cookies=True) + inputs['action'] = '1' + self.log_debug(inputs) + + self.data = self.load(url, post=inputs) + if self.WRONG_CAPTCHA_PATTERN in self.data: + self.retry_captcha() + + self.link = re.search(self.LINK_FREE_PATTERN, self.data).group(1) diff --git a/module/plugins/hoster/IronfilesNet.py b/module/plugins/hoster/IronfilesNet.py new file mode 100644 index 0000000000..a84ef4700a --- /dev/null +++ b/module/plugins/hoster/IronfilesNet.py @@ -0,0 +1,53 @@ +# -*- coding: utf-8 -*- + +from ..internal.SimpleHoster import SimpleHoster +from ..internal.misc import json + +class IronfilesNet(SimpleHoster): + __name__ = "IronfilesNet" + __type__ = "hoster" + __version__ = "0.02" + __status__ = "testing" + + __pattern__ = r'https?://ironfiles\.net/file/download/id/(?P\d+)(?:/key/(?P[-\w]+))?' + __config__ = [("activated", "bool", "Activated", True), + ("use_premium", "bool", "Use premium account if available", True), + ("fallback", "bool", "Fallback to free download if premium fails", False), + ("chk_filesize", "bool", "Check file size", True), + ("max_wait", "int", "Reconnect if waiting time is greater than minutes", 10)] + + __description__ = """Ironfiles.net hoster plugin""" + __license__ = "GPLv3" + __authors__ = [("GammaC0de", "nitzo2001[AT]yahoo[DOT]com"), + ("djraw", None)] + + LOGIN_PREMIUM = True + + OFFLINE_PATTERN = "^unmatchable$" + + API_URL = "https://ironfiles.net/api/" + + def api_response(self, method, **kwargs): + json_data = self.load(self.API_URL + method, get=kwargs) + return json.loads(json_data) + + def handle_premium(self, pyfile): + _id = self.info['pattern']['ID'] + _key = self.info['pattern']['KEY'] + + file_info = json.loads(self.load("https://ironfiles.net/api/fileInfo/file/" + _id + + ("/key/" + _key) if _key else "")) + + if file_info['result']: + pyfile.name = file_info['filename'] + pyfile.size = file_info['size'] + self.link = "https://ironfiles.net/download/file/id/" + _id + ("/key/" + _key) if _key else "" + + else: + message = file_info['message'] + if message == "File not available": + self.offline() + + else: + self.fail(message) + diff --git a/module/plugins/hoster/JumbofilesCom.py b/module/plugins/hoster/JumbofilesCom.py index 1b8a2d73b1..ce724160ac 100644 --- a/module/plugins/hoster/JumbofilesCom.py +++ b/module/plugins/hoster/JumbofilesCom.py @@ -1,31 +1,40 @@ # -*- coding: utf-8 -*- + import re -from module.plugins.internal.SimpleHoster import SimpleHoster, create_getInfo + +from ..internal.SimpleHoster import SimpleHoster class JumbofilesCom(SimpleHoster): __name__ = "JumbofilesCom" __type__ = "hoster" - __pattern__ = r"http://(?:\w*\.)*jumbofiles.com/(\w{12}).*" - __version__ = "0.02" - __description__ = """JumboFiles.com hoster plugin""" - __author_name__ = ("godofdream") - __author_mail__ = ("soilfiction@gmail.com") + __version__ = "0.08" + __status__ = "testing" - FILE_INFO_PATTERN = '(?P[^<]+?)\s*\((?P[\d.]+)\s*(?P[KMG][bB])\)' - FILE_OFFLINE_PATTERN = 'Not Found or Deleted / Disabled due to inactivity or DMCA' - DIRECT_LINK_PATTERN = '' + __pattern__ = r'http://(?:www\.)?jumbofiles\.com/(?P\w{12})' + __config__ = [("activated", "bool", "Activated", True), + ("use_premium", "bool", "Use premium account if available", True), + ("fallback", "bool", + "Fallback to free download if premium fails", True), + ("chk_filesize", "bool", "Check file size", True), + ("max_wait", "int", "Reconnect if waiting time is greater than minutes", 10)] - def setup(self): - self.resumeDownload = self.multiDL = True + __description__ = """JumboFiles.com hoster plugin""" + __license__ = "GPLv3" + __authors__ = [("godofdream", "soilfiction@gmail.com")] - def handleFree(self): - ukey = re.search(self.__pattern__, self.pyfile.url).group(1) - post_data = {"id": ukey, "op": "download3", "rand": ""} - html = self.load(self.pyfile.url, post=post_data, decode=True) - url = re.search(self.DIRECT_LINK_PATTERN, html).group(1) - self.logDebug("Download " + url) - self.download(url) + INFO_PATTERN = r'(?P.+?)\s*\((?P[\d.,]+)\s*(?P[\w^_]+)' + OFFLINE_PATTERN = r'Not Found or Deleted / Disabled due to inactivity or DMCA' + LINK_FREE_PATTERN = r'' + def setup(self): + self.resume_download = True + self.multiDL = True -getInfo = create_getInfo(JumbofilesCom) + def handle_free(self, pyfile): + post_data = { + 'id': self.info['pattern']['ID'], + 'op': "download3", + 'rand': ""} + html = self.load(self.pyfile.url, post=post_data) + self.link = re.search(self.LINK_FREE_PATTERN, html).group(1) diff --git a/module/plugins/hoster/JunocloudMe.py b/module/plugins/hoster/JunocloudMe.py new file mode 100644 index 0000000000..938e6be463 --- /dev/null +++ b/module/plugins/hoster/JunocloudMe.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- + +from ..internal.XFSHoster import XFSHoster + + +class JunocloudMe(XFSHoster): + __name__ = "JunocloudMe" + __type__ = "hoster" + __version__ = "0.11" + __status__ = "testing" + + __pattern__ = r'http://(?:\w+\.)?junocloud\.me/\w{12}' + __config__ = [("activated", "bool", "Activated", True), + ("use_premium", "bool", "Use premium account if available", True), + ("fallback", "bool", + "Fallback to free download if premium fails", True), + ("chk_filesize", "bool", "Check file size", True), + ("max_wait", "int", "Reconnect if waiting time is greater than minutes", 10)] + + __description__ = """Junocloud.me hoster plugin""" + __license__ = "GPLv3" + __authors__ = [("guidobelix", "guidobelix@hotmail.it")] + + PLUGIN_DOMAIN = "junocloud.me" + + URL_REPLACEMENTS = [(r'//(www\.)?junocloud', "//dl3.junocloud")] + + OFFLINE_PATTERN = r'>No such file with this filename<' + TEMP_OFFLINE_PATTERN = r'The page may have been renamed, removed or be temporarily unavailable.<' diff --git a/module/plugins/hoster/KatfileCom.py b/module/plugins/hoster/KatfileCom.py new file mode 100644 index 0000000000..487954cb6c --- /dev/null +++ b/module/plugins/hoster/KatfileCom.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- + +from ..internal.XFSHoster import XFSHoster + + +class KatfileCom(XFSHoster): + __name__ = "KatfileCom" + __type__ = "hoster" + __version__ = "0.06" + __status__ = "testing" + + __pattern__ = r'https?://(?:www\.)?katfile\.(?:com|cloud)/(?P\w{12})' + __config__ = [("activated", "bool", "Activated", True), + ("use_premium", "bool", "Use premium account if available", True), + ("fallback", "bool", "Fallback to free download if premium fails", True), + ("chk_filesize", "bool", "Check file size", True), + ("max_wait", "int", "Reconnect if waiting time is greater than minutes", 10)] + + __description__ = """katfile.com hoster plugin""" + __license__ = "GPLv3" + __authors__ = [("GammaC0de", "nitzo2001[AT]yahoo[DOT]com")] + + NAME_PATTERN = r'name="fname" value="(?P.+?)"' + SIZE_PATTERN = r'(?P[\d.,]+) (?P[\w^_]+)<' + + OFFLINE_PATTERN = r"File has been removed" + WAIT_PATTERN = r'var estimated_time = ([\w ]+?);' + DL_LIMIT_PATTERN = r'Delay between downloads must be not less than ([\w ]+?),' + LINK_PATTERN = r'
    \w+)' + __config__ = [("activated", "bool", "Activated", True), + ("use_premium", "bool", "Use premium account if available", True), + ("fallback", "bool", "Fallback to free download if premium fails", True), + ("chk_filesize", "bool", "Check file size", True), + ("max_wait", "int", "Reconnect if waiting time is greater than minutes", 10)] + + __description__ = """Keep2Share.cc hoster plugin""" + __license__ = "GPLv3" + __authors__ = [("stickell", "l.stickell@yahoo.it"), + ("Walter Purcaro", "vuolter@gmail.com"), + ("GammaC0de", "nitzo2001[AT]yahoo[DOT]com"), + ("zep6yr", "Ievu6hah[AT]protonmail[DOT]com")] + + DISPOSITION = False # @TODO: Recheck in v0.4.10 + + URL_REPLACEMENTS = [(__pattern__ + ".*", "https://k2s.cc/file/\g")] + + API_URL = "https://keep2share.cc/api/v2/" + #: See https://keep2share.github.io/api/ https://github.com/keep2share/api + + class ErrorCode: + FILE_IS_NOT_AVAILABLE = 21 # also used for: Traffic limit exceed + CAPTCHA_REQUIRED = 30 + CAPTCHA_INVALID = 31 + DOWNLOAD_NOT_AVAILABLE = 42 + + def api_request(self, method, **kwargs): + html = self.load(self.API_URL + method, post=json.dumps(kwargs)) + return json.loads(html) + + def api_info(self, url): + file_id = re.match(self.__pattern__, url).group('ID') + file_info = self.api_request("GetFilesInfo", ids=[file_id], extended_info=False) + + if file_info['code'] != 200 or \ + len(file_info['files']) == 0 or \ + file_info['files'][0].get("is_available", False) is False: + return {'status': 1} + + else: + return {'name': file_info['files'][0]['name'], + 'size': file_info['files'][0]['size'], + 'md5': file_info['files'][0]['md5'], + 'access': file_info['files'][0]['access'], + 'free_access': file_info['files'][0]['isAvailableForFree'], + 'status': 2 if file_info['files'][0]['is_available'] else 1} + + def setup(self): + self.multiDL = self.premium + self.resume_download = True + + @staticmethod + def seconds_until_midnight_utc(): + """calculate seconds until UTC 0:00 and add some randomness""" + now_utc = datetime.utcnow() + midnight_utc = datetime.utcnow().replace( + hour=random.randint(0, 2), + minute=random.randint(0, 59), + second=random.randint(0, 59), + ) # prevent clients from retrying all at the same time + midnight_utc += timedelta(days=1) + return (midnight_utc - now_utc).total_seconds() + + def solve_captcha(self, req): + json_data = self.api_request("RequestCaptcha") + if json_data.get("code") != 200: + return False + captcha_response = self.captcha.decrypt(json_data["captcha_url"]) + req["captcha_challenge"] = json_data["challenge"] + req["captcha_response"] = captcha_response + return True + + def check_errors(self, data=None): + if data is not None: + json_data = json.loads(data) + if json_data.get("errorCode"): + err = json_data["errorCode"] + self.log_debug(_("Handling error code: %s") % err) + + if err in [Keep2ShareCc.ErrorCode.CAPTCHA_REQUIRED, Keep2ShareCc.ErrorCode.CAPTCHA_INVALID]: + self.retry_captcha() + + elif err == Keep2ShareCc.ErrorCode.DOWNLOAD_NOT_AVAILABLE: + self.retry(wait=json_data["errors"][0]["timeRemaining"]) + + elif err == Keep2ShareCc.ErrorCode.FILE_IS_NOT_AVAILABLE: + # apparently traffic will reset at 0:00 UTC + self.retry(wait=Keep2ShareCc.seconds_until_midnight_utc()) + + else: + self.fail(json_data["message"]) + + def handle_free(self, pyfile): + self.do_download(pyfile) + + def handle_premium(self, pyfile): + self.do_download(pyfile) + + def do_download(self, pyfile): + file_id = self.info["pattern"]["ID"] + + req = { + "file_id": file_id, + "free_download_key": None, + "captcha_challenge": None, + "captcha_response": None, + } + + if self.info["access"] == "private": + self.fail(_("This is a private file")) + elif self.premium: + req["auth_token"] = self.account.info["data"]["token"] + elif self.info["access"] == "premium" or self.info["free_access"] is False: + self.fail(_("File can be downloaded by premium users only")) + + if not self.premium: + if not self.solve_captcha(req): + self.fail(_("Request captcha API failed")) + + try: + # The first request will return with "free_download_key" + # The second request will return with "url" + for _ in range(2): + json_data = self.api_request("GetUrl", **req) + + if json_data.get("code") == 200: + if req.get("captcha_response", None) is not None: + self.captcha.correct() + req["captcha_challenge"] = None + req["captcha_response"] = None + + if "url" in json_data: + self.link = json_data["url"] + break + + if "free_download_key" in json_data: + req["free_download_key"] = json_data["free_download_key"] + + if "time_wait" in json_data: + self.wait(json_data["time_wait"]) + else: + self.fail(json_data["message"]) + + except BadHeader as exc: + if exc.code == 406: + self.check_errors(exc.content) + else: + raise diff --git a/module/plugins/hoster/Keep2shareCC.py b/module/plugins/hoster/Keep2shareCC.py deleted file mode 100644 index 29e40bc7f9..0000000000 --- a/module/plugins/hoster/Keep2shareCC.py +++ /dev/null @@ -1,95 +0,0 @@ -# -*- coding: utf-8 -*- - -############################################################################ -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU Affero 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 Affero General Public License for more details. # -# # -# You should have received a copy of the GNU Affero General Public License # -# along with this program. If not, see . # -############################################################################ - -# Test links (random.bin): -# http://k2s.cc/file/527111edfb9ba/random.bin - -import re - -from module.plugins.internal.SimpleHoster import SimpleHoster, create_getInfo -from module.plugins.internal.CaptchaService import ReCaptcha - - -class Keep2shareCC(SimpleHoster): - __name__ = "Keep2shareCC" - __type__ = "hoster" - __pattern__ = r"https?://(?:www\.)?(keep2share|k2s|keep2s)\.cc/file/(?P\w+)" - __version__ = "0.07" - __description__ = """Keep2share.cc hoster plugin""" - __author_name__ = ("stickell") - __author_mail__ = ("l.stickell@yahoo.it") - - FILE_NAME_PATTERN = r'File: (?P.+)' - FILE_SIZE_PATTERN = r'Size: (?P[^<]+)
    ' - FILE_OFFLINE_PATTERN = r'File not found or deleted|Sorry, this file is blocked or deleted|Error 404' - - DIRECT_LINK_PATTERN = r'To download this file with slow speed, use this link' - WAIT_PATTERN = r'Please wait ([\d:]+) to download this file' - - RECAPTCHA_KEY = '6LcYcN0SAAAAABtMlxKj7X0hRxOY8_2U86kI1vbb' - - FILE_URL_REPLACEMENTS = [(__pattern__, r"http://keep2share.cc/file/\g")] - - def handleFree(self): - self.fid = re.search(r'', self.html).group(1) - self.html = self.load(self.pyfile.url, post={'yt0': '', 'slow_id': self.fid}) - - m = re.search(r"function download\(\){.*window\.location\.href = '([^']+)';", self.html, re.DOTALL) - if m: # Direct mode - self.startDownload("http://www.keep2share.cc" + m.group(1)) - else: - self.handleCaptcha() - - self.setWait(30) - self.wait() - - self.html = self.load(self.pyfile.url, post={'uniqueId': self.fid, 'free': 1}) - - m = re.search(self.DIRECT_LINK_PATTERN, self.html) - if not m: - self.parseError("Unable to detect direct link") - self.startDownload('http://keep2share.cc' + m.group(1)) - - def handleCaptcha(self): - recaptcha = ReCaptcha(self) - for i in xrange(5): - challenge, response = recaptcha.challenge(self.RECAPTCHA_KEY) - post_data = {'recaptcha_challenge_field': challenge, - 'recaptcha_response_field': response, - 'CaptchaForm%5Bcode%5D': '', - 'free': 1, - 'freeDownloadRequest': 1, - 'uniqueId': self.fid, - 'yt0': ''} - - self.html = self.load(self.pyfile.url, post=post_data) - - if 'recaptcha' not in self.html: - self.correctCaptcha() - break - else: - self.logInfo('Wrong captcha') - self.invalidCaptcha() - else: - self.fail("All captcha attempts failed") - - def startDownload(self, url): - self.logDebug('Direct Link: ' + url) - self.download(url, disposition=True) - - -getInfo = create_getInfo(Keep2shareCC) diff --git a/module/plugins/hoster/KingfilesNet.py b/module/plugins/hoster/KingfilesNet.py new file mode 100644 index 0000000000..ad1f385082 --- /dev/null +++ b/module/plugins/hoster/KingfilesNet.py @@ -0,0 +1,79 @@ +# -*- coding: utf-8 -*- + +import re + +from ..captcha.SolveMedia import SolveMedia +from ..internal.SimpleHoster import SimpleHoster + + +class KingfilesNet(SimpleHoster): + __name__ = "KingfilesNet" + __type__ = "hoster" + __version__ = "0.13" + __status__ = "testing" + + __pattern__ = r'http://(?:www\.)?kingfiles\.net/(?P\w{12})' + __config__ = [("activated", "bool", "Activated", True), + ("use_premium", "bool", "Use premium account if available", True), + ("fallback", "bool", + "Fallback to free download if premium fails", True), + ("chk_filesize", "bool", "Check file size", True), + ("max_wait", "int", "Reconnect if waiting time is greater than minutes", 10)] + + __description__ = """Kingfiles.net hoster plugin""" + __license__ = "GPLv3" + __authors__ = [("zapp-brannigan", "fuerst.reinje@web.de"), + ("Walter Purcaro", "vuolter@gmail.com")] + + NAME_PATTERN = r'name="fname" value="(?P.+?)">' + SIZE_PATTERN = r'>Size: .+?">(?P[\d.,]+) (?P[\w^_]+)' + + OFFLINE_PATTERN = r'>(File Not Found

    |File Not Found

    )' + + RAND_ID_PATTERN = r'type=\"hidden\" name=\"rand\" value=\"(.+)\">' + + LINK_FREE_PATTERN = r'var download_url = \'(.+)\';' + + def setup(self): + self.resume_download = True + self.multiDL = True + + def handle_free(self, pyfile): + #: Click the free user button + post_data = {'op': "download1", + 'usr_login': "", + 'id': self.info['pattern']['ID'], + 'fname': pyfile.name, + 'referer': "", + 'method_free': "+"} + + self.data = self.load(pyfile.url, post=post_data) + + self.captcha = SolveMedia(pyfile) + response, challenge = self.captcha.challenge() + + #: Make the downloadlink appear and load the file + m = re.search(self.RAND_ID_PATTERN, self.data) + if m is None: + self.error(_("Random key not found")) + + rand = m.group(1) + self.log_debug("rand = " + rand) + + post_data = {'op': "download2", + 'id': self.info['pattern']['ID'], + 'rand': rand, + 'referer': pyfile.url, + 'method_free': "+", + 'method_premium': "", + 'adcopy_response': response, + 'adcopy_challenge': challenge, + 'down_direct': "1"} + + self.data = self.load(pyfile.url, post=post_data) + + m = re.search(self.LINK_FREE_PATTERN, self.data) + if m is None: + self.error(_("Download url not found")) + + self.link = m.group(1) diff --git a/module/plugins/hoster/KrakenfilesCom.py b/module/plugins/hoster/KrakenfilesCom.py new file mode 100644 index 0000000000..53f80aa779 --- /dev/null +++ b/module/plugins/hoster/KrakenfilesCom.py @@ -0,0 +1,48 @@ +# -*- coding: utf-8 -*- + +import re + +import pycurl + +from ..internal.misc import json +from ..internal.SimpleHoster import SimpleHoster + + +class KrakenfilesCom(SimpleHoster): + __name__ = "KrakenfilesCom" + __type__ = "hoster" + __version__ = "0.03" + __status__ = "testing" + + __pattern__ = r'https?://(?:www\.)?krakenfiles\.com/view/\w+/file.html' + __config__ = [("activated", "bool", "Activated", True), + ("use_premium", "bool", "Use premium account if available", True), + ("fallback", "bool", "Fallback to free download if premium fails", True), + ("chk_filesize", "bool", "Check file size", True), + ("max_wait", "int", "Reconnect if waiting time is greater than minutes", 10)] + + __description__ = """krakenfiles.com hoster plugin""" + __license__ = "GPLv3" + __authors__ = [("GammaC0de", "nitzo2001[AT]yahoo[DOT]com")] + + NAME_PATTERN = r'class="coin-name">
    (?P.+?)<' + SIZE_PATTERN = r'File size
    \s.+?>(?P[\d.,]+) (?P[\w^_]+)<' + + def handle_free(self, pyfile): + url, inputs = self.parse_html_form('id="dl-form"') + if url is None: + self.fail(_("Free download form not found")) + + m = re.search(r'', self.data) + if m is None: + self.fail(_("hash pattern not found")) + + self.req.http.c.setopt(pycurl.HTTPHEADER, ["X-Requested-With: XMLHttpRequest", + "hash: %s" % m.group(1)]) + self.data = self.load(self.fixurl(url), + post=inputs) + self.req.http.c.setopt(pycurl.HTTPHEADER, ["X-Requested-With:"]) + + json_data = json.loads(self.data) + if json_data.get('status') == "ok": + self.download(json_data['url'], ref="https://krakenfiles.com/") diff --git a/module/plugins/hoster/LeechThreeHundreedSixtyCom.py b/module/plugins/hoster/LeechThreeHundreedSixtyCom.py new file mode 100644 index 0000000000..743ea053b4 --- /dev/null +++ b/module/plugins/hoster/LeechThreeHundreedSixtyCom.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- + +from ..internal.MultiHoster import MultiHoster +from ..internal.misc import json, parse_size + +class LeechThreeHundreedSixtyCom(MultiHoster): + __name__ = "LeechThreeHundreedSixtyCom" + __type__ = "hoster" + __version__ = "0.01" + __status__ = "testing" + + __pattern__ = r'^unmatchable$' + __config__ = [("activated", "bool", "Activated", True), + ("use_premium", "bool", "Use premium account if available", True), + ("fallback", "bool", "Fallback to free download if premium fails", False), + ("chk_filesize", "bool", "Check file size", True), + ("max_wait", "int", "Reconnect if waiting time is greater than minutes", 10), + ("revert_failed", "bool", "Revert to standard download if fails", True)] + + __description__ = """Leech360.com multi-hoster plugin""" + __license__ = "GPLv3" + __authors__ = [("GammaC0de", "nitzo2001[AT]yahoo[DOT]com")] + + def handle_premium(self, pyfile): + json_data = self.load("https://leech360.com/generate", + get={'token': self.account.info['data']['token'], + 'link': pyfile.url}) + api_data = json.loads(json_data) + + if api_data['error']: + self.fail(api_data['error_message']) + + pyfile.name = api_data.get('filename', "") or pyfile.name + pyfile.size = parse_size(api_data.get('message', "0")) + self.link = api_data['download_url'] + + + diff --git a/module/plugins/hoster/LetitbitNet.py b/module/plugins/hoster/LetitbitNet.py deleted file mode 100644 index 45f029c7b2..0000000000 --- a/module/plugins/hoster/LetitbitNet.py +++ /dev/null @@ -1,171 +0,0 @@ -# -*- 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 . - - @author: zoidberg -""" - -# API Documentation: -# http://api.letitbit.net/reg/static/api.pdf - -# Test links (random.bin): -# http://letitbit.net/download/07874.0b5709a7d3beee2408bb1f2eefce/random.bin.html - -import re -import urllib - -from module.plugins.internal.SimpleHoster import SimpleHoster -from module.common.json_layer import json_loads, json_dumps -from module.plugins.internal.CaptchaService import ReCaptcha - - -def api_download_info(url): - json_data = ['yw7XQy2v9', ["download/info", {"link": url}]] - post_data = urllib.urlencode({'r': json_dumps(json_data)}) - api_rep = urllib.urlopen('http://api.letitbit.net/json', data=post_data).read() - return json_loads(api_rep) - - -def getInfo(urls): - for url in urls: - api_rep = api_download_info(url) - if api_rep['status'] == 'OK': - info = api_rep['data'][0] - yield (info['name'], info['size'], 2, url) - else: - yield (url, 0, 1, url) - - -class LetitbitNet(SimpleHoster): - __name__ = "LetitbitNet" - __type__ = "hoster" - __pattern__ = r"http://(?:\w*\.)*(letitbit|shareflare).net/download/.*" - __version__ = "0.23" - __description__ = """letitbit.net""" - __author_name__ = ("zoidberg", "z00nx") - __author_mail__ = ("zoidberg@mujmail.cz", "z00nx0@gmail.com") - - CHECK_URL_PATTERN = r"ajax_check_url\s*=\s*'((http://[^/]+)[^']+)';" - SECONDS_PATTERN = r"seconds\s*=\s*(\d+);" - CAPTCHA_CONTROL_FIELD = r"recaptcha_control_field\s=\s'(?P[^']+)'" - - DOMAIN = "http://letitbit.net" - FILE_URL_REPLACEMENTS = [(r"(?<=http://)([^/]+)", "letitbit.net")] - RECAPTCHA_KEY = "6Lc9zdMSAAAAAF-7s2wuQ-036pLRbM0p8dDaQdAM" - - def setup(self): - self.resumeDownload = True - #TODO confirm that resume works - - def getFileInfo(self): - api_rep = api_download_info(self.pyfile.url) - if api_rep['status'] == 'OK': - self.api_data = api_rep['data'][0] - self.pyfile.name = self.api_data['name'] - self.pyfile.size = self.api_data['size'] - else: - self.offline() - - def handleFree(self): - action, inputs = self.parseHtmlForm('id="ifree_form"') - if not action: - self.parseError("page 1 / ifree_form") - self.pyfile.size = float(inputs['sssize']) - self.logDebug(action, inputs) - inputs['desc'] = "" - - self.html = self.load(self.DOMAIN + action, post=inputs, cookies=True) - - # action, inputs = self.parseHtmlForm('id="d3_form"') - # if not action: self.parseError("page 2 / d3_form") - # #self.logDebug(action, inputs) - # - # self.html = self.load(action, post = inputs, cookies = True) - # - # try: - # ajax_check_url, captcha_url = re.search(self.CHECK_URL_PATTERN, self.html).groups() - # found = re.search(self.SECONDS_PATTERN, self.html) - # seconds = int(found.group(1)) if found else 60 - # self.setWait(seconds+1) - # self.wait() - # except Exception, e: - # self.logError(e) - # self.parseError("page 3 / js") - - found = re.search(self.SECONDS_PATTERN, self.html) - seconds = int(found.group(1)) if found else 60 - self.logDebug("Seconds found", seconds) - found = re.search(self.CAPTCHA_CONTROL_FIELD, self.html) - recaptcha_control_field = found.group(1) - self.logDebug("ReCaptcha control field found", recaptcha_control_field) - self.setWait(seconds + 1) - self.wait() - - response = self.load("%s/ajax/download3.php" % self.DOMAIN, post=" ", cookies=True) - if response != '1': - self.parseError('Unknown response - ajax_check_url') - self.logDebug(response) - - recaptcha = ReCaptcha(self) - challenge, response = recaptcha.challenge(self.RECAPTCHA_KEY) - post_data = {"recaptcha_challenge_field": challenge, "recaptcha_response_field": response, - "recaptcha_control_field": recaptcha_control_field} - self.logDebug("Post data to send", post_data) - response = self.load('%s/ajax/check_recaptcha.php' % self.DOMAIN, post=post_data, cookies=True) - self.logDebug(response) - if not response: - self.invalidCaptcha() - if response == "error_free_download_blocked": - self.logInfo("Daily limit reached, waiting 24 hours") - self.setWait(24 * 60 * 60) - self.wait() - if response == "error_wrong_captcha": - self.logInfo("Wrong Captcha") - self.invalidCaptcha() - self.retry() - elif response.startswith('['): - urls = json_loads(response) - elif response.startswith('http://'): - urls = [response] - else: - self.parseError("Unknown response - captcha check") - - self.correctCaptcha() - - for download_url in urls: - try: - self.logDebug("Download URL", download_url) - self.download(download_url) - break - except Exception, e: - self.logError(e) - else: - self.fail("Download did not finish correctly") - - def handlePremium(self): - api_key = self.user - premium_key = self.account.getAccountData(self.user)["password"] - - json_data = [api_key, ["download/direct_links", {"pass": premium_key, "link": self.pyfile.url}]] - api_rep = self.load('http://api.letitbit.net/json', post={'r': json_dumps(json_data)}) - self.logDebug('API Data: ' + api_rep) - api_rep = json_loads(api_rep) - - if api_rep['status'] == 'FAIL': - self.fail(api_rep['data']) - - direct_link = api_rep['data'][0][0] - self.logDebug('Direct Link: ' + direct_link) - - self.download(direct_link, disposition=True) diff --git a/module/plugins/hoster/LetsuploadCc.py b/module/plugins/hoster/LetsuploadCc.py new file mode 100644 index 0000000000..241b2aa801 --- /dev/null +++ b/module/plugins/hoster/LetsuploadCc.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- + +from ..internal.SimpleHoster import SimpleHoster + + +class LetsuploadCc(SimpleHoster): + __name__ = "LetsuploadCc" + __type__ = "hoster" + __version__ = "0.02" + __status__ = "testing" + + __pattern__ = r'https?://(?:www\.)?letsupload\.cc/(?P\w{10})' + __config__ = [("activated", "bool", "Activated", True), + ("use_premium", "bool", "Use premium account if available", True), + ("fallback", "bool", "Fallback to free download if premium fails", True), + ("chk_filesize", "bool", "Check file size", True), + ("max_wait", "int", "Reconnect if waiting time is greater than minutes", 10)] + + __description__ = """Letsupload.cc hoster plugin""" + __license__ = "GPLv3" + __authors__ = [("GammaC0de", "nitzo2001[AT]yahoo[DOT]com")] + + URL_REPLACEMENTS = [(__pattern__ + ".*", "https://letsupload.cc/\g")] + + NAME_PATTERN = r'

    (?P.+?)

    ' + SIZE_PATTERN = r'>Download\s*\((?P[\d.,]+) (?P[\w^_]+)\)' + + LINK_FREE_PATTERN = r'href="(https://cdn-\d+\.letsupload\.cc/.+?)"' diff --git a/module/plugins/hoster/LetsuploadCo.py b/module/plugins/hoster/LetsuploadCo.py new file mode 100644 index 0000000000..ca603c05f9 --- /dev/null +++ b/module/plugins/hoster/LetsuploadCo.py @@ -0,0 +1,59 @@ +# -*- coding: utf-8 -*- + +import os +import re + +from ..internal.SimpleHoster import SimpleHoster +from ..internal.misc import fs_encode + + +class LetsuploadCo(SimpleHoster): + __name__ = "LetsuploadCo" + __type__ = "hoster" + __version__ = "0.03" + __status__ = "testing" + + __pattern__ = r'https?://(?:www\.)?letsupload\.co/\w+' + __config__ = [("activated", "bool", "Activated", True), + ("use_premium", "bool", "Use premium account if available", True), + ("fallback", "bool", "Fallback to free download if premium fails", True), + ("chk_filesize", "bool", "Check file size", True), + ("max_wait", "int", "Reconnect if waiting time is greater than minutes", 10)] + + __description__ = """Letsupload.co hoster plugin""" + __license__ = "GPLv3" + __authors__ = [("gonapo", "nh189[AT]uranus.uni-freiburg[DOT]de"), + ("GammaC0de", "nitzo2001[AT]yahoo[DOT]com")] + + WAIT_PATTERN = r'var seconds = (\d+)' + + NAME_PATTERN = r'(?P.+?)<' + SIZE_PATTERN = r' size :

    (?P[\d.,]+) (?P[\w^_]+)

    ' + + OFFLINE_PATTERN = r'> File has been removed\.<' + LINK_FREE_PATTERN = r"0 and 'id' in resp[0]: + self.log_debug("Got book details: {}".format(resp[0])) + return resp[0] + except: + self.log_debug("Error calling libgen API at {}".format(url)) + + self.log_debug("No working API results") + return {} + + + def get_book_info(self, url): + self.log_debug("Getting book info for URL {}".format(url)) + info = {'url':url} + match = re.search(r'(?i)(?:/|md5=)(?P[a-f0-9]{32})\b', url) + + if not match: + self.log_error("Could not extract MD5 from URL "+url) + + else: + info['md5'] = match.group('md5') + topic = '' + topicmatch = re.search(r'(?i)\b(fiction|foreignfiction|comics|scimag)\b', url) + if topicmatch and topicmatch.group(): + topic = topicmatch.group() + + info['topic'] = topic + info['topicshort'] = re.sub(r'^foreign',"", topic) + info['topiclong'] = re.sub(r'^fiction',"foreignfiction", topic) + + # enrich with API call? + if self.config.get('query_api'): + self.log_debug("Enriching book info by calling libgen API") + api_info = self.libgen_api(info['topicshort'],info['md5']) + if api_info and api_info['id']: + # Add all info from API response... this will override any existing keys... + info.update(api_info) + + self.log_debug("File info for this download: {}".format(info)) + + return info + + + def process(self, pyfile): + url = re.sub(r'^(jd|py)', "http", pyfile.url) + self.log_debug("Start LibGen process for URL {}".format(url)) + # Normalize folder name + if isinstance(pyfile.package().folder, unicode): + pyfile.package().folder = unicode(normalize(pyfile.package().folder)) + self.log_debug("Using download folder {}".format(pyfile.package().folder)) + + + # Check if it's an md5 link (single download from the structured archive) or an unsorted comic + if re.search(r'/comics0/',url): + # It's an unsorted comic, download file or recurse through folder... + self.log_debug("This seems to be an unsorted comics link") + self.processComic(pyfile) + return + + # Get file info + self.log_debug("Detecting type for non-Comic URL {}".format(url)) + bookinfo = self.get_book_info(pyfile.url) + if not bookinfo['md5']: + self.fail("Unrecognizable URL") + return + + # Loop through mirrors + found = False + mirrors = self.config.get('mirrors').split() + + for mirror in mirrors: + url = mirror.format(**bookinfo) + self.log_debug("Trying mirror: "+url) + for _i in range(2): + try: + self.log_debug("Download attempt " + str(_i)) + self.download(url, disposition=True) + self.log_debug("Response: {:d}".format(self.req.code)) + + except BadHeader as e: + if e.code not in (400, 401, 403, 404, 410, 500, 503): + raise + + if self.req.code in (400,404,410): + self.log_warning("Not found on this mirror, skipping") + break + + elif self.req.code in (500,503): + self.log_warning("Temporary server error, retrying...") + time.sleep(5) + + else: + self.log_debug("Download successful") + found = True + break + + # Stop mirror iteration if success + if found: + break + + # End of the loop! + if not found: + self.log_error("Could not find a working mirror") + self.fail("No working mirror") + + else: + self.log_debug("End of download loop, checking download") + self.check_download() + + def check_download(self): + errmsg = self.scan_download({ + 'Html error': re.compile(r'(?i)\A(?:\s*<.+>)?((?:[\w\s]*(?:error)\s*\:?)?\s*\d{3})(?:\Z|\s+)'), + 'Html file': re.compile(r'(?i)\A\s*= max: + self.log_warning("Reached max link count for this package (%d/%d), skipping" % (nlinks,max)) + break + + self.log_debug("Detected new link") + + href = link.get('href') + self.log_debug("href: "+href) + + abslink = urlparse.urljoin(pyfile.url,href) + self.log_debug("Abslink: "+abslink) + + new_domain = urlparse.urlparse(abslink).netloc.lower() + self.log_debug("Domain: "+new_domain) + + if new_domain != domain: + self.log_debug("Different domain, ignoring link...") + break + + self.log_debug("Adding link "+abslink) + self.pyload.api.addFiles(pyfile.package().id, [abslink]) + + # Ignore this link as it's a directory page + self.skip("Link was a directory listing") diff --git a/module/plugins/hoster/LinkifierCom.py b/module/plugins/hoster/LinkifierCom.py new file mode 100644 index 0000000000..3a763ea1a3 --- /dev/null +++ b/module/plugins/hoster/LinkifierCom.py @@ -0,0 +1,62 @@ +# -*- coding: utf-8 -*- + +import hashlib +import pycurl + +from ..internal.MultiHoster import MultiHoster +from ..internal.misc import json, seconds_to_midnight + + +class LinkifierCom(MultiHoster): + __name__ = "LinkifierCom" + __type__ = "hoster" + __version__ = "0.04" + __status__ = "testing" + + __pattern__ = r'^unmatchable$' + __config__ = [("activated", "bool", "Activated", True), + ("use_premium", "bool", "Use premium account if available", True), + ("fallback", "bool", "Fallback to free download if premium fails", False), + ("chk_filesize", "bool", "Check file size", True), + ("max_wait", "int", "Reconnect if waiting time is greater than minutes", 10), + ("revert_failed", "bool", "Revert to standard download if fails", True)] + + __description__ = """Linkifier.com multi-hoster plugin""" + __license__ = "GPLv3" + __authors__ = [("GammaC0de", "nitzo2001[AT]yahoo[DOT]com")] + + API_KEY = "d046c4309bb7cabd19f49118a2ab25e0" + API_URL = "https://api.linkifier.com/downloadapi.svc/" + + def api_response(self, method, user, password, **kwargs): + post = {'login': user, + 'md5Pass': hashlib.md5(password).hexdigest(), + 'apiKey': self.API_KEY} + post.update(kwargs) + self.req.http.c.setopt(pycurl.HTTPHEADER, ["Content-Type: application/json; charset=utf-8"]) + res = json.loads(self.load(self.API_URL + method, + post=json.dumps(post))) + self.req.http.c.setopt(pycurl.HTTPHEADER, ["Content-Type: text/html; charset=utf-8"]) + return res + + def setup(self): + self.multiDL = True + + def handle_premium(self, pyfile): + json_data = self.api_response("stream", + self.account.user, + self.account.info['login']['password'], + url=pyfile.url) + + if json_data['hasErrors']: + error_msg = json_data['ErrorMSG'] or "Unknown error" + if error_msg in ("Customer reached daily limit for current hoster", + "Accounts are maxed out for current hoster"): + self.retry(wait=seconds_to_midnight()) + + self.fail(error_msg) + + self.resume_download = json_data['con_resume'] + self.chunk_limit = json_data.get('con_max', 1) or 1 + self.download(json_data['url']) + diff --git a/module/plugins/hoster/LinksnappyCom.py b/module/plugins/hoster/LinksnappyCom.py new file mode 100644 index 0000000000..c7910dc1f8 --- /dev/null +++ b/module/plugins/hoster/LinksnappyCom.py @@ -0,0 +1,97 @@ +# -*- coding: utf-8 -*- + +import time +import urlparse + +from ..internal.misc import format_size, json +from ..internal.MultiHoster import MultiHoster + + +class LinksnappyCom(MultiHoster): + __name__ = "LinksnappyCom" + __type__ = "hoster" + __version__ = "0.20" + __status__ = "testing" + + __pattern__ = r'https?://(?:[^/]+\.)?linksnappy\.com' + __config__ = [("activated", "bool", "Activated", True), + ("use_premium", "bool", "Use premium account if available", True), + ("fallback", "bool", "Fallback to free download if premium fails", False), + ("chk_filesize", "bool", "Check file size", True), + ("max_wait", "int", "Reconnect if waiting time is greater than minutes", 10), + ("revert_failed", "bool", "Revert to standard download if fails", True)] + + __description__ = """Linksnappy.com multi-hoster plugin""" + __license__ = "GPLv3" + __authors__ = [("stickell", "l.stickell@yahoo.it"), + ("Bilal Ghouri", None), + ("GammaC0de", "nitzo2001[AT]yahoo[DOT]com")] + + CHECK_TRAFFIC = True + + API_URL = "https://linksnappy.com/api/" + + def api_response(self, method, **kwargs): + return json.loads(self.load(self.API_URL + method, + get=kwargs)) + + def handle_premium(self, pyfile): + json_params = json.dumps({'link': pyfile.url}) + + api_data = self.api_response("linkgen", genLinks=json_params)['links'][0] + + if api_data['status'] != "OK": + self.fail(api_data['error']) + + if api_data.get('cacheDL', False): + self._cache_dl(api_data['hash']) + + pyfile.name = api_data['filename'] + self.link = api_data['generated'] + + def out_of_traffic(self): + url_p = urlparse.urlparse(self.pyfile.url) + json_data = self.api_response("FILEHOSTS") + + for k, v in json_data['return'].items(): + url = urlparse.urlunparse(url_p._replace(netloc=k)) + if self.pyload.pluginManager.plugins['hoster'][self.pyfile.pluginname]['re'].match(url) is not None: + quota = v['Quota'] + if quota == "unlimited": + return False + + else: + size = self.pyfile.size + usage = v.get('Usage', 0) + traffic_left = quota - usage + self.log_info(_("Filesize: %s") % format_size(size), + _("Traffic left for user `%s`: %s") % (self.account.user, format_size(traffic_left))) + + return size > traffic_left + + else: + self.log_warning(_("Could not determine traffic usage for host %s") % url_p.netloc) + return False + + def _cache_dl(self,file_hash): + self.pyfile.setCustomStatus("server dl") + self.pyfile.setProgress(0) + + while True: + api_data = self.api_response("CACHEDLSTATUS", id=file_hash) + + if api_data['status'] != "OK": + self.fail(api_data['error']) + + progress = api_data['return']['percent'] + self.pyfile.setProgress(progress) + if progress == 100: + break + + self._sleep(2) + + def _sleep(self, sec): + for _i in range(sec): + if self.pyfile.abort: + break + time.sleep(1) diff --git a/module/plugins/hoster/LinksnappyComTorrent.py b/module/plugins/hoster/LinksnappyComTorrent.py new file mode 100644 index 0000000000..1676a8b62b --- /dev/null +++ b/module/plugins/hoster/LinksnappyComTorrent.py @@ -0,0 +1,204 @@ +# -*- coding: utf-8 -*- + +import os +import time +import urllib + +from ..internal.Hoster import Hoster +from ..internal.misc import exists, json, parse_size + +try: + from module.network.HTTPRequest import FormFile +except ImportError: + pass + + + +class LinksnappyComTorrent(Hoster): + __name__ = "LinksnappyComTorrent" + __type__ = "hoster" + __version__ = "0.01" + __status__ = "testing" + + __pattern__ = r'^unmatchable$' + __config__ = [("activated", "bool", "Activated", True), + ("folder_per_package", "Default;Yes;No", "Create folder for each package", "Default"), + ("max_wait", "int", "Reconnect if waiting time is greater than minutes", 10), + ("del_finished", "bool", "Delete downloaded torrents from the server", True)] + + __description__ = """Linksnappy.com torrents crypter plugin""" + __license__ = "GPLv3" + __authors__ = [("GammaC0de", "nitzo2001[AT}yahoo[DOT]com")] + + API_URL = "https://linksnappy.com/api/" + + def api_response(self, method, **kwargs): + return json.loads(self.load(self.API_URL + method, + get=kwargs)) + + def sleep(self, sec): + for _i in range(sec): + if self.pyfile.abort: + break + time.sleep(1) + + def send_request_to_server(self): + """ Send torrent/magnet to the server """ + + if self.pyfile.url.endswith(".torrent"): + #: torrent URL + if self.pyfile.url.startswith("http"): + #: remote URL, download the torrent to tmp directory + api_data = self.api_response("torrents/ADDURL", url=self.pyfile.url).items()[0][1] + + if api_data['status'] == "FAILED" and api_data['error'] != "This torrent already exists in your account": + self.fail(api_data['error']) + + torrent_id = api_data['torrentid'] + + else: + #: URL is local torrent file (uploaded container) + torrent_filename = urllib.url2pathname(self.pyfile.url[7:]).encode('latin1').decode('utf8') #: trim the starting `file://` + if not exists(torrent_filename): + self.fail(_("Torrent file does not exist")) + + #: Check if the torrent file path is inside pyLoad's config directory + if os.path.abspath(torrent_filename).startswith(os.path.abspath(os.getcwd()) + os.sep): + try: + #: send the torrent content to the server + api_data = self.load("https://linksnappy.com/includes/ajaxupload.php", + post={'torrents[]': FormFile(torrent_filename, mimetype="application/octet-stream")}, + multipart=True) + api_data = json.loads(api_data).items()[0][1] + + except NameError: + self.fail(_("Posting file attachments is not supported by HTTPRequest, please update your pyLoad installation")) + + if api_data['error']: + self.fail(api_data['error']) + + torrent_id = api_data['torrentid'] + + else: + self.fail(_("Illegal URL")) #: We don't allow files outside pyLoad's config directory + + else: + #: magnet URL, send to the server + api_data = self.api_response("torrents/ADDMAGNET", magnetlinks=self.pyfile.url) + + if api_data['status'] != "OK": + self.fail(api_data['error']) + + api_data = api_data['return'][0] + + if api_data['status'] != "OK" and api_data['error'] != "This torrent already exists in your account": + self.fail(api_data['error']) + + torrent_id = api_data['torrentid'] + + return torrent_id + + def wait_for_server_dl(self, torrent_id): + """ Show progress while the server does the download """ + + api_data = self.api_response("torrents/STATUS", tid=torrent_id) + if api_data['status'] != "OK": + self.fail(api_data['error']) + + if api_data['return']['status'] == "ERROR": + self.fail(api_data['return']['error']) + + self.pyfile.name = api_data['return']['name'] + + self.pyfile.setCustomStatus("torrent") + self.pyfile.setProgress(0) + + if api_data['return']['status'] != "FINISHED": + api_data = self.api_response("torrents/START", tid=torrent_id) + if api_data['status'] != "OK": + if api_data['error'] == "Magnet URI processing in progress. Please wait.": + for _i in range(8): + self.sleep(3) + api_data = self.api_response("torrents/START", tid=torrent_id) + if api_data['status'] == "OK": + break + else: + self.fail(api_data['error']) + + elif api_data['error'] != "Already started.": + self.fail(api_data['error']) + + while True: + api_data = self.api_response("torrents/STATUS", tid=torrent_id) + if api_data['status'] != "OK": + self.fail(api_data['error']) + + if api_data['return']['status'] == "ERROR": + self.fail(api_data['return']['error']) + + torrent_size = api_data['return'].get('getSize') + if torrent_size is not None and self.pyfile.size == 0: + self.pyfile.size = parse_size(torrent_size) + + progress = int(api_data['return']['percentDone']) + self.pyfile.setProgress(progress) + + if api_data['return']['status'] == "FINISHED": + break + + self.sleep(2) + + self.pyfile.setProgress(100) + + self.sleep(1) + + self.pyfile.setCustomStatus("makezip") + self.pyfile.setProgress(0) + while True: + api_data = self.api_response("torrents/GENZIP", torrentid=torrent_id) + if api_data['status'] == "ERROR": + self.fail(api_data['error']) + + elif api_data['status'] == "PENDING": + self.sleep(2) + + else: + break + + self.pyfile.setProgress(100) + return api_data['return'] + + def delete_torrent_from_server(self, torrent_id): + """ Remove the torrent from the server """ + self.api_response("torrents/DELETETORRENT", tid=torrent_id, delFiles=1) + + def setup(self): + self.multiDL = True + self.resume_download = True + self.chunk_limit = 1 + + if 'LinksnappyCom' not in self.pyload.accountManager.plugins: + self.fail(_("This plugin requires an active Linksnappy.com account")) + + self.account = self.pyload.accountManager.getAccountPlugin("LinksnappyCom") + if len(self.account.accounts) == 0: + self.fail(_("This plugin requires an active Linksnappy.com account")) + + self.load_account() + + #: Use the cookiejar of account plugin (for the logged on session cookie) + cj = self.pyload.requestFactory.getCookieJar("LinksnappyCom", self.account.user) + self.req.setCookieJar(cj) + + def process(self, pyfile): + torrent_id = False + try: + torrent_id = self.send_request_to_server() + torrent_url = self.wait_for_server_dl(torrent_id) + + self.pyfile.name = os.path.basename(torrent_url) + self.download(torrent_url) + + finally: + if torrent_id is not False and self.config.get("del_finished"): + self.delete_torrent_from_server(torrent_id) diff --git a/module/plugins/hoster/LoadTo.py b/module/plugins/hoster/LoadTo.py index 0f99c272a2..4303a94398 100644 --- a/module/plugins/hoster/LoadTo.py +++ b/module/plugins/hoster/LoadTo.py @@ -1,61 +1,67 @@ # -*- 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 . - - @author: halfman -""" - -# Test links (random.bin): -# http://www.load.to/dNsmgXRk4/random.bin -# http://www.load.to/edbNTxcUb/random100.bin +# +# Test links: +# http://www.load.to/JWydcofUY6/random.bin +# http://www.load.to/oeSmrfkXE/random100.bin import re -from module.plugins.internal.SimpleHoster import SimpleHoster, create_getInfo + +from ..captcha.SolveMedia import SolveMedia +from ..internal.SimpleHoster import SimpleHoster class LoadTo(SimpleHoster): __name__ = "LoadTo" __type__ = "hoster" - __pattern__ = r"http://(?:www\.)?load\.to/\w+" - __version__ = "0.12" - __description__ = """Load.to hoster plugin""" - __author_name__ = ("halfman", "stickell") - __author_mail__ = ("Pulpan3@gmail.com", "l.stickell@yahoo.it") - - FILE_INFO_PATTERN = r']+>(?P.+)\s*Size: (?P\d+) Bytes' - URL_PATTERN = r'
    ' - WAIT_PATTERN = r'type="submit" value="Download \((\d+)\)"' - - def setup(self): - self.multiDL = False - - def process(self, pyfile): + __version__ = "0.29" + __status__ = "testing" - self.html = self.load(pyfile.url, decode=True) + __pattern__ = r'http://(?:www\.)?load\.to/\w+' + __config__ = [("activated", "bool", "Activated", True), + ("use_premium", "bool", "Use premium account if available", True), + ("fallback", "bool", + "Fallback to free download if premium fails", True), + ("chk_filesize", "bool", "Check file size", True), + ("max_wait", "int", "Reconnect if waiting time is greater than minutes", 10)] - found = re.search(self.URL_PATTERN, self.html) - if not found: - self.parseError('URL') - download_url = found.group(1) + __description__ = """Load.to hoster plugin""" + __license__ = "GPLv3" + __authors__ = [("halfman", "Pulpan3@gmail.com"), + ("stickell", "l.stickell@yahoo.it")] - timmy = re.search(self.WAIT_PATTERN, self.html) - if timmy: - self.setWait(timmy.group(1)) - self.wait() + NAME_PATTERN = r'

    (?P.+?)

    ' + SIZE_PATTERN = r'Size: (?P[\d.,]+) (?P[\w^_]+)' + OFFLINE_PATTERN = r'>Can\'t find file' - self.download(download_url, disposition=True) + LINK_FREE_PATTERN = r'\d{10,})" - __version__ = "0.02" - __description__ = """LuckyShare.net Download Hoster""" - __author_name__ = ("stickell") - __author_mail__ = ("l.stickell@yahoo.it") - - FILE_INFO_PATTERN = r"

    (?P\S+)

    \s*Filesize: (?P[\d.]+)(?P\w+)" - FILE_OFFLINE_PATTERN = 'There is no such file available' - RECAPTCHA_KEY = '6LdivsgSAAAAANWh-d7rPE1mus4yVWuSQIJKIYNw' - - def parseJson(self, rep): - if 'AJAX Error' in rep: - html = self.load(self.pyfile.url, decode=True) - m = re.search(r"waitingtime = (\d+);", html) - if m: - waittime = int(m.group(1)) - self.logDebug('You have to wait %d seconds between free downloads' % waittime) - self.retry(wait_time=waittime) - else: - self.parseError('Unable to detect wait time between free downloads') - elif 'Hash expired' in rep: - self.retry(reason='Hash expired') - return json_loads(rep) - - # TODO: There should be a filesize limit for free downloads - # TODO: Some files could not be downloaded in free mode - def handleFree(self): - file_id = re.search(self.__pattern__, self.pyfile.url).group('ID') - self.logDebug('File ID: ' + file_id) - rep = self.load(r"http://luckyshare.net/download/request/type/time/file/" + file_id, decode=True) - self.logDebug('JSON: ' + rep) - json = self.parseJson(rep) - - self.setWait(int(json['time'])) - self.wait() - - recaptcha = ReCaptcha(self) - for i in xrange(5): - challenge, response = recaptcha.challenge(self.RECAPTCHA_KEY) - rep = self.load(r"http://luckyshare.net/download/verify/challenge/%s/response/%s/hash/%s" % - (challenge, response, json['hash']), decode=True) - self.logDebug('JSON: ' + rep) - if 'link' in rep: - json.update(self.parseJson(rep)) - self.correctCaptcha() - break - elif 'Verification failed' in rep: - self.logInfo('Wrong captcha') - self.invalidCaptcha() - else: - self.parseError('Unable to get downlaod link') - - if not json['link']: - self.fail("No Download url retrieved/all captcha attempts failed") - - self.logDebug('Direct URL: ' + json['link']) - self.download(json['link']) - - -getInfo = create_getInfo(LuckyShareNet) diff --git a/module/plugins/hoster/MediafireCom.py b/module/plugins/hoster/MediafireCom.py index 494d0049e9..4c82f9f14f 100644 --- a/module/plugins/hoster/MediafireCom.py +++ b/module/plugins/hoster/MediafireCom.py @@ -1,137 +1,93 @@ # -*- 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. +import base64 +import re - 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. +from ..captcha.ReCaptcha import ReCaptcha +from ..captcha.SolveMedia import SolveMedia +from ..internal.SimpleHoster import SimpleHoster - You should have received a copy of the GNU General Public License - along with this program; if not, see . - @author: zoidberg -""" +class MediafireCom(SimpleHoster): + __name__ = "MediafireCom" + __type__ = "hoster" + __version__ = "1.01" + __status__ = "testing" -import re -from module.plugins.internal.SimpleHoster import SimpleHoster, parseFileInfo -from module.plugins.internal.CaptchaService import SolveMedia -from module.network.RequestFactory import getURL - - -def replace_eval(js_expr): - return js_expr.replace(r'eval("', '').replace(r"\'", r"'").replace(r'\"', r'"') - - -def checkHTMLHeader(url): - try: - for i in range(3): - header = getURL(url, just_header=True) - for line in header.splitlines(): - line = line.lower() - if 'location' in line: - url = line.split(':', 1)[1].strip() - if 'error.php?errno=320' in url: - return url, 1 - if not url.startswith('http://'): - url = 'http://www.mediafire.com' + url - break - elif 'content-disposition' in line: - return url, 2 - else: - break - except: - return url, 3 + __pattern__ = r'https?://(?:www\.)?mediafire\.com/(file/|view/\??|download(\.php\?|/)|\?)(?P\w+)' + __config__ = [("activated", "bool", "Activated", True), + ("use_premium", "bool", "Use premium account if available", True), + ("fallback", "bool", "Fallback to free download if premium fails", True), + ("chk_filesize", "bool", "Check file size", True), + ("max_wait", "int", "Reconnect if waiting time is greater than minutes", 10)] - return url, 0 + __description__ = """Mediafire.com hoster plugin""" + __license__ = "GPLv3" + __authors__ = [("zoidberg", "zoidberg@mujmail.cz"), + ("stickell", "l.stickell@yahoo.it"), + ("Walter Purcaro", "vuolter@gmail.com"), + ("GammaC0de", "nitzo2001[AT]yahoo[DOT]com")] + NAME_PATTERN = r'
    (?P.+?)
    ' + SIZE_PATTERN = r'>File size: (?P[\d.,]+)(?P[\w^_]+)<' -def getInfo(urls): - for url in urls: - location, status = checkHTMLHeader(url) - if status: - file_info = (url, 0, status, url) - else: - file_info = parseFileInfo(MediafireCom, url, getURL(url, decode=True)) - yield file_info + TEMP_OFFLINE_PATTERN = r'^unmatchable$' + OFFLINE_PATTERN = r'class="error_msg_title"' + LINK_FREE_PATTERN = r'aria-label="Download file"\s+href="(.+?)"' -class MediafireCom(SimpleHoster): - __name__ = "MediafireCom" - __type__ = "hoster" - __pattern__ = r"http://(?:\w*\.)*mediafire\.com/(file/|(view/?|download.php)?\?)(\w{11}|\w{15})($|/)" - __version__ = "0.79" - __description__ = """Mediafire.com plugin - free only""" - __author_name__ = ("zoidberg", "stickell") - __author_mail__ = ("zoidberg@mujmail.cz", "l.stickell@yahoo.it") - - DOWNLOAD_LINK_PATTERN = r'' - def setup(self): - self.multiDL = False + self.resume_download = True + self.multiDL = True - def process(self, pyfile): - pyfile.url = re.sub(r'/view/?\?', '/?', pyfile.url) + def handle_captcha(self): + solvemedia = SolveMedia(self.pyfile) + captcha_key = solvemedia.detect_key() - self.url, result = checkHTMLHeader(pyfile.url) - self.logDebug('Location (%d): %s' % (result, self.url)) + if captcha_key: + self.captcha = solvemedia + response, challenge = solvemedia.challenge(captcha_key) + self.data = self.load("http://www.mediafire.com/?" + self.info['pattern']['ID'], + post={'adcopy_challenge': challenge, + 'adcopy_response': response}) + return - if result == 0: - self.html = self.load(self.url, decode=True) - self.checkCaptcha() - self.multiDL = True - self.check_data = self.getFileInfo() + recaptcha = ReCaptcha(self.pyfile) + captcha_key = recaptcha.detect_key() + + if captcha_key: + url, inputs = self.parse_html_form('name="form_captcha"') + self.log_debug(("form_captcha url:%s inputs:%s") % (url, inputs)) + + if url: + self.captcha = recaptcha + response = recaptcha.challenge(captcha_key) + + inputs['g-recaptcha-response'] = response + self.data = self.load(self.fixurl(url), post=inputs) - if self.account: - self.handlePremium() - else: - self.handleFree() - elif result == 1: - self.offline() - else: - self.multiDL = True - self.download(self.url, disposition=True) - - def handleFree(self): - passwords = self.getPassword().splitlines() - while self.PASSWORD_PATTERN in self.html: - if len(passwords): - password = passwords.pop(0) - self.logInfo("Password protected link, trying " + password) - self.html = self.load(self.url, post={"downloadp": password}) else: - self.fail("No or incorrect password") - - found = re.search(r'kNO = "(http://.*?)";', self.html) - if not found: - self.parseError("Download URL") - download_url = found.group(1) - self.logDebug("DOWNLOAD LINK:", download_url) - - self.download(download_url) - - def checkCaptcha(self): - for i in xrange(5): - found = re.search(self.SOLVEMEDIA_PATTERN, self.html) - if found: - captcha_key = found.group(1) - solvemedia = SolveMedia(self) - captcha_challenge, captcha_response = solvemedia.challenge(captcha_key) - self.html = self.load(self.url, post={"adcopy_challenge": captcha_challenge, - "adcopy_response": captcha_response}, decode=True) + self.fail("ReCaptcha form not found") + + def handle_free(self, pyfile): + self.handle_captcha() + + if self.PASSWORD_PATTERN in self.data: + password = self.get_password() + + if not password: + self.fail(_("No password found")) else: - break + self.log_info(_("Password protected link, trying: %s") % password) + self.data = self.load(self.link, post={'downloadp': password}) + + if self.PASSWORD_PATTERN in self.data: + self.fail(_("Wrong password")) + + m = re.search(r'data-scrambled-url="([^"]+)"', self.data) + if m is not None: + self.link = base64.b64decode(m.group(1)).decode() + else: - self.fail("No valid recaptcha solution received") + SimpleHoster.handle_free(self, pyfile) diff --git a/module/plugins/hoster/MegaCoNz.py b/module/plugins/hoster/MegaCoNz.py new file mode 100644 index 0000000000..cc3105a52a --- /dev/null +++ b/module/plugins/hoster/MegaCoNz.py @@ -0,0 +1,460 @@ +# -*- coding: utf-8 -*- + +import base64 +import os +import random +import re +import struct + +import Crypto.Cipher.AES +import Crypto.Util.Counter + +from module.network.HTTPRequest import BadHeader + +from ..internal.Hoster import Hoster +from ..internal.misc import decode, exists, fs_encode, fsjoin, json + + +############################ General errors ################################### +# EINTERNAL (-1): An internal error has occurred. Please submit a bug report, detailing the exact circumstances in which this error occurred +# EARGS (-2): You have passed invalid arguments to this command +# EAGAIN (-3): (always at the request level) A temporary congestion or server malfunction prevented your request from being processed. No data was altered. Retry. Retries must be spaced with exponential backoff +# ERATELIMIT (-4): You have exceeded your command weight per time quota. Please wait a few seconds, then try again (this should never happen in sane real-life applications) +# +############################ Upload errors #################################### +# EFAILED (-5): The upload failed. Please restart it from scratch +# ETOOMANY (-6): Too many concurrent IP addresses are accessing this upload target URL +# ERANGE (-7): The upload file packet is out of range or not starting and ending on a chunk boundary +# EEXPIRED (-8): The upload target URL you are trying to access has expired. Please request a fresh one +# +############################ Stream/System errors ############################# +# ENOENT (-9): Object (typically, node or user) not found +# ECIRCULAR (-10): Circular linkage attempted +# EACCESS (-11): Access violation (e.g., trying to write to a read-only share) +# EEXIST (-12): Trying to create an object that already exists +# EINCOMPLETE (-13): Trying to access an incomplete resource +# EKEY (-14): A decryption operation failed (never returned by the API) +# ESID (-15): Invalid or expired user session, please relogin +# EBLOCKED (-16): User blocked +# EOVERQUOTA (-17): Request over quota +# ETEMPUNAVAIL (-18): Resource temporarily not available, please try again later +# ETOOMANYCONNECTIONS (-19): Too many connections on this resource +# EWRITE (-20): Write failed +# EREAD (-21): Read failed +# EAPPKEY (-22): Invalid application key; request not processed +# ESSL (-23): SSL verification failed +# EGOINGOVERQUOTA (-24): Not enough quota +# EMFAREQUIRED (-26): Multi-factor authentication required + +class MegaCrypto(object): + + @staticmethod + def base64_decode(data): + #: Add padding, we need a string with a length multiple of 4 + data += '=' * (-len(data) % 4) + return base64.b64decode(str(data), "-_") + + @staticmethod + def base64_encode(data): + return base64.b64encode(data, "-_") + + @staticmethod + def a32_to_str(a): + return struct.pack(">%dI" % len(a), *a) #: big-endian, unsigned int + + @staticmethod + def str_to_a32(s): + # Add padding, we need a string with a length multiple of 4 + s += '\0' * (-len(s) % 4) + #: big-endian, unsigned int + return struct.unpack(">%dI" % (len(s) / 4), s) + + @staticmethod + def a32_to_base64(a): + return MegaCrypto.base64_encode(MegaCrypto.a32_to_str(a)) + + @staticmethod + def base64_to_a32(s): + return MegaCrypto.str_to_a32(MegaCrypto.base64_decode(s)) + + @staticmethod + def cbc_decrypt(data, key): + cbc = Crypto.Cipher.AES.new(MegaCrypto.a32_to_str(key), Crypto.Cipher.AES.MODE_CBC, "\0" * 16) + return cbc.decrypt(data) + + @staticmethod + def cbc_encrypt(data, key): + cbc = Crypto.Cipher.AES.new(MegaCrypto.a32_to_str(key), Crypto.Cipher.AES.MODE_CBC, "\0" * 16) + return cbc.encrypt(data) + + @staticmethod + def ecb_decrypt(data, key): + ecb = Crypto.Cipher.AES.new(MegaCrypto.a32_to_str(key), Crypto.Cipher.AES.MODE_ECB) + return ecb.decrypt(data) + + @staticmethod + def ecb_encrypt(data, key): + ecb = Crypto.Cipher.AES.new(MegaCrypto.a32_to_str(key), Crypto.Cipher.AES.MODE_ECB) + return ecb.encrypt(data) + + @staticmethod + def get_cipher_key(key): + """ + Construct the cipher key from the given data + """ + k = (key[0] ^ key[4], + key[1] ^ key[5], + key[2] ^ key[6], + key[3] ^ key[7]) + iv = key[4:6] + (0, 0) + meta_mac = key[6:8] + + return k, iv, meta_mac + + @staticmethod + def decrypt_attr(data, key): + """ + Decrypt an encrypted attribute (usually 'a' or 'at' member of a node) + """ + data = MegaCrypto.base64_decode(data) + k, iv, meta_mac = MegaCrypto.get_cipher_key(key) + attr = MegaCrypto.cbc_decrypt(data, k) + + #: Data is padded, 0-bytes must be stripped + return json.loads(re.search(r'{.+}', attr).group(0)) if attr[:6] == 'MEGA{"' else False + + @staticmethod + def decrypt_key(data, key): + """ + Decrypt an encrypted key ('k' member of a node) + """ + data = MegaCrypto.base64_decode(data) + return MegaCrypto.str_to_a32(MegaCrypto.ecb_decrypt(data, key)) + + @staticmethod + def encrypt_key(data, key): + """ + Encrypt a decrypted key + """ + data = MegaCrypto.a32_to_str(data) + return MegaCrypto.str_to_a32(MegaCrypto.ecb_encrypt(data, key)) + + @staticmethod + def get_chunks(size): + """ + Calculate chunks for a given encrypted file size + """ + chunk_start = 0 + chunk_size = 0x20000 + + while chunk_start + chunk_size < size: + yield (chunk_start, chunk_size) + chunk_start += chunk_size + if chunk_size < 0x100000: + chunk_size += 0x20000 + + if chunk_start < size: + yield (chunk_start, size - chunk_start) + + class Checksum(object): + """ + interface for checking CBC-MAC checksum + """ + + def __init__(self, key): + k, iv, meta_mac = MegaCrypto.get_cipher_key(key) + self.hash = '\0' * 16 + self.key = MegaCrypto.a32_to_str(k) + self.iv = MegaCrypto.a32_to_str(iv[0:2] * 2) + self.AES = Crypto.Cipher.AES.new(self.key, mode=Crypto.Cipher.AES.MODE_CBC, IV=self.hash) + + def update(self, chunk): + cbc = Crypto.Cipher.AES.new(self.key, mode=Crypto.Cipher.AES.MODE_CBC, IV=self.iv) + for j in range(0, len(chunk), 16): + block = chunk[j:j + 16].ljust(16, '\0') + hash = cbc.encrypt(block) + + self.hash = self.AES.encrypt(hash) + + def digest(self): + """ + Return the **binary** (non-printable) CBC-MAC of the message that has been authenticated so far. + """ + d = MegaCrypto.str_to_a32(self.hash) + return (d[0] ^ d[1], d[2] ^ d[3]) + + def hexdigest(self): + """ + Return the **printable** CBC-MAC of the message that has been authenticated so far. + """ + return "".join("%02x" % ord(x) + for x in MegaCrypto.a32_to_str(self.digest())) + + @staticmethod + def new(key): + return MegaCrypto.Checksum(key) + + +class MegaClient(object): + API_URL = "https://eu.api.mega.co.nz/cs" + + def __init__(self, plugin, node_id): + self.plugin = plugin + self.node_id = node_id + + def api_response(self, **kwargs): + """ + Dispatch a call to the api, see https://mega.co.nz/#developers + """ + uid = random.randint(10 << 9, 10 ** 10) #: Generate a session id, no idea where to obtain elsewhere + get_params = {'id': uid} + + if self.node_id: + get_params['n'] = self.node_id + + if hasattr(self.plugin, 'account'): + if self.plugin.account: + mega_session_id = self.plugin.account.info['data'].get('mega_session_id', None) + + else: + mega_session_id = None + + else: + mega_session_id = self.plugin.info['data'].get('mega_session_id', None) + + if mega_session_id: + get_params['sid'] = mega_session_id + + try: + res = self.plugin.load(self.API_URL, + get=get_params, + post=json.dumps([kwargs])) + + except BadHeader, e: + if e.code == 500: + self.plugin.retry(wait_time=60, reason=_("Server busy")) + else: + raise + + self.plugin.log_debug("Api Response: " + res) + + res = json.loads(res) + if isinstance(res, list): + res = res[0] + + return res + + def check_error(self, code): + ecode = abs(code) + + if ecode in (9, 16, 21): + self.plugin.offline() + + elif ecode in (3, 13, 17, 18, 19, 24): + self.plugin.temp_offline() + + elif ecode in (1, 4, 6, 10, 15): + self.plugin.retry(max_tries=5, wait_time=30, reason=_("Error code: [%s]") % -ecode) + + else: + self.plugin.fail(_("Error code: [%s]") % -ecode) + + +class MegaCoNz(Hoster): + __name__ = "MegaCoNz" + __type__ = "hoster" + __version__ = "0.58" + __status__ = "testing" + + __pattern__ = r'https?://(?:www\.)?mega(?:\.co)?\.nz/(?:file/(?P[\w^_]+)#(?P[\w\-,=]+)|folder/(?P[\w^_]+)#(?P[\w\-,=]+)/file/(?P[\w^_]+)|#!(?P[\w^_]+)!(?P[\w\-,=]+))' + __config__ = [("activated", "bool", "Activated", True)] + + __description__ = """Mega.co.nz hoster plugin""" + __license__ = "GPLv3" + __authors__ = [("RaNaN", "ranan@pyload.net"), + ("Walter Purcaro", "vuolter@gmail.com"), + ("GammaC0de", "nitzo2001[AT}yahoo[DOT]com")] + + FILE_SUFFIX = ".crypted" + + def decrypt_file(self, key): + """ + Decrypts and verifies checksum to the file at 'last_download' + """ + k, iv, meta_mac = MegaCrypto.get_cipher_key(key) + ctr = Crypto.Util.Counter.new(128, initial_value=((iv[0] << 32) + iv[1]) << 64) + cipher = Crypto.Cipher.AES.new(MegaCrypto.a32_to_str(k), Crypto.Cipher.AES.MODE_CTR, counter=ctr) + + self.pyfile.setStatus("decrypting") + self.pyfile.setProgress(0) + + file_crypted = self.last_download + file_decrypted = file_crypted.rsplit(self.FILE_SUFFIX)[0] + + try: + f = open(fs_encode(file_crypted), "rb") + df = open(fs_encode(file_decrypted), "wb") + + except IOError, e: + self.fail(e.message) + + encrypted_size = os.path.getsize(file_crypted) + + checksum_activated = self.config.get("activated", default=False, plugin="Checksum") + check_checksum = self.config.get("check_checksum", default=True, plugin="Checksum") + + cbc_mac = MegaCrypto.Checksum(key) if checksum_activated and check_checksum else None + + progress = 0 + for chunk_start, chunk_size in MegaCrypto.get_chunks(encrypted_size): + buf = f.read(chunk_size) + if not buf: + break + + chunk = cipher.decrypt(buf) + df.write(chunk) + + progress += chunk_size + self.pyfile.setProgress(int((100.0 / encrypted_size) * progress)) + + if checksum_activated and check_checksum: + cbc_mac.update(chunk) + + self.pyfile.setProgress(100) + + f.close() + df.close() + + self.log_info(_("File decrypted")) + os.remove(file_crypted) + + if checksum_activated and check_checksum: + file_mac = cbc_mac.digest() + if file_mac == meta_mac: + self.log_info(_('File integrity of "%s" verified by CBC-MAC checksum (%s)') % + (self.pyfile.name.rsplit(self.FILE_SUFFIX)[0], meta_mac)) + else: + self.log_warning(_('CBC-MAC checksum for file "%s" does not match (%s != %s)') % + (self.pyfile.name.rsplit(self.FILE_SUFFIX)[0], file_mac, meta_mac)) + self.checksum_failed(file_decrypted, _("Checksums do not match")) + + self.last_download = decode(file_decrypted) + + def checksum_failed(self, local_file, msg): + check_action = self.config.get("check_action", default="retry", plugin="Checksum") + + if check_action == "retry": + max_tries = self.config.get("max_tries", default=2, plugin="Checksum") + retry_action = self.config.get("retry_action", default="fail", plugin="Checksum") + + if all(_r < max_tries for _id, _r in self.retries.items()): + os.remove(local_file) + wait_time = self.config.get("wait_time", default=1, plugin="Checksum") + self.retry(max_tries, wait_time, msg) + + elif retry_action == "nothing": + return + + elif check_action == "nothing": + return + + os.remove(local_file) + self.fail(msg) + + def check_exists(self, name): + """ + Because of Mega downloads a temporary encrypted file with the extension of '.crypted', + pyLoad cannot correctly detect if the file exists before downloading. + This function corrects this. + + Raises Skip() if file exists and 'skip_existing' configuration option is set to True. + """ + if self.pyload.config.get("download", "skip_existing"): + download_folder = self.pyload.config.get('general', 'download_folder') + dest_file = fsjoin(download_folder, + self.pyfile.package().folder if self.pyload.config.get("general", "folder_per_package") else "", + name) + if exists(dest_file): + self.pyfile.name = name + self.skip(_("File exists.")) + + def process(self, pyfile): + node_id = self.info['pattern']['NID'] + public = node_id in ("", None) + id = self.info['pattern']['ID1'] or self.info['pattern']['ID2'] or self.info['pattern']['ID3'] + key = self.info['pattern']['K1'] or self.info['pattern']['K2'] or self.info['pattern']['K3'] + + self.log_debug("ID: %s" % id, + "Key: %s" % key, + "Type: %s" % ("public" if public else "node"), + "Owner: %s" % node_id) + + mega = MegaClient(self, id) + + master_key = MegaCrypto.base64_to_a32(key) + if not public: + #: F is for requesting folder listing (kind like a `ls` command) + res = mega.api_response(a="f", c=1, r=1, ca=1, ssl=1) + if isinstance(res, int): + mega.check_error(res) + elif isinstance(res, dict) and 'e' in res: + mega.check_error(res['e']) + + for node in res['f']: + if node['t'] == 0 and ":" in node["k"] and node['h'] == node_id: + master_key = MegaCrypto.decrypt_key(node['k'][node['k'].index(':') + 1:], master_key) + break + + else: + self.offline() + + if len(master_key) != 8: + self.log_error(_("Invalid key length")) + self.fail(_("Invalid key length")) + + #: G is for requesting a download url + #: This is similar to the calls in the mega js app, documentation is very bad + if public: + res = mega.api_response(a="g", g=1, p=id, ssl=1) + else: + res = mega.api_response(a="g", g=1, n=node_id, ssl=1) + + if isinstance(res, int): + mega.check_error(res) + elif isinstance(res, dict) and 'e' in res: + mega.check_error(res['e']) + + attr = MegaCrypto.decrypt_attr(res['at'], master_key) + if not attr: + self.fail(_("Decryption failed")) + + self.log_debug("Decrypted Attr: %s" % decode(attr)) + + name = attr['n'] + + self.check_exists(name) + + pyfile.name = name + self.FILE_SUFFIX + pyfile.size = res['s'] + + time_left = res.get('tl', 0) + if time_left: + self.log_warning(_("Free download limit reached")) + self.retry(wait=time_left, msg=_("Free download limit reached")) + + # self.req.http.c.setopt(pycurl.SSL_CIPHER_LIST, "RC4-MD5:DEFAULT") + + try: + self.download(res['g'], disposition=False) + + except BadHeader, e: + if e.code == 509: + self.fail(_("Bandwidth Limit Exceeded")) + + else: + raise + + self.decrypt_file(master_key) + + #: Everything is finished and final name can be set + pyfile.name = name diff --git a/module/plugins/hoster/MegaDebridEu.py b/module/plugins/hoster/MegaDebridEu.py new file mode 100644 index 0000000000..181886b673 --- /dev/null +++ b/module/plugins/hoster/MegaDebridEu.py @@ -0,0 +1,72 @@ +# -*- coding: utf-8 -*- + +import pycurl +from module.network.HTTPRequest import BadHeader + +from ..internal.misc import json +from ..internal.MultiHoster import MultiHoster + + +def args(**kwargs): + return kwargs + + +class MegaDebridEu(MultiHoster): + __name__ = "MegaDebridEu" + __type__ = "hoster" + __version__ = "0.60" + __status__ = "testing" + + __pattern__ = r'http://((?:www\d+\.|s\d+\.)?mega-debrid\.eu|\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})/download/file/[\w^_]+' + __config__ = [("activated", "bool", "Activated", True), + ("use_premium", "bool", "Use premium account if available", True), + ("fallback", "bool", "Fallback to free download if premium fails", False), + ("chk_filesize", "bool", "Check file size", True), + ("max_wait", "int", "Reconnect if waiting time is greater than minutes", 10), + ("revert_failed", "bool", "Revert to standard download if fails", True)] + + __description__ = """Mega-debrid.eu multi-hoster plugin""" + __license__ = "GPLv3" + __authors__ = [("Devirex Hazzard", "naibaf_11@yahoo.de"), + ("GammaC0de", "nitzo2001[AT]yahoo[DOT]com"), + ("FoxyDarnec", "goupildavid[AT]gmail[DOT]com")] + + API_URL = "https://www.mega-debrid.eu/api.php" + + def api_response(self, action, get={}, post={}): + get['action'] = action + + # Better use pyLoad User-Agent so we don't get blocked + self.req.http.c.setopt(pycurl.USERAGENT, "pyLoad/%s" % self.pyload.version) + + json_data = self.load(self.API_URL, get=get, post=post) + + return json.loads(json_data) + + def handle_premium(self, pyfile): + try: + res = self.api_response("getLink", + get=args(token=self.account.info['data']['cache_info'][self.account.user]['token']), + post=args(link=pyfile.url)) + + except BadHeader, e: + if e.code == 405: + self.fail(_("Banned IP")) + + else: + raise + + if res['response_code'] == "ok": + self.link = res['debridLink'] + + elif res['response_code'] == "TOKEN_ERROR": + self.account.relogin() + self.retry() + + elif res['response_code'] == "UNALLOWED_IP": + self.fail(_("Banned IP")) + + else: + self.log_error(res['response_text']) + self.fail(res['response_text']) + diff --git a/module/plugins/hoster/MegaNz.py b/module/plugins/hoster/MegaNz.py deleted file mode 100644 index bf4223213f..0000000000 --- a/module/plugins/hoster/MegaNz.py +++ /dev/null @@ -1,132 +0,0 @@ -# -*- coding: utf-8 -*- - -import re -import random -from array import array -from os import remove -from base64 import standard_b64decode - -from Crypto.Cipher import AES -from Crypto.Util import Counter - -from module.common.json_layer import json_loads, json_dumps -from module.plugins.Hoster import Hoster - -#def getInfo(urls): -# pass - - -class MegaNz(Hoster): - __name__ = "MegaNz" - __type__ = "hoster" - __pattern__ = r"https?://([a-z0-9]+\.)?mega\.co\.nz/#!([a-zA-Z0-9!_\-]+)" - __version__ = "0.14" - __description__ = """mega.co.nz hoster plugin""" - __author_name__ = ("RaNaN", ) - __author_mail__ = ("ranan@pyload.org", ) - - API_URL = "https://g.api.mega.co.nz/cs?id=%d" - FILE_SUFFIX = ".crypted" - - def b64_decode(self, data): - data = data.replace("-", "+").replace("_", "/") - return standard_b64decode(data + '=' * (-len(data) % 4)) - - def getCipherKey(self, key): - """ Construct the cipher key from the given data """ - a = array("I", key) - key_array = array("I", [a[0] ^ a[4], a[1] ^ a[5], a[2] ^ a[6], a[3] ^ a[7]]) - return key_array - - def callApi(self, **kwargs): - """ Dispatch a call to the api, see https://mega.co.nz/#developers """ - # generate a session id, no idea where to obtain elsewhere - uid = random.randint(10 << 9, 10 ** 10) - - resp = self.load(self.API_URL % uid, post=json_dumps([kwargs])) - self.logDebug("Api Response: " + resp) - return json_loads(resp) - - def decryptAttr(self, data, key): - - cbc = AES.new(self.getCipherKey(key), AES.MODE_CBC, "\0" * 16) - attr = cbc.decrypt(self.b64_decode(data)) - self.logDebug("Decrypted Attr: " + attr) - if not attr.startswith("MEGA"): - self.fail(_("Decryption failed")) - - # Data is padded, 0-bytes must be stripped - return json_loads(attr.replace("MEGA", "").rstrip("\0").strip()) - - def decryptFile(self, key): - """ Decrypts the file at lastDownload` """ - - # upper 64 bit of counter start - n = key[16:24] - - # convert counter to long and shift bytes - ctr = Counter.new(128, initial_value=long(n.encode("hex"), 16) << 64) - cipher = AES.new(self.getCipherKey(key), AES.MODE_CTR, counter=ctr) - - self.pyfile.setStatus("decrypting") - - file_crypted = self.lastDownload - file_decrypted = file_crypted.rsplit(self.FILE_SUFFIX)[0] - f = open(file_crypted, "rb") - df = open(file_decrypted, "wb") - - # TODO: calculate CBC-MAC for checksum - - size = 2 ** 15 # buffer size, 32k - while True: - buf = f.read(size) - if not buf: - break - - df.write(cipher.decrypt(buf)) - - f.close() - df.close() - remove(file_crypted) - - self.lastDownload = file_decrypted - - def process(self, pyfile): - - key = None - - # match is guaranteed because plugin was chosen to handle url - node = re.search(self.__pattern__, pyfile.url).group(2) - if "!" in node: - node, key = node.split("!") - - self.logDebug("File id: %s | Key: %s" % (node, key)) - - if not key: - self.fail(_("No file key provided in the URL")) - - # g is for requesting a download url - # this is similar to the calls in the mega js app, documentation is very bad - dl = self.callApi(a="g", g=1, p=node, ssl=1)[0] - - if "e" in dl: - e = dl["e"] - # ETEMPUNAVAIL (-18): Resource temporarily not available, please try again later - if e == -18: - self.retry() - else: - self.fail(_("Error code:") + e) - - # TODO: map other error codes, e.g - # EACCESS (-11): Access violation (e.g., trying to write to a read-only share) - - key = self.b64_decode(key) - attr = self.decryptAttr(dl["at"], key) - - pyfile.name = attr["n"] + self.FILE_SUFFIX - - self.download(dl["g"]) - self.decryptFile(key) - - # Everything is finished and final name can be set - pyfile.name = attr["n"] diff --git a/module/plugins/hoster/MegaRapidCz.py b/module/plugins/hoster/MegaRapidCz.py new file mode 100644 index 0000000000..f775af7adb --- /dev/null +++ b/module/plugins/hoster/MegaRapidCz.py @@ -0,0 +1,53 @@ +# -*- coding: utf-8 -*- + +import re + +from ..internal.SimpleHoster import SimpleHoster + + +class MegaRapidCz(SimpleHoster): + __name__ = "MegaRapidCz" + __type__ = "hoster" + __version__ = "0.64" + __status__ = "testing" + + __pattern__ = r'http://(?:www\.)?(share|mega)rapid\.cz/soubor/\d+/.+' + __config__ = [("activated", "bool", "Activated", True), + ("use_premium", "bool", "Use premium account if available", True), + ("fallback", "bool", + "Fallback to free download if premium fails", True), + ("chk_filesize", "bool", "Check file size", True), + ("max_wait", "int", "Reconnect if waiting time is greater than minutes", 10)] + + __description__ = """MegaRapid.cz hoster plugin""" + __license__ = "GPLv3" + __authors__ = [("MikyWoW", "mikywow@seznam.cz"), + ("zoidberg", "zoidberg@mujmail.cz"), + ("stickell", "l.stickell@yahoo.it"), + ("Walter Purcaro", "vuolter@gmail.com")] + + NAME_PATTERN = r'(?:)?(?P.+?)' + SIZE_PATTERN = r'Velikost:\s*\s*(?P[\d.,]+) (?P[\w^_]+)' + OFFLINE_PATTERN = ur'Nastala chyba 404|Soubor byl smazán' + + CHECK_TRAFFIC = True + + LINK_PREMIUM_PATTERN = r'(.+?)' + + ERR_LOGIN_PATTERN = ur'
    Stahování je přístupné pouze přihlášeným uživatelům' + ERR_CREDIT_PATTERN = ur'
    Stahování zdarma je možné jen přes náš' + + def setup(self): + self.chunk_limit = 1 + + def handle_premium(self, pyfile): + m = re.search(self.LINK_PREMIUM_PATTERN, self.data) + if m is not None: + self.link = m.group(1) + + elif re.search(self.ERR_LOGIN_PATTERN, self.data): + self.account.relogin() + self.retry(wait=60, msg=_("User login failed")) + + elif re.search(self.ERR_CREDIT_PATTERN, self.data): + self.fail(_("Not enough credit left")) diff --git a/module/plugins/hoster/MegaRapidoNet.py b/module/plugins/hoster/MegaRapidoNet.py new file mode 100644 index 0000000000..fb1c72f5fe --- /dev/null +++ b/module/plugins/hoster/MegaRapidoNet.py @@ -0,0 +1,58 @@ +# -*- coding: utf-8 -*- + +import random + +from ..internal.MultiHoster import MultiHoster + + +def random_with_n_digits(n): + rand = "0." + not_zero = 0 + for i in range(1, n + 1): + r = random.randint(0, 9) + if(r > 0): + not_zero += 1 + rand += str(r) + + if not_zero > 0: + return rand + else: + return random_with_n_digits(n) + + +class MegaRapidoNet(MultiHoster): + __name__ = "MegaRapidoNet" + __type__ = "hoster" + __version__ = "0.12" + __status__ = "testing" + + __pattern__ = r'http://(?:www\.)?\w+\.megarapido\.net/\?file=\w+' + __config__ = [("activated", "bool", "Activated", True), + ("use_premium", "bool", "Use premium account if available", True), + ("fallback", "bool", "Fallback to free download if premium fails", False), + ("chk_filesize", "bool", "Check file size", True), + ("max_wait", "int", "Reconnect if waiting time is greater than minutes", 10), + ("revert_failed", "bool", "Revert to standard download if fails", True)] + + __description__ = """MegaRapido.net multi-hoster plugin""" + __license__ = "GPLv3" + __authors__ = [("Kagenoshin", "kagenoshin@gmx.ch")] + + LINK_PREMIUM_PATTERN = r'<\s*?a[^>]*?title\s*?=\s*?["\'].*?download["\'][^>]*?href=["\']([^"\']+)' + + ERROR_PATTERN = r'<\s*?div[^>]*?class\s*?=\s*?["\']?alert-message error.*?>([^<]*)' + + def handle_premium(self, pyfile): + self.data = self.load("http://megarapido.net/gerar.php", + post={'rand': random_with_n_digits(16), + 'urllist': pyfile.url, + 'links': pyfile.url, + 'exibir': "normal", + 'usar': "premium", + 'user': self.account.get_data('sid'), + 'autoreset': ""}) + + if "desloga e loga novamente para gerar seus links" in self.data.lower(): + self.error(_("You have logged in at another place")) + + return MultiHoster.handle_premium(self, pyfile) diff --git a/module/plugins/hoster/MegacrypterCom.py b/module/plugins/hoster/MegacrypterCom.py index c166146ddc..1d6cecb145 100644 --- a/module/plugins/hoster/MegacrypterCom.py +++ b/module/plugins/hoster/MegacrypterCom.py @@ -1,49 +1,57 @@ # -*- coding: utf-8 -*- + import re -from module.common.json_layer import json_loads, json_dumps -from module.plugins.hoster.MegaNz import MegaNz +from ..internal.misc import json +from .MegaCoNz import MegaCoNz, MegaCrypto -class MegacrypterCom(MegaNz): +class MegacrypterCom(MegaCoNz): __name__ = "MegacrypterCom" __type__ = "hoster" - __pattern__ = r"(https?://[a-z0-9]{0,10}\.?megacrypter\.com/[a-zA-Z0-9!_\-]+)" - __version__ = "0.2" - __description__ = """megacrypter plugin, based and inherits from RaNaN's MegaNz plugin""" - __author_name__ = ("GonzaloSR", ) - __author_mail__ = ("gonzalo@gonzalosr.com", ) + __version__ = "0.28" + __status__ = "testing" + + __pattern__ = r'https?://\w{0,10}\.?megacrypter\.com/[\w\-!]+' + __config__ = [("activated", "bool", "Activated", True)] + + __description__ = """Megacrypter.com decrypter plugin""" + __license__ = "GPLv3" + __authors__ = [("GonzaloSR", "gonzalo@gonzalosr.com")] API_URL = "http://megacrypter.com/api" FILE_SUFFIX = ".crypted" - def callApi(self, **kwargs): - """ Dispatch a call to the api, see megacrypter.com/api_doc """ - self.logDebug("JSON request: " + json_dumps(kwargs)) - resp = self.load(self.API_URL, post=json_dumps(kwargs)) - self.logDebug("API Response: " + resp) - return json_loads(resp) + def api_response(self, **kwargs): + """ + Dispatch a call to the api, see megacrypter.com/api_doc + """ + self.log_debug("JSON request: " + json.dumps(kwargs)) + res = self.load(self.API_URL, post=json.dumps(kwargs)) + self.log_debug("API Response: " + res) + return json.loads(res) def process(self, pyfile): - # match is guaranteed because plugin was chosen to handle url - node = re.search(self.__pattern__, pyfile.url).group(1) + #: Match is guaranteed because plugin was chosen to handle url + node = re.match(self.__pattern__, pyfile.url).group(0) + + #: get Mega.co.nz link info + info = self.api_response(link=node, m="info") - # get Mega.co.nz link info - info = self.callApi(link=node, m="info") + #: Get crypted file URL + dl = self.api_response(link=node, m="dl") - # get crypted file URL - dl = self.callApi(link=node, m="dl") + #@TODO: map error codes, implement password protection + # if info['pass'] is True: + # crypted_file_key, md5_file_key = info['key'].split("#") - # TODO: map error codes, implement password protection - # if info["pass"] == true: - # crypted_file_key, md5_file_key = info["key"].split("#") + key = MegaCrypto.base64_decode(info['key']) - key = self.b64_decode(info["key"]) + pyfile.name = info['name'] + self.FILE_SUFFIX - pyfile.name = info["name"] + self.FILE_SUFFIX + self.download(dl['url']) - self.download(dl["url"]) - self.decryptFile(key) + self.decrypt_file(key) - # Everything is finished and final name can be set - pyfile.name = info["name"] + #: Everything is finished and final name can be set + pyfile.name = info['name'] diff --git a/module/plugins/hoster/MegadyskPl.py b/module/plugins/hoster/MegadyskPl.py new file mode 100644 index 0000000000..40729066eb --- /dev/null +++ b/module/plugins/hoster/MegadyskPl.py @@ -0,0 +1,89 @@ +# -*- coding: utf-8 -*- + +import base64 +import re +import urllib + +from ..internal.misc import json +from ..internal.SimpleHoster import SimpleHoster + + +def xor_decrypt(data, key): + data = base64.b64decode(data) + return "".join(map(lambda x: chr(ord(x[1]) ^ ord(key[x[0] % len(key)])), [ + (i, c) for i, c in enumerate(data)])) + + +class MegadyskPl(SimpleHoster): + __name__ = "MegadyskPl" + __type__ = "hoster" + __version__ = "0.05" + __status__ = "testing" + + __pattern__ = r'https?://(?:www\.)?megadysk\.pl/dl/.+' + __config__ = [("activated", "bool", "Activated", True), + ("use_premium", "bool", "Use premium account if available", True), + ("fallback", "bool", + "Fallback to free download if premium fails", True), + ("chk_filesize", "bool", "Check file size", True), + ("max_wait", "int", "Reconnect if waiting time is greater than minutes", 10)] + + __description__ = """Megadysk.pl hoster plugin""" + __license__ = "GPLv3" + __authors__ = [("GammaC0de", "nitzo2001[AT]yahoo[DOT]com")] + + NAME_PATTERN = r'data-reactid="25">(?P.+?)<' + SIZE_PATTERN = r'(?P[\d.,]+)(?P[\w^_]+)' + + OFFLINE_PATTERN = r'(?:Nothing has been found|have been deleted)<' + + def api_info(self, url): + html = self.load(url) + info = {} + + m = re.search(r"window\['.*?'\]\s*=\s*\"(.*?)\"", html) + if m is None: + info['status'] = 8 + info['error'] = _("Encrypted info pattern not found") + return info + + encrypted_info = m.group(1) + + html = self.load("https://megadysk.pl/dist/index.js") + + m = re.search(r't.ISK\s*=\s*"(\w+)"', html) + if m is None: + info['status'] = 8 + info['error'] = _("Encryption key pattern not found") + return info + + key = m.group(1) + + res = xor_decrypt(encrypted_info, key) + json_data = json.loads(urllib.unquote(res)) + + if json_data['app']['maintenance']: + info['status'] = 6 + return info + + if json_data['app']['downloader'] is None or json_data[ + 'app']['downloader']['file']['deleted']: + info['status'] = 1 + return info + + info['name'] = json_data['app']['downloader']['file']['name'] + info['size'] = json_data['app']['downloader']['file']['size'] + info['download_url'] = json_data['app']['downloader']['url'] + + return info + + def setup(self): + self.multiDL = True + self.resume_download = False + self.chunk_limit = 1 + + def handle_free(self, pyfile): + if 'download_url' not in self.info: + self.error(_("Missing JSON data")) + + self.link = self.fixurl(self.info['download_url']) diff --git a/module/plugins/hoster/MegareleaseOrg.py b/module/plugins/hoster/MegareleaseOrg.py deleted file mode 100644 index 8e90d21192..0000000000 --- a/module/plugins/hoster/MegareleaseOrg.py +++ /dev/null @@ -1,34 +0,0 @@ -# -*- coding: utf-8 -*- - -############################################################################ -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU Affero 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 Affero General Public License for more details. # -# # -# You should have received a copy of the GNU Affero General Public License # -# along with this program. If not, see . # -############################################################################ - -from module.plugins.hoster.XFileSharingPro import XFileSharingPro, create_getInfo - - -class MegareleaseOrg(XFileSharingPro): - __name__ = "MegareleaseOrg" - __type__ = "hoster" - __pattern__ = r"https?://(?:www\.)?megarelease.org/\w{12}" - __version__ = "0.01" - __description__ = """Megarelease.Org hoster plugin""" - __author_name__ = ("derek3x", "stickell") - __author_mail__ = ("derek3x@vmail.me", "l.stickell@yahoo.it") - - FILE_INFO_PATTERN = r'%s/(?P.+) \((?P[^)]+)\)' % __pattern__ - - HOSTER_NAME = "megarelease.org" - -getInfo = create_getInfo(MegareleaseOrg) diff --git a/module/plugins/hoster/MegasharesCom.py b/module/plugins/hoster/MegasharesCom.py index 26cf8ab8ec..ab607928c5 100644 --- a/module/plugins/hoster/MegasharesCom.py +++ b/module/plugins/hoster/MegasharesCom.py @@ -1,114 +1,114 @@ # -*- 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 . - - @author: zoidberg -""" import re -from time import time -from module.plugins.internal.SimpleHoster import SimpleHoster, create_getInfo +import time + +from ..internal.SimpleHoster import SimpleHoster class MegasharesCom(SimpleHoster): __name__ = "MegasharesCom" __type__ = "hoster" - __pattern__ = r"http://(\w+\.)?megashares.com/.*" - __version__ = "0.24" - __description__ = """megashares.com plugin - free only""" - __author_name__ = ("zoidberg") - __author_mail__ = ("zoidberg@mujmail.cz") - - FILE_NAME_PATTERN = '

    ]*title="(?P[^"]+)">' - FILE_SIZE_PATTERN = 'Filesize: (?P[0-9.]+) (?P[kKMG])i?B
    ' - DOWNLOAD_URL_PATTERN = r'
    ]*>\s*' - PASSPORT_LEFT_PATTERN = r'Your Download Passport is: <[^>]*>(\w+).*\s*You have\s*<[^>]*>\s*([0-9.]+) ([kKMG]i?B)' - PASSPORT_RENEW_PATTERN = r'Your download passport will renew in\s*(\d+):(\d+):(\d+)' + __version__ = "0.37" + __status__ = "testing" + + __pattern__ = r'http://(?:www\.)?(d\d{2}\.)?megashares\.com/((index\.php)?\?d\d{2}=|dl/)\w+' + __config__ = [("activated", "bool", "Activated", True), + ("use_premium", "bool", "Use premium account if available", True), + ("fallback", "bool", + "Fallback to free download if premium fails", True), + ("chk_filesize", "bool", "Check file size", True), + ("max_wait", "int", "Reconnect if waiting time is greater than minutes", 10)] + + __description__ = """Megashares.com hoster plugin""" + __license__ = "GPLv3" + __authors__ = [("zoidberg", "zoidberg@mujmail.cz"), + ("Walter Purcaro", "vuolter@gmail.com")] + + NAME_PATTERN = r'

    ]*title="(?P.+?)">' + SIZE_PATTERN = r'Filesize: (?P[\d.,]+) (?P[\w^_]+)' + OFFLINE_PATTERN = r'
    (Invalid Link Request|Link has been deleted|Invalid link)' + + LINK_PATTERN = r'
    All download slots for this link are currently filled' - FILE_OFFLINE_PATTERN = r'
    (Invalid Link Request|Link has been deleted)' def setup(self): - self.resumeDownload = True + self.resume_download = True self.multiDL = self.premium - def handlePremium(self): - self.handleDownload(True) - - def handleFree(self): - self.html = self.load(self.pyfile.url, decode=True) - - if self.NO_SLOTS_PATTERN in self.html: - self.retry(wait_time=300) - - self.getFileInfo() - #if self.pyfile.size > 576716800: self.fail("This file is too large for free download") - - # Reactivate passport if needed - found = re.search(self.REACTIVATE_PASSPORT_PATTERN, self.html) - if found: - passport_num = found.group(1) - request_uri = re.search(self.REQUEST_URI_PATTERN, self.html).group(1) - - for _ in range(5): - random_num = re.search(self.REACTIVATE_NUM_PATTERN, self.html).group(1) - - verifyinput = self.decryptCaptcha( - "http://d01.megashares.com/index.php?secgfx=gfx&random_num=%s" % random_num) - self.logInfo("Reactivating passport %s: %s %s" % (passport_num, random_num, verifyinput)) - - url = ("http://d01.megashares.com%s&rs=check_passport_renewal" % request_uri + - "&rsargs[]=%s&rsargs[]=%s&rsargs[]=%s" % (verifyinput, random_num, passport_num) + - "&rsargs[]=replace_sec_pprenewal&rsrnd=%s" % str(int(time() * 1000))) - self.logDebug(url) - response = self.load(url) - - if 'Thank you for reactivating your passport.' in response: - self.correctCaptcha() - self.retry(0) - else: - self.invalidCaptcha() + def handle_premium(self, pyfile): + self.handle_download(True) + + def handle_free(self, pyfile): + if self.NO_SLOTS_PATTERN in self.data: + self.retry(wait=5 * 60) + + m = re.search(self.REACTIVATE_PASSPORT_PATTERN, self.data) + if m is not None: + passport_num = m.group(1) + request_uri = re.search( + self.REQUEST_URI_PATTERN, + self.data).group(1) + + random_num = re.search( + self.REACTIVATE_NUM_PATTERN, + self.data).group(1) + + verifyinput = self.captcha.decrypt("http://d01.megashares.com/index.php", + get={'secgfx': "gfx", 'random_num': random_num}) + + self.log_info( + _("Reactivating passport %s: %s %s") % + (passport_num, random_num, verifyinput)) + + res = self.load("http://d01.megashares.com%s" % request_uri, + get={'rs': "check_passport_renewal", + 'rsargs[]': verifyinput, + 'rsargs[]': random_num, + 'rsargs[]': passport_num, + 'rsargs[]': "replace_sec_pprenewal", + 'rsrnd[]': str(int(time.time() * 1000))}) + + if 'Thank you for reactivating your passport' in res: + self.captcha.correct() + self.restart() else: - self.fail("Failed to reactivate passport") - - # Check traffic left on passport - found = re.search(self.PASSPORT_LEFT_PATTERN, self.html) - if not found: - self.fail('Passport not found') - self.logInfo("Download passport: %s" % found.group(1)) - data_left = float(found.group(2)) * 1024 ** {'KB': 1, 'MB': 2, 'GB': 3}[found.group(3)] - self.logInfo("Data left: %s %s (%d MB needed)" % (found.group(2), found.group(3), self.pyfile.size / 1048576)) + self.retry_captcha(msg=_("Failed to reactivate passport")) + + m = re.search(self.PASSPORT_RENEW_PATTERN, self.data) + if m is not None: + time = [int(x) for x in m.groups()] + renew = time[0] + (time[1] * 60) + (time[2] * 60) + self.log_debug("Waiting %d seconds for a new passport" % renew) + self.retry(wait=renew, msg=_("Passport renewal")) + + #: Check traffic left on passport + m = re.search(self.PASSPORT_LEFT_PATTERN, self.data, re.M | re.S) + if m is None: + self.fail(_("Passport not found")) + + self.log_info(_("Download passport: %s") % m.group(1)) + data_left = float(m.group(2)) * \ + 1024 ** {'B': 0, 'KB': 1, 'MB': 2, 'GB': 3}[m.group(3)] + self.log_info(_("Data left: %s %s (%d MB needed)") % + (m.group(2), m.group(3), self.pyfile.size / 1048576)) if not data_left: - found = re.search(self.PASSPORT_RENEW_PATTERN, self.html) - renew = (found.group(1) + 60 * (found.group(2) + 60 * found.group(3))) if found else 600 - self.retry(renew, 15, "Unable to get passport") - - self.handleDownload(False) - - def handleDownload(self, premium=False): - # Find download link; - found = re.search(self.DOWNLOAD_URL_PATTERN % (1 if premium else 2), self.html) - msg = '%s download URL' % ('Premium' if premium else 'Free') - if not found: - self.parseError(msg) + self.retry(wait=600, msg=_("Passport renewal")) - download_url = found.group(1) - self.logDebug("%s: %s" % (msg, download_url)) - self.download(download_url) + self.handle_download(False) + def handle_download(self, premium=False): + m = re.search(self.LINK_PATTERN % (1 if premium else 2), self.data) + msg = _('%s download URL' % ('Premium' if premium else 'Free')) + if m is None: + self.error(msg) -getInfo = create_getInfo(MegasharesCom) + self.link = m.group(1) + self.log_debug("%s: %s" % (msg, self.link)) diff --git a/module/plugins/hoster/MegaupNet.py b/module/plugins/hoster/MegaupNet.py new file mode 100644 index 0000000000..d698f344c2 --- /dev/null +++ b/module/plugins/hoster/MegaupNet.py @@ -0,0 +1,68 @@ +# -*- coding: utf-8 -*- + +import re + +from ..internal.SimpleHoster import SimpleHoster +from ..internal.misc import json + + +class MegaupNet(SimpleHoster): + __name__ = "MegaupNet" + __type__ = "hoster" + __version__ = "0.03" + __status__ = "testing" + + __pattern__ = r'https?://megaup.net/.+' + __config__ = [("activated", "bool", "Activated", True), + ("use_premium", "bool", "Use premium account if available", True), + ("fallback", "bool", "Fallback to free download if premium fails", True), + ("chk_filesize", "bool", "Check file size", True), + ("max_wait", "int", "Reconnect if waiting time is greater than minutes", 10)] + + __description__ = """Megaup.net hoster plugin""" + __license__ = "GPLv3" + __authors__ = [("GammaC0de", "nitzo2001[AT]yahoo[DOT]com")] + + NAME_PATTERN = r'File: (?P.+?)<' + SIZE_PATTERN = r'Size: (?P[\d.,]+) (?P[\w^_]+)' + + OFFLINE_PATTERN = r'The file you are trying to download is no longer available!' + WAIT_PATTERN = r'var seconds = (\d+);' + LINK_PATTERN = r'window.location.replace\("(.+?)"\)' + + def handle_free(self, pyfile): + s = [x for x in re.findall(r"([\s\S]*?)", self.data, re.I) + if "function DeObfuscate_String_and_Create_Form_With_Mhoa_URL" in x] + if len(s) != 1: + self.fail(_("deobfuscate function not found")) + + init = """window = { + innerWidth: 1280, + innerHeight: 567, + }; + var document = { + documentElement: {clientWidth: 1280, clientHeight: 567}, + body: {clientWidth: 1280, clientHeight: 567} + };""" + deobfuscate_script = init + s[0] + deobfuscate_script = re.sub(r"if\s*\(width_trinh_duyet[\s\S]*", + "return JSON.stringify({idurl:url_da_encrypt, idfilename:FileName, idfilesize:FileSize})};", + deobfuscate_script) + + m = re.search(r"if \(seconds == 0\)[\s\S]+?(DeObfuscate_String_and_Create_Form_With_Mhoa_URL\(.+?\);)", self.data) + if m is None: + self.fail(_("deobfuscate function call not found")) + + deobfuscate_script += m.group(1) + json_data = self.js.eval(deobfuscate_script) + + if not json_data.startswith('{'): + self.fail(_("Unexpected response, expected JSON data")) + + params = json.loads(json_data) + + self.data = self.load("https://download.megaup.net/", get=params) + m = re.search(self.LINK_PATTERN, self.data) + if m is not None: + self.link = m.group(1) + diff --git a/module/plugins/hoster/MexaSh.py b/module/plugins/hoster/MexaSh.py new file mode 100644 index 0000000000..90e05aa5c4 --- /dev/null +++ b/module/plugins/hoster/MexaSh.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- + +from ..internal.XFSHoster import XFSHoster + + +class MexaSh(XFSHoster): + __name__ = "MexaSh" + __type__ = "hoster" + __version__ = "0.01" + __status__ = "testing" + + __pattern__ = r"https?://(?:www\.)?(?:mexa\.sh|mexashare\.com)/(?P\w{12})" + __config__ = [("activated", "bool", "Activated", True), + ("use_premium", "bool", "Use premium account if available", True), + ("fallback", "bool", "Fallback to free download if premium fails", True), + ("chk_filesize", "bool", "Check file size", True), + ("max_wait", "int", "Reconnect if waiting time is greater than minutes", 10)] + + __description__ = """Mexa.sh hoster plugin""" + __license__ = "GPLv3" + __authors__ = [("GammaC0de", "nitzo2001[AT]yahoo[DOT]com")] + + PLUGIN_DOMAIN = "mexa.sh" + + URL_REPLACEMENTS = [(__pattern__ + ".*", "https://mexa.sh/\g")] + + NAME_PATTERN = r">You have requested the file.+?>(?P.+?)" + SIZE_PATTERN = r">\sFile Size\s: (?P[\d.,]+)\s*(?P[\w^_]+)" + + WAIT_PATTERN = r'class="seconds">(\d+)<' + DL_LIMIT_PATTERN = r"you can download this file after :.+?Filename:(?P.*?)
    ' - #FILE_SIZE_PATTERN = r'Size:(?P.*?)
    ' - FILE_INFO_PATTERN = r'

    (?P.+?) (?P[\d.]+) (?P..)

    ' - FILE_OFFLINE_PATTERN = r'File Not Found

    ' - DIRECT_LINK_PATTERN = r'
    Download Link' - #OVR_DOWNLOAD_LINK_PATTERN = "var file_link = '(.*)';" - HOSTER_NAME = "movreel.com" - -getInfo = create_getInfo(MovReelCom) + LINK_PATTERN = r'Download Link' diff --git a/module/plugins/hoster/Mp4uploadCom.py b/module/plugins/hoster/Mp4uploadCom.py new file mode 100644 index 0000000000..046b5175ae --- /dev/null +++ b/module/plugins/hoster/Mp4uploadCom.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- + +from ..internal.XFSHoster import XFSHoster + + +class Mp4uploadCom(XFSHoster): + __name__ = "Mp4uploadCom" + __type__ = "hoster" + __version__ = "0.01" + __status__ = "testing" + + __pattern__ = r"https?://(?:www\.)?mp4upload\.com/(?P\w{12})" + __config__ = [("activated", "bool", "Activated", True), + ("use_premium", "bool", "Use premium account if available", True), + ("fallback", "bool", "Fallback to free download if premium fails", True), + ("chk_filesize", "bool", "Check file size", True), + ("max_wait", "int", "Reconnect if waiting time is greater than minutes", 10)] + + __description__ = """Mp4upload.com hoster plugin""" + __license__ = "GPLv3" + __authors__ = [("GammaC0de", "nitzo2001[AT]yahoo[DOT]com")] + + PLUGIN_DOMAIN = "mp4upload.com" diff --git a/module/plugins/hoster/MultiDebridCom.py b/module/plugins/hoster/MultiDebridCom.py deleted file mode 100644 index 83f477f348..0000000000 --- a/module/plugins/hoster/MultiDebridCom.py +++ /dev/null @@ -1,57 +0,0 @@ -# -*- coding: utf-8 -*- - -############################################################################ -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU Affero 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 Affero General Public License for more details. # -# # -# You should have received a copy of the GNU Affero General Public License # -# along with this program. If not, see . # -############################################################################ - -import re - -from module.plugins.Hoster import Hoster -from module.common.json_layer import json_loads - - -class MultiDebridCom(Hoster): - __name__ = "MultiDebridCom" - __version__ = "0.03" - __type__ = "hoster" - __pattern__ = r"http://\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/dl/" - __description__ = """Multi-debrid.com hoster plugin""" - __author_name__ = ("stickell") - __author_mail__ = ("l.stickell@yahoo.it") - - def setup(self): - self.chunkLimit = -1 - self.resumeDownload = True - - def process(self, pyfile): - if re.match(self.__pattern__, pyfile.url): - new_url = pyfile.url - elif not self.account: - self.logError(_("Please enter your %s account or deactivate this plugin") % "Multi-debrid.com") - self.fail("No Multi-debrid.com account provided") - else: - self.logDebug("Original URL: %s" % pyfile.url) - page = self.req.load('http://multi-debrid.com/api.php', - get={'user': self.user, 'pass': self.account.getAccountData(self.user)['password'], - 'link': pyfile.url}) - self.logDebug("JSON data: " + page) - page = json_loads(page) - if page['status'] != 'ok': - self.fail('Unable to unrestrict link') - new_url = page['link'] - - if new_url != pyfile.url: - self.logDebug("Unrestricted URL: " + new_url) - - self.download(new_url, disposition=True) diff --git a/module/plugins/hoster/MultihostersCom.py b/module/plugins/hoster/MultihostersCom.py new file mode 100644 index 0000000000..4763ac9399 --- /dev/null +++ b/module/plugins/hoster/MultihostersCom.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- + +from .ZeveraCom import ZeveraCom + + +class MultihostersCom(ZeveraCom): + __name__ = "MultihostersCom" + __type__ = "hoster" + __version__ = "0.08" + __status__ = "testing" + + __pattern__ = r'https?://(?:www\.)multihosters\.com/(getFiles\.ashx|Members/download\.ashx)\?.*ourl=.+' + __config__ = [("activated", "bool", "Activated", True)] + + __description__ = """Multihosters.com multi-hoster plugin""" + __license__ = "GPLv3" + __authors__ = [("tjeh", "tjeh@gmx.net")] diff --git a/module/plugins/hoster/MultishareCz.py b/module/plugins/hoster/MultishareCz.py index f9289a923b..9590f4194c 100644 --- a/module/plugins/hoster/MultishareCz.py +++ b/module/plugins/hoster/MultishareCz.py @@ -1,82 +1,64 @@ # -*- 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. +import pycurl - You should have received a copy of the GNU General Public License - along with this program; if not, see . +from ..internal.misc import json +from ..internal.MultiHoster import MultiHoster - @author: zoidberg -""" -import re -from random import random -from module.plugins.internal.SimpleHoster import SimpleHoster, create_getInfo - - -class MultishareCz(SimpleHoster): +class MultishareCz(MultiHoster): __name__ = "MultishareCz" __type__ = "hoster" - __pattern__ = r"http://(?:\w*\.)?multishare.cz/stahnout/(?P\d+).*" - __version__ = "0.34" - __description__ = """MultiShare.cz""" - __author_name__ = ("zoidberg") - - FILE_INFO_PATTERN = ur'(?:
  • Název|Soubor): (?P[^<]+)<(?:/li>Velikost: (?P[^<]+)' - FILE_OFFLINE_PATTERN = ur'

    Stáhnout soubor

    Požadovaný soubor neexistuje.

    ' - FILE_SIZE_REPLACEMENTS = [(' ', '')] + __version__ = "0.49" + __status__ = "testing" + + __pattern__ = r'http://(?:www\.)?multishare\.cz/stahnout/(?P\d+)' + __config__ = [("activated", "bool", "Activated", True), + ("use_premium", "bool", "Use premium account if available", True), + ("fallback", "bool", + "Fallback to free download if premium fails", True), + ("chk_filesize", "bool", "Check file size", True), + ("max_wait", "int", "Reconnect if waiting time is greater than minutes", 10)] + + __description__ = """MultiShare.cz multi-hoster plugin""" + __license__ = "GPLv3" + __authors__ = [("zoidberg", "zoidberg@mujmail.cz"), + ("GammaC0de", "nitzo2001[AT]yahoo[DOT]com")] + + #: See https://multishare.cz/api/ + API_URL = "https://www.multishare.cz/api/" + + def api_response(self, method, **kwargs): + get = {'sub': method} + get.update(kwargs) + self.req.http.c.setopt(pycurl.USERAGENT, "JDownloader") + json_data = self.load(self.API_URL, + get=get) + + if not json_data.startswith('{'): + if json_data.startswith("ERR:"): + json_data = json_data[4:].strip() + return {'err': json_data} - def process(self, pyfile): - msurl = re.match(self.__pattern__, pyfile.url) - if msurl: - self.fileID = msurl.group('ID') - self.html = self.load(pyfile.url, decode=True) - self.getFileInfo() - - if self.premium: - self.handlePremium() - else: - self.handleFree() else: - self.handleOverriden() - - def handleFree(self): - self.download("http://www.multishare.cz/html/download_free.php?ID=%s" % self.fileID) + return json.loads(json_data) - def handlePremium(self): - if not self.checkCredit(): - self.logWarning("Not enough credit left to download file") - self.resetAccount() + def handle_premium(self, pyfile): + api_data = self.api_response("check-file", link=pyfile.url) + if 'err' in api_data: + if "Given link is dead" in api_data['err']: + self.offline() - self.download("http://www.multishare.cz/html/download_premium.php?ID=%s" % self.fileID) - - def handleOverriden(self): - if not self.premium: - self.fail("Only premium users can download from other hosters") - - self.html = self.load('http://www.multishare.cz/html/mms_ajax.php', post={"link": self.pyfile.url}, decode=True) - self.getFileInfo() - - if not self.checkCredit(): - self.fail("Not enough credit left to download file") - - url = "http://dl%d.mms.multishare.cz/html/mms_process.php" % round(random() * 10000 * random()) - params = {"u_ID": self.acc_info["u_ID"], "u_hash": self.acc_info["u_hash"], "link": self.pyfile.url} - self.logDebug(url, params) - self.download(url, get=params) - - def checkCredit(self): - self.acc_info = self.account.getAccountInfo(self.user, True) - self.logInfo("User %s has %i MB left" % (self.user, self.acc_info["trafficleft"] / 1024)) + else: + self.fail(api_data['err']) - return self.pyfile.size / 1024 <= self.acc_info["trafficleft"] + pyfile.name = api_data['file_name'] + pyfile.size = api_data['file_size'] + api_data = self.api_response("download-link", + link=pyfile.url, + login=self.account.user, + password=self.account.info['login']['password']) -getInfo = create_getInfo(MultishareCz) + self.chunk_limit = api_data['chunks'] + self.link = api_data['link'] diff --git a/module/plugins/hoster/MyfastfileCom.py b/module/plugins/hoster/MyfastfileCom.py new file mode 100644 index 0000000000..8912dddfeb --- /dev/null +++ b/module/plugins/hoster/MyfastfileCom.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- + + +from ..internal.misc import json +from ..internal.MultiHoster import MultiHoster + + +class MyfastfileCom(MultiHoster): + __name__ = "MyfastfileCom" + __type__ = "hoster" + __version__ = "0.16" + __status__ = "testing" + + __pattern__ = r'http://\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/dl/' + __config__ = [("activated", "bool", "Activated", True), + ("use_premium", "bool", "Use premium account if available", True), + ("fallback", "bool", "Fallback to free download if premium fails", False), + ("chk_filesize", "bool", "Check file size", True), + ("max_wait", "int", "Reconnect if waiting time is greater than minutes", 10), + ("revert_failed", "bool", "Revert to standard download if fails", True)] + + __description__ = """Myfastfile.com multi-hoster plugin""" + __license__ = "GPLv3" + __authors__ = [("stickell", "l.stickell@yahoo.it")] + + def setup(self): + self.chunk_limit = -1 + + def handle_premium(self, pyfile): + self.data = self.load('http://myfastfile.com/api.php', + get={'user': self.account.user, + 'pass': self.account.get_login('password'), + 'link': pyfile.url}) + self.log_debug("JSON data: " + self.data) + + self.data = json.loads(self.data) + if self.data['status'] != 'ok': + self.fail(_("Unable to unrestrict link")) + + self.link = self.data['link'] diff --git a/module/plugins/hoster/MystoreTo.py b/module/plugins/hoster/MystoreTo.py new file mode 100644 index 0000000000..efa5aa1dbe --- /dev/null +++ b/module/plugins/hoster/MystoreTo.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- +# +# Test link: +# http://mystore.to/dl/mxcA50jKfP + +import re + +from ..internal.SimpleHoster import SimpleHoster + + +class MystoreTo(SimpleHoster): + __name__ = "MystoreTo" + __type__ = "hoster" + __version__ = "0.08" + __status__ = "testing" + + __pattern__ = r'https?://(?:www\.)?mystore\.to/dl/.+' + __config__ = [("activated", "bool", "Activated", True), + ("use_premium", "bool", "Use premium account if available", True), + ("fallback", "bool", + "Fallback to free download if premium fails", True), + ("chk_filesize", "bool", "Check file size", True), + ("max_wait", "int", "Reconnect if waiting time is greater than minutes", 10)] + + __description__ = """Mystore.to hoster plugin""" + __license__ = "GPLv3" + __authors__ = [("zapp-brannigan", "fuerst.reinje@web.de")] + + NAME_PATTERN = r'

    (?P.+?)<' + SIZE_PATTERN = r'FILESIZE: (?P[\d\.,]+) (?P[\w^_]+)' + OFFLINE_PATTERN = r'>file not found<' + + def setup(self): + self.chunk_limit = 1 + self.resume_download = True + self.multiDL = True + + def handle_free(self, pyfile): + try: + fid = re.search(r'wert="(.+?)"', self.data).group(1) + + except AttributeError: + self.error(_("File-ID not found")) + + self.link = self.load("http://mystore.to/api/download", + post={'FID': fid}) diff --git a/module/plugins/hoster/MyvideoDe.py b/module/plugins/hoster/MyvideoDe.py deleted file mode 100644 index 1bd73e3762..0000000000 --- a/module/plugins/hoster/MyvideoDe.py +++ /dev/null @@ -1,41 +0,0 @@ -# -*- coding: utf-8 -*- - -import re -from module.plugins.Hoster import Hoster -from module.unescape import unescape - - -class MyvideoDe(Hoster): - __name__ = "MyvideoDe" - __type__ = "hoster" - __pattern__ = r"http://(www\.)?myvideo.de/watch/" - __version__ = "0.9" - __description__ = """Myvideo.de Video Download Hoster""" - __author_name__ = ("spoob") - __author_mail__ = ("spoob@pyload.org") - - def process(self, pyfile): - self.pyfile = pyfile - self.download_html() - pyfile.name = self.get_file_name() - self.download(self.get_file_url()) - - def download_html(self): - self.html = self.load(self.pyfile.url) - - def get_file_url(self): - videoId = re.search(r"addVariable\('_videoid','(.*)'\);p.addParam\('quality'", self.html).group(1) - videoServer = re.search("rel='image_src' href='(.*)thumbs/.*' />", self.html).group(1) - file_url = videoServer + videoId + ".flv" - return file_url - - def get_file_name(self): - file_name_pattern = r"

    (.*)

    " - return unescape(re.search(file_name_pattern, self.html).group(1).replace("/", "") + '.flv') - - def file_exists(self): - self.download_html() - self.load(str(self.pyfile.url), cookies=False, just_header=True) - if self.req.lastEffectiveURL == "http://www.myvideo.de/": - return False - return True diff --git a/module/plugins/hoster/NarodRu.py b/module/plugins/hoster/NarodRu.py index 590268910b..6c2c7fe0c7 100644 --- a/module/plugins/hoster/NarodRu.py +++ b/module/plugins/hoster/NarodRu.py @@ -1,70 +1,59 @@ # -*- 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 . - - @author: zoidberg -""" +import random import re -from random import random -from module.plugins.internal.SimpleHoster import SimpleHoster, create_getInfo +import urlparse + +from ..internal.SimpleHoster import SimpleHoster class NarodRu(SimpleHoster): __name__ = "NarodRu" __type__ = "hoster" - __pattern__ = r"http://(www\.)?narod(\.yandex)?\.ru/(disk|start/[0-9]+\.\w+-narod\.yandex\.ru)/(?P\d+)/.+" - __version__ = "0.1" - __description__ = """Narod.ru""" - __author_name__ = ("zoidberg") + __version__ = "0.17" + __status__ = "testing" - FILE_NAME_PATTERN = r'
    (?:<[^<]*>)*(?P[^<]+)
    ' - FILE_SIZE_PATTERN = r'
    (?P\d[^<]*)
    ' - FILE_OFFLINE_PATTERN = r'404|Файл удален с сервиса|Закончился срок хранения файла\.' + __pattern__ = r'http://(?:www\.)?narod(\.yandex)?\.ru/(disk|start/\d+\.\w+\-narod\.yandex\.ru)/(?P\d+)/.+' + __config__ = [("activated", "bool", "Activated", True), + ("use_premium", "bool", "Use premium account if available", True), + ("fallback", "bool", + "Fallback to free download if premium fails", True), + ("chk_filesize", "bool", "Check file size", True), + ("max_wait", "int", "Reconnect if waiting time is greater than minutes", 10)] - FILE_SIZE_REPLACEMENTS = [(u'КБ', 'KB'), (u'МБ', 'MB'), (u'ГБ', 'GB')] - FILE_URL_REPLACEMENTS = [("narod.yandex.ru/", "narod.ru/"), - (r"/start/[0-9]+\.\w+-narod\.yandex\.ru/([0-9]{6,15})/\w+/(\w+)", r"/disk/\1/\2")] + __description__ = """Narod.ru hoster plugin""" + __license__ = "GPLv3" + __authors__ = [("zoidberg", "zoidberg@mujmail.cz")] + + NAME_PATTERN = r'
    (?:<.*?>)*(?P.+?)
    ' + SIZE_PATTERN = r'
    (?P\d.*?)
    ' + OFFLINE_PATTERN = r'404|Файл удален с сервиса|Закончился срок хранения файла\.' + + SIZE_REPLACEMENTS = [(u'КБ', 'KB'), (u'МБ', 'MB'), (u'ГБ', 'GB')] + URL_REPLACEMENTS = [("narod.yandex.ru/", "narod.ru/"), + (r'/start/\d+\.\w+\-narod\.yandex\.ru/(\d{6,15})/\w+/(\w+)', r'/disk/\1/\2')] CAPTCHA_PATTERN = r'(\w+)' - DOWNLOAD_LINK_PATTERN = r'
    ' + LINK_FREE_PATTERN = r'' + + def handle_free(self, pyfile): + self.data = self.load( + 'http://narod.ru/disk/getcapchaxml/?rnd=%d' % int(random.random() * 777)) - def handleFree(self): - for i in range(5): - self.html = self.load('http://narod.ru/disk/getcapchaxml/?rnd=%d' % int(random() * 777)) - found = re.search(self.CAPTCHA_PATTERN, self.html) - if not found: - self.parseError('Captcha') - post_data = {"action": "sendcapcha"} - captcha_url, post_data['key'] = found.groups() - post_data['rep'] = self.decryptCaptcha(captcha_url) + m = re.search(self.CAPTCHA_PATTERN, self.data) + if m is None: + self.error(_("Captcha")) - self.html = self.load(self.pyfile.url, post=post_data, decode=True) - found = re.search(self.DOWNLOAD_LINK_PATTERN, self.html) - if found: - url = 'http://narod.ru' + found.group(1) - self.correctCaptcha() - break - elif u'Ошиблись?' in self.html: - self.invalidCaptcha() - else: - self.parseError('Download link') - else: - self.fail("No valid captcha code entered") + post_data = {'action': "sendcapcha"} + captcha_url, post_data['key'] = m.groups() + post_data['rep'] = self.captcha.decrypt(captcha_url) - self.logDebug('Download link: ' + url) - self.download(url) + self.data = self.load(pyfile.url, post=post_data) + m = re.search(self.LINK_FREE_PATTERN, self.data) + if m is not None: + self.captcha.correct() + self.link = urlparse.urljoin("http://narod.ru/", m.group(1)) -getInfo = create_getInfo(NarodRu) + elif u'Ошиблись?' in self.data: + self.retry_captcha() diff --git a/module/plugins/hoster/NetloadIn.py b/module/plugins/hoster/NetloadIn.py deleted file mode 100644 index 71c8dd4f20..0000000000 --- a/module/plugins/hoster/NetloadIn.py +++ /dev/null @@ -1,255 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -import re -from time import sleep, time - -from module.plugins.Hoster import Hoster -from module.network.RequestFactory import getURL -from module.plugins.Plugin import chunks - - -def getInfo(urls): - ## returns list of tupels (name, size (in bytes), status (see FileDatabase), url) - - apiurl = "http://api.netload.in/info.php?auth=Zf9SnQh9WiReEsb18akjvQGqT0I830e8&bz=1&md5=1&file_id=" - id_regex = re.compile(NetloadIn.__pattern__) - urls_per_query = 80 - - for chunk in chunks(urls, urls_per_query): - ids = "" - for url in chunk: - match = id_regex.search(url) - if match: - ids = ids + match.group(1) + ";" - - api = getURL(apiurl + ids, decode=True) - - if api is None or len(api) < 10: - print "Netload prefetch: failed " - return - if api.find("unknown_auth") >= 0: - print "Netload prefetch: Outdated auth code " - return - - result = [] - - for i, r in enumerate(api.splitlines()): - try: - tmp = r.split(";") - try: - size = int(tmp[2]) - except: - size = 0 - result.append((tmp[1], size, 2 if tmp[3] == "online" else 1, chunk[i] )) - except: - print "Netload prefetch: Error while processing response: " - print r - - yield result - - -class NetloadIn(Hoster): - __name__ = "NetloadIn" - __type__ = "hoster" - __pattern__ = r"https?://.*netload\.in/(?:datei(.*?)(?:\.htm|/)|index.php?id=10&file_id=)" - __version__ = "0.45" - __description__ = """Netload.in Download Hoster""" - __author_name__ = ("spoob", "RaNaN", "Gregy") - __author_mail__ = ("spoob@pyload.org", "ranan@pyload.org", "gregy@gregy.cz") - - def setup(self): - self.multiDL = self.resumeDownload = self.premium - - def process(self, pyfile): - self.url = pyfile.url - self.prepare() - self.pyfile.setStatus("downloading") - self.proceed(self.url) - - def prepare(self): - self.download_api_data() - - if self.api_data and self.api_data["filename"]: - self.pyfile.name = self.api_data["filename"] - - if self.premium: - self.logDebug("Netload: Use Premium Account") - settings = self.load("http://www.netload.in/index.php?id=2&lang=en") - if 'Or click here" - attempt = re.search(file_url_pattern, page) - if attempt is not None: - return attempt.group(1) - else: - self.logDebug("Netload: Backup try for final link") - file_url_pattern = r"Click here" - attempt = re.search(file_url_pattern, page) - return "http://netload.in/" + attempt.group(1) - except: - self.logDebug("Netload: Getting final link failed") - return None - - def get_wait_time(self, page): - wait_seconds = int(re.search(r"countdown\((.+),'change\(\)'\)", page).group(1)) / 100 - return wait_seconds - - def proceed(self, url): - self.logDebug("Netload: Downloading..") - - self.download(url, disposition=True) - - check = self.checkDownload({"empty": re.compile(r"^$"), "offline": re.compile("The file was deleted")}) - - if check == "empty": - self.logInfo(_("Downloaded File was empty")) - self.retry() - elif check == "offline": - self.offline() diff --git a/module/plugins/hoster/NippyshareCom.py b/module/plugins/hoster/NippyshareCom.py new file mode 100644 index 0000000000..ec0ac323b6 --- /dev/null +++ b/module/plugins/hoster/NippyshareCom.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- + +from ..internal.XFSHoster import XFSHoster + + +class NippyshareCom(XFSHoster): + __name__ = "NippyshareCom" + __type__ = "hoster" + __version__ = "0.01" + __status__ = "testing" + + __pattern__ = r'https?://nippyshare.com/v/\w{6}' + __config__ = [("activated", "bool", "Activated", True), + ("chk_filesize", "bool", "Check file size", True), + ("max_wait", "int", + "Reconnect if waiting time is greater than minutes", 10)] + + __description__ = """Nippyshare.com hoster plugin""" + __license__ = "GPLv3" + __authors__ = [("OzzieIsaacs", "Ozzie.Fernandez.Isaacs@googlemail.com")] + + PLUGIN_DOMAIN = "nippyshare.com" + + NAME_PATTERN = r'>
  • Name:(?P.+?)
  • ' + SIZE_PATTERN = r'
  • Size: (?P[\d.,]+) (?P[\w^_]+)
  • ' + + LINK_PATTERN = r"
    Download" diff --git a/module/plugins/hoster/NitrobitNet.py b/module/plugins/hoster/NitrobitNet.py new file mode 100644 index 0000000000..43401b0e27 --- /dev/null +++ b/module/plugins/hoster/NitrobitNet.py @@ -0,0 +1,59 @@ +# -*- coding: utf-8 -*- + +import pycurl +import re +import time + +from ..internal.SimpleHoster import SimpleHoster +from ..internal.misc import format_size + + +class NitrobitNet(SimpleHoster): + __name__ = "NitrobitNet" + __type__ = "hoster" + __version__ = "0.02" + __status__ = "testing" + + __pattern__ = r'https?://(?:www\.)?nitrobit.net/(?:view|watch)/(?P\w+)' + __config__ = [("activated", "bool", "Activated", True), + ("use_premium", "bool", "Use premium account if available", True), + ("fallback", "bool", "Fallback to free download if premium fails", False), + ("chk_filesize", "bool", "Check file size", True), + ("max_wait", "int", "Reconnect if waiting time is greater than minutes", 10)] + + __description__ = """Nitrobit.net hoster plugin""" + __license__ = "GPLv3" + __authors__ = [("GammaC0de", "nitzo2001[AT]yahoo[DOT]com")] + + LOGIN_PREMIUM = True + URL_REPLACEMENTS = [(__pattern__ + ".*", r'http://www.nitrobit.net/view/\g')] + + NAME_PATTERN = ur'שם הקובץ: גודל הקובץ:
    (?P[\d.,]+) (?P[\w^_]+)' + + DL_LIMIT_PATTERN = ur'daily downloadlimit reached|הורדת קובץ זה תעבור על המכסה היומית' + LINK_PREMIUM_PATTERN = r'id="download" href="(.+?)"' + + def handle_premium(self, pyfile): + current_millis = int(time.time() * 1000) + + self.req.http.c.setopt(pycurl.HTTPHEADER, ["X-Requested-With: XMLHttpRequest"]) + self.data = self.load("http://www.nitrobit.net/ajax/unlock.php", + get={'password': self.account.info['login']['password'], + 'file': self.info['pattern']['ID'], + 'keep': "false", + '_': current_millis}) + + m = re.search(r'id="unlockedTick".+?alt="(\d+)"', self.data) + if m is not None: + validuntil = time.time() + float(m.group(1)) + self.log_info(_("Account valid until %s") % time.strftime("%d/%m/%Y", time.gmtime(validuntil))) + + m = re.search(r'id="dailyVolume" value="(\d+)?/(\d+)"', self.data) + if m is not None: + trafficleft = int(m.group(2)) - int((m.group(1) or "0")) + self.log_info(_("Daily traffic left %s") % format_size(trafficleft)) + + m = re.search(self.LINK_PREMIUM_PATTERN, self.data) + if m is not None: + self.link = m.group(1) diff --git a/module/plugins/hoster/NitroflareCom.py b/module/plugins/hoster/NitroflareCom.py new file mode 100644 index 0000000000..6788001106 --- /dev/null +++ b/module/plugins/hoster/NitroflareCom.py @@ -0,0 +1,130 @@ +# -*- coding: utf-8 -*- + +import re + +from ..captcha.HCaptcha import HCaptcha +from ..captcha.ReCaptcha import ReCaptcha +from ..internal.misc import json +from ..internal.SimpleHoster import SimpleHoster + + +class NitroflareCom(SimpleHoster): + __name__ = "NitroflareCom" + __type__ = "hoster" + __version__ = "0.42" + __status__ = "testing" + + __pattern__ = r'https?://(?:www\.)?(?:nitro\.download|nitroflare\.com)/view/(?P[\w^_]+)' + __config__ = [("activated", "bool", "Activated", True), + ("use_premium", "bool", "Use premium account if available", True), + ("fallback", "bool", + "Fallback to free download if premium fails", True), + ("chk_filesize", "bool", "Check file size", True), + ("max_wait", "int", "Reconnect if waiting time is greater than minutes", 10)] + + __description__ = """Nitroflare.com hoster plugin""" + __license__ = "GPLv3" + __authors__ = [("sahil", "sahilshekhawat01@gmail.com"), + ("Walter Purcaro", "vuolter@gmail.com"), + ("Stickell", "l.stickell@yahoo.it"), + ("GammaC0de", "nitzo2001[AT]yahoo[DOT]com")] + + INFO_PATTERN = r'title="(?P.+?)".+>(?P[\d.,]+) (?P[\w^_]+)' + LINK_PATTERN = r'(https?://[\w\-]+\.nitroflare\.com/.+?)"' + + DIRECT_LINK = False + DISPOSITION = False + + PREMIUM_ONLY_PATTERN = r'This file is available with Premium only' + DL_LIMIT_PATTERN = r'You have to wait \d+ minutes to download your next file.' + + URL_REPLACEMENTS = [(r'nitro\.download', "nitroflare.com")] + + # See https://nitroflare.com/member?s=general-api + API_URL = "https://nitroflare.com/api/v2/" + + def api_request(self, method, **kwargs): + json_data = self.load(self.API_URL + method, get=kwargs) + return json.loads(json_data) + + def api_info(self, url): + info = {} + file_id = re.search(self.__pattern__, url).group('ID') + + api_data = self.api_request("getFileInfo", files=file_id) + + if api_data['type'] == 'success': + fileinfo = api_data['result']['files'][file_id] + info['status'] = 2 if fileinfo['status'] == 'online' else 1 + info['name'] = fileinfo['name'] + info['size'] = fileinfo['size'] #: In bytes + info['post_url'] = fileinfo['url'] + + return info + + def handle_free(self, pyfile): + #: Used here to load the cookies which will be required later + self.load("https://nitroflare.com/ajax/setCookie.php", + post={'fileId': self.info['pattern']['ID']}) + + self.data = self.load(self.info["post_url"], + post={'goToFreePage': ""}) + + try: + wait_time = int(re.search(r'var timerSeconds = (\d+);', self.data).group(1)) + + except (IndexError, ValueError, AttributeError): + wait_time = 120 + + data = self.load("https://nitroflare.com/ajax/freeDownload.php", + post={'method': "startTimer", + 'fileId': self.info['pattern']['ID']}, + ref=self.req.lastEffectiveURL) + + self.set_wait(wait_time) + + self.check_errors(data=data) + + inputs = {'method': "fetchDownload"} + + recaptcha = ReCaptcha(pyfile) + recaptcha_key = recaptcha.detect_key() + if recaptcha_key: + self.captcha = recaptcha + response = self.captcha.challenge(recaptcha_key) + inputs['g-recaptcha-response'] = response + + else: + hcaptcha = HCaptcha(pyfile) + hcaptcha_key = hcaptcha.detect_key() + if hcaptcha_key: + self.captcha = hcaptcha + response = self.captcha.challenge(hcaptcha_key) + inputs["g-recaptcha-response"] = inputs["h-captcha-response"] = response + else: + response = self.captcha.decrypt("https://nitroflare.com/plugins/cool-captcha/captcha.php") + + inputs['captcha'] = response + + self.wait() + + self.data = self.load("https://nitroflare.com/ajax/freeDownload.php", + post=inputs) + + if "The captcha wasn't entered correctly" in self.data: + self.retry_captcha() + + return SimpleHoster.handle_free(self, pyfile) + + def handle_premium(self, pyfile): + api_data = self.api_request( + "getDownloadLink", + file=self.info["pattern"]["ID"], + user=self.account.user, + premiumKey=self.account.get_login("password"), + ) + + if api_data['type'] == 'success': + pyfile.name = api_data['result']['name'] + pyfile.size = int(api_data['result']['size']) + self.link = api_data['result']['url'] diff --git a/module/plugins/hoster/NoPremiumPl.py b/module/plugins/hoster/NoPremiumPl.py new file mode 100644 index 0000000000..31f5beab23 --- /dev/null +++ b/module/plugins/hoster/NoPremiumPl.py @@ -0,0 +1,92 @@ +# -*- coding: utf-8 -*- + +from ..internal.misc import json +from ..internal.MultiHoster import MultiHoster + + +class NoPremiumPl(MultiHoster): + __name__ = "NoPremiumPl" + __type__ = "hoster" + __version__ = "0.13" + __status__ = "testing" + + __pattern__ = r'https?://direct\.nopremium\.pl.+' + __config__ = [("activated", "bool", "Activated", True), + ("use_premium", "bool", "Use premium account if available", True), + ("fallback", "bool", "Fallback to free download if premium fails", False), + ("chk_filesize", "bool", "Check file size", True), + ("max_wait", "int", "Reconnect if waiting time is greater than minutes", 10), + ("revert_failed", "bool", "Revert to standard download if fails", True)] + + __description__ = """NoPremium.pl multi-hoster plugin""" + __license__ = "GPLv3" + __authors__ = [("goddie", "dev@nopremium.pl")] + + API_URL = "http://crypt.nopremium.pl" + + API_QUERY = {'site': "nopremium", + 'output': "json", + 'username': "", + 'password': "", + 'url': ""} + + ERROR_CODES = {0: "Incorrect login credentials", + 1: "Not enough transfer to download - top-up your account", + 2: "Incorrect / dead link", + 3: "Error connecting to hosting, try again later", + 9: "Premium account has expired", + 15: "Hosting no longer supported", + 80: "Too many incorrect login attempts, account blocked for 24h"} + + def run_file_query(self, url, mode=None): + query = self.API_QUERY.copy() + + query['username'] = self.account.user + query['password'] = self.account.info['data']['hash_password'] + query['url'] = url + + if mode == "fileinfo": + query['check'] = 2 + query['loc'] = 1 + + self.log_debug(query) + + return self.load(self.API_URL, post=query, redirect=20) + + def handle_premium(self, pyfile): + try: + data = self.run_file_query(pyfile.url, 'fileinfo') + + except Exception: + self.fail("Query error #1") + + try: + parsed = json.loads(data) + + except Exception: + self.temp_offline("Data not found") + + self.log_debug(parsed) + + if "errno" in parsed.keys(): + if parsed['errno'] in self.ERROR_CODES: + #: Error code in known + self.fail(self.ERROR_CODES[parsed['errno']]) + else: + #: Error code isn't yet added to plugin + self.fail(parsed['errstring'] or + _("Unknown error (code: %s)") % parsed['errno']) + + if "sdownload" in parsed: + if parsed['sdownload'] == "1": + self.fail(_("Download from %s is possible only using NoPremium.pl website directly") + % parsed['hosting']) + + pyfile.name = parsed['filename'] + pyfile.size = parsed['filesize'] + + try: + self.link = self.run_file_query(pyfile.url, 'filedownload') + + except Exception: + self.fail("Query error #2") diff --git a/module/plugins/hoster/NofileIo.py b/module/plugins/hoster/NofileIo.py new file mode 100644 index 0000000000..4568436528 --- /dev/null +++ b/module/plugins/hoster/NofileIo.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- + +from ..internal.SimpleHoster import SimpleHoster + + +class NofileIo(SimpleHoster): + __name__ = "NofileIo" + __type__ = "hoster" + __version__ = "0.01" + __status__ = "testing" + + __pattern__ = r'https?://(?:www\.)?nofile\.io/f/[\w^_]+' + __config__ = [("activated", "bool", "Activated", True), + ("use_premium", "bool", "Use premium account if available", True), + ("fallback", "bool", "Fallback to free download if premium fails", True), + ("chk_filesize", "bool", "Check file size", True), + ("max_wait", "int", "Reconnect if waiting time is greater than minutes", 10)] + + __description__ = """Nofile.io hoster plugin""" + __license__ = "GPLv3" + __authors__ = [("GammaC0de", "nitzo2001[AT]yahoo[DOT]com")] + + + NAME_PATTERN = r'(?P.+?)' + SIZE_PATTERN = r'File size
    \s*(?P[\d\.,]+) (?P[\w^_]+)\s*<' + + LINK_PATTERN = r'data-url="(https://\w+\.nofilecdn\.io/g/.+?)"' + diff --git a/module/plugins/hoster/NosuploadCom.py b/module/plugins/hoster/NosuploadCom.py new file mode 100644 index 0000000000..8238928bd7 --- /dev/null +++ b/module/plugins/hoster/NosuploadCom.py @@ -0,0 +1,49 @@ +# -*- coding: utf-8 -*- + +import re + +from ..internal.XFSHoster import XFSHoster + + +class NosuploadCom(XFSHoster): + __name__ = "NosuploadCom" + __type__ = "hoster" + __version__ = "0.38" + __status__ = "testing" + + __pattern__ = r'http://(?:www\.)?nosupload\.com/\?d=\w{12}' + __config__ = [("activated", "bool", "Activated", True), + ("use_premium", "bool", "Use premium account if available", True), + ("fallback", "bool", + "Fallback to free download if premium fails", True), + ("chk_filesize", "bool", "Check file size", True), + ("max_wait", "int", "Reconnect if waiting time is greater than minutes", 10)] + + __description__ = """Nosupload.com hoster plugin""" + __license__ = "GPLv3" + __authors__ = [("igel", "igelkun@myopera.com")] + + PLUGIN_DOMAIN = "nosupload.com" + + SIZE_PATTERN = r'

    Size: (?P[\d.,]+) (?P[\w^_]+)

    ' + LINK_PATTERN = r'Download' + + WAIT_PATTERN = r'Please wait.*?>(\d+)' + + def get_download_link(self): + #: Stage1: press the "Free Download" button + data = self._post_parameters() + self.data = self.load(self.pyfile.url, post=data) + + #: Stage2: wait some time and press the "Download File" button + data = self._post_parameters() + wait_time = re.search( + self.WAIT_PATTERN, + self.data, + re.M | re.S).group(1) + self.log_debug("Hoster told us to wait %s seconds" % wait_time) + self.wait(wait_time) + self.data = self.load(self.pyfile.url, post=data) + + #: Stage3: get the download link + return re.search(self.LINK_PATTERN, self.data, re.S).group(1) diff --git a/module/plugins/hoster/NovafileCom.py b/module/plugins/hoster/NovafileCom.py index 599ec5f7d8..41a8f4758d 100644 --- a/module/plugins/hoster/NovafileCom.py +++ b/module/plugins/hoster/NovafileCom.py @@ -1,30 +1,76 @@ # -*- coding: utf-8 -*- - -# Test links (random.bin): +# +# Test links: # http://novafile.com/vfun4z6o2cit # http://novafile.com/s6zrr5wemuz4 -from module.plugins.hoster.XFileSharingPro import XFileSharingPro, create_getInfo +import re +import urlparse + +import pycurl + +from ..captcha.HCaptcha import HCaptcha +from ..internal.XFSHoster import XFSHoster -class NovafileCom(XFileSharingPro): +class NovafileCom(XFSHoster): __name__ = "NovafileCom" __type__ = "hoster" - __pattern__ = r"http://(?:\w*\.)*novafile\.com/\w{12}" - __version__ = "0.02" - __description__ = """novafile.com hoster plugin""" - __author_name__ = ("zoidberg", "stickell") - __author_mail__ = ("zoidberg@mujmail.cz", "l.stickell@yahoo.it") + __version__ = "0.14" + __status__ = "testing" + + __pattern__ = r'https?://(?:www\.)?novafile\.(?:com|org)/(?:file/)?\w{12}' + __config__ = [("activated", "bool", "Activated", True), + ("use_premium", "bool", "Use premium account if available", True), + ("fallback", "bool", + "Fallback to free download if premium fails", True), + ("chk_filesize", "bool", "Check file size", True), + ("max_wait", "int", "Reconnect if waiting time is greater than minutes", 10)] + + __description__ = """Novafile.com hoster plugin""" + __license__ = "GPLv3" + __authors__ = [("zoidberg", "zoidberg@mujmail.cz"), + ("stickell", "l.stickell@yahoo.it")] + + PLUGIN_DOMAIN = "novafile.org" + URL_REPLACEMENTS = [(r"novafile\.com", "novafile.org"), + ("http://", "https://")] + + DIRECT_LINK = False + + ERROR_PATTERN = r'class="alert.+?alert-separate".*?>\s*(?:

    )?(.*?)\s*Please wait (\d+) seconds

    ' + DL_LIMIT_PATTERN = r'You have to wait (.+?) until the next download becomes available.' + + LINK_PATTERN = r'Download File' + + def handle_captcha(self, inputs): + m = re.search(r'\$\.post\( "/ddl",\s*\{(.+?) \} \);', self.data) + if m is not None: + hcaptcha = HCaptcha(self.pyfile) + captcha_key = hcaptcha.detect_key() + if captcha_key: + self.captcha = hcaptcha + response = hcaptcha.challenge(captcha_key) + + captcha_inputs = {} + for _i in m.group(1).split(','): + _k, _v = _i.split(':', 1) + _k = _k.strip('" ') + if "g-recaptcha-response" in _v: + _v = response + + captcha_inputs[_k] = _v.strip('" ') - FILE_SIZE_PATTERN = r'
    (?P.+?)
    ' - ERROR_PATTERN = r'class="alert[^"]*alert-separate"[^>]*>\s*(?:

    )?(.*?)\s*Download File' - WAIT_PATTERN = r'

    Please wait ]*>(\d+) seconds

    ' + self.req.http.c.setopt(pycurl.HTTPHEADER, ["X-Requested-With: XMLHttpRequest"]) - HOSTER_NAME = "novafile.com" + html = self.load(urlparse.urljoin(self.pyfile.url, "/ddl"), + post=captcha_inputs) - def setup(self): - self.multiDL = False + self.req.http.c.setopt(pycurl.HTTPHEADER, ["X-Requested-With:"]) + if html == "OK": + self.captcha.correct() -getInfo = create_getInfo(NovafileCom) + else: + self.retry_captcha() diff --git a/module/plugins/hoster/NowDownloadEu.py b/module/plugins/hoster/NowDownloadEu.py deleted file mode 100644 index fb00e933e1..0000000000 --- a/module/plugins/hoster/NowDownloadEu.py +++ /dev/null @@ -1,72 +0,0 @@ -# -*- 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 . - - @author: zoidberg -""" - -import re -from module.plugins.internal.SimpleHoster import SimpleHoster, create_getInfo -from module.utils import fixup - - -class NowDownloadEu(SimpleHoster): - __name__ = "NowDownloadEu" - __type__ = "hoster" - __pattern__ = r"http://(?:www\.)?nowdownload\.(ch|co|eu|sx)/(dl/|download\.php\?id=)(?P\w+)" - __version__ = "0.05" - __description__ = """NowDownloadCh""" - __author_name__ = ("godofdream", "Walter Purcaro") - __author_mail__ = ("", "vuolter@gmail.com") - - FILE_INFO_PATTERN = r'Downloading
    (?P.*) (?P[0-9,.]+) (?P[kKMG])i?B

    ' - FILE_OFFLINE_PATTERN = r'(This file does not exist!)' - FILE_TOKEN_PATTERN = r'"(/api/token\.php\?token=[a-z0-9]+)"' - FILE_CONTINUE_PATTERN = r'"(/dl2/[a-z0-9]+/[a-z0-9]+)"' - FILE_WAIT_PATTERN = r'\.countdown\(\{until: \+(\d+),' - FILE_DOWNLOAD_LINK = r'"(http://f\d+\.nowdownload\.ch/dl/[a-z0-9]+/[a-z0-9]+/[^<>"]*?)"' - - FILE_NAME_REPLACEMENTS = [("&#?\w+;", fixup), (r'<[^>]*>', '')] - - def setup(self): - self.multiDL = self.resumeDownload = True - self.chunkLimit = -1 - - def handleFree(self): - tokenlink = re.search(self.FILE_TOKEN_PATTERN, self.html) - continuelink = re.search(self.FILE_CONTINUE_PATTERN, self.html) - if not tokenlink or not continuelink: - self.fail('Plugin out of Date') - - found = re.search(self.FILE_WAIT_PATTERN, self.html) - if found: - wait = int(found.group(1)) - else: - wait = 60 - - baseurl = "http://www.nowdownload.ch" - self.html = self.load(baseurl + str(tokenlink.group(1))) - self.setWait(wait) - self.wait() - - self.html = self.load(baseurl + str(continuelink.group(1))) - - url = re.search(self.FILE_DOWNLOAD_LINK, self.html) - if not url: - self.fail('Download Link not Found (Plugin out of Date?)') - self.logDebug('Download link: ' + str(url.group(1))) - self.download(str(url.group(1))) - - -getInfo = create_getInfo(NowDownloadEu) diff --git a/module/plugins/hoster/NowVideoSx.py b/module/plugins/hoster/NowVideoSx.py new file mode 100644 index 0000000000..bfcaca227a --- /dev/null +++ b/module/plugins/hoster/NowVideoSx.py @@ -0,0 +1,48 @@ +# -*- coding: utf-8 -*- + +import re + +from ..internal.SimpleHoster import SimpleHoster + + +class NowVideoSx(SimpleHoster): + __name__ = "NowVideoSx" + __type__ = "hoster" + __version__ = "0.17" + __status__ = "testing" + + __pattern__ = r'http://(?:www\.)?nowvideo\.[a-zA-Z]{2,}/(video/|mobile/(#/videos/|.+?id=))(?P\w+)' + __config__ = [("activated", "bool", "Activated", True), + ("use_premium", "bool", "Use premium account if available", True), + ("fallback", "bool", + "Fallback to free download if premium fails", True), + ("chk_filesize", "bool", "Check file size", True), + ("max_wait", "int", "Reconnect if waiting time is greater than minutes", 10)] + + __description__ = """NowVideo.sx hoster plugin""" + __license__ = "GPLv3" + __authors__ = [("Walter Purcaro", "vuolter@gmail.com")] + + URL_REPLACEMENTS = [ + (__pattern__ + ".*", + r'http://www.nowvideo.sx/video/\g')] + + NAME_PATTERN = r'

    (?P.+?)<' + OFFLINE_PATTERN = r'>This file no longer exists' + + LINK_FREE_PATTERN = r'\s*.+? (?P[\d.]+) (?P[\w^_]+)<' + + LINK_PREMIUM_PATTERN = r'File name :\s*(?P[^<]+)' - FILE_SIZE_PATTERN = r'File size :\s*(?P[^<]+)' - FILE_OFFLINE_PATTERN = r'The (requested)? file (could not be found|has been deleted)' - FILE_URL_REPLACEMENTS = [(r'(http://[^/]*).*', r'\1/en/')] - - DOWNLOAD_LINK_PATTERN = r'
     
     
     \s+
    \w+)\.)?(?P1fichier\.com|alterupload\.com|cjoint\.net|d(?:es)?fichiers\.com|dl4free\.com|megadl\.fr|mesfichiers\.org|piecejointe\.net|pjointe\.com|tenvoi\.com)(?:/\?(?P\w+))?' + __config__ = [("activated", "bool", "Activated", True), + ("use_premium", "bool", "Use premium account if available", True), + ("fallback", "bool", "Fallback to free download if premium fails", True), + ("chk_filesize", "bool", "Check file size", True), + ("max_wait", "int", "Reconnect if waiting time is greater than minutes", 10)] + + __description__ = """1fichier.com hoster plugin""" + __license__ = "GPLv3" + __authors__ = [("fragonib", "fragonib[AT]yahoo[DOT]es"), + ("the-razer", "daniel_ AT gmx DOT net"), + ("zoidberg", "zoidberg@mujmail.cz"), + ("imclem", None), + ("stickell", "l.stickell@yahoo.it"), + ("Elrick69", "elrick69[AT]rocketmail[DOT]com"), + ("Walter Purcaro", "vuolter@gmail.com"), + ("Ludovic Lehmann", "ludo.lehmann@gmail.com"), + ("GammaC0de", "nitzo2001[AT]yahoo[DOT]com")] + + URL_REPLACEMENTS = [(__pattern__ + '.*', lambda m:"https://1fichier.com/?" + (m.group('ID1') if m.group('ID1') else m.group('ID2')))] + + COOKIES = [("1fichier.com", "LG", "en")] + + NAME_PATTERN = r'(?P.+?)<' + SIZE_PATTERN = r"(?P[\d.,]+) (?P[\w^_]+)" + OFFLINE_PATTERN = r'(?:File not found !\s*<|>\s*The requested file (?:has been deleted|do(?:es)? not exist))' + LINK_PATTERN = r'Click here to download the file' + TEMP_OFFLINE_PATTERN = r'Without subscription, you can only download one file at|Our services are in maintenance' + PREMIUM_ONLY_PATTERN = r'is not possible to unregistered users|need a subscription' + + WAIT_PATTERN = r'>You must wait \d+ minutes' + + def setup(self): + self.multiDL = self.premium + self.chunk_limit = -1 if self.premium else 1 + self.resume_download = True + + def handle_free(self, pyfile): + url, inputs = self.parse_html_form('action="https://1fichier.com/\?[\w^_]+') + + if not url: + self.log_error(_("Free download form not found")) + return + if "pass" in inputs: - inputs['pass'] = self.getPassword() + password = self.get_password() - self.download(url, post=inputs) + if password: + inputs['pass'] = password - # Check download - self.checkDownloadedFile() + else: + self.fail(_("Download is password protected")) - def checkDownloadedFile(self): - check = self.checkDownload({"wait": self.WAITING_PATTERN}) - if check == "wait": - self.waitAndRetry(int(self.lastcheck.group(1)) * 60) + inputs.pop('save', None) + inputs['dl_no_ssl'] = "on" - def waitAndRetry(self, wait_time): - self.setWait(wait_time, True) - self.wait() - self.retry() + self.data = self.load(url, post=inputs) - def setup(self): - self.multiDL = self.premium - self.resumeDownload = True + self.check_errors() + + m = re.search(self.LINK_PATTERN, self.data) + if m is not None: + self.link = m.group(1) + + def handle_premium(self, pyfile): + self.download(pyfile.url, + post={'did': 0, + 'dl_no_ssl': "on"}) -getInfo = create_getInfo(OneFichierCom) diff --git a/module/plugins/hoster/OnlineTvRecorder.py b/module/plugins/hoster/OnlineTvRecorder.py new file mode 100644 index 0000000000..d920b2bb6b --- /dev/null +++ b/module/plugins/hoster/OnlineTvRecorder.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- + + +from module.network.HTTPRequest import BadHeader + +from .Http import Http + + +# Support onlinetvrecorder.com + + +class OnlineTvRecorder(Http): + __name__ = "OnlineTvRecorder" + __type__ = "hoster" + __version__ = "0.05" + __status__ = "testing" + + # RIPE Database: + # inetnum: 81.95.11.0 - 81.95.11.63 + # route: 81.95.8.0/21 + # additional: 93.115.84.162 + __pattern__ = r'https?://(81\.95\.11\.\d{1,2}|93\.115\.84\.162|download\d{1,2}.onlinetvrecorder.com)/download/\d+/\d+/\d*/[0-9a-f]+/.+' + __config__ = [("activated", "bool", "Activated", True)] + __description__ = """OnlineTvRecorder hoster plugin""" + __license__ = "GPLv3" + __authors__ = [("Tim Gregory", "bogeyman@valar.de")] + + def setup(self): + # OnlineTvRecorder policy + self.multiDL = False + self.chunk_limit = 1 + self.resume_download = True + + def process(self, pyfile): + try: + return Http.process(self, pyfile) + + except BadHeader, e: + self.log_debug("OnlineTvRecorder httpcode: %d" % e.code) + if e.code == 503: + # max queueing for 3 hours + self.retry(360, 30, _("Waiting in download queue")) diff --git a/module/plugins/hoster/OverLoadMe.py b/module/plugins/hoster/OverLoadMe.py new file mode 100644 index 0000000000..efa3a34e03 --- /dev/null +++ b/module/plugins/hoster/OverLoadMe.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- + +from ..internal.misc import json, parse_size +from ..internal.MultiHoster import MultiHoster + + +class OverLoadMe(MultiHoster): + __name__ = "OverLoadMe" + __type__ = "hoster" + __version__ = "0.20" + __status__ = "testing" + + __pattern__ = r'https?://.*overload\.me/.+' + __config__ = [("activated", "bool", "Activated", True), + ("use_premium", "bool", "Use premium account if available", True), + ("fallback", "bool", "Fallback to free download if premium fails", False), + ("chk_filesize", "bool", "Check file size", True), + ("max_wait", "int", "Reconnect if waiting time is greater than minutes", 10), + ("revert_failed", "bool", "Revert to standard download if fails", True)] + + __description__ = """Over-Load.me multi-hoster plugin""" + __license__ = "GPLv3" + __authors__ = [("marley", "marley@over-load.me")] + + def setup(self): + self.chunk_limit = 5 + + def handle_premium(self, pyfile): + data = self.account.get_data() + page = self.load("https://api.over-load.me/getdownload.php", + get={'auth': data['password'], + 'link': pyfile.url}) + + data = json.loads(page) + + self.log_debug(data) + + if data['error'] == 1: + self.log_warning(data['msg']) + self.temp_offline() + else: + self.link = data['downloadlink'] + if pyfile.name and pyfile.name.endswith('.tmp') and data[ + 'filename']: + pyfile.name = data['filename'] + pyfile.size = parse_size(data['filesize']) diff --git a/module/plugins/hoster/PixeldrainCom.py b/module/plugins/hoster/PixeldrainCom.py new file mode 100644 index 0000000000..711b9a823a --- /dev/null +++ b/module/plugins/hoster/PixeldrainCom.py @@ -0,0 +1,53 @@ +# -*- coding: utf-8 -*- + +import re + +from ..internal.misc import json +from ..internal.SimpleHoster import SimpleHoster + + +class PixeldrainCom(SimpleHoster): + __name__ = "PixeldrainCom" + __type__ = "hoster" + __version__ = "0.03" + __status__ = "testing" + + __pattern__ = r'https?://(?:www\.)?pixeldrain\.com/u/(?P\w+)' + __config__ = [("activated", "bool", "Activated", True), + ("use_premium", "bool", "Use premium account if available", True), + ("fallback", "bool", "Fallback to free download if premium fails", True), + ("chk_filesize", "bool", "Check file size", True), + ("max_wait", "int", "Reconnect if waiting time is greater than minutes", 10)] + + __description__ = """Pixeldrain.com hoster plugin""" + __license__ = "GPLv3" + __authors__ = [("GammaC0de", "nitzo2001[AT]yahoo[DOT]com")] + + DIRECT_LINK = False + + #: See https://pixeldrain.com/api/ + API_URL = "https://pixeldrain.com/api/" + + def api_info(self, url): + file_id = re.match(self.__pattern__, url).group('ID') + json_data = self.load("%s/file/%s/info" % (self.API_URL, file_id)) + file_info = json.loads(json_data) + + if file_info['success'] is False: + return {'status': 1} + + else: + return {'name': file_info['name'], + 'size': file_info['size'], + 'status': 2} + + def setup(self): + if self.premium: + self.req.addAuth(":%s" % self.account.info["login"]["password"]) + + def handle_free(self, pyfile): + file_id = self.info['pattern']['ID'] + self.download("%s/file/%s" % (self.API_URL, file_id)) + + def handle_premium(self, pyfile): + self.handle_free(pyfile) diff --git a/module/plugins/hoster/PornhostCom.py b/module/plugins/hoster/PornhostCom.py index 1d340694b1..97123b5314 100644 --- a/module/plugins/hoster/PornhostCom.py +++ b/module/plugins/hoster/PornhostCom.py @@ -1,75 +1,42 @@ -#!/usr/bin/env python # -*- coding: utf-8 -*- import re -from module.plugins.Hoster import Hoster + +from ..internal.Hoster import Hoster class PornhostCom(Hoster): __name__ = "PornhostCom" __type__ = "hoster" - __pattern__ = r'http://[\w\.]*?pornhost\.com/([0-9]+/[0-9]+\.html|[0-9]+)' - __version__ = "0.2" - __description__ = """Pornhost.com Download Hoster""" - __author_name__ = ("jeix") - __author_mail__ = ("jeix@hasnomail.de") - - def process(self, pyfile): - self.download_html() - if not self.file_exists(): - self.offline() - - pyfile.name = self.get_file_name() - self.download(self.get_file_url()) + __version__ = "0.27" + __status__ = "testing" - ### old interface - def download_html(self): - url = self.pyfile.url - self.html = self.load(url) + __pattern__ = r'https?://(?:www\.)?pornhost\.com/\d+' + __config__ = [("activated", "bool", "Activated", True)] - def get_file_url(self): - """ returns the absolute downloadable filepath - """ - if self.html is None: - self.download_html() + __description__ = """Pornhost.com hoster plugin""" + __license__ = "GPLv3" + __authors__ = [("jeix", "jeix@hasnomail.de"), + ("GammaC0de", "nitzo2001[AT}yahoo[DOT]com")] - file_url = re.search(r'download this file.*?.*?(.+?)<' + LINK_PATTERN = r'' + OFFLINE_PATTERN = r'>Gallery not found<' - file_url = file_url.group(1).strip() - - return file_url - - def get_file_name(self): - if self.html is None: - self.download_html() + def process(self, pyfile): + self.data = self.load(pyfile.url) - name = re.search(r'pornhost\.com - free file hosting with a twist - gallery(.*?)', self.html) - if not name: - name = re.search(r'id="url" value="http://www\.pornhost\.com/(.*?)/"', self.html) - if not name: - name = re.search(r'pornhost\.com - free file hosting with a twist -(.*?)', self.html) - if not name: - name = re.search(r'"http://file[0-9]+\.pornhost\.com/.*?/(.*?)"', self.html) + if re.search(self.OFFLINE_PATTERN, self.data) is not None: + self.offline() - name = name.group(1).strip() + ".flv" + m = re.search(self.NAME_PATTERN, self.data) + if m is None: + self.error(_("name pattern not found")) - return name + pyfile.name = m.group(1) + ".mp4" - def file_exists(self): - """ returns True or False - """ - if self.html is None: - self.download_html() + m = re.search(self.LINK_PATTERN, self.data) + if m is None: + self.error(_("link pattern not found")) - if (re.search(r'gallery not found', self.html) is not None or - re.search(r'You will be redirected to', self.html) is not None): - return False - else: - return True + self.download(m.group(1)) diff --git a/module/plugins/hoster/PornhubCom.py b/module/plugins/hoster/PornhubCom.py index 7bbb8d9198..9bfcb62866 100644 --- a/module/plugins/hoster/PornhubCom.py +++ b/module/plugins/hoster/PornhubCom.py @@ -1,84 +1,81 @@ -#!/usr/bin/env python # -*- coding: utf-8 -*- import re -from module.plugins.Hoster import Hoster +from ..internal.misc import BIGHTTPRequest, json +from ..internal.SimpleHoster import SimpleHoster -class PornhubCom(Hoster): + +class PornhubCom(SimpleHoster): __name__ = "PornhubCom" __type__ = "hoster" - __pattern__ = r'http://[\w\.]*?pornhub\.com/view_video\.php\?viewkey=[\w\d]+' - __version__ = "0.5" - __description__ = """Pornhub.com Download Hoster""" - __author_name__ = ("jeix") - __author_mail__ = ("jeix@hasnomail.de") - - def process(self, pyfile): - self.download_html() - if not self.file_exists(): - self.offline() - - pyfile.name = self.get_file_name() - self.download(self.get_file_url()) - - def download_html(self): - url = self.pyfile.url - self.html = self.load(url) - - def get_file_url(self): - """ returns the absolute downloadable filepath - """ - if self.html is None: - self.download_html() - - url = "http://www.pornhub.com//gateway.php" - video_id = self.pyfile.url.split('=')[-1] - # thanks to jD team for this one v - post_data = "\x00\x03\x00\x00\x00\x01\x00\x0c\x70\x6c\x61\x79\x65\x72\x43\x6f\x6e\x66\x69\x67\x00\x02\x2f\x31\x00\x00\x00\x44\x0a\x00\x00\x00\x03\x02\x00" - post_data += chr(len(video_id)) - post_data += video_id - post_data += "\x02\x00\x02\x2d\x31\x02\x00\x20" - post_data += "add299463d4410c6d1b1c418868225f7" - - content = self.req.load(url, post=str(post_data)) - - new_content = "" - for x in content: - if ord(x) < 32 or ord(x) > 176: - new_content += '#' - else: - new_content += x - - content = new_content - - file_url = re.search(r'flv_url.*(http.*?)##post_roll', content).group(1) - - return file_url - - def get_file_name(self): - if self.html is None: - self.download_html() - - match = re.search(r']+>([^<]+) - ', self.html) - if match: - name = match.group(1) - else: - matches = re.findall('

    (.*?)

    ', self.html) - if len(matches) > 1: - name = matches[1] - else: - name = matches[0] - - return name + '.flv' - - def file_exists(self): - """ returns True or False - """ - if self.html is None: - self.download_html() - - if re.search(r'This video is no longer in our database or is in conversion', self.html) is not None: - return False - else: - return True + __version__ = "0.62" + __status__ = "testing" + + __pattern__ = r'https?://(?:www\.)?pornhub\.com/view_video\.php\?viewkey=\w+' + __config__ = [("activated", "bool", "Activated", True), + ("use_premium", "bool", "Use premium account if available", True), + ("fallback", "bool", "Fallback to free download if premium fails", True), + ("chk_filesize", "bool", "Check file size", True), + ("max_wait", "int", "Reconnect if waiting time is greater than minutes", 10)] + + __description__ = """Pornhub.com hoster plugin""" + __license__ = "GPLv3" + __authors__ = [("jeix", "jeix@hasnomail.de"), + ("GammaC0de", "nitzo2001[AT]yahoo[DOT]com")] + + NAME_PATTERN = r'"video_title":"(?P.+?)"' + + TEMP_OFFLINE_PATTERN = r'^unmatchable$' # Who knows? + OFFLINE_PATTERN = r'^unmatchable$' # Who knows? + + + def get_info(self, url="", html=""): + info = super(PornhubCom, self).get_info(url, html) + # Unfortunately, NAME_PATTERN does not include file extension so we blindly add '.mp4' as an extension. + # (hopefully all links are '.mp4' files) + if 'name' in info: + info['name'] += ".mp4" + + return info + + def setup(self): + self.resume_download = True + self.multiDL = True + + try: + self.req.http.close() + except Exception: + pass + + self.req.http = BIGHTTPRequest( + cookies=self.req.cj, + options=self.pyload.requestFactory.getOptions(), + limit=2000000) + + def handle_free(self, pyfile): + m = re.search(r'
    .+?', self.data, re.S) + if m is None: + self.error(_("Player Javascript data not found")) + + script = m.group(1) + + m = re.search(r'qualityItems_\d+', script) + if m is None: + self.error(_("`qualityItems` variable no found")) + + result_var = re.search(r'qualityItems_\d+', script).group(0) + + script = "".join(re.findall(r'^\s*var .+', script, re.M)) + script = re.sub(r"[\n\t]|/\*.+?\*/", "", script) + script += "JSON.stringify(%s);" % result_var + + res = self.js.eval(script) + json_data = json.loads(res) + + urls = dict([(int(re.search("^(\d+)", _x['text']).group(0)), _x['url']) + for _x in json_data if _x['url']]) + + quality = max(urls.keys()) + + self.link = urls[quality] diff --git a/module/plugins/hoster/PornovkaCz.py b/module/plugins/hoster/PornovkaCz.py new file mode 100644 index 0000000000..fcd97229a4 --- /dev/null +++ b/module/plugins/hoster/PornovkaCz.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- + +import re + +from ..internal.Hoster import Hoster + + +class PornovkaCz(Hoster): + __name__ = "PornovkaCz" + __type__ = "hoster" + __version__ = "0.02" + __status__ = "testing" + + __pattern__ = r'https?://(?:www\.)?pornovka\.cz/(.+)' + __config__ = [("activated", "bool", "Activated", True)] + + __description__ = """Pornovka.cz hoster plugin""" + __license__ = "GPLv3" + __authors__ = [("ondrej", "git@ondrej.it")] + + NAME_PATTERN = r'

    ([^<]+)' + + def setup(self): + self.resume_download = True + self.multiDL = True + + def process(self, pyfile): + pornovka_resp = self.load(pyfile.url) + data_url = re.findall(r'data-url="([^"]+)', pornovka_resp) + if not data_url: + self.error(_("Data url not found")) + + data_resp = self.load(data_url[0]) + video_url = re.findall(r"""src=.([^'"]+).>""", data_resp) + if not video_url: + self.error(_("Video url not found")) + + # ascii codec can't encode character... + self.pyfile.name = re.search(self.NAME_PATTERN, pornovka_resp).group(1) + self.pyfile.name += "." + video_url[0].split(".")[-1] + + self.log_info(_("Downloading file...")) + self.download(video_url[0]) diff --git a/module/plugins/hoster/PorntrexCom.py b/module/plugins/hoster/PorntrexCom.py new file mode 100644 index 0000000000..07082e5dec --- /dev/null +++ b/module/plugins/hoster/PorntrexCom.py @@ -0,0 +1,49 @@ +# -*- coding: utf-8 -*- + +import re + +from ..internal.SimpleHoster import SimpleHoster + + +class PorntrexCom(SimpleHoster): + __name__ = "PorntrexCom" + __type__ = "hoster" + __version__ = "0.01" + __status__ = "testing" + + __pattern__ = r'https?://(?:www\.)?porntrex\.com/video/.+' + + __config__ = [("activated", "bool", "Activated", True), + ("chk_filesize", "bool", "Check file size", True), + ("quality", "360p;480p;720p;1080p;1440p;2160p", "Quality Setting", "1080p")] + + __description__ = """Porntrex.com hoster plugin""" + __license__ = "GPLv3" + __authors__ = [("ondrej", "git@ondrej.it")] + + NAME_PATTERN = r'

    (?P.+?)

    ' + OFFLINE_PATTERN = r'page not found' + + DISPOSITION = False + + def setup(self): + self.multiDL = True + self.resume_download = False + + def handle_free(self, pyfile): + html = self.load(pyfile.url) + + quality = self.config.get("quality") + all_quality = ["2160p", "1440p", "1080p", "720p", "480p", "360p"] + + for i in all_quality[all_quality.index(quality):]: + video_url = re.findall(r"https://www.porntrex.com/get_file/[\w\d/]+_{0}.mp4".format(i), html) + if video_url: + self.link = video_url[0] + break + + if not self.link: + self.error(_("Video url not found")) + + self.pyfile.name = re.search(self.NAME_PATTERN, html).group(1) + self.pyfile.name += "." + self.link.split(".")[-1] diff --git a/module/plugins/hoster/Premium4Me.py b/module/plugins/hoster/Premium4Me.py deleted file mode 100644 index d6c154693e..0000000000 --- a/module/plugins/hoster/Premium4Me.py +++ /dev/null @@ -1,71 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -from urllib import quote -from os.path import exists -from os import remove - -from module.plugins.Hoster import Hoster -from module.utils import fs_encode - - -class Premium4Me(Hoster): - __name__ = "Premium4Me" - __version__ = "0.08" - __type__ = "hoster" - - __pattern__ = r"http://premium.to/.*" - __description__ = """Premium.to hoster plugin""" - __author_name__ = ("RaNaN", "zoidberg", "stickell") - __author_mail__ = ("RaNaN@pyload.org", "zoidberg@mujmail.cz", "l.stickell@yahoo.it") - - def setup(self): - self.resumeDownload = True - self.chunkLimit = 1 - - def process(self, pyfile): - if not self.account: - self.logError(_("Please enter your %s account or deactivate this plugin") % "premium.to") - self.fail("No premium.to account provided") - - self.logDebug("premium.to: Old URL: %s" % pyfile.url) - - tra = self.getTraffic() - - #raise timeout to 2min - self.req.setOption("timeout", 120) - - self.download( - "http://premium.to/api/getfile.php?authcode=%s&link=%s" % (self.account.authcode, quote(pyfile.url, "")), - disposition=True) - - check = self.checkDownload({"nopremium": "No premium account available"}) - - if check == "nopremium": - self.retry(60, 300, 'No premium account available') - - err = '' - if self.req.http.code == '420': - # Custom error code send - fail - lastDownload = fs_encode(self.lastDownload) - - if exists(lastDownload): - f = open(lastDownload, "rb") - err = f.read(256).strip() - f.close() - remove(lastDownload) - else: - err = 'File does not exist' - - trb = self.getTraffic() - self.logInfo("Filesize: %d, Traffic used %d, traffic left %d" % (pyfile.size, tra - trb, trb)) - - if err: - self.fail(err) - - def getTraffic(self): - try: - traffic = int(self.load("http://premium.to/api/traffic.php?authcode=%s" % self.account.authcode)) - except: - traffic = 0 - return traffic diff --git a/module/plugins/hoster/PremiumTo.py b/module/plugins/hoster/PremiumTo.py new file mode 100644 index 0000000000..4a661d1804 --- /dev/null +++ b/module/plugins/hoster/PremiumTo.py @@ -0,0 +1,52 @@ +# -*- coding: utf-8 -*- + +from __future__ import with_statement + +import re + +from ..internal.misc import json, fs_encode +from ..internal.MultiHoster import MultiHoster + + +class PremiumTo(MultiHoster): + __name__ = "PremiumTo" + __type__ = "hoster" + __version__ = "0.36" + __status__ = "testing" + + __pattern__ = r'^unmatchable$' + __config__ = [("activated", "bool", "Activated", True), + ("use_premium", "bool", "Use premium account if available", True), + ("fallback", "bool", "Fallback to free download if premium fails", False), + ("revert_failed", "bool", "Revert to standard download if fails", True), + ("chk_filesize", "bool", "Check file size", True), + ("max_wait", "int", "Reconnect if waiting time is greater than minutes", 10)] + + __description__ = """Premium.to multi-hoster plugin""" + __license__ = "GPLv3" + __authors__ = [("RaNaN", "RaNaN@pyload.net"), + ("zoidberg", "zoidberg@mujmail.cz"), + ("stickell", "l.stickell@yahoo.it"), + ("GammaC0de", "nitzo2001[AT]yahoo[DOT]com")] + + CHECK_TRAFFIC = True + + # See https://premium.to/API.html + API_URL = "http://api.premium.to/api/2/" + + def handle_premium(self, pyfile): + self.download(self.API_URL + "getfile.php", + get={'userid': self.account.user, + 'apikey': self.account.info['login']['password'], + 'link': pyfile.url}, + disposition=True) + + def check_download(self): + if self.scan_download({'json': re.compile(r'\A{["\']code["\']:\d+,["\']message["\']:(["\']).+?\1}\Z')}): + with open(fs_encode(self.last_download), "rb") as f: + json_data = json.loads(f.read()) + + self.remove(self.last_download) + self.fail(_("API error %s - %s") % (json_data['code'], json_data['message'])) + + return MultiHoster.check_download(self) diff --git a/module/plugins/hoster/PremiumizeMe.py b/module/plugins/hoster/PremiumizeMe.py index c5c09857fc..db57da08dd 100644 --- a/module/plugins/hoster/PremiumizeMe.py +++ b/module/plugins/hoster/PremiumizeMe.py @@ -1,55 +1,64 @@ -from module.plugins.Hoster import Hoster +# -*- coding: utf-8 -*- -from module.common.json_layer import json_loads +import re +from ..internal.misc import json +from ..internal.MultiHoster import MultiHoster -class PremiumizeMe(Hoster): + +class PremiumizeMe(MultiHoster): __name__ = "PremiumizeMe" - __version__ = "0.12" __type__ = "hoster" - __description__ = """Premiumize.Me hoster plugin""" - - # Since we want to allow the user to specify the list of hoster to use we let MultiHoster.coreReady - # create the regex patterns for us using getHosters in our PremiumizeMe hook. - __pattern__ = None - - __author_name__ = ("Florian Franzen") - __author_mail__ = ("FlorianFranzen@gmail.com") - - def process(self, pyfile): - # Check account - if not self.account or not self.account.canUse(): - self.logError(_("Please enter your %s account or deactivate this plugin") % "premiumize.me") - self.fail("No valid premiumize.me account provided") - - # In some cases hostsers do not supply us with a filename at download, so we - # are going to set a fall back filename (e.g. for freakshare or xfileshare) - self.pyfile.name = self.pyfile.name.split('/').pop() # Remove everthing before last slash - - # Correction for automatic assigned filename: Removing html at end if needed - suffix_to_remove = ["html", "htm", "php", "php3", "asp", "shtm", "shtml", "cfml", "cfm"] - temp = self.pyfile.name.split('.') - if temp.pop() in suffix_to_remove: - self.pyfile.name = ".".join(temp) - - # Get account data - (user, data) = self.account.selectAccount() - - # Get rewritten link using the premiumize.me api v1 (see https://secure.premiumize.me/?show=api) - answer = self.load( - "https://api.premiumize.me/pm-api/v1.php?method=directdownloadlink¶ms[login]=%s¶ms[pass]=%s¶ms[link]=%s" % ( - user, data['password'], self.pyfile.url)) - data = json_loads(answer) - - # Check status and decide what to do - status = data['status'] - if status == 200: - self.download(data['result']['location'], disposition=True) - elif status == 400: - self.fail("Invalid link") - elif status == 404: - self.offline() - elif status >= 500: - self.tempOffline() + __version__ = "0.34" + __status__ = "testing" + + __pattern__ = r'https?://(?:www\.)?premiumize\.me/file\?id=(?P\w+)' + __config__ = [("activated", "bool", "Activated", True), + ("use_premium", "bool", "Use premium account if available", True), + ("fallback", "bool", "Fallback to free download if premium fails", False), + ("chk_filesize", "bool", "Check file size", True), + ("max_wait", "int", "Reconnect if waiting time is greater than minutes", 10), + ("revert_failed", "bool", "Revert to standard download if fails", True)] + + __description__ = """Premiumize.me multi-hoster plugin""" + __license__ = "GPLv3" + __authors__ = [("Florian Franzen", "FlorianFranzen@gmail.com"), + ("GammaC0de", "nitzo2001[AT]yahoo[DOT]com")] + + DIRECT_LINK = False + + # See https://www.premiumize.me/api + API_URL = "https://www.premiumize.me/api/" + + def api_respond(self, method, **kwargs): + json_data = self.load(self.API_URL + method, get=kwargs) + + return json.loads(json_data) + + def handle_premium(self, pyfile): + m = re.search(self.__pattern__, pyfile.url) + if m is None: + res = self.api_respond("transfer/directdl", + src=pyfile.url, + apikey=self.account.info['login']['password']) + + if res['status'] == "success": + self.pyfile.name = res['content'][0]['path'] + self.pyfile.size = res['content'][0]['size'] + self.download(res['content'][0]['link']) + + else: + self.fail(res['message']) + else: - self.fail(data['statusmessage']) + res = self.api_respond("item/details", + id=m.group('ID'), + apikey=self.account.info['login']['password']) + + if res.get('status') != "error": + self.pyfile.name = res['name'] + self.pyfile.size = res['size'] + self.download(res['link']) + + else: + self.fail(res['message']) diff --git a/module/plugins/hoster/PromptfileCom.py b/module/plugins/hoster/PromptfileCom.py new file mode 100644 index 0000000000..5f3a407775 --- /dev/null +++ b/module/plugins/hoster/PromptfileCom.py @@ -0,0 +1,51 @@ +# -*- coding: utf-8 -*- + +import re + +from ..internal.SimpleHoster import SimpleHoster + + +class PromptfileCom(SimpleHoster): + __name__ = "PromptfileCom" + __type__ = "hoster" + __version__ = "0.19" + __status__ = "testing" + + __pattern__ = r'https?://(?:www\.)?promptfile\.com/' + __config__ = [("activated", "bool", "Activated", True), + ("use_premium", "bool", "Use premium account if available", True), + ("fallback", "bool", + "Fallback to free download if premium fails", True), + ("chk_filesize", "bool", "Check file size", True), + ("max_wait", "int", "Reconnect if waiting time is greater than minutes", 10)] + + __description__ = """Promptfile.com hoster plugin""" + __license__ = "GPLv3" + __authors__ = [("igel", "igelkun@myopera.com"), + ("ondrej", "git@ondrej.it")] + + INFO_PATTERN = r'(?P.*?) \((?P[\d.,]+) (?P[\w^_]+)\)' + OFFLINE_PATTERN = r'File Not Found' + + CHASH_PATTERN = r'input.+"([a-z\d]{10,})".+"([a-z\d]{10,})"' + MODIFY_PATTERN = r'\$\("#chash"\)\.val\("(.+)"\+\$\("#chash"\)' + LINK_FREE_PATTERN = r'
    . - - @author: jeix -""" - -import re -from os import rename - -from module.plugins.internal.SimpleHoster import SimpleHoster - - -class PutlockerCom(SimpleHoster): - __name__ = "PutlockerCom" - __type__ = "hoster" - __pattern__ = r'http://(?:www\.)?putlocker\.com/(mobile/)?(file|embed)/(?P[a-zA-Z0-9]+)' - __version__ = "0.32" - __description__ = """Putlocker.Com""" - __author_name__ = ("jeix", "stickell", "Walter Purcaro") - __author_mail__ = ("", "l.stickell@yahoo.it", "vuolter@gmail.com") - - FILE_OFFLINE_PATTERN = r"This file doesn't exist, or has been removed." - FILE_INFO_PATTERN = r'site-content">\s*

    (?P.+)\( (?P[^)]+) \)

    ' - - FILE_URL_REPLACEMENTS = [(__pattern__, r'http://www.putlocker.com/file/\g')] - HOSTER_NAME = "putlocker.com" - - def setup(self): - self.multiDL = self.resumeDownload = True - self.chunkLimit = -1 - - def handleFree(self): - name = self.pyfile.name - link = self._getLink() - self.logDebug("Direct link: " + link) - self.download(link, disposition=True) - self.processName(name) - - def _getLink(self): - hash_data = re.search(r'', self.html) - if not hash_data: - self.parseError('Unable to detect hash') - - post_data = {"hash": hash_data.group(1), "confirm": "Continue+as+Free+User"} - self.html = self.load(self.pyfile.url, post=post_data) - if (">You have exceeded the daily stream limit for your country\\. You can wait until tomorrow" in self.html or - "(>This content server has been temporarily disabled for upgrades|Try again soon\\. You can still download it below\\.<)" in self.html): - self.retry(wait_time=60 * 60 * 2, reason="Download limit exceeded or server disabled") # 2 hours wait - - patterns = (r'(/get_file\.php\?id=[A-Z0-9]+&key=[a-zA-Z0-9=]+&original=1)', - r'(/get_file\.php\?download=[A-Z0-9]+&key=[a-z0-9]+)', - r'(/get_file\.php\?download=[A-Z0-9]+&key=[a-z0-9]+&original=1)', - r'
    Tired of ads and waiting\? Go Pro![\t\n\rn ]+

    [\t\n\rn ]+. - - @author: zoidberg -""" import re -from pycurl import FOLLOWLOCATION -from module.plugins.internal.SimpleHoster import SimpleHoster, create_getInfo +from ..internal.SimpleHoster import SimpleHoster class QuickshareCz(SimpleHoster): __name__ = "QuickshareCz" __type__ = "hoster" - __pattern__ = r"http://.*quickshare.cz/stahnout-soubor/.*" - __version__ = "0.54" - __description__ = """Quickshare.cz""" - __author_name__ = ("zoidberg") + __version__ = "0.64" + __status__ = "testing" - FILE_NAME_PATTERN = r'Název:\s*(?P[^<]+)' - FILE_SIZE_PATTERN = r'Velikost:\s*(?P[0-9.]+) (?P[kKMG])i?B' - FILE_OFFLINE_PATTERN = r'' + __pattern__ = r'http://(?:[^/]*\.)?quickshare\.cz/stahnout-soubor/.+' + __config__ = [("activated", "bool", "Activated", True), + ("use_premium", "bool", "Use premium account if available", True), + ("fallback", "bool", + "Fallback to free download if premium fails", True), + ("chk_filesize", "bool", "Check file size", True), + ("max_wait", "int", "Reconnect if waiting time is greater than minutes", 10)] - def process(self, pyfile): - self.html = self.load(pyfile.url, decode=True) - self.getFileInfo() + __description__ = """Quickshare.cz hoster plugin""" + __license__ = "GPLv3" + __authors__ = [("zoidberg", "zoidberg@mujmail.cz")] + + NAME_PATTERN = r'Název:\s*(?P.+?)' + SIZE_PATTERN = r'Velikost:\s*(?P[\d.,]+) (?P[\w^_]+)' + OFFLINE_PATTERN = r'' - # parse js variables - self.jsvars = dict((x, y.strip("'")) for x, y in re.findall(r"var (\w+) = ([0-9.]+|'[^']*')", self.html)) - self.logDebug(self.jsvars) + def process(self, pyfile): + self.data = self.load(pyfile.url) + self.get_fileInfo() + + #: Parse js variables + self.jsvars = dict( + (x, y.strip("'")) for x, y in re.findall( + r"var (\w+) = ([\d.]+|'.+?')", self.data)) + self.log_debug(self.jsvars) pyfile.name = self.jsvars['ID3'] - # determine download type - free or premium + #: Determine download type - free or premium if self.premium: if 'UU_prihlasen' in self.jsvars: - if self.jsvars['UU_prihlasen'] == '0': - self.logWarning('User not logged in') - self.relogin(self.user) + if self.jsvars['UU_prihlasen'] == "0": + self.log_warning(_("User not logged in")) + self.account.relogin() self.retry() elif float(self.jsvars['UU_kredit']) < float(self.jsvars['kredit_odecet']): - self.logWarning('Not enough credit left') + self.log_warning(_("Not enough credit left")) self.premium = False if self.premium: - self.handlePremium() + self.handle_premium(pyfile) else: - self.handleFree() + self.handle_free(pyfile) - check = self.checkDownload({"err": re.compile(r"\AChyba!")}, max_size=100) - if check == "err": - self.fail("File not found or plugin defect") + if self.scan_download( + {'error': re.compile(r'\AChyba!')}, read_size=100): + self.fail(_("File not found or plugin defect")) - def handleFree(self): - # get download url + def handle_free(self, pyfile): + #: Get download url download_url = '%s/download.php' % self.jsvars['server'] - data = dict((x, self.jsvars[x]) for x in self.jsvars if x in ('ID1', 'ID2', 'ID3', 'ID4')) - self.logDebug("FREE URL1:" + download_url, data) - - self.req.http.c.setopt(FOLLOWLOCATION, 0) - self.load(download_url, post=data) - self.header = self.req.http.header - self.req.http.c.setopt(FOLLOWLOCATION, 1) - - found = re.search("Location\s*:\s*(.*)", self.header, re.I) - if not found: - self.fail('File not found') - download_url = found.group(1) - self.logDebug("FREE URL2:" + download_url) - - # check errors - found = re.search(r'/chyba/(\d+)', download_url) - if found: - if found.group(1) == '1': - self.retry(max_tries=60, wait_time=120, reason="This IP is already downloading") - elif found.group(1) == '2': - self.retry(max_tries=60, wait_time=60, reason="No free slots available") + data = dict( + (x, self.jsvars[x]) for x in self.jsvars if x in ( + "ID1", "ID2", "ID3", "ID4")) + self.log_debug("FREE URL1:" + download_url, data) + + header = self.load(download_url, post=data, just_header=True) + self.link = header.get('location') + if not self.link: + self.fail(_("File not found")) + + self.log_debug("FREE URL2:" + self.link) + + #: Check errors + m = re.search(r'/chyba/(\d+)', self.link) + if m is not None: + if m.group(1) == "1": + self.retry(60, 2 * 60, "This IP is already downloading") + elif m.group(1) == "2": + self.retry(60, 60, "No free slots available") else: - self.fail('Error %d' % found.group(1)) - - # download file - self.download(download_url) + self.fail(_("Error %d") % m.group(1)) - def handlePremium(self): + def handle_premium(self, pyfile): download_url = '%s/download_premium.php' % self.jsvars['server'] - data = dict((x, self.jsvars[x]) for x in self.jsvars if x in ('ID1', 'ID2', 'ID4', 'ID5')) - self.logDebug("PREMIUM URL:" + download_url, data) + data = dict( + (x, self.jsvars[x]) for x in self.jsvars if x in ( + "ID1", "ID2", "ID4", "ID5")) self.download(download_url, get=data) - - -getInfo = create_getInfo(QuickshareCz) diff --git a/module/plugins/hoster/RPNetBiz.py b/module/plugins/hoster/RPNetBiz.py index ae8ccf8a93..5f39f720a4 100644 --- a/module/plugins/hoster/RPNetBiz.py +++ b/module/plugins/hoster/RPNetBiz.py @@ -1,76 +1,84 @@ -import re +# -*- coding: utf-8 -*- -from module.plugins.Hoster import Hoster -from module.common.json_layer import json_loads +from ..internal.misc import json +from ..internal.MultiHoster import MultiHoster -class RPNetBiz(Hoster): + +class RPNetBiz(MultiHoster): __name__ = "RPNetBiz" - __version__ = "0.1" __type__ = "hoster" - __description__ = """RPNet.Biz hoster plugin""" - __pattern__ = r"https?://.*rpnet\.biz" - __author_name__ = ("Dman") - __author_mail__ = ("dmanugm@gmail.com") + __version__ = "0.22" + __status__ = "testing" - def setup(self): - self.chunkLimit = -1 - self.resumeDownload = True + __pattern__ = r'https?://.+rpnet\.biz' + __config__ = [("activated", "bool", "Activated", True), + ("use_premium", "bool", "Use premium account if available", True), + ("fallback", "bool", "Fallback to free download if premium fails", False), + ("chk_filesize", "bool", "Check file size", True), + ("max_wait", "int", "Reconnect if waiting time is greater than minutes", 10), + ("revert_failed", "bool", "Revert to standard download if fails", True)] - def process(self, pyfile): + __description__ = """RPNet.biz multi-hoster plugin""" + __license__ = "GPLv3" + __authors__ = [("Dman", "dmanugm@gmail.com")] - if re.match(self.__pattern__, pyfile.url): - link_status = {'generated': pyfile.url} - elif not self.account: - # Check account - self.logError(_("Please enter your %s account or deactivate this plugin") % "rpnet") - self.fail("No rpnet account provided") - else: - (user, data) = self.account.selectAccount() + def setup(self): + self.chunk_limit = -1 + + def handle_premium(self, pyfile): + user, info = self.account.select() - self.logDebug("Original URL: %s" % pyfile.url) - # Get the download link - response = self.load("https://premium.rpnet.biz/client_api.php", - get={"username": user, "password": data['password'], - "action": "generate", "links": self.pyfile.url}) + res = self.load("https://premium.rpnet.biz/client_api.php", + get={'username': user, + 'password': info['login']['password'], + 'action': "generate", + 'links': pyfile.url}) - self.logDebug("JSON data: %s" % response) - link_status = json_loads(response)['links'][0] # get the first link... since we only queried one + self.log_debug("JSON data: %s" % res) + #: Get the first link... since we only queried one + link_status = json.loads(res)['links'][0] - # Check if we only have an id as a HDD link - if 'id' in link_status: - self.logDebug("Need to wait at least 30 seconds before requery") - self.setWait(30) # wait for 30 seconds - self.wait() - # Lets query the server again asking for the status on the link, - # we need to keep doing this until we reach 100 - max_tries = 30 - my_try = 0 - while (my_try <= max_tries): - self.logDebug("Try: %d ; Max Tries: %d" % (my_try, max_tries)) - response = self.load("https://premium.rpnet.biz/client_api.php", - get={"username": user, "password": data['password'], - "action": "downloadInformation", "id": link_status['id']}) - self.logDebug("JSON data hdd query: %s" % response) - download_status = json_loads(response)['download'] + #: Check if we only have an id as a HDD link + if 'id' in link_status: + self.log_debug("Need to wait at least 30 seconds before requery") + self.wait(30) #: Wait for 30 seconds + #: Lets query the server again asking for the status on the link, + #: We need to keep doing this until we reach 100 + attemps = 30 + my_try = 0 + while (my_try <= attemps): + self.log_debug("Try: %d ; Max Tries: %d" % (my_try, attemps)) + res = self.load("https://premium.rpnet.biz/client_api.php", + get={'username': user, + 'password': info['login']['password'], + 'action': "downloadInformation", + 'id': link_status['id']}) + self.log_debug("JSON data hdd query: %s" % res) + download_status = json.loads(res)['download'] - if download_status['status'] == '100': - link_status['generated'] = download_status['rpnet_link'] - self.logDebug("Successfully downloaded to rpnet HDD: %s" % link_status['generated']) - break - else: - self.logDebug("At %s%% for the file download" % download_status['status']) + if download_status['status'] == "100": + link_status['generated'] = download_status['rpnet_link'] + self.log_debug( + "Successfully downloaded to rpnet HDD: %s" % + link_status['generated']) + break + else: + self.log_debug( + "At %s%% for the file download" % + download_status['status']) - self.setWait(30) - self.wait() - my_try += 1 + self.wait(30) + my_try += 1 - if my_try > max_tries: # We went over the limit! - self.fail("Waited for about 15 minutes for download to finish but failed") + if my_try > attemps: #: We went over the limit! + self.fail( + _("Waited for about 15 minutes for download to finish but failed")) if 'generated' in link_status: - self.download(link_status['generated'], disposition=True) + self.link = link_status['generated'] + return elif 'error' in link_status: self.fail(link_status['error']) else: - self.fail("Something went wrong, not supposed to enter here") + self.fail(_("Something went wrong, not supposed to enter here")) diff --git a/module/plugins/hoster/RapideoPl.py b/module/plugins/hoster/RapideoPl.py new file mode 100644 index 0000000000..bca7be0474 --- /dev/null +++ b/module/plugins/hoster/RapideoPl.py @@ -0,0 +1,102 @@ +# -*- coding: utf-8 -*- + +from ..internal.misc import json +from ..internal.MultiHoster import MultiHoster + + +class RapideoPl(MultiHoster): + __name__ = "RapideoPl" + __type__ = "hoster" + __version__ = "0.14" + __status__ = "testing" + + __pattern__ = r'^unmatchable$' + __config__ = [("activated", "bool", "Activated", True), + ("use_premium", "bool", "Use premium account if available", True), + ("fallback", "bool", "Fallback to free download if premium fails", False), + ("chk_filesize", "bool", "Check file size", True), + ("max_wait", "int", "Reconnect if waiting time is greater than minutes", 10), + ("revert_failed", "bool", "Revert to standard download if fails", True)] + + __description__ = """Rapideo.pl multi-hoster plugin""" + __license__ = "GPLv3" + __authors__ = [("goddie", "dev@rapideo.pl")] + + API_URL = "http://enc.rapideo.pl" + + API_QUERY = {'site': "newrd", + 'output': "json", + 'username': "", + 'password': "", + 'url': ""} + + ERROR_CODES = {0: "Incorrect login credentials", + 1: "Not enough transfer to download - top-up your account", + 2: "Incorrect / dead link", + 3: "Error connecting to hosting, try again later", + 9: "Premium account has expired", + 15: "Hosting no longer supported", + 80: "Too many incorrect login attempts, account blocked for 24h"} + + def _prepare(self): + MultiHoster._prepare(self) + + data = self.account.get_data() + + self.usr = data['usr'] + self.pwd = data['pwd'] + + def run_file_query(self, url, mode=None): + query = self.API_QUERY.copy() + + query['username'] = self.usr + query['password'] = self.pwd + query['url'] = url + + if mode == "fileinfo": + query['check'] = 2 + query['loc'] = 1 + + self.log_debug(query) + + return self.load(self.API_URL, post=query) + + def handle_free(self, pyfile): + try: + data = self.run_file_query(pyfile.url, 'fileinfo') + + except Exception: + self.temp_offline("Query error #1") + + try: + parsed = json.loads(data) + + except Exception: + self.temp_offline("Data not found") + + self.log_debug(parsed) + + if "errno" in parsed.keys(): + if parsed['errno'] in self.ERROR_CODES: + #: Error code in known + self.fail(self.ERROR_CODES[parsed['errno']]) + else: + #: Error code isn't yet added to plugin + self.fail(parsed['errstring'] or + _("Unknown error (code: %s)") % parsed['errno']) + + if "sdownload" in parsed: + if parsed['sdownload'] == "1": + self.fail( + _("Download from %s is possible only using Rapideo.pl website \ + directly") % parsed['hosting']) + + pyfile.name = parsed['filename'] + pyfile.size = parsed['filesize'] + + try: + link = self.run_file_query(pyfile.url, 'filedownload') + self.download(link) + + except Exception: + self.temp_offline("Query error #2") diff --git a/module/plugins/hoster/RapidfileshareNet.py b/module/plugins/hoster/RapidfileshareNet.py new file mode 100644 index 0000000000..46eda1676e --- /dev/null +++ b/module/plugins/hoster/RapidfileshareNet.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- + +from ..internal.XFSHoster import XFSHoster + + +class RapidfileshareNet(XFSHoster): + __name__ = "RapidfileshareNet" + __type__ = "hoster" + __version__ = "0.09" + __status__ = "testing" + + __pattern__ = r'http://(?:www\.)?rapidfileshare\.net/\w{12}' + __config__ = [("activated", "bool", "Activated", True), + ("use_premium", "bool", "Use premium account if available", True), + ("fallback", "bool", + "Fallback to free download if premium fails", True), + ("chk_filesize", "bool", "Check file size", True), + ("max_wait", "int", "Reconnect if waiting time is greater than minutes", 10)] + + __description__ = """Rapidfileshare.net hoster plugin""" + __license__ = "GPLv3" + __authors__ = [("guidobelix", "guidobelix@hotmail.it")] + + PLUGIN_DOMAIN = "rapidfileshare.net" + + NAME_PATTERN = r'' + SIZE_PATTERN = r'>http://www.rapidfileshare.net/\w+? \((?P[\d.,]+) (?P[\w^_]+)\)' + + OFFLINE_PATTERN = r'>No such file with this filename' + TEMP_OFFLINE_PATTERN = r'The page may have been renamed, removed or be temporarily unavailable.<' diff --git a/module/plugins/hoster/RapidgatorNet.py b/module/plugins/hoster/RapidgatorNet.py index 611d2ba5dd..9ee3d717e3 100644 --- a/module/plugins/hoster/RapidgatorNet.py +++ b/module/plugins/hoster/RapidgatorNet.py @@ -1,196 +1,188 @@ # -*- 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 . - - @author: zoidberg -""" import re -from pycurl import HTTPHEADER -from module.plugins.internal.SimpleHoster import SimpleHoster, create_getInfo -from module.plugins.internal.CaptchaService import ReCaptcha, SolveMedia, AdsCaptcha -from module.common.json_layer import json_loads +import pycurl from module.network.HTTPRequest import BadHeader +from ..captcha.ReCaptcha import ReCaptcha +from ..captcha.SolveMedia import SolveMedia +from ..internal.misc import json, seconds_to_midnight +from ..internal.SimpleHoster import SimpleHoster + class RapidgatorNet(SimpleHoster): __name__ = "RapidgatorNet" __type__ = "hoster" - __pattern__ = r"http://(?:www\.)?(rapidgator.net)/file/(\w+)" - __version__ = "0.19" - __description__ = """rapidgator.net""" - __author_name__ = ("zoidberg", "chrox", "stickell") + __version__ = "0.59" + __status__ = "testing" - API_URL = 'http://rapidgator.net/api/file' + __pattern__ = r'https?://(?:www\.)?(?:rapidgator\.(?:net|asia|)|rg\.to)/file/(?P\w+)' + __config__ = [("activated", "bool", "Activated", True), + ("use_premium", "bool", "Use premium account if available", True), + ("fallback", "bool", "Fallback to free download if premium fails", True), + ("chk_filesize", "bool", "Check file size", True), + ("max_wait", "int", "Reconnect if waiting time is greater than minutes", 10)] - FILE_NAME_PATTERN = r'Downloading:(?:\s*<[^>]*>)*\s*(?P.*?)(?:\s*<[^>]*>)' - FILE_SIZE_PATTERN = r'File size:\s*(?P.*?)' - FILE_OFFLINE_PATTERN = r'File not found' + __description__ = """Rapidgator.net hoster plugin""" + __license__ = "GPLv3" + __authors__ = [("zoidberg", "zoidberg@mujmail.cz"), + ("chrox", None), + ("stickell", "l.stickell@yahoo.it"), + ("Walter Purcaro", "vuolter@gmail.com"), + ("GammaCode", "nitzo2001[AT]yahoo[DOT]com")] - JSVARS_PATTERN = r"\s+var\s*(startTimerUrl|getDownloadUrl|captchaUrl|fid|secs)\s*=\s*'?(.*?)'?;" - DOWNLOAD_LINK_PATTERN = r"return '(http[^']+)';\s*}\s*}\s*}?\);" - RECAPTCHA_KEY_PATTERN = r'"http://api.recaptcha.net/challenge?k=(.*?)"' - ADSCAPTCHA_SRC_PATTERN = r'(http://api.adscaptcha.com/Get.aspx[^"\']*)' - SOLVEMEDIA_PATTERN = r'http:\/\/api\.solvemedia\.com\/papi\/challenge\.script\?k=(.*?)"' + COOKIES = [("rapidgator.net", "lang", "en")] - def setup(self): - self.resumeDownload = self.multiDL = False - self.sid = None - self.chunkLimit = 1 - self.req.setOption("timeout", 120) + NAME_PATTERN = r'Download file (?P<N>.*)' + SIZE_PATTERN = r'File size:\s*(?P[\d.,]+) (?P[\w^_]+)' + OFFLINE_PATTERN = r'>(404 File not found|Error 404)' - def process(self, pyfile): - if self.account: - self.sid = self.account.getAccountData(self.user).get('SID', None) + JSVARS_PATTERN = r'\s+var\s*(startTimerUrl|getDownloadUrl|captchaUrl|fid|secs)\s*=\s*\'?(.*?)\'?;' - if self.sid: - self.handlePremium() - else: - self.handleFree() + PREMIUM_ONLY_PATTERN = r'You can download files up to|This file can be downloaded by premium only<' + DOWNLOAD_LIMIT_ERROR_PATTERN = r'You have reached your (daily|hourly) downloads limit' + IP_BLOCKED_ERROR_PATTERN = 'You can`t download more than 1 file at a time in free mode\.' \ + '' + WAIT_PATTERN = r'(?:Delay between downloads must be not less than|Try again in).+' + + LINK_FREE_PATTERN = r'return \'(https?://\w+.rapidgator.net/.*)\';' + + RECAPTCHA_PATTERN = r'"http://api\.recaptcha\.net/challenge\?k=(.*?)"' + SOLVEMEDIA_PATTERN = r'http://api\.solvemedia\.com/papi/challenge\.script\?k=(.*?)"' + + URL_REPLACEMENTS = [(r'//(?:www\.)?rg\.to/', "//rapidgator.net/"), + (r'(//rapidgator.net/file/[0-9A-z]+).*', r'\1')] + URL_REPLACEMENTS = [(__pattern__ + '.*', r'https://rapidgator.net/file/\g')] - def getAPIResponse(self, cmd): + API_URL = "https://rapidgator.net/api/" + + def api_response(self, method, **kwargs): try: - json = self.load('%s/%s' % (self.API_URL, cmd), - get={'sid': self.sid, - 'url': self.pyfile.url}, decode=True) - self.logDebug('API:%s' % cmd, json, "SID: %s" % self.sid) - json = json_loads(json) - status = json['response_status'] - msg = json['response_details'] + html = self.load(self.API_URL + method, + get=kwargs) + json_data = json.loads(html) + status = json_data['response_status'] + message = json_data['response_details'] + except BadHeader, e: - self.logError('API:%s' % cmd, e, "SID: %s" % self.sid) status = e.code - msg = e + message = e.content if status == 200: - return json['response'] + return json_data['response'] + + elif status == 404: + self.offline() + elif status == 423: - self.account.empty(self.user) - self.retry() - else: - self.account.relogin(self.user) - self.retry(wait_time=60) - - def handlePremium(self): - #self.logDebug("ACCOUNT_DATA", self.account.getAccountData(self.user)) - self.api_data = self.getAPIResponse('info') - self.api_data['md5'] = self.api_data['hash'] - self.pyfile.name = self.api_data['filename'] - self.pyfile.size = self.api_data['size'] - url = self.getAPIResponse('download')['url'] - self.multiDL = True - self.download(url) - - def handleFree(self): - self.html = self.load(self.pyfile.url, decode=True) - self.getFileInfo() - - if ("You can download files up to 500 MB in free mode" in self.html or - "This file can be downloaded by premium only" in self.html): - self.fail("Premium account needed for download") - - self.checkWait() - - jsvars = dict(re.findall(self.JSVARS_PATTERN, self.html)) - self.logDebug(jsvars) - - self.req.http.lastURL = self.pyfile.url - self.req.http.c.setopt(HTTPHEADER, ["X-Requested-With: XMLHttpRequest"]) - - url = "http://rapidgator.net%s?fid=%s" % ( - jsvars.get('startTimerUrl', '/download/AjaxStartTimer'), jsvars["fid"]) - jsvars.update(self.getJsonResponse(url)) - - self.setWait(int(jsvars.get('secs', 30)) + 1, False) - self.wait() - - url = "http://rapidgator.net%s?sid=%s" % ( - jsvars.get('getDownloadUrl', '/download/AjaxGetDownload'), jsvars["sid"]) - jsvars.update(self.getJsonResponse(url)) - - self.req.http.lastURL = self.pyfile.url - self.req.http.c.setopt(HTTPHEADER, ["X-Requested-With:"]) - - url = "http://rapidgator.net%s" % jsvars.get('captchaUrl', '/download/captcha') - self.html = self.load(url) - found = re.search(self.ADSCAPTCHA_SRC_PATTERN, self.html) - if found: - captcha_key = found.group(1) - captcha = AdsCaptcha(self) + self.restart(message, premium=False) + else: - found = re.search(self.RECAPTCHA_KEY_PATTERN, self.html) - if found: - captcha_key = found.group(1) - captcha = ReCaptcha(self) + self.account.relogin() + self.retry(wait=60) + def setup(self): + self.resume_download = self.multiDL = self.premium + self.chunk_limit = -1 if self.premium else 1 + + def handle_premium(self, pyfile): + json_data = self.api_response("file/info", + sid=self.account.info['data']['sid'], + url=pyfile.url) + + self.info['md5'] = json_data['hash'] + pyfile.name = json_data['filename'] + pyfile.size = json_data['size'] + + json_data = self.api_response("file/download", + sid=self.account.info['data']['sid'], + url=pyfile.url) + self.link = json_data['url'] + + def check_errors(self): + SimpleHoster.check_errors(self) + m = re.search(self.DOWNLOAD_LIMIT_ERROR_PATTERN, self.data) + if m is not None: + self.log_warning(m.group(0)) + if m.group(1) == "daily": + wait_time = seconds_to_midnight() else: - found = re.search(self.SOLVEMEDIA_PATTERN, self.html) - if found: - captcha_key = found.group(1) - captcha = SolveMedia(self) - else: - self.parseError("Captcha") - - for i in range(5): - self.checkWait() - captcha_challenge, captcha_response = captcha.challenge(captcha_key) - - self.html = self.load(url, post={ - "DownloadCaptchaForm[captcha]": "", - "adcopy_challenge": captcha_challenge, - "adcopy_response": captcha_response - }) - - if 'The verification code is incorrect' in self.html: - self.invalidCaptcha() - else: - self.correctCaptcha() - break - else: - self.fail("No valid captcha solution received") - - found = re.search(self.DOWNLOAD_LINK_PATTERN, self.html) - if not found: - self.parseError("download link") - download_url = found.group(1) - self.logDebug(download_url) - self.download(download_url) - - def checkWait(self): - found = re.search(r"(?:Delay between downloads must be not less than|Try again in)\s*(\d+)\s*(hour|min)", - self.html) - if found: - wait_time = int(found.group(1)) * {"hour": 60, "min": 1}[found.group(2)] + wait_time = 1 * 60 * 60 + + self.retry(wait=wait_time, msg=m.group(0)) + + m = re.search(self.IP_BLOCKED_ERROR_PATTERN, self.data) + if m is not None: + msg = _("You can't download more than one file within a certain time period in free mode") + self.log_warning(msg) + self.retry(wait=24 * 60 * 60, msg=msg) + + def handle_free(self, pyfile): + jsvars = dict(re.findall(self.JSVARS_PATTERN, self.data)) + self.log_debug(jsvars) + + url = "https://rapidgator.net%s?fid=%s" % ( + jsvars.get('startTimerUrl', '/download/AjaxStartTimer'), jsvars['fid']) + jsvars.update(self.get_json_response(url)) + + self.wait(jsvars.get('secs', 180), False) + + url = "https://rapidgator.net%s?sid=%s" % ( + jsvars.get('getDownloadUrl', '/download/AjaxGetDownloadLink'), jsvars['sid']) + jsvars.update(self.get_json_response(url)) + + url = "https://rapidgator.net%s" % jsvars.get('captchaUrl', '/download/captcha') + self.data = self.load(url, ref=pyfile.url) + + m = re.search(self.LINK_FREE_PATTERN, self.data) + if m is not None: + # self.link = m.group(1) + self.download(m.group(1), ref=url) + else: - found = re.search(r"You have reached your (daily|hourly) downloads limit", self.html) - if found: - wait_time = 60 + captcha = self.handle_captcha() + + if not captcha: + self.error(_("Captcha pattern not found")) + + if isinstance(captcha, ReCaptcha): + response = captcha.challenge() + post_params = {'g-recaptcha-response': response} + + elif isinstance(captcha, SolveMedia): + response, challenge = captcha.challenge() + post_params = {'adcopy_challenge': challenge, + 'adcopy_response': response} + + post_params['DownloadCaptchaForm[verifyCode]'] = response + self.data = self.load(url, + post=post_params, + ref=url) + + if "The verification code is incorrect" in self.data: + self.retry_captcha() + else: - return + m = re.search(self.LINK_FREE_PATTERN, self.data) + if m is not None: + # self.link = m.group(1) + self.download(m.group(1), ref=url) - self.logDebug("Waiting %d minutes" % wait_time) - self.setWait(wait_time * 60, True) - self.wait() - self.retry(max_tries=24) + def handle_captcha(self): + for klass in (ReCaptcha, SolveMedia): + captcha = klass(self.pyfile) + if captcha.detect_key(): + self.captcha = captcha + return captcha - def getJsonResponse(self, url): - response = self.load(url, decode=True) - if not response.startswith('{'): - self.retry() - self.logDebug(url, response) - return json_loads(response) + def get_json_response(self, url): + self.req.http.c.setopt(pycurl.HTTPHEADER, ["X-Requested-With: XMLHttpRequest"]) + res = self.load(url, ref=self.pyfile.url) + self.req.http.c.setopt(pycurl.HTTPHEADER, ["X-Requested-With:"]) -getInfo = create_getInfo(RapidgatorNet) + if not res.startswith('{'): + self.retry() + self.log_debug(url, res) + return json.loads(res) diff --git a/module/plugins/hoster/RapidshareCom.py b/module/plugins/hoster/RapidshareCom.py deleted file mode 100644 index 05adb9fe2b..0000000000 --- a/module/plugins/hoster/RapidshareCom.py +++ /dev/null @@ -1,229 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -# v1.36 -# * fixed call checkfiles subroutine -# v1.35 -# * fixed rs-urls in handleFree(..) and freeWait(..) -# * removed getInfo(..) function as it was not used anywhere (in this file) -# * removed some (old?) comment blocks - -import re - -from module.network.RequestFactory import getURL -from module.plugins.Hoster import Hoster - - -def getInfo(urls): - ids = "" - names = "" - - p = re.compile(RapidshareCom.__pattern__) - - for url in urls: - r = p.search(url) - if r.group("name"): - ids += "," + r.group("id") - names += "," + r.group("name") - elif r.group("name_new"): - ids += "," + r.group("id_new") - names += "," + r.group("name_new") - - url = "http://api.rapidshare.com/cgi-bin/rsapi.cgi?sub=checkfiles&files=%s&filenames=%s" % (ids[1:], names[1:]) - - api = getURL(url) - result = [] - i = 0 - for res in api.split(): - tmp = res.split(",") - if tmp[4] in ("0", "4", "5"): - status = 1 - elif tmp[4] == "1": - status = 2 - else: - status = 3 - - result.append((tmp[1], tmp[2], status, urls[i])) - i += 1 - - yield result - - -class RapidshareCom(Hoster): - __name__ = "RapidshareCom" - __type__ = "hoster" - __pattern__ = r"https?://[\w\.]*?rapidshare.com/(?:files/(?P\d*?)/(?P[^?]+)|#!download\|(?:\w+)\|(?P\d+)\|(?P[^|]+))" - __version__ = "1.39" - __description__ = """Rapidshare.com Download Hoster""" - __config__ = [["server", - "Cogent;Deutsche Telekom;Level(3);Level(3) #2;GlobalCrossing;Level(3) #3;Teleglobe;GlobalCrossing #2;TeliaSonera #2;Teleglobe #2;TeliaSonera #3;TeliaSonera", - "Preferred Server", "None"]] - __author_name__ = ("spoob", "RaNaN", "mkaay") - __author_mail__ = ("spoob@pyload.org", "ranan@pyload.org", "mkaay@mkaay.de") - - def setup(self): - self.no_download = True - self.api_data = None - self.offset = 0 - self.dl_dict = {} - - self.id = None - self.name = None - - self.chunkLimit = -1 if self.premium else 1 - self.multiDL = self.resumeDownload = self.premium - - def process(self, pyfile): - self.url = self.pyfile.url - self.prepare() - - def prepare(self): - m = re.search(self.__pattern__, self.url) - - if m.group("name"): - self.id = m.group("id") - self.name = m.group("name") - else: - self.id = m.group("id_new") - self.name = m.group("name_new") - - self.download_api_data() - if self.api_data["status"] == "1": - self.pyfile.name = self.get_file_name() - - if self.premium: - self.handlePremium() - else: - self.handleFree() - - elif self.api_data["status"] == "2": - self.logInfo(_("Rapidshare: Traffic Share (direct download)")) - self.pyfile.name = self.get_file_name() - - self.download(self.pyfile.url, get={"directstart": 1}) - - elif self.api_data["status"] in ("0", "4", "5"): - self.offline() - elif self.api_data["status"] == "3": - self.tempOffline() - else: - self.fail("Unknown response code.") - - def handleFree(self): - - while self.no_download: - self.dl_dict = self.freeWait() - - #tmp = "#!download|%(server)s|%(id)s|%(name)s|%(size)s" - download = "http://%(host)s/cgi-bin/rsapi.cgi?sub=download&editparentlocation=0&bin=1&fileid=%(id)s&filename=%(name)s&dlauth=%(auth)s" % self.dl_dict - - self.logDebug("RS API Request: %s" % download) - self.download(download, ref=False) - - check = self.checkDownload({"ip": "You need RapidPro to download more files from your IP address", - "auth": "Download auth invalid"}) - if check == "ip": - self.setWait(60) - self.logInfo(_("Already downloading from this ip address, waiting 60 seconds")) - self.wait() - self.handleFree() - elif check == "auth": - self.logInfo(_("Invalid Auth Code, download will be restarted")) - self.offset += 5 - self.handleFree() - - def handlePremium(self): - info = self.account.getAccountInfo(self.user, True) - self.logDebug("%s: Use Premium Account" % self.__name__) - url = self.api_data["mirror"] - self.download(url, get={"directstart": 1}) - - def download_api_data(self, force=False): - """ - http://images.rapidshare.com/apidoc.txt - """ - if self.api_data and not force: - return - api_url_base = "http://api.rapidshare.com/cgi-bin/rsapi.cgi" - api_param_file = {"sub": "checkfiles", "incmd5": "1", "files": self.id, "filenames": self.name} - src = self.load(api_url_base, cookies=False, get=api_param_file).strip() - self.logDebug("RS INFO API: %s" % src) - if src.startswith("ERROR"): - return - fields = src.split(",") - - # status codes: - # 0=File not found - # 1=File OK (Anonymous downloading) - # 3=Server down - # 4=File marked as illegal - # 5=Anonymous file locked, because it has more than 10 downloads already - # 50+n=File OK (TrafficShare direct download type "n" without any logging.) - # 100+n=File OK (TrafficShare direct download type "n" with logging. - # Read our privacy policy to see what is logged.) - - self.api_data = {"fileid": fields[0], "filename": fields[1], "size": int(fields[2]), "serverid": fields[3], - "status": fields[4], "shorthost": fields[5], "checksum": fields[6].strip().lower()} - - if int(self.api_data["status"]) > 100: - self.api_data["status"] = str(int(self.api_data["status"]) - 100) - elif int(self.api_data["status"]) > 50: - self.api_data["status"] = str(int(self.api_data["status"]) - 50) - - self.api_data["mirror"] = "http://rs%(serverid)s%(shorthost)s.rapidshare.com/files/%(fileid)s/%(filename)s" % self.api_data - - def freeWait(self): - """downloads html with the important information - """ - self.no_download = True - - id = self.id - name = self.name - - prepare = "https://api.rapidshare.com/cgi-bin/rsapi.cgi?sub=download&fileid=%(id)s&filename=%(name)s&try=1&cbf=RSAPIDispatcher&cbid=1" % { - "name": name, "id": id} - - self.logDebug("RS API Request: %s" % prepare) - result = self.load(prepare, ref=False) - self.logDebug("RS API Result: %s" % result) - - between_wait = re.search("You need to wait (\d+) seconds", result) - - if "You need RapidPro to download more files from your IP address" in result: - self.setWait(60) - self.logInfo(_("Already downloading from this ip address, waiting 60 seconds")) - self.wait() - elif ("Too many users downloading from this server right now" in result or - "All free download slots are full" in result): - self.setWait(120) - self.logInfo(_("RapidShareCom: No free slots")) - self.wait() - elif "This file is too big to download it for free" in result: - self.fail(_("You need a premium account for this file")) - elif "Filename invalid." in result: - self.fail(_("Filename reported invalid")) - elif between_wait: - self.setWait(int(between_wait.group(1))) - self.wantReconnect = True - self.wait() - else: - self.no_download = False - - tmp, info = result.split(":") - data = info.split(",") - - dl_dict = {"id": id, - "name": name, - "host": data[0], - "auth": data[1], - "server": self.api_data["serverid"], - "size": self.api_data["size"]} - self.setWait(int(data[2]) + 2 + self.offset) - self.wait() - - return dl_dict - - def get_file_name(self): - if self.api_data["filename"]: - return self.api_data["filename"] - return self.url.split("/")[-1] diff --git a/module/plugins/hoster/RapiduNet.py b/module/plugins/hoster/RapiduNet.py new file mode 100644 index 0000000000..b642bb904b --- /dev/null +++ b/module/plugins/hoster/RapiduNet.py @@ -0,0 +1,105 @@ +# -*- coding: utf-8 -*- + +import re +import time + +import pycurl + +from ..captcha.ReCaptcha import ReCaptcha +from ..internal.misc import json, seconds_to_midnight +from ..internal.SimpleHoster import SimpleHoster + + +class RapiduNet(SimpleHoster): + __name__ = "RapiduNet" + __type__ = "hoster" + __version__ = "0.20" + __status__ = "testing" + + __pattern__ = r'https?://(?:www\.)?rapidu\.net/(?P\d+)' + __config__ = [("activated", "bool", "Activated", True), + ("use_premium", "bool", "Use premium account if available", True), + ("fallback", "bool", "Fallback to free download if premium fails", True), + ("chk_filesize", "bool", "Check file size", True), + ("max_wait", "int", "Reconnect if waiting time is greater than minutes", 10)] + + __description__ = """Rapidu.net hoster plugin""" + __license__ = "GPLv3" + __authors__ = [("prOq", None), + ("GammaC0de", "nitzo2001[AT]yahoo[DOT]com")] + + COOKIES = [("rapidu.net", "rapidu_lang", "en")] + + URL_REPLACEMENTS = [(__pattern__ + ".*", "https://rapidu.net/\g")] + + RECAPTCHA_KEY = r'6LcOuQkUAAAAAF8FPp423qz-U2AXon68gJSdI_W4' + + # https://rapidu.net/documentation/api/ + API_URL = 'https://rapidu.net/api/' + + def api_request(self, method, **kwargs): + json_data = self.load(self.API_URL + method + "/", post=kwargs) + return json.loads(json_data) + + def api_info(self, url): + file_id = re.match(self.__pattern__, url).group('ID') + api_data = self.api_request("getFileDetails", id=file_id)['0'] + + if api_data['fileStatus'] == 1: + return {'status': 2, + 'name': api_data['fileName'], + 'size': int(api_data['fileSize'])} + else: + return {'status': 1} + + def setup(self): + self.resume_download = True + self.multiDL = self.premium + + def handle_free(self, pyfile): + self.req.http.lastURL = pyfile.url + self.req.http.c.setopt(pycurl.HTTPHEADER, ["X-Requested-With: XMLHttpRequest"]) + + json_data = self.get_json_response("https://rapidu.net/ajax.php", + get={'a': "getLoadTimeToDownload"}, + post={'_go': ""}) + + if str(json_data['timeToDownload']) == "stop": + self.log_warning(_("You've reach your daily download transfer")) + self.retry(10, wait=seconds_to_midnight(), msg=_("You've reach your daily download transfer")) + + self.set_wait(int(json_data['timeToDownload']) - int(time.time())) + + self.captcha = ReCaptcha(pyfile) + response = self.captcha.challenge(self.RECAPTCHA_KEY) + + self.wait() + + json_data = self.get_json_response("https://rapidu.net/ajax.php", + get={'a': "getCheckCaptcha"}, + post={'_go': "", + 'captcha1': response, + 'fileId': self.info['pattern']['ID']}) + + if json_data['message'] == "success": + self.link = json_data['url'] + + def handle_premium(self, pyfile): + api_data = self.api_request("getFileDownload", + id=self.info['pattern']['ID'], + login=self.account.user, + password=self.account.info['login']['password']) + + if "message" in api_data: + self.fail(api_data['message']['error']) + else: + self.link = api_data.get("fileLocation") + + def get_json_response(self, *args, **kwargs): + res = self.load(*args, **kwargs) + if not res.startswith('{'): + self.retry() + + self.log_debug(res) + + return json.loads(res) diff --git a/module/plugins/hoster/RarefileNet.py b/module/plugins/hoster/RarefileNet.py index 1ede51953f..394c5cc53e 100644 --- a/module/plugins/hoster/RarefileNet.py +++ b/module/plugins/hoster/RarefileNet.py @@ -1,35 +1,27 @@ # -*- coding: utf-8 -*- -import re -from module.plugins.hoster.XFileSharingPro import XFileSharingPro, create_getInfo -from module.utils import html_unescape +from ..internal.XFSHoster import XFSHoster -class RarefileNet(XFileSharingPro): +class RarefileNet(XFSHoster): __name__ = "RarefileNet" __type__ = "hoster" - __pattern__ = r"http://(?:\w*\.)*rarefile.net/\w{12}" - __version__ = "0.03" - __description__ = """Rarefile.net hoster plugin""" - __author_name__ = ("zoidberg") - __author_mail__ = ("zoidberg@mujmail.cz") - - FILE_NAME_PATTERN = r'(?P.*?)' - FILE_SIZE_PATTERN = r'Size : (?P.+?) ' - DIRECT_LINK_PATTERN = r'(?P=link)' - HOSTER_NAME = "rarefile.net" + __version__ = "0.15" + __status__ = "testing" - def setup(self): - self.resumeDownload = self.multiDL = self.premium + __pattern__ = r'http://(?:www\.)?rarefile\.net/\w{12}' + __config__ = [("activated", "bool", "Activated", True), + ("use_premium", "bool", "Use premium account if available", True), + ("fallback", "bool", + "Fallback to free download if premium fails", True), + ("chk_filesize", "bool", "Check file size", True), + ("max_wait", "int", "Reconnect if waiting time is greater than minutes", 10)] - def handleCaptcha(self, inputs): - captcha_div = re.search(r'Enter code.*?(.*?)

  • ', self.html, re.S).group(1) - self.logDebug(captcha_div) - numerals = re.findall('(\d)', 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 3 + __description__ = """Rarefile.net hoster plugin""" + __license__ = "GPLv3" + __authors__ = [("zoidberg", "zoidberg@mujmail.cz")] + PLUGIN_DOMAIN = "rarefile.net" -getInfo = create_getInfo(RarefileNet) + LINK_PATTERN = r'\1' diff --git a/module/plugins/hoster/RealdebridCom.py b/module/plugins/hoster/RealdebridCom.py index 40ee96df93..73a9b7d6b4 100644 --- a/module/plugins/hoster/RealdebridCom.py +++ b/module/plugins/hoster/RealdebridCom.py @@ -1,89 +1,80 @@ -#!/usr/bin/env python # -*- coding: utf-8 -*- -import re -from time import time -from urllib import quote, unquote -from random import randrange +import pycurl +from module.network.HTTPRequest import BadHeader -from module.utils import parseFileSize -from module.common.json_layer import json_loads -from module.plugins.Hoster import Hoster +from ..internal.misc import json +from ..internal.MultiHoster import MultiHoster -class RealdebridCom(Hoster): +def args(**kwargs): + return kwargs + + +class RealdebridCom(MultiHoster): __name__ = "RealdebridCom" - __version__ = "0.53" __type__ = "hoster" + __version__ = "0.81" + __status__ = "testing" + + __pattern__ = r'https?://((?:www\.|s\d+\.)?real-debrid\.com/dl?/|[\w^_]\.rdb\.so/d/)[\w^_]+' + __config__ = [("use_premium", "bool", "Use premium account if available", True), + ("fallback", "bool", "Fallback to free download if premium fails", False), + ("revert_failed", "bool", "Revert to standard download if fails", True), + ("chk_filesize", "bool", "Check file size", True), + ("max_wait", "int", "Reconnect if waiting time is greater than minutes", 10)] + + __description__ = """Real-Debrid.com multi-hoster plugin""" + __license__ = "GPLv3" + __authors__ = [("Devirex Hazzard", "naibaf_11@yahoo.de"), + ("GammaC0de", "nitzo2001[AT]yahoo[DOT]com")] - __pattern__ = r"https?://.*real-debrid\..*" - __description__ = """Real-Debrid.com hoster plugin""" - __author_name__ = ("Devirex, Hazzard") - __author_mail__ = ("naibaf_11@yahoo.de") + # See https://api.real-debrid.com/ + API_URL = "https://api.real-debrid.com/rest/1.0" + + def api_response(self, namespace, get={}, post={}): + self.req.http.c.setopt(pycurl.USERAGENT, "pyLoad/%s" % self.pyload.version) - def getFilename(self, url): try: - name = unquote(url.rsplit("/", 1)[1]) - except IndexError: - name = "Unknown_Filename..." - if not name or name.endswith(".."): # incomplete filename, append random stuff - name += "%s.tmp" % randrange(100, 999) - return name + json_data = self.load(self.API_URL + namespace, get=get, post=post) + + except BadHeader, e: + json_data = e.content + + return json.loads(json_data) def setup(self): - self.chunkLimit = 3 - self.resumeDownload = True - - def process(self, pyfile): - if re.match(self.__pattern__, pyfile.url): - new_url = pyfile.url - elif not self.account: - self.logError(_("Please enter your %s account or deactivate this plugin") % "Real-debrid") - self.fail("No Real-debrid account provided") - else: - self.logDebug("Old URL: %s" % pyfile.url) - password = self.getPassword().splitlines() - if not password: - password = "" - else: - password = password[0] + self.chunk_limit = 3 - url = "https://real-debrid.com/ajax/unrestrict.php?lang=en&link=%s&password=%s&time=%s" % ( - quote(pyfile.url, ""), password, int(time() * 1000)) - page = self.load(url) - data = json_loads(page) + def handle_premium(self, pyfile): + user = self.account.accounts.keys()[0] + api_token = self.account.accounts[user]["api_token"] - self.logDebug("Returned Data: %s" % data) + data = self.api_response("/unrestrict/link", + args(auth_token=api_token), + args(link=pyfile.url, password=self.get_password())) - if data["error"] != 0: - if data["message"] == "Your file is unavailable on the hoster.": - self.offline() - else: - self.logWarning(data["message"]) - self.tempOffline() - else: - if self.pyfile.name is not None and self.pyfile.name.endswith('.tmp') and data["file_name"]: - self.pyfile.name = data["file_name"] - self.pyfile.size = parseFileSize(data["file_size"]) - new_url = data['generated_links'][0][-1] + self.log_debug("Returned Data: %s" % data) - if self.getConfig("https"): - new_url = new_url.replace("http://", "https://") - else: - new_url = new_url.replace("https://", "http://") + if "error" in data: + if data['error_code'] == 24: + self.offline() - if new_url != pyfile.url: - self.logDebug("New URL: %s" % new_url) + elif data['error_code'] == 8: #: Token expired? + self.account.relogin() + self.retry() - if pyfile.name.startswith("http") or pyfile.name.startswith("Unknown") or pyfile.name.endswith('..'): - #only use when name wasnt already set - pyfile.name = self.getFilename(new_url) + else: + self.fail("%s (code: %s)" % (data["error"], data["error_code"])) - self.download(new_url, disposition=True) + else: + if data['filename']: + pyfile.name = data['filename'] - check = self.checkDownload( - {"error": "An error occured while processing your request"}) + pyfile.size = data['filesize'] + self.link = data['download'] - if check == "error": - #usual this download can safely be retried - self.retry(reason="An error occured while generating link.", wait_time=60) + # if self.getConfig('ssl'): + # self.link = self.link.replace("http://", "https://") + # else: + # self.link = self.link.replace("https://", "http://") diff --git a/module/plugins/hoster/RedtubeCom.py b/module/plugins/hoster/RedtubeCom.py index caf33eeac6..115a91032a 100644 --- a/module/plugins/hoster/RedtubeCom.py +++ b/module/plugins/hoster/RedtubeCom.py @@ -1,56 +1,45 @@ -#!/usr/bin/env python # -*- coding: utf-8 -*- import re -from module.plugins.Hoster import Hoster -from module.unescape import unescape + +from ..internal.Hoster import Hoster +from ..internal.misc import json class RedtubeCom(Hoster): __name__ = "RedtubeCom" __type__ = "hoster" - __pattern__ = r'http://[\w\.]*?redtube\.com/\d+' - __version__ = "0.2" - __description__ = """Redtube.com Download Hoster""" - __author_name__ = ("jeix") - __author_mail__ = ("jeix@hasnomail.de") + __version__ = "0.28" + __status__ = "testing" - def process(self, pyfile): - self.download_html() - if not self.file_exists(): - self.offline() + __pattern__ = r'https?://(?:www\.)?redtube\.com/\d+' + __config__ = [("activated", "bool", "Activated", True)] - pyfile.name = self.get_file_name() - self.download(self.get_file_url()) + __description__ = """Redtube.com hoster plugin""" + __license__ = "GPLv3" + __authors__ = [("jeix", "jeix@hasnomail.de"), + ("GammaC0de", "nitzo2001[AT}yahoo[DOT]com")] - def download_html(self): - url = self.pyfile.url - self.html = self.load(url) + def process(self, pyfile): + html = self.load(pyfile.url) - def get_file_url(self): - """ returns the absolute downloadable filepath - """ - if self.html is None: - self.download_html() + m = re.search(r'playervars: ({.+}),', html) + if m is None: + self.error(_("playervars pattern not found")) - file_url = unescape(re.search(r'hashlink=(http.*?)"', self.html).group(1)) + playervars = json.loads(m.group(1)) - return file_url + media_info = [x["videoUrl"] + for x in playervars['mediaDefinitions'] + if x.get("format") == "mp4" and x.get("remote") is True] + if len(media_info) == 0: + self.fail(_("no media definitions found")) - def get_file_name(self): - if self.html is None: - self.download_html() + video_info = json.loads(self.load(media_info[0])) + video_info = sorted(video_info, key=lambda k: int(k['quality']), reverse=True) - name = re.search('(.*?)- RedTube - Free Porn Videos', self.html).group(1).strip() + ".flv" - return name + link = video_info[0]["videoUrl"] - def file_exists(self): - """ returns True or False - """ - if self.html is None: - self.download_html() + pyfile.name = playervars['video_title'] + ".mp4" - if re.search(r'This video has been removed.', self.html) is not None: - return False - else: - return True + self.download(link) diff --git a/module/plugins/hoster/RehostTo.py b/module/plugins/hoster/RehostTo.py index bb6110415d..023c8e1704 100644 --- a/module/plugins/hoster/RehostTo.py +++ b/module/plugins/hoster/RehostTo.py @@ -1,38 +1,30 @@ -#!/usr/bin/env python # -*- coding: utf-8 -*- -from urllib import quote, unquote -from module.plugins.Hoster import Hoster +from ..internal.MultiHoster import MultiHoster -class RehostTo(Hoster): + +class RehostTo(MultiHoster): __name__ = "RehostTo" - __version__ = "0.13" __type__ = "hoster" - __pattern__ = r"https?://.*rehost.to\..*" - __description__ = """rehost.com hoster plugin""" - __author_name__ = ("RaNaN") - __author_mail__ = ("RaNaN@pyload.org") - - def getFilename(self, url): - return unquote(url.rsplit("/", 1)[1]) - - def setup(self): - self.chunkLimit = 1 - self.resumeDownload = True - - def process(self, pyfile): - if not self.account: - self.logError(_("Please enter your %s account or deactivate this plugin") % "rehost.to") - self.fail("No rehost.to account provided") - - data = self.account.getAccountInfo(self.user) - long_ses = data["long_ses"] - - self.logDebug("Rehost.to: Old URL: %s" % pyfile.url) - new_url = "http://rehost.to/process_download.php?user=cookie&pass=%s&dl=%s" % (long_ses, quote(pyfile.url, "")) - - #raise timeout to 2min - self.req.setOption("timeout", 120) - - self.download(new_url, disposition=True) + __version__ = "0.29" + __status__ = "testing" + + __pattern__ = r'https?://.*rehost\.to\..+' + __config__ = [("activated", "bool", "Activated", True), + ("use_premium", "bool", "Use premium account if available", True), + ("fallback", "bool", "Fallback to free download if premium fails", False), + ("chk_filesize", "bool", "Check file size", True), + ("max_wait", "int", "Reconnect if waiting time is greater than minutes", 10), + ("revert_failed", "bool", "Revert to standard download if fails", True)] + + __description__ = """Rehost.com multi-hoster plugin""" + __license__ = "GPLv3" + __authors__ = [("RaNaN", "RaNaN@pyload.net")] + + def handle_premium(self, pyfile): + self.download("http://rehost.to/process_download.php", + get={'user': "cookie", + 'pass': self.account.get_data('session'), + 'dl': pyfile.url}, + disposition=True) diff --git a/module/plugins/hoster/ReloadCc.py b/module/plugins/hoster/ReloadCc.py deleted file mode 100644 index 2295c792a3..0000000000 --- a/module/plugins/hoster/ReloadCc.py +++ /dev/null @@ -1,113 +0,0 @@ -from module.plugins.Hoster import Hoster - -from module.common.json_layer import json_loads - -from module.network.HTTPRequest import BadHeader - - -class ReloadCc(Hoster): - __name__ = "ReloadCc" - __version__ = "0.5" - __type__ = "hoster" - __description__ = """Reload.Cc hoster plugin""" - - # Since we want to allow the user to specify the list of hoster to use we let MultiHoster.coreReady - # create the regex patterns for us using getHosters in our ReloadCc hook. - __pattern__ = None - - __author_name__ = ("Reload Team") - __author_mail__ = ("hello@reload.cc") - - def process(self, pyfile): - # Check account - if not self.account or not self.account.canUse(): - self.logError(_("Please enter your %s account or deactivate this plugin") % "reload.cc") - self.fail("No valid reload.cc account provided") - - # In some cases hostsers do not supply us with a filename at download, so we - # are going to set a fall back filename (e.g. for freakshare or xfileshare) - self.pyfile.name = self.pyfile.name.split('/').pop() # Remove everthing before last slash - - # Correction for automatic assigned filename: Removing html at end if needed - suffix_to_remove = ["html", "htm", "php", "php3", "asp", "shtm", "shtml", "cfml", "cfm"] - temp = self.pyfile.name.split('.') - if temp.pop() in suffix_to_remove: - self.pyfile.name = ".".join(temp) - - # Get account data - (user, data) = self.account.selectAccount() - - query_params = dict( - via='pyload', - v=1, - user=user, - uri=self.pyfile.url - ) - - try: - query_params.update(dict(hash=self.account.infos[user]['pwdhash'])) - except Exception: - query_params.update(dict(pwd=data['password'])) - - try: - answer = self.load("http://api.reload.cc/dl", get=query_params) - except BadHeader, e: - if e.code == 400: - self.fail("The URI is not supported by Reload.cc.") - elif e.code == 401: - self.fail("Wrong username or password") - elif e.code == 402: - self.fail("Your account is inactive. A payment is required for downloading!") - elif e.code == 403: - self.fail("Your account is disabled. Please contact the Reload.cc support!") - elif e.code == 409: - self.logWarning("The hoster seems to be a limited hoster and you've used your daily traffic for this hoster: %s" % self.pyfile.url) - # Wait for 6 hours and retry up to 4 times => one day - self.retry(max_retries=4, wait_time=(3600 * 6), reason="Limited hoster traffic limit exceeded") - elif e.code == 429: - # Too many connections, wait 2 minutes and try again - self.retry(max_retries=5, wait_time=120, reason="Too many concurrent connections") - elif e.code == 503: - # Retry in 10 minutes - self.retry(wait_time=600, - reason="Reload.cc is currently in maintenance mode! Please check again later.") - else: - self.fail( - "Internal error within Reload.cc. Please contact the Reload.cc support for further information.") - return - - data = json_loads(answer) - - # Check status and decide what to do - status = data.get('status', None) - if status == "ok": - conn_limit = data.get('msg', 0) - # API says these connections are limited - # Make sure this limit is used - the download will fail if not - if conn_limit > 0: - try: - self.limitDL = int(conn_limit) - except ValueError: - self.limitDL = 1 - else: - self.limitDL = 0 - - try: - self.download(data['link'], disposition=True) - except BadHeader, e: - if e.code == 404: - self.fail("File Not Found") - elif e.code == 412: - self.fail("File access password is wrong") - elif e.code == 417: - self.fail("Password required for file access") - elif e.code == 429: - # Too many connections, wait 2 minutes and try again - self.retry(max_retries=5, wait_time=120, reason="Too many concurrent connections") - else: - self.fail( - "Internal error within Reload.cc. Please contact the Reload.cc support for further information." - ) - return - else: - self.fail("Internal error within Reload.cc. Please contact the Reload.cc support for further information.") diff --git a/module/plugins/hoster/RemixshareCom.py b/module/plugins/hoster/RemixshareCom.py new file mode 100644 index 0000000000..7b5ca62792 --- /dev/null +++ b/module/plugins/hoster/RemixshareCom.py @@ -0,0 +1,58 @@ +# -*- coding: utf-8 -*- +# +# Test links: +# http://remixshare.com/download/z8uli +# +# Note: +# The remixshare.com website is very very slow, so +# if your download not starts because of pycurl timeouts: +# Adjust timeouts in /usr/share/pyload/module/network/HTTPRequest.py + +import re + +from ..internal.SimpleHoster import SimpleHoster + + +class RemixshareCom(SimpleHoster): + __name__ = "RemixshareCom" + __type__ = "hoster" + __version__ = "0.11" + __status__ = "testing" + + __pattern__ = r'https?://remixshare\.com/(download|dl)/\w+' + __config__ = [("activated", "bool", "Activated", True), + ("use_premium", "bool", "Use premium account if available", True), + ("fallback", "bool", + "Fallback to free download if premium fails", True), + ("chk_filesize", "bool", "Check file size", True), + ("max_wait", "int", "Reconnect if waiting time is greater than minutes", 10)] + + __description__ = """Remixshare.com hoster plugin""" + __license__ = "GPLv3" + __authors__ = [("zapp-brannigan", "fuerst.reinje@web.de"), + ("Walter Purcaro", "vuolter@gmail.com"), + ("sraedler", "simon.raedler@yahoo.de")] + + INFO_PATTERN = r'title=\'.+?\'>(?P.+?) \((?P\d+) (?P[\w^_]+)\)<' + HASHSUM_PATTERN = r'>(?PMD5): (?P\w+)' + OFFLINE_PATTERN = r'

    Ooops!' + + LINK_PATTERN = r'var uri = "(.+?)"' + TOKEN_PATTERN = r'var acc = (\d+)' + + WAIT_PATTERN = r'var XYZ = "(\d+)"' + + def setup(self): + self.multiDL = True + self.chunk_limit = 1 + + def handle_free(self, pyfile): + b = re.search(self.LINK_PATTERN, self.data) + if not b: + self.error(_("File url")) + + c = re.search(self.TOKEN_PATTERN, self.data) + if not c: + self.error(_("File token")) + + self.link = b.group(1) + "/zzz/" + c.group(1) diff --git a/module/plugins/hoster/RgHostNet.py b/module/plugins/hoster/RgHostNet.py index a46b51733a..b07b766609 100644 --- a/module/plugins/hoster/RgHostNet.py +++ b/module/plugins/hoster/RgHostNet.py @@ -1,28 +1,29 @@ -#!/usr/bin/env python # -*- coding: utf-8 -*- -import re -from module.plugins.internal.SimpleHoster import SimpleHoster, create_getInfo + +from ..internal.SimpleHoster import SimpleHoster class RgHostNet(SimpleHoster): __name__ = "RgHostNet" __type__ = "hoster" - __pattern__ = r"http://(?:www\.)?rghost\.net/\d+(?:r=\d+)?" - __version__ = "0.01" - __description__ = """RgHost.net Download Hoster""" - __author_name__ = ("z00nx") - __author_mail__ = ("z00nx0@gmail.com") - - FILE_INFO_PATTERN = r'

    \s+(]+>)?(?P[^<]+)()?\s+]+>\s+\((?P[^)]+)\)\s+\s+

    ' - FILE_OFFLINE_PATTERN = r'File is deleted|this page is not found' - DOWNLOAD_LINK_PATTERN = ''']+>Download''' - - def handleFree(self): - found = re.search(self.DOWNLOAD_LINK_PATTERN, self.html) - if not found: - self.parseError("Unable to detect the direct link") - download_link = found.group(1) - self.download(download_link, disposition=True) - -getInfo = create_getInfo(RgHostNet) + __version__ = "0.09" + __status__ = "testing" + + __pattern__ = r'http://(?:www\.)?rghost\.(net|ru)/[\d\-]+' + __config__ = [("activated", "bool", "Activated", True), + ("use_premium", "bool", "Use premium account if available", True), + ("fallback", "bool", + "Fallback to free download if premium fails", True), + ("chk_filesize", "bool", "Check file size", True), + ("max_wait", "int", "Reconnect if waiting time is greater than minutes", 10)] + + __description__ = """RgHost.net hoster plugin""" + __license__ = "GPLv3" + __authors__ = [("z00nx", "z00nx0@gmail.com")] + + INFO_PATTERN = r'data-share42-text="(?P.+?) \((?P[\d.,]+) (?P[\w^_]+)' + HASHSUM_PATTERN = r'
    (?P\w+)
    \s*
    (?P\w+)' + OFFLINE_PATTERN = r'>(File is deleted|page not found)' + + LINK_FREE_PATTERN = r'\d+) hour[s]?, )?((?P\d+) minute[s], )?(?P\d+) second[s]' - DIRECT_LINK_PATTERN = r'(http://([^/]*?ryushare.com|\d+\.\d+\.\d+\.\d+)(:\d+/d/|/files/\w+/\w+/)[^"\'<]+)' - SOLVEMEDIA_PATTERN = r'http:\/\/api\.solvemedia\.com\/papi\/challenge\.script\?k=(.*?)"' - - def setup(self): - self.resumeDownload = self.multiDL = True - if not self.premium: - self.limitDL = 2 - # Up to 3 chunks allowed in free downloads. Unknown for premium - self.chunkLimit = 3 - - def getDownloadLink(self): - retry = False - self.html = self.load(self.pyfile.url) - action, inputs = self.parseHtmlForm(input_names={"op": re.compile("^download")}) - if 'method_premium' in inputs: - del inputs['method_premium'] - - self.html = self.load(self.pyfile.url, post=inputs) - action, inputs = self.parseHtmlForm('F1') - - self.setWait(65) - # Wait - if 'You have reached the download-limit!!!' in self.html: - self.setWait(3600, True) - retry = True - - match = re.search(self.WAIT_PATTERN, self.html) - if match: - m = match.groupdict(0) - waittime = int(m["hour"])*60*60 + int(m['min']) * 60 + int(m['sec']) - self.setWait(waittime, True) - retry = True - - self.wait() - if retry: - self.retry() - - for i in xrange(5): - - m = re.search(self.SOLVEMEDIA_PATTERN, self.html) - if not m: - self.parseError("Error parsing captcha") - - captchaKey = m.group(1) - captcha = SolveMedia(self) - challenge, response = captcha.challenge(captchaKey) - - inputs["adcopy_challenge"] = challenge - inputs["adcopy_response"] = response - - self.html = self.load(self.pyfile.url, post = inputs) - if "WRONG CAPTCHA" in self.html: - self.invalidCaptcha() - self.logInfo("Invalid Captcha") - else: - self.correctCaptcha() - break - - else: - self.fail("You have entered 5 invalid captcha codes") - - if 'Click here to download' in self.html: - m = re.search(r'Click here to download', self.html) - return m.group(1) - -getInfo = create_getInfo(RyushareCom) diff --git a/module/plugins/hoster/SafesharingEu.py b/module/plugins/hoster/SafesharingEu.py new file mode 100644 index 0000000000..0ee30b7653 --- /dev/null +++ b/module/plugins/hoster/SafesharingEu.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- + +from ..internal.XFSHoster import XFSHoster + + +class SafesharingEu(XFSHoster): + __name__ = "SafesharingEu" + __type__ = "hoster" + __version__ = "0.11" + __status__ = "testing" + + __pattern__ = r'https?://(?:www\.)?safesharing\.eu/\w{12}' + __config__ = [("activated", "bool", "Activated", True), + ("use_premium", "bool", "Use premium account if available", True), + ("fallback", "bool", + "Fallback to free download if premium fails", True), + ("chk_filesize", "bool", "Check file size", True), + ("max_wait", "int", "Reconnect if waiting time is greater than minutes", 10)] + + __description__ = """Safesharing.eu hoster plugin""" + __license__ = "GPLv3" + __authors__ = [("zapp-brannigan", "fuerst.reinje@web.de")] + + PLUGIN_DOMAIN = "safesharing.eu" + + ERROR_PATTERN = r'(?:
    )(.+?)(?:
    )' diff --git a/module/plugins/hoster/SecureUploadEu.py b/module/plugins/hoster/SecureUploadEu.py index dc220c5083..789459c3f1 100644 --- a/module/plugins/hoster/SecureUploadEu.py +++ b/module/plugins/hoster/SecureUploadEu.py @@ -1,19 +1,26 @@ # -*- coding: utf-8 -*- -from module.plugins.hoster.XFileSharingPro import XFileSharingPro, create_getInfo +from ..internal.XFSHoster import XFSHoster -class SecureUploadEu(XFileSharingPro): + +class SecureUploadEu(XFSHoster): __name__ = "SecureUploadEu" __type__ = "hoster" - __pattern__ = r"http://(www\.)?secureupload\.eu/(\w){12}(/\w+)" - __version__ = "0.01" - __description__ = """SecureUpload.eu hoster plugin""" - __author_name__ = ("z00nx") - __author_mail__ = ("z00nx0@gmail.com") + __version__ = "0.11" + __status__ = "testing" - HOSTER_NAME = "secureupload.eu" - FILE_INFO_PATTERN = '

    Downloading (?P[^<]+) \((?P[^<]+)\)

    ' - FILE_OFFLINE_PATTERN = 'The file was removed|File Not Found' + __pattern__ = r'https?://(?:www\.)?secureupload\.eu/\w{12}' + __config__ = [("activated", "bool", "Activated", True), + ("use_premium", "bool", "Use premium account if available", True), + ("fallback", "bool", + "Fallback to free download if premium fails", True), + ("chk_filesize", "bool", "Check file size", True), + ("max_wait", "int", "Reconnect if waiting time is greater than minutes", 10)] + + __description__ = """SecureUpload.eu hoster plugin""" + __license__ = "GPLv3" + __authors__ = [("z00nx", "z00nx0@gmail.com")] + PLUGIN_DOMAIN = "secureupload.eu" -getInfo = create_getInfo(SecureUploadEu) + INFO_PATTERN = r'

    Downloading (?P.+?) \((?P.+?)\)

    ' diff --git a/module/plugins/hoster/SendCm.py b/module/plugins/hoster/SendCm.py new file mode 100644 index 0000000000..b97c5c1a5f --- /dev/null +++ b/module/plugins/hoster/SendCm.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- + +from ..internal.XFSHoster import XFSHoster + + +class SendCm(XFSHoster): + __name__ = "SendCm" + __type__ = "hoster" + __version__ = "0.02" + __status__ = "testing" + + __pattern__ = r'https?://(?:www\.)?send\.cm/(?:\w{12}|d/\w{5})' + __config__ = [("activated", "bool", "Activated", True), + ("use_premium", "bool", "Use premium account if available", True), + ("fallback", "bool", "Fallback to free download if premium fails", True), + ("chk_filesize", "bool", "Check file size", True), + ("max_wait", "int", "Reconnect if waiting time is greater than minutes", 10)] + + __description__ = """Send.cm downloader plugin""" + __license__ = "GPLv3" + __authors__ = [("GammaC0de", "nitzo2001[AT]yahoo[DOT]com")] + + PLUGIN_DOMAIN = "send.cm" + + LINK_PATTERN = r"(https://d\d+.download-send.com/d/.*?)[\"'<]" + NAME_PATTERN = r"\"\[URL=https://send\.cm/\w{12}\](?P.+?) - \d+\[/URL\]\"" + + DISPOSITION = False diff --git a/module/plugins/hoster/SenditCloud.py b/module/plugins/hoster/SenditCloud.py new file mode 100644 index 0000000000..6e07583b15 --- /dev/null +++ b/module/plugins/hoster/SenditCloud.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- + +from ..internal.SimpleHoster import SimpleHoster + + +class SenditCloud(SimpleHoster): + __name__ = "SenditCloud" + __type__ = "hoster" + __version__ = "0.01" + __status__ = "testing" + + __pattern__ = r'https?://(?:www\.)?sendit\.cloud/\w+' + __config__ = [("activated", "bool", "Activated", True), + ("use_premium", "bool", "Use premium account if available", True), + ("fallback", "bool", "Fallback to free download if premium fails", True), + ("chk_filesize", "bool", "Check file size", True), + ("max_wait", "int", "Reconnect if waiting time is greater than minutes", 10)] + + __description__ = """Sendit.cloud hoster plugin""" + __license__ = "GPLv3" + __authors__ = [("GammaC0de", "nitzo2001[AT]yahoo[DOT]com")] + + NAME_PATTERN = r'
    .+>\s*(.+?)\s*
    ' + SIZE_PATTERN = r'Download \((?P[\d.,]+) (?P[\w_^]+)\)<' + + OFFLINE_PATTERN = r'The file you are trying to download is no longer available' + + def setup(self): + self.multiDL = True + self.resume_download = False + + def handle_free(self, pyfile): + url, inputs = self.parse_html_form('name="F1"') + if inputs is not None: + self.download(pyfile.url, post=inputs) \ No newline at end of file diff --git a/module/plugins/hoster/SendmywayCom.py b/module/plugins/hoster/SendmywayCom.py deleted file mode 100644 index 9b4fc14bc9..0000000000 --- a/module/plugins/hoster/SendmywayCom.py +++ /dev/null @@ -1,19 +0,0 @@ -# -*- coding: utf-8 -*- -from module.plugins.hoster.XFileSharingPro import XFileSharingPro, create_getInfo - - -class SendmywayCom(XFileSharingPro): - __name__ = "SendmywayCom" - __type__ = "hoster" - __pattern__ = r"http://(?:\w*\.)*?sendmyway.com/\w{12}" - __version__ = "0.01" - __description__ = """SendMyWay hoster plugin""" - __author_name__ = ("zoidberg") - __author_mail__ = ("zoidberg@mujmail.cz") - - FILE_NAME_PATTERN = r'

    <.*?>\s*(?P.+)' - FILE_SIZE_PATTERN = r'\((?P\d+) bytes\)' - HOSTER_NAME = "sendmyway.com" - - -getInfo = create_getInfo(SendmywayCom) diff --git a/module/plugins/hoster/SendspaceCom.py b/module/plugins/hoster/SendspaceCom.py index 729ee17bc4..2d23750a93 100644 --- a/module/plugins/hoster/SendspaceCom.py +++ b/module/plugins/hoster/SendspaceCom.py @@ -1,70 +1,60 @@ # -*- 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 . - - @author: zoidberg -""" import re -from module.plugins.internal.SimpleHoster import SimpleHoster, create_getInfo + +from ..internal.SimpleHoster import SimpleHoster class SendspaceCom(SimpleHoster): __name__ = "SendspaceCom" __type__ = "hoster" - __pattern__ = r"http://(www\.)?sendspace.com/file/.*" - __version__ = "0.13" - __description__ = """sendspace.com plugin - free only""" - __author_name__ = ("zoidberg") + __version__ = "0.23" + __status__ = "testing" - DOWNLOAD_URL_PATTERN = r'\s*<(?:b|strong)>(?P[^<]+)\s*File Size:\s*(?P[0-9.]+)(?P[kKMG])i?B\s*

    ' - FILE_OFFLINE_PATTERN = r'
    Sorry, the file you requested is not available.
    ' - CAPTCHA_PATTERN = r'' - USER_CAPTCHA_PATTERN = r'' + __pattern__ = r'https?://(?:www\.)?sendspace\.com/file/\w+' + __config__ = [("activated", "bool", "Activated", True), + ("use_premium", "bool", "Use premium account if available", True), + ("fallback", "bool", + "Fallback to free download if premium fails", True), + ("chk_filesize", "bool", "Check file size", True), + ("max_wait", "int", "Reconnect if waiting time is greater than minutes", 10)] - def handleFree(self): - params = {} - for i in range(3): - found = re.search(self.DOWNLOAD_URL_PATTERN, self.html) - if found: - if 'captcha_hash' in params: - self.correctCaptcha() - download_url = found.group(1) - break + __description__ = """Sendspace.com hoster plugin""" + __license__ = "GPLv3" + __authors__ = [("zoidberg", "zoidberg@mujmail.cz")] - found = re.search(self.CAPTCHA_PATTERN, self.html) - if found: - if 'captcha_hash' in params: - self.invalidCaptcha() - captcha_url1 = "http://www.sendspace.com/" + found.group(1) - found = re.search(self.USER_CAPTCHA_PATTERN, self.html) - captcha_url2 = "http://www.sendspace.com/" + found.group(1) - params = {'captcha_hash': found.group(2), - 'captcha_submit': 'Verify', - 'captcha_answer': self.decryptCaptcha(captcha_url1) + " " + self.decryptCaptcha(captcha_url2)} - else: - params = {'download': "Regular Download"} + NAME_PATTERN = r'

    \s*<(?:b|strong)>(?P.+?)\s*File Size:\s*(?P[\d.,]+)(?P[\w^_]+)\s*

    ' + OFFLINE_PATTERN = r'
    Sorry, the file you requested is not available.
    ' + + LINK_FREE_PATTERN = r'
    ' + USER_CAPTCHA_PATTERN = r'' + + def handle_free(self, pyfile): + m = re.search(self.LINK_FREE_PATTERN, self.data) + if m is not None: + self.link = m.group(1) - self.logDebug(params) - self.html = self.load(self.pyfile.url, post=params) else: - self.fail("Download link not found") + m = re.search(self.CAPTCHA_PATTERN, self.data) + if m is None: + params = {'download': "Regular Download"} + else: + captcha_url1 = "http://www.sendspace.com/" + m.group(1) + m = re.search(self.USER_CAPTCHA_PATTERN, self.data) + captcha_url2 = "http://www.sendspace.com/" + m.group(1) + params = {'captcha_hash': m.group(2), + 'captcha_submit': 'Verify', + 'captcha_answer': self.captcha.decrypt(captcha_url1) + " " + self.captcha.decrypt(captcha_url2)} - self.logDebug("Download URL: %s" % download_url) - self.download(download_url) + self.log_debug(params) + self.data = self.load(pyfile.url, post=params) -create_getInfo(SendspaceCom) + m = re.search(self.LINK_FREE_PATTERN, self.data) + if m is None: + self.retry_captcha() + else: + self.link = m.group(1) diff --git a/module/plugins/hoster/Share4WebCom.py b/module/plugins/hoster/Share4WebCom.py new file mode 100644 index 0000000000..95a72ba040 --- /dev/null +++ b/module/plugins/hoster/Share4WebCom.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- + +from .UnibytesCom import UnibytesCom + + +class Share4WebCom(UnibytesCom): + __name__ = "Share4WebCom" + __type__ = "hoster" + __version__ = "0.17" + __status__ = "testing" + + __pattern__ = r'https?://(?:www\.)?share4web\.com/get/\w+' + __config__ = [("activated", "bool", "Activated", True)] + + __description__ = """Share4web.com hoster plugin""" + __license__ = "GPLv3" + __authors__ = [("zoidberg", "zoidberg@mujmail.cz")] + + PLUGIN_DOMAIN = "share4web.com" diff --git a/module/plugins/hoster/Share4webCom.py b/module/plugins/hoster/Share4webCom.py deleted file mode 100644 index ead8080244..0000000000 --- a/module/plugins/hoster/Share4webCom.py +++ /dev/null @@ -1,18 +0,0 @@ -# -*- coding: utf-8 -*- - -from module.plugins.hoster.UnibytesCom import UnibytesCom -from module.plugins.internal.SimpleHoster import create_getInfo - - -class Share4webCom(UnibytesCom): - __name__ = "Share4webCom" - __type__ = "hoster" - __pattern__ = r"http://(www\.)?share4web\.com/get/\w+" - __version__ = "0.1" - __description__ = """Share4web.com""" - __author_name__ = ("zoidberg") - - DOMAIN = 'http://www.share4web.com' - - -getInfo = create_getInfo(UnibytesCom) diff --git a/module/plugins/hoster/Share76Com.py b/module/plugins/hoster/Share76Com.py deleted file mode 100644 index 81a1695271..0000000000 --- a/module/plugins/hoster/Share76Com.py +++ /dev/null @@ -1,15 +0,0 @@ -# -*- coding: utf-8 -*- - -from module.plugins.internal.DeadHoster import DeadHoster, create_getInfo - - -class Share76Com(DeadHoster): - __name__ = "Share76Com" - __type__ = "hoster" - __pattern__ = r"http://(?:\w*\.)*?share76.com/\w{12}" - __version__ = "0.04" - __description__ = """share76.com hoster plugin""" - __author_name__ = ("me") - - -getInfo = create_getInfo(Share76Com) diff --git a/module/plugins/hoster/ShareFilesCo.py b/module/plugins/hoster/ShareFilesCo.py deleted file mode 100644 index 245e20ea6a..0000000000 --- a/module/plugins/hoster/ShareFilesCo.py +++ /dev/null @@ -1,28 +0,0 @@ -# -*- coding: utf-8 -*- -import re - -from module.plugins.hoster.XFileSharingPro import XFileSharingPro, create_getInfo - - -class ShareFilesCo(XFileSharingPro): - __name__ = "ShareFilesCo" - __type__ = "hoster" - __pattern__ = r"http://(www\.)?sharefiles\.co/\w{12}" - __version__ = "0.01" - __description__ = """Sharefiles.co hoster plugin""" - __author_name__ = ("stickell") - __author_mail__ = ("l.stickell@yahoo.it") - - HOSTER_NAME = "sharefiles.co" - - def startDownload(self, link): - link = link.strip() - if link.startswith('http://adf.ly'): - link = re.sub('http://adf.ly/\d+/', '', link) - if self.captcha: - self.correctCaptcha() - self.logDebug('DIRECT LINK: %s' % link) - self.download(link) - - -getInfo = create_getInfo(ShareFilesCo) diff --git a/module/plugins/hoster/ShareRapidCom.py b/module/plugins/hoster/ShareRapidCom.py deleted file mode 100644 index 82f98d73cc..0000000000 --- a/module/plugins/hoster/ShareRapidCom.py +++ /dev/null @@ -1,71 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -import re - -from pycurl import HTTPHEADER -from module.network.HTTPRequest import BadHeader -from module.network.RequestFactory import getRequest -from module.plugins.internal.SimpleHoster import SimpleHoster, parseFileInfo, replace_patterns - - -def getInfo(urls): - h = getRequest() - h.c.setopt(HTTPHEADER, - ["Accept: text/html", - "User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:25.0) Gecko/20100101 Firefox/25.0"]) - for url in urls: - html = h.load(url, decode=True) - file_info = parseFileInfo(ShareRapidCom, replace_patterns(url, ShareRapidCom.FILE_URL_REPLACEMENTS), html) - yield file_info - - -class ShareRapidCom(SimpleHoster): - __name__ = "ShareRapidCom" - __type__ = "hoster" - __pattern__ = r"http://(?:www\.)?((share(-?rapid\.(biz|com|cz|info|eu|net|org|pl|sk)|-(central|credit|free|net)\.cz|-ms\.net)|(s-?rapid|rapids)\.(cz|sk))|(e-stahuj|mediatack|premium-rapidshare|rapidshare-premium|qiuck)\.cz|kadzet\.com|stahuj-zdarma\.eu|strelci\.net|universal-share\.com)/stahuj/(?P\w+)" - __version__ = "0.53" - __description__ = """Share-rapid.com plugin - premium only""" - __author_name__ = ("MikyWoW", "zoidberg", "stickell") - __author_mail__ = ("MikyWoW@seznam.cz", "zoidberg@mujmail.cz", "l.stickell@yahoo.it") - - FILE_NAME_PATTERN = r']*>]*>(?:]*>)?(?P[^<]+)' - FILE_SIZE_PATTERN = r'Velikost:\s*\s*(?P[0-9.]+) (?P[kKMG])i?B' - FILE_OFFLINE_PATTERN = ur'Nastala chyba 404|Soubor byl smazán' - - DOWNLOAD_URL_PATTERN = r'([^<]+)' - ERR_LOGIN_PATTERN = ur'
    Stahování je přístupné pouze přihlášeným uživatelům' - ERR_CREDIT_PATTERN = ur'
    Stahování zdarma je možné jen přes náš' - - FILE_URL_REPLACEMENTS = [(__pattern__, r'http://share-rapid.com/stahuj/\g')] - - def setup(self): - self.chunkLimit = 1 - self.resumeDownload = True - - def process(self, pyfile): - if not self.account: - self.fail("User not logged in") - - try: - self.html = self.load(pyfile.url, decode=True) - except BadHeader, e: - self.account.relogin(self.user) - self.retry(3, 0, str(e)) - - self.getFileInfo() - - found = re.search(self.DOWNLOAD_URL_PATTERN, self.html) - if found: - link = found.group(1) - self.logDebug("Premium link: %s" % link) - - self.download(link, disposition=True) - else: - if re.search(self.ERR_LOGIN_PATTERN, self.html): - self.relogin(self.user) - self.retry(3, 0, "User login failed") - elif re.search(self.ERR_CREDIT_PATTERN, self.html): - self.fail("Not enough credit left") - else: - self.fail("Download link not found") diff --git a/module/plugins/hoster/SharebeesCom.py b/module/plugins/hoster/SharebeesCom.py deleted file mode 100644 index 5eaaf24f5d..0000000000 --- a/module/plugins/hoster/SharebeesCom.py +++ /dev/null @@ -1,16 +0,0 @@ -# -*- coding: utf-8 -*- - -from module.plugins.internal.DeadHoster import DeadHoster, create_getInfo - - -class SharebeesCom(DeadHoster): - __name__ = "SharebeesCom" - __type__ = "hoster" - __pattern__ = r"http://(?:\w*\.)*?sharebees.com/\w{12}" - __version__ = "0.02" - __description__ = """ShareBees hoster plugin""" - __author_name__ = ("zoidberg") - __author_mail__ = ("zoidberg@mujmail.cz") - - -getInfo = create_getInfo(SharebeesCom) diff --git a/module/plugins/hoster/ShareonlineBiz.py b/module/plugins/hoster/ShareonlineBiz.py deleted file mode 100644 index 5b7a0e913c..0000000000 --- a/module/plugins/hoster/ShareonlineBiz.py +++ /dev/null @@ -1,209 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -import re -from base64 import b64decode -import hashlib -import random -from time import time, sleep - -from module.plugins.Hoster import Hoster -from module.network.RequestFactory import getURL -from module.plugins.Plugin import chunks -from module.plugins.internal.CaptchaService import ReCaptcha as _ReCaptcha - - -def getInfo(urls): - api_url_base = "http://api.share-online.biz/linkcheck.php" - - for chunk in chunks(urls, 90): - api_param_file = {"links": "\n".join(x.replace("http://www.share-online.biz/dl/", "").rstrip("/") for x in - chunk)} # api only supports old style links - src = getURL(api_url_base, post=api_param_file, decode=True) - result = [] - for i, res in enumerate(src.split("\n")): - if not res: - continue - fields = res.split(";") - - if fields[1] == "OK": - status = 2 - elif fields[1] in ("DELETED", "NOT FOUND"): - status = 1 - else: - status = 3 - - result.append((fields[2], int(fields[3]), status, chunk[i])) - yield result - - -#suppress ocr plugin -class ReCaptcha(_ReCaptcha): - def result(self, server, challenge): - return self.plugin.decryptCaptcha("%simage" % server, get={"c": challenge}, - cookies=True, forceUser=True, imgtype="jpg") - - -class ShareonlineBiz(Hoster): - __name__ = "ShareonlineBiz" - __type__ = "hoster" - __pattern__ = r"https?://[\w\.]*?(share\-online\.biz|egoshare\.com)/(download.php\?id\=|dl/)[\w]+" - __version__ = "0.37" - __description__ = """Shareonline.biz Download Hoster""" - __author_name__ = ("spoob", "mkaay", "zoidberg") - __author_mail__ = ("spoob@pyload.org", "mkaay@mkaay.de", "zoidberg@mujmail.cz") - - ERROR_INFO_PATTERN = r'

    Information:

    \s*
    \s*(.*?)' - - def setup(self): - # range request not working? - # api supports resume, only one chunk - # website isn't supporting resuming in first place - self.file_id = re.search(r"(id\=|/dl/)([a-zA-Z0-9]+)", self.pyfile.url).group(2) - self.pyfile.url = "http://www.share-online.biz/dl/" + self.file_id - - self.resumeDownload = self.premium - self.multiDL = False - #self.chunkLimit = 1 - - self.check_data = None - - def process(self, pyfile): - if self.premium: - self.handleAPIPremium() - #web-download fallback removed - didn't work anyway - else: - self.handleFree() - - # check = self.checkDownload({"failure": re.compile(self.ERROR_INFO_PATTERN)}) - # if check == "failure": - # try: - # self.retry(reason = self.lastCheck.group(1).decode("utf8")) - # except: - # self.retry(reason = "Unknown error") - - if self.api_data: - self.check_data = {"size": int(self.api_data['size']), "md5": self.api_data['md5']} - - def downloadAPIData(self): - api_url_base = "http://api.share-online.biz/linkcheck.php?md5=1" - api_param_file = {"links": self.pyfile.url.replace( - "http://www.share-online.biz/dl/", "")} # api only supports old style links - src = self.load(api_url_base, cookies=False, post=api_param_file, decode=True) - - fields = src.split(";") - self.api_data = {"fileid": fields[0], - "status": fields[1]} - if not self.api_data["status"] == "OK": - self.offline() - self.api_data["filename"] = fields[2] - self.api_data["size"] = fields[3] # in bytes - self.api_data["md5"] = fields[4].strip().lower().replace("\n\n", "") # md5 - - def handleFree(self): - self.downloadAPIData() - self.pyfile.name = self.api_data["filename"] - self.pyfile.size = int(self.api_data["size"]) - - self.html = self.load(self.pyfile.url, cookies=True) # refer, stuff - self.setWait(3) - self.wait() - - self.html = self.load("%s/free/" % self.pyfile.url, post={"dl_free": "1", "choice": "free"}, decode=True) - self.checkErrors() - - found = re.search(r'var wait=(\d+);', self.html) - - recaptcha = ReCaptcha(self) - for i in range(5): - challenge, response = recaptcha.challenge("6LdatrsSAAAAAHZrB70txiV5p-8Iv8BtVxlTtjKX") - self.setWait(int(found.group(1)) if found else 30) - response = self.load("%s/free/captcha/%d" % (self.pyfile.url, int(time() * 1000)), post={ - 'dl_free': '1', - 'recaptcha_challenge_field': challenge, - 'recaptcha_response_field': response}) - - if not response == '0': - break - - else: - self.fail("No valid captcha solution received") - - download_url = response.decode("base64") - self.logDebug(download_url) - if not download_url.startswith("http://"): - self.parseError("download url") - - self.wait() - self.download(download_url) - # check download - check = self.checkDownload({ - "cookie": re.compile(r'
    Share-Online") - }) - if check == "cookie": - self.retry(5, 60, "Cookie failure") - elif check == "fail": - self.retry(5, 300, "Download failed") - - def checkErrors(self): - found = re.search(r"/failure/(.*?)/1", self.req.lastEffectiveURL) - if found: - err = found.group(1) - found = re.search(self.ERROR_INFO_PATTERN, self.html) - msg = found.group(1) if found else "" - self.logError(err, msg or "Unknown error occurred") - - if err in ('freelimit', 'size', 'proxy'): - self.fail(msg or "Premium account needed") - if err in 'invalid': - self.fail(msg or "File not available") - elif err in 'server': - self.setWait(600, False) - elif err in 'expired': - self.setWait(30, False) - else: - self.setWait(300, True) - - self.wait() - self.retry(max_tries=25, reason=msg) - - def handleAPIPremium(self): # should be working better - self.account.getAccountInfo(self.user, True) - src = self.load("http://api.share-online.biz/account.php", - {"username": self.user, "password": self.account.accounts[self.user]["password"], - "act": "download", "lid": self.file_id}) - - self.api_data = dlinfo = {} - for line in src.splitlines(): - key, value = line.split(": ") - dlinfo[key.lower()] = value - - self.logDebug(dlinfo) - if not dlinfo["status"] == "online": - self.offline() - - self.pyfile.name = dlinfo["name"] - self.pyfile.size = int(dlinfo["size"]) - - dlLink = dlinfo["url"] - if dlLink == "server_under_maintenance": - self.tempoffline() - else: - self.multiDL = True - self.download(dlLink) - - def checksum(self, local_file): - if self.api_data and "md5" in self.api_data and self.api_data["md5"]: - h = hashlib.md5() - f = open(local_file, "rb") - h.update(f.read()) - f.close() - hexd = h.hexdigest() - if hexd == self.api_data["md5"]: - return True, 0 - else: - return False, 1 - else: - self.logWarning("MD5 checksum missing") - return True, 5 diff --git a/module/plugins/hoster/ShareplaceCom.py b/module/plugins/hoster/ShareplaceCom.py index e51c3160f7..91e5940afd 100644 --- a/module/plugins/hoster/ShareplaceCom.py +++ b/module/plugins/hoster/ShareplaceCom.py @@ -1,80 +1,44 @@ -#!/usr/bin/env python # -*- coding: utf-8 -*- -import re import urllib -from module.plugins.Hoster import Hoster +import re +from ..internal.SimpleHoster import SimpleHoster -class ShareplaceCom(Hoster): + +class ShareplaceCom(SimpleHoster): __name__ = "ShareplaceCom" __type__ = "hoster" - __pattern__ = r"(http://)?(www\.)?shareplace\.(com|org)/\?[a-zA-Z0-9]+" - __version__ = "0.11" - __description__ = """Shareplace.com Download Hoster""" - __author_name__ = ("ACCakut, based on YourfilesTo by jeix and skydancer") - __author_mail__ = ("none") - - def process(self, pyfile): - self.pyfile = pyfile - self.prepare() - self.download(self.get_file_url()) - - def prepare(self): - if not self.file_exists(): - self.offline() - - self.pyfile.name = self.get_file_name() - - wait_time = self.get_waiting_time() - self.setWait(wait_time) - self.logDebug("%s: Waiting %d seconds." % (self.__name__, wait_time)) - self.wait() + __version__ = "0.19" + __status__ = "testing" - def get_waiting_time(self): - if self.html is None: - self.download_html() + __pattern__ = r'http://(?:www\.)?shareplace\.(com|org)/\?\w+' + __config__ = [("activated", "bool", "Activated", True)] - #var zzipitime = 15; - m = re.search(r'var zzipitime = (\d+);', self.html) - if m: - sec = int(m.group(1)) - else: - sec = 0 + __description__ = """Shareplace.com hoster plugin""" + __license__ = "GPLv3" + __authors__ = [("ACCakut", None), + ("GammaC0de", "nitzo2001[AT]yahoo[DOT]com")] - return sec + NAME_PATTERN = r'Filename:\s*(?P.+?)
    ' + SIZE_PATTERN = r'Filesize:
    \s*(?P[\d.,]+) (?P[\w^_]+)
    ' - def download_html(self): - url = re.sub("shareplace.com\/\?", "shareplace.com//index1.php/?a=", self.pyfile.url) - self.html = self.load(url, decode=True) + TEMP_OFFLINE_PATTERN = r'^unmatchable$' + OFFLINE_PATTERN = r'Your requested file is not found' - def get_file_url(self): - """ returns the absolute downloadable filepath - """ - url = re.search(r"var beer = '(.*?)';", self.html) - if url: - url = url.group(1) - url = urllib.unquote( - url.replace("http://http:/", "").replace("vvvvvvvvv", "").replace("lllllllll", "").replace( - "teletubbies", "")) - self.logDebug("URL: %s" % url) - return url - else: - self.fail("absolute filepath could not be found. offline? ") + WAIT_PATTERN = r'var zzipitime = (\d+);' - def get_file_name(self): - if self.html is None: - self.download_html() + def handle_free(self, pyfile): + response = self.captcha.decrypt("http://shareplace.com/captcha.php") - return re.search("\s*(.*?)\s*", self.html).group(1) + self.data = self.load(pyfile.url, post={'captchacode': response}) + if "Captcha number error or expired" in self.data: + self.retry_captcha() - def file_exists(self): - """ returns True or False - """ - if self.html is None: - self.download_html() + self.captcha.correct() + self.check_errors() - if re.search(r"HTTP Status 404", self.html) is not None: - return False - else: - return True + m = re.search(r"var beer = '(.+?)'", self.data) + if m is not None: + self.link = urllib.unquote(urllib.unquote(m.group(1).replace("vvvvvvvvv", "") + .replace("lllllllll", "")).replace("teletubbies", ""))[13:] diff --git a/module/plugins/hoster/ShareplaceOrg.py b/module/plugins/hoster/ShareplaceOrg.py new file mode 100644 index 0000000000..a669217faa --- /dev/null +++ b/module/plugins/hoster/ShareplaceOrg.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- + +from ..internal.SimpleHoster import SimpleHoster + + +class ShareplaceOrg(SimpleHoster): + __name__ = "ShareplaceOrg" + __type__ = "hoster" + __version__ = "0.01" + __status__ = "testing" + + __pattern__ = r"https?://(?:www\.)?shareplace\.org/(?P\w+)" + __config__ = [("activated", "bool", "Activated", True), + ("use_premium", "bool", "Use premium account if available", True), + ("fallback", "bool","Fallback to free download if premium fails", True), + ("chk_filesize", "bool", "Check file size", True), + ("max_wait", "int", "Reconnect if waiting time is greater than minutes", 10)] + + + __description__ = """Shareplace.org hoster plugin""" + __license__ = "GPLv3" + __authors__ = [("GammaC0de", "nitzo2001[AT]yahoo[DOT]com")] + + URL_REPLACEMENTS = [(__pattern__ + ".*", r"https://shareplace.org/\g")] + + INFO_PATTERN = r"\s*(?P.+?) \((?P[\d.]+) (?P[\w^_]+)\)\s*\s*

    Choose free or premium download

    " + WAIT_PATTERN = r"\$\('\.download-timer-seconds'\)\.html\((\d+)\);" + + LINK_FREE_PATTERN = r"href='(https://shareplace.org/\w+\?pt=[^']+)'" diff --git a/module/plugins/hoster/ShragleCom.py b/module/plugins/hoster/ShragleCom.py deleted file mode 100644 index 2b1a8b80a3..0000000000 --- a/module/plugins/hoster/ShragleCom.py +++ /dev/null @@ -1,17 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -from module.plugins.internal.DeadHoster import DeadHoster, create_getInfo - - -class ShragleCom(DeadHoster): - __name__ = "ShragleCom" - __type__ = "hoster" - __pattern__ = r"http://(?:www.)?(cloudnator|shragle).com/files/(?P.*?)/" - __version__ = "0.22" - __description__ = """Cloudnator.com (Shragle.com) Download PLugin""" - __author_name__ = ("RaNaN", "zoidberg") - __author_mail__ = ("RaNaN@pyload.org", "zoidberg@mujmail.cz") - - -getInfo = create_getInfo(ShragleCom) diff --git a/module/plugins/hoster/SimplyPremiumCom.py b/module/plugins/hoster/SimplyPremiumCom.py new file mode 100644 index 0000000000..18a1dd7e33 --- /dev/null +++ b/module/plugins/hoster/SimplyPremiumCom.py @@ -0,0 +1,92 @@ +# -*- coding: utf-8 -*- + +import re + +from ..internal.misc import seconds_to_midnight +from ..internal.MultiHoster import MultiHoster + + +class SimplyPremiumCom(MultiHoster): + __name__ = "SimplyPremiumCom" + __type__ = "hoster" + __version__ = "0.17" + __status__ = "testing" + + __pattern__ = r'https?://.+simply-premium\.com' + __config__ = [("activated", "bool", "Activated", True), + ("use_premium", "bool", "Use premium account if available", True), + ("fallback", "bool", "Fallback to free download if premium fails", False), + ("chk_filesize", "bool", "Check file size", True), + ("max_wait", "int", "Reconnect if waiting time is greater than minutes", 10), + ("revert_failed", "bool", "Revert to standard download if fails", True)] + + __description__ = """Simply-Premium.com multi-hoster plugin""" + __license__ = "GPLv3" + __authors__ = [("EvolutionClip", "evolutionclip@live.de")] + + def setup(self): + self.chunk_limit = 16 + + def check_errors(self): + if '0' in self.data or ( + "You are not allowed to download from this host" in self.data and self.premium): + self.account.relogin() + self.retry() + + elif "NOTFOUND" in self.data: + self.offline() + + elif "downloadlimit" in self.data: + self.log_warning(_("Reached maximum connctions")) + self.retry(5, 60, _("Reached maximum connctions")) + + elif "trafficlimit" in self.data: + self.log_warning(_("Reached daily limit for this host")) + self.retry( + wait=seconds_to_midnight(), + msg="Daily limit for this host reached") + + elif "hostererror" in self.data: + self.log_warning( + _("Hoster temporarily unavailable, waiting 1 minute and retry")) + self.retry(5, 60, _("Hoster is temporarily unavailable")) + + def handle_premium(self, pyfile): + for i in range(5): + self.data = self.load( + "http://www.simply-premium.com/premium.php", + get={ + 'info': "", + 'link': self.pyfile.url}) + + if self.data: + self.log_debug("JSON data: " + self.data) + break + else: + self.log_info( + _("Unable to get API data, waiting 1 minute and retry")) + self.retry(5, 60, _("Unable to get API data")) + + self.check_errors() + + try: + self.pyfile.name = re.search( + r'(.+?)', self.data).group(1) + + except AttributeError: + self.pyfile.name = "" + + try: + self.pyfile.size = re.search( + r'(\d+)', self.data).group(1) + + except AttributeError: + self.pyfile.size = 0 + + try: + self.link = re.search( + r'(.+?)', + self.data).group(1) + + except AttributeError: + self.link = 'http://www.simply-premium.com/premium.php?link=' + self.pyfile.url diff --git a/module/plugins/hoster/SimplydebridCom.py b/module/plugins/hoster/SimplydebridCom.py index 67cc392558..e5680c3168 100644 --- a/module/plugins/hoster/SimplydebridCom.py +++ b/module/plugins/hoster/SimplydebridCom.py @@ -1,61 +1,58 @@ -#!/usr/bin/env python # -*- coding: utf-8 -*- -from urllib import quote, unquote -import re -from module.plugins.Hoster import Hoster +from ..internal.misc import replace_patterns +from ..internal.MultiHoster import MultiHoster -class SimplydebridCom(Hoster): +class SimplydebridCom(MultiHoster): __name__ = "SimplydebridCom" - __version__ = "0.1" __type__ = "hoster" - __pattern__ = r"http://\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/sd.php/*" - __description__ = """simply-debrid.com hoster plugin""" - __author_name__ = ("Kagenoshin") - __author_mail__ = ("kagenoshin@gmx.ch") - - def setup(self): - self.resumeDownload = self.multiDL = True - self.chunkLimit = 1 - - def process(self, pyfile): - if not self.account: - self.logError(_("Please enter your %s account or deactivate this plugin") % "simply-debrid.com") - self.fail("No simply-debrid.com account provided") - - self.logDebug("Old URL: %s" % pyfile.url) - - #fix the links for simply-debrid.com! - new_url = pyfile.url - new_url = new_url.replace("clz.to", "cloudzer.net/file") - new_url = new_url.replace("http://share-online", "http://www.share-online") - new_url = new_url.replace("ul.to", "uploaded.net/file") - new_url = new_url.replace("uploaded.com", "uploaded.net") - new_url = new_url.replace("filerio.com", "filerio.in") - new_url = new_url.replace("lumfile.com", "lumfile.se") - if('fileparadox' in new_url): - new_url = new_url.replace("http://", "https://") - - if re.match(self.__pattern__, new_url): - new_url = new_url - - self.logDebug("New URL: %s" % new_url) - - if not re.match(self.__pattern__, new_url): - page = self.load('http://simply-debrid.com/api.php', get={'dl': new_url}) #+'&u='+self.user+'&p='+self.account.getAccountData(self.user)['password']) - if 'tiger Link' in page or 'Invalid Link' in page or ('API' in page and 'ERROR' in page): - self.fail('Unable to unrestrict link') - new_url = page - - self.setWait(5) - self.wait() - self.logDebug("Unrestricted URL: " + new_url) - - self.download(new_url, disposition=True) - - check = self.checkDownload({"bad1": "No address associated with hostname", "bad2": "\w+)' + __config__ = [("activated", "bool", "Activated", True), + ("use_premium", "bool", "Use premium account if available", True), + ("fallback", "bool", + "Fallback to free download if premium fails", True), + ("chk_filesize", "bool", "Check file size", True), + ("max_wait", "int", "Reconnect if waiting time is greater than minutes", 10)] + + __description__ = """Sizedrive.com hoster plugin""" + __license__ = "GPLv3" + __authors__ = [("GammaC0de", None)] + + NAME_PATTERN = r'>Nome:
    (?P.+?)<' + SIZE_PATTERN = r'>Tamanho:(?P[\d.,]+) (?P[\w^_]+)' + OFFLINE_PATTERN = r'ARQUIVO DELATADO POR' + + def setup(self): + self.resume_download = False + self.multiDL = False + self.chunk_limit = 1 + + def handle_free(self, pyfile): + self.wait(5) + self.data = self.load("http://www.sizedrive.com/getdownload.php", + post={'id': self.info['pattern']['ID']}) + + m = re.search(r'function \w+\(\w+\){.+?ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789\+.+?}).+?;(?Pvar r=.+?;)' + JS_PROCESS_PATTERN = r'processRecording\s*=\s*function.+?}' + # all interesting parts of the javascript function occur before the first + # occurance of this word + JS_SPLIT_WORD = r'EXIF' + NAME_PATTERN = r'initPlayer\(.+?["\']title["\']:["\'](?P.+?)["\']' + COMMUNITY_JS_PATTERN = r'. - - @author: Walter Purcaro -""" - -from module.plugins.hoster.PutlockerCom import PutlockerCom - - -class SockshareCom(PutlockerCom): - __name__ = "SockshareCom" - __type__ = "hoster" - __pattern__ = r'http://(?:www\.)?sockshare\.com/(mobile/)?(file|embed)/(?P[A-Z0-9]+)' - __version__ = "0.01" - __description__ = """Sockshare.Com""" - __author_name__ = ("Walter Purcaro") - __author_mail__ = ("vuolter@gmail.com") - - FILE_URL_REPLACEMENTS = [(__pattern__, r'http://www.sockshare.com/file/\g')] - HOSTER_NAME = "sockshare.com" diff --git a/module/plugins/hoster/SolidfilesCom.py b/module/plugins/hoster/SolidfilesCom.py new file mode 100644 index 0000000000..f3602b75eb --- /dev/null +++ b/module/plugins/hoster/SolidfilesCom.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- +# +# Test links: +# http://www.solidfiles.com/d/609cdb4b1b + +import re + +from ..internal.SimpleHoster import SimpleHoster + + +class SolidfilesCom(SimpleHoster): + __name__ = "SolidfilesCom" + __type__ = "hoster" + __version__ = "0.09" + __status__ = "testing" + + __pattern__ = r'https?://(?:www\.)?solidfiles\.com\/[dv]/\w+' + __config__ = [("activated", "bool", "Activated", True), + ("use_premium", "bool", "Use premium account if available", True), + ("fallback", "bool", "Fallback to free download if premium fails", True), + ("chk_filesize", "bool", "Check file size", True), + ("max_wait", "int", "Reconnect if waiting time is greater than minutes", 10)] + + __description__ = """Solidfiles.com hoster plugin""" + __license__ = "GPLv3" + __authors__ = [("sraedler", "simon.raedler@yahoo.de"), + ("GammaC0de", "nitzo2001[AT]yahoo[DOT]com")] + + NAME_PATTERN = r'

    (?P.+?)

    ' + SIZE_PATTERN = r'\s*(?P[\d.,]+) (?P[\w_^]+)' + OFFLINE_PATTERN = r'

    404' + + def setup(self): + self.multiDL = True + self.chunk_limit = 1 + + def handle_free(self, pyfile): + m = re.search(r'"downloadUrl":"(.+?)"', self.data) + if m is not None: + self.link = m.group(1) diff --git a/module/plugins/hoster/SoundcloudCom.py b/module/plugins/hoster/SoundcloudCom.py new file mode 100644 index 0000000000..126e9f2dd7 --- /dev/null +++ b/module/plugins/hoster/SoundcloudCom.py @@ -0,0 +1,82 @@ +# -*- coding: utf-8 -*- + +import re + +from ..internal.misc import BIGHTTPRequest, json +from ..internal.SimpleHoster import SimpleHoster + + +class SoundcloudCom(SimpleHoster): + __name__ = "SoundcloudCom" + __type__ = "hoster" + __version__ = "0.22" + __status__ = "testing" + + __pattern__ = r'https?://(?:www\.)?soundcloud\.com/[\w\-]+/[\w\-]+' + __config__ = [("activated", "bool", "Activated", True), + ("use_premium", "bool", "Use premium account if available", True), + ("fallback", "bool", + "Fallback to free download if premium fails", True), + ("chk_filesize", "bool", "Check file size", True), + ("max_wait", "int", "Reconnect if waiting time is greater than minutes", 10)] + + __description__ = """SoundCloud.com hoster plugin""" + __license__ = "GPLv3" + __authors__ = [("Walter Purcaro", "vuolter@gmail.com"), + ("GammaC0de", "nitzo2001[AT]yahoo[DOT]com")] + + NAME_PATTERN = r'title" content="(?P.+?)"' + OFFLINE_PATTERN = r'"SoundCloud - Hear the world’s sounds"' + + def setup(self): + try: + self.req.http.close() + except Exception: + pass + + self.req.http = BIGHTTPRequest( + cookies=self.req.cj, + options=self.pyload.requestFactory.getOptions(), + limit=5000000, + ) + + def get_info(self, url="", html=""): + info = super(SoundcloudCom, self).get_info(url, html) + # Unfortunately, NAME_PATTERN does not include file extension, so we add '.mp3' as an extension. + if "name" in info: + info["name"] += ".mp3" + + return info + + def handle_free(self, pyfile): + try: + json_data = re.search( + r"", self.data + ).group(1) + + except (AttributeError, IndexError): + self.fail("Failed to retrieve json_data") + + try: + js_url = re.findall(r'script crossorigin src="(.+?)">', self.data)[-1] + except IndexError: + self.fail(_("Failed to find js_url")) + + js_data = self.load(js_url) + m = re.search(r'[ ,]client_id:"(.+?)"', js_data) + if m is None: + self.fail(_("client_id not found")) + + client_id = m.group(1) + + hydra_table = dict([(table["hydratable"], table["data"]) for table in json.loads(json_data)]) + streams = [s["url"] + for s in hydra_table["sound"]["media"]["transcodings"] + if s["format"]["protocol"] == "progressive" and s["format"]["mime_type"] == "audio/mpeg"] + track_authorization = hydra_table["sound"]["track_authorization"] + + if streams: + json_data = self.load(streams[0], + get={"client_id": client_id, + "track_authorization": track_authorization}) + self.link = json.loads(json_data).get("url") diff --git a/module/plugins/hoster/SpeedLoadOrg.py b/module/plugins/hoster/SpeedLoadOrg.py deleted file mode 100644 index 2360b77739..0000000000 --- a/module/plugins/hoster/SpeedLoadOrg.py +++ /dev/null @@ -1,16 +0,0 @@ -# -*- coding: utf-8 -*- - -from module.plugins.internal.DeadHoster import DeadHoster, create_getInfo - - -class SpeedLoadOrg(DeadHoster): - __name__ = "SpeedLoadOrg" - __type__ = "hoster" - __pattern__ = r"http://(www\.)?speedload\.org/(?P\w+)" - __version__ = "1.02" - __description__ = """Speedload.org hoster plugin""" - __author_name__ = ("stickell") - __author_mail__ = ("l.stickell@yahoo.it") - - -getInfo = create_getInfo(SpeedLoadOrg) diff --git a/module/plugins/hoster/SpeedfileCz.py b/module/plugins/hoster/SpeedfileCz.py deleted file mode 100644 index b8eaa775cc..0000000000 --- a/module/plugins/hoster/SpeedfileCz.py +++ /dev/null @@ -1,31 +0,0 @@ -# -*- 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 . - - @author: zoidberg -""" - -from module.plugins.internal.DeadHoster import DeadHoster, create_getInfo - - -class SpeedfileCz(DeadHoster): - __name__ = "SpeedFileCz" - __type__ = "hoster" - __pattern__ = r"http://speedfile.cz/.*" - __version__ = "0.32" - __description__ = """speedfile.cz""" - __author_name__ = ("zoidberg") - - -getInfo = create_getInfo(SpeedfileCz) diff --git a/module/plugins/hoster/SpeedyshareCom.py b/module/plugins/hoster/SpeedyshareCom.py new file mode 100644 index 0000000000..bb048a46ca --- /dev/null +++ b/module/plugins/hoster/SpeedyshareCom.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- +# +# Test links: +# http://speedy.sh/ep2qY/Zapp-Brannigan.jpg + +import re + +from ..internal.SimpleHoster import SimpleHoster + + +class SpeedyshareCom(SimpleHoster): + __name__ = "SpeedyshareCom" + __type__ = "hoster" + __version__ = "0.11" + __status__ = "testing" + + __pattern__ = r'https?://(?:www\.)?(speedyshare\.com|speedy\.sh)/\w+' + __config__ = [("activated", "bool", "Activated", True), + ("use_premium", "bool", "Use premium account if available", True), + ("fallback", "bool", + "Fallback to free download if premium fails", True), + ("chk_filesize", "bool", "Check file size", True), + ("max_wait", "int", "Reconnect if waiting time is greater than minutes", 10)] + + __description__ = """Speedyshare.com hoster plugin""" + __license__ = "GPLv3" + __authors__ = [("zapp-brannigan", "fuerst.reinje@web.de")] + + NAME_PATTERN = r'class=downloadfilename>(?P.*)' + SIZE_PATTERN = r'class=sizetagtext>(?P.*) (?P[kKmM]?[iI]?[bB]?)

    ' + + OFFLINE_PATTERN = r'class=downloadfilenamenotfound>.*' + + LINK_FREE_PATTERN = r'\'Slow. +from ..internal.misc import json +from ..internal.SimpleHoster import SimpleHoster - @author: zoidberg -""" -import re -from module.plugins.Hoster import Hoster -from module.network.RequestFactory import getURL +def get_api_password(episode): + api_key = "fb5f58a820353bd7095de526253c14fd" + timestamp = int(round(time.time() / 24 / 3600)) + api_pass = api_key + "/episode/" + episode + str(timestamp) -def getInfo(urls): - result = [] + m = hashlib.md5(api_pass) - for url in urls: + return m.hexdigest() + + +def get_all_link(data, container): + videos = [] + + for i in range(0, len(data["video_qualities"])): + if container == "webm" and len( + data["video_qualities"][i]["formats"]) != 1: + videos.append(data["video_qualities"][i]["formats"][1]["source"]) - html = getURL(url) - if re.search(StreamCz.FILE_OFFLINE_PATTERN, html): - # File offline - result.append((url, 0, 1, url)) else: - result.append((url, 0, 2, url)) - yield result + videos.append(data["video_qualities"][i]["formats"][0]["source"]) + + return videos -class StreamCz(Hoster): +def get_link_quality(videos, quality): + quality_index = ["144p", "240p", "360p", "480p", "720p", "1080p"] + quality = quality_index.index(quality) + + link = None + while quality >= 0: + if len(videos) >= quality + 1: + link = videos[quality] + break + + else: + quality -= 1 + + return link + + +class StreamCz(SimpleHoster): __name__ = "StreamCz" __type__ = "hoster" - __pattern__ = r"http://www.stream.cz/[^/]+/\d+.*" - __version__ = "0.1" - __description__ = """stream.cz""" - __author_name__ = ("zoidberg") + __version__ = "0.41" + __status__ = "testing" - FILE_OFFLINE_PATTERN = r'

    Str.nku nebylo mo.n. nal.zt \(404\)

    ' - FILE_NAME_PATTERN = r'' - CDN_PATTERN = r'\d+)(?:&cdnLQ=(?P\d*))?(?:&cdnHQ=(?P\d*))?(?:&cdnHD=(?P\d*))?&' + __pattern__ = r'https?://(?:www\.)?stream\.cz/[^/]+/(?P\d+).+' + __config__ = [("activated", "bool", "Activated", True), + ("quality", "144p;240p;360p;480p;720p;1080p", "Quality", "720p"), + ("container", "mp4;webm", "Container", "mp4"), ] + + __description__ = """Stream.cz hoster plugin""" + __authors__ = [("ondrej", "git@ondrej.it")] def setup(self): + self.resume_download = True self.multiDL = True - self.resumeDownload = True def process(self, pyfile): + episode = self.info['pattern']['EP'] + api_password = get_api_password(episode) - self.html = self.load(pyfile.url, decode=True) + api_url = urlparse.urljoin( + "https://www.stream.cz/API/episode/", episode) + self.req.putHeader("Api-Password", api_password) + resp = self.load(api_url) - if re.search(self.FILE_OFFLINE_PATTERN, self.html): - self.offline() + data = json.loads(resp) - found = re.search(self.CDN_PATTERN, self.html) - if found is None: - self.fail("Parse error (CDN)") - cdn = found.groupdict() - self.logDebug(cdn) - for cdnkey in ("cdnHD", "cdnHQ", "cdnLQ"): - if cdnkey in cdn and cdn[cdnkey] > '': - cdnid = cdn[cdnkey] - break - else: - self.fail("Stream URL not found") + quality = self.config.get("quality") + container = self.config.get("container") + + videos = get_all_link(data, container) + link = get_link_quality(videos, quality) - found = re.search(self.FILE_NAME_PATTERN, self.html) - if found is None: - self.fail("Parse error (NAME)") - pyfile.name = "%s-%s.%s.mp4" % (found.group(2), found.group(1), cdnkey[-2:]) + if link: + link_name, container = os.path.splitext(link) + self.pyfile.name = data["name"] + container - download_url = "http://cdn-dispatcher.stream.cz/?id=" + cdnid - self.logInfo("STREAM (%s): %s" % (cdnkey[-2:], download_url)) - self.download(download_url) + self.log_info(_("Downloading file...")) + self.download(link) diff --git a/module/plugins/hoster/StreamcloudEu.py b/module/plugins/hoster/StreamcloudEu.py index dd9bf9429c..84e364267f 100644 --- a/module/plugins/hoster/StreamcloudEu.py +++ b/module/plugins/hoster/StreamcloudEu.py @@ -1,118 +1,32 @@ # -*- coding: utf-8 -*- -from time import sleep -import re -from module.plugins.hoster.XFileSharingPro import XFileSharingPro, create_getInfo -from module.network.HTTPRequest import HTTPRequest +from ..internal.XFSHoster import XFSHoster -class StreamcloudEu(XFileSharingPro): + +class StreamcloudEu(XFSHoster): __name__ = "StreamcloudEu" __type__ = "hoster" - __pattern__ = r"http://(www\.)?streamcloud\.eu/\S+" - __version__ = "0.02" - __description__ = """Streamcloud.eu hoster plugin""" - __author_name__ = ("seoester") - __author_mail__ = ("seoester@googlemail.com") - - HOSTER_NAME = "streamcloud.eu" - DIRECT_LINK_PATTERN = r'file: "(http://(stor|cdn)\d+\.streamcloud.eu:?\d*/.*/video\.mp4)",' - - def setup(self): - super(StreamcloudEu, self).setup() - self.multiDL = True - - def getDownloadLink(self): - found = re.search(self.DIRECT_LINK_PATTERN, self.html, re.S) - if found: - return found.group(1) - - for i in range(5): - self.logDebug("Getting download link: #%d" % i) - data = self.getPostParameters() - httpRequest = HTTPRequest(options=self.req.options) - httpRequest.cj = self.req.cj - sleep(10) - self.html = httpRequest.load(self.pyfile.url, post=data, referer=False, cookies=True, decode=True) - self.header = httpRequest.header - - found = re.search("Location\s*:\s*(.*)", self.header, re.I) - if found: - break - - found = re.search(self.DIRECT_LINK_PATTERN, self.html, re.S) - if found: - 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 found.group(1) - - def getPostParameters(self): - for i in range(3): - if not self.errmsg: - self.checkErrors() + __version__ = "0.16" + __status__ = "testing" - if hasattr(self, "FORM_PATTERN"): - action, inputs = self.parseHtmlForm(self.FORM_PATTERN) - else: - action, inputs = self.parseHtmlForm(input_names={"op": re.compile("^download")}) + __pattern__ = r'http://(?:www\.)?streamcloud\.eu/\w{12}' + __config__ = [("activated", "bool", "Activated", True), + ("use_premium", "bool", "Use premium account if available", True), + ("fallback", "bool", + "Fallback to free download if premium fails", True), + ("chk_filesize", "bool", "Check file size", True), + ("max_wait", "int", "Reconnect if waiting time is greater than minutes", 10)] - 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 ('download1', '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: - found = re.search(self.WAIT_PATTERN, self.html) - if found: - wait_time = int(found.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 - self.logDebug("getPostParameters {0}".format(i)) - 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=False) - self.errmsg = None + __description__ = """Streamcloud.eu hoster plugin""" + __license__ = "GPLv3" + __authors__ = [("seoester", "seoester@googlemail.com")] - else: - self.parseError('FORM: %s' % (inputs['op'] if 'op' in inputs else 'UNKNOWN')) + PLUGIN_DOMAIN = "streamcloud.eu" + WAIT_PATTERN = r'var count = (\d+)' -getInfo = create_getInfo(StreamcloudEu) + def setup(self): + self.multiDL = True + self.chunk_limit = 1 + self.resume_download = self.premium diff --git a/module/plugins/hoster/SuprafilesMe.py b/module/plugins/hoster/SuprafilesMe.py new file mode 100644 index 0000000000..eaef19af68 --- /dev/null +++ b/module/plugins/hoster/SuprafilesMe.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- + +import re + +from ..internal.XFSHoster import XFSHoster + + +class SuprafilesMe(XFSHoster): + __name__ = "SuprafilesMe" + __type__ = "hoster" + __version__ = "0.01" + __status__ = "testing" + + __pattern__ = r'https?://(?:www\.)?(?:suprafiles\.me|sfiles\.org)/(?P\w+)' + __config__ = [("activated", "bool", "Activated", True), + ("use_premium", "bool", "Use premium account if available", True), + ("fallback", "bool", "Fallback to free download if premium fails", True), + ("chk_filesize", "bool", "Check file size", True), + ("max_wait", "int", "Reconnect if waiting time is greater than minutes", 10)] + + __description__ = """Suprafiles.me hoster plugin""" + __license__ = "GPLv3" + __authors__ = [("GammaC0de", "nitzo2001[AT]yahoo[DOT]com")] + + PLUGIN_DOMAIN = "suprafiles.me" + + URL_REPLACEMENTS = [((__pattern__ + '.*', r'http://sfiles.org/\g'))] + + NAME_PATTERN = r'Download File (?P.+?)' + SIZE_PATTERN = r'Size\s*(?P[\d.,]+) (?P[\w^_]+?)<' + OFFLINE_PATTERN = r'>File Not Found' + ERROR_PATTERN = r'(?:class=["\']err["\'].*?>|>Error|>\(ERROR:)(?:\s*<.+?>\s*)*(.+?)(?:["\']|<|\))' + + LINK_PATTERN = r'
    (?P.+?)<' + SIZE_PATTERN = r'Rozmiar: (?P[\d.,]+) (?P[\w_^]+)<' + + LINK_PATTERN = r'Pobierz' + + def handle_premium(self, pyfile): + self.data = self.load("https://tb7.pl/mojekonto/sciagaj", + post={'step': 1, + 'content': pyfile.url}) + + m = re.search(self.NAME_PATTERN, self.data) + if m is not None: + pyfile.name = m.group('N') + + m = re.search(self.SIZE_PATTERN, self.data) + if m is not None: + pyfile.size = parse_size(m.group('S'), m.group('U')) + + self.data = self.load("https://tb7.pl/mojekonto/sciagaj", + post={'step': 2, + '0': "on"}) + + if "Nieaktywne linki" in self.data: + self.temp_offline() + + m = re.search(self.LINK_PATTERN, self.data) + if m is not None: + self.link = m.group(1) diff --git a/module/plugins/hoster/TenluaVn.py b/module/plugins/hoster/TenluaVn.py new file mode 100644 index 0000000000..eed34dc07a --- /dev/null +++ b/module/plugins/hoster/TenluaVn.py @@ -0,0 +1,86 @@ +# -*- coding: utf-8 -*- + +import random +import re + +from ..internal.SimpleHoster import SimpleHoster +from ..internal.misc import json + + +def gen_r(): + return "0." + "".join([random.choice("0123456789") for x in range(16)]) + + +class TenluaVn(SimpleHoster): + __name__ = "TenluaVn" + __type__ = "hoster" + __version__ = "0.04" + __status__ = "testing" + + __pattern__ = r'https?://(?:www\.)?tenlua\.vn(?!/folder)/.+?/(?P[0-9a-f]+)/' + __config__ = [("activated", "bool", "Activated", True), + ("use_premium", "bool", "Use premium account if available", True), + ("fallback", "bool", "Fallback to free download if premium fails", True), + ("chk_filesize", "bool", "Check file size", True), + ("max_wait", "int", "Reconnect if waiting time is greater than minutes", 10)] + + __description__ = """Tenlua.vn hoster plugin""" + __license__ = "GPLv3" + __authors__ = [("GammaC0de", "nitzo2001[AT]yahoo[DOT]com")] + + API_URL = "https://api2.tenlua.vn/" + + def api_response(self, method, **kwargs): + kwargs['a'] = method + sid = kwargs.pop('sid', None) + return json.loads(self.load(self.API_URL, + get={'sid': sid} if sid is not None else {}, + post=json.dumps([kwargs]))) + + def api_info(self, url): + file_id = re.match(self.__pattern__, url).group('ID') + file_info = self.api_response("filemanager_builddownload_getinfo", n=file_id, r=gen_r())[0] + + if file_info['type'] == "none": + return {'status': 1} + + else: + return {'name': file_info['n'], + 'size': file_info['real_size'], + 'status': 2, + 'tenlua': {'link': file_info['dlink'], + 'password': bool(file_info['passwd'])}} + + def handle_free(self, pyfile): + self.handle_download() + + def handle_premium(self, pyfile): + sid = self.account.info['data']['sid'] + self.handle_download(sid) + + def handle_download(self, sid=None): + if self.info['tenlua']['password']: + password = self.get_password() + if password: + file_id = self.info['pattern']['ID'] + args = dict(n=file_id, p=password, r=gen_r()) + if sid is not None: + args['sid'] = sid + + password_status = self.api_response("filemanager_builddownload_checkpassword", **args) + if password_status['status'] == "0": + self.fail(_("Wrong password")) + + else: + url = password_status['url'] + + else: + self.fail(_("Download is password protected")) + + else: + url = self.info['tenlua']['link'] + + if sid is None: + self.wait(30) + + self.link = url diff --git a/module/plugins/hoster/TorboxApp.py b/module/plugins/hoster/TorboxApp.py new file mode 100644 index 0000000000..3f3dd5f7df --- /dev/null +++ b/module/plugins/hoster/TorboxApp.py @@ -0,0 +1,172 @@ +# -*- coding: utf-8 -*- + +import re +import time + +import pycurl +import urlparse + +from module.network.HTTPRequest import BadHeader + +from ..internal.misc import json +from ..internal.MultiHoster import MultiHoster + + +class TorboxApp(MultiHoster): + __name__ = "TorboxApp" + __type__ = "hoster" + __version__ = "0.01" + __status__ = "testing" + + __pattern__ = r"https://store-\d+\.wnam\.tb-cdn\.io/dld/.*|(?Phttps://api\.torbox\.app/v1/api/(?Pwebdl|torrents)/requestdl\?.*redirect=true.*)" + __config__ = [ + ("activated", "bool", "Activated", True), + ("use_premium", "bool", "Use premium account if available", True), + ("fallback", "bool", "Fallback to free download if premium fails", False), + ("chk_filesize", "bool", "Check file size", True), + ("max_wait", "int", "Reconnect if waiting time is greater than minutes", 10), + ("revert_failed", "bool", "Revert to standard download if fails", True), + ] + + __description__ = """Torbox.app multi-hoster plugin""" + __license__ = "GPLv3" + __authors__ = [("GammaC0de", "nitzo2001[AT]yahoo[DOT]com")] + + # See https://api-docs.torbox.app/ + API_URL = "https://api.torbox.app/v1/api/" + + def api_request(self, method, api_key=None, get={}, post={}): + if api_key is not None: + self.req.http.c.setopt( + pycurl.HTTPHEADER, ["Authorization: Bearer " + api_key] + ) + + try: + json_data = self.load(self.API_URL + method, get=get, post=post) + except BadHeader as exc: + json_data = exc.content + + api_data = json.loads(json_data) + return api_data + + def sleep(self, sec): + for _i in range(sec): + if self.pyfile.abort: + break + time.sleep(1) + + def setup(self): + self.chunk_limit = 16 + + def grab_info(self): + super(TorboxApp, self).grab_info() + m = re.match(self.__pattern__, self.pyfile.url) + if m is not None: + api_url = m.group("APIURL") + if api_url is not None: + url_p = urlparse.urlparse(api_url) + parse_qs = urlparse.parse_qs(url_p.query) + endpoint = m.group("ENDPOINT") + api_data = self.api_request("%s/mylist" % endpoint, + api_key=parse_qs["token"][0], + get={ + "id": parse_qs["web_id" if endpoint == "webdl" else "torrent_id"][0] + }) + + if api_data.get("success", False) and api_data.get("data"): + file_id = int(parse_qs["file_id"][0]) + for file in api_data["data"]["files"]: + if file["id"] == file_id: + self.pyfile.name = file["short_name"] + self.pyfile.size = file["size"] + break + + def handle_direct(self, pyfile): + link = self.isresource(pyfile.url) + if link: + self.link = pyfile.url + + else: + self.link = None + + def handle_premium(self, pyfile): + api_key = self.account.info["login"]["password"] + + post = {"link": pyfile.url} + password = self.get_password() + if password: + post["password"] = password + + api_data = self.api_request("webdl/createwebdownload", + api_key=api_key, + post=post) + self.check_errors(api_data) + + file_id = api_data["data"]["webdownload_id"] + file_hash = api_data["data"]["hash"] + + api_data = self.api_request("webdl/checkcached", + api_key=api_key, + get={ + "hash": file_hash, + "format": "object", + "bypass_cache": True, + }) + + if api_data.get("success", False) and api_data.get("data"): + pyfile.name = api_data["data"][file_hash]["name"] + pyfile.size = api_data["data"][file_hash]["size"] + + else: + pyfile.set_custom_status("web caching") + pyfile.set_progress(0) + while True: + api_data = self.api_request("webdl/mylist", + api_key=api_key, + get={ + "id": file_id, + "bypass_cache": True, + }) + self.check_errors(api_data) + + file_size = api_data["data"].get("size") + if file_size: + pyfile.size = file_size + file_name = api_data["data"].get("name") + if file_name: + pyfile.name = file_name + + progress = int(api_data["data"].get("progress", 0) * 100) + pyfile.set_progress(progress) + if api_data["data"].get("download_state") == "completed": + break + + self.sleep(5) + + pyfile.set_progress(100) + + api_data = self.api_request("webdl/requestdl", + api_key=api_key, + get={ + "web_id": file_id, + "zip": False, + "token": api_key + }) + self.check_errors(api_data) + + self.link = api_data["data"] + + def check_errors(self, data=None): + if isinstance(data, dict): + if not data.get("success", False): + error_code = data.get("error") + if error_code == "DOWNLOAD_SERVER_ERROR": + self.offline() + + elif error_code == "DOWNLOAD_LIMIT_REACHED": + self.log_error(data["detail"]) + self.temp_offline() + + else: + self.log_error(data["detail"]) + self.fail(data["detail"]) diff --git a/module/plugins/hoster/TurbobitNet.py b/module/plugins/hoster/TurbobitNet.py index 78d8b3deb7..f6c0351e63 100644 --- a/module/plugins/hoster/TurbobitNet.py +++ b/module/plugins/hoster/TurbobitNet.py @@ -1,183 +1,106 @@ # -*- coding: utf-8 -*- -""" - Copyright (C) 2012 pyLoad team - Copyright (C) 2012 JD-Team support@jdownloader.org - - 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 . - - @author: zoidberg -""" +import pycurl import re -import random -from urllib import quote -from binascii import hexlify, unhexlify -import time -from pycurl import HTTPHEADER -from Crypto.Cipher import ARC4 -from module.network.RequestFactory import getURL -from module.plugins.internal.SimpleHoster import SimpleHoster, create_getInfo, timestamp -from module.plugins.internal.CaptchaService import ReCaptcha +from ..captcha.HCaptcha import HCaptcha +from ..captcha.ReCaptcha import ReCaptcha +from ..internal.SimpleHoster import SimpleHoster class TurbobitNet(SimpleHoster): __name__ = "TurbobitNet" __type__ = "hoster" - __pattern__ = r"http://(?:\w*\.)?(turbobit.net|unextfiles.com)/(?!download/folder/)(?:download/free/)?(?P\w+).*" - __version__ = "0.11" - __description__ = """Turbobit.net plugin""" - __author_name__ = ("zoidberg") - __author_mail__ = ("zoidberg@mujmail.cz") - - # long filenames are shortened - FILE_INFO_PATTERN = r"\w+).*", - "http://turbobit.net/\g.html")] - SH_COOKIES = [("turbobit.net", "user_lang", "en")] - - CAPTCHA_KEY_PATTERN = r'src="http://api\.recaptcha\.net/challenge\?k=([^"]+)"' - DOWNLOAD_URL_PATTERN = r'(?P/download/redirect/[^"\']+)' - LIMIT_WAIT_PATTERN = r'
    \s*.*?(\d+)' - CAPTCHA_SRC_PATTERN = r'Captcha 60) - self.wait() - self.retry() - - action, inputs = self.parseHtmlForm("action='#'") - if not inputs: - self.parseError("captcha form") - self.logDebug(inputs) - - if inputs['captcha_type'] == 'recaptcha': - recaptcha = ReCaptcha(self) - found = re.search(self.CAPTCHA_KEY_PATTERN, self.html) - captcha_key = found.group(1) if found else '6LcTGLoSAAAAAHCWY9TTIrQfjUlxu6kZlTYP50_c' - inputs['recaptcha_challenge_field'], inputs['recaptcha_response_field'] = recaptcha.challenge( - captcha_key) - else: - found = re.search(self.CAPTCHA_SRC_PATTERN, self.html) - if not found: - self.parseError('captcha') - captcha_url = found.group(1) - inputs['captcha_response'] = self.decryptCaptcha(captcha_url) + __version__ = "0.56" + __status__ = "testing" - self.logDebug(inputs) - self.html = self.load(self.url, post=inputs) + __pattern__ = r'https?://(?:(?:www|m)\.)?(?:(?:trbbt|turbo(?:beet|bit[ea]?)|torbobit)\.net|(?:tourbobit|turbobi(?:tn?|f))\.com|turbo?\.(?:to|cc)|turb\.pw|trbt\.cc)/(?:download/free/)?(?P\w+)' + __config__ = [("activated", "bool", "Activated", True), + ("use_premium", "bool", "Use premium account if available", True), + ("fallback", "bool","Fallback to free download if premium fails", True), + ("chk_filesize", "bool", "Check file size", True), + ("max_wait", "int", "Reconnect if waiting time is greater than minutes", 10)] - if not "
    " in self.html: - self.invalidCaptcha() - else: - self.correctCaptcha() - break - else: - self.fail("Invalid captcha") - - def getRtUpdate(self): - rtUpdate = self.getStorage("rtUpdate") - if not rtUpdate: - if self.getStorage("version") != self.__version__ or int( - self.getStorage("timestamp", 0)) + 86400000 < timestamp(): - # that's right, we are even using jdownloader updates - rtUpdate = getURL("http://update0.jdownloader.org/pluginstuff/tbupdate.js") - rtUpdate = self.decrypt(rtUpdate.splitlines()[1]) - # but we still need to fix the syntax to work with other engines than rhino - rtUpdate = re.sub(r'for each\(var (\w+) in(\[[^\]]+\])\)\{', - r'zza=\2;for(var zzi=0;zzi.html")] + SIZE_REPLACEMENTS = [(r' ', "")] + + COOKIES = [("turbobit.net", "user_lang", "en")] + + INFO_PATTERN = r'\s*Download file (?P<N>.+?) \((?P<S>[\d., ]+) (?P<U>[\w^_]+)\)' + OFFLINE_PATTERN = r'<h2>File Not Found</h2>|html\(\'File (?:was )?not found|>Searching for the file\.\.\.Please wait' + TEMP_OFFLINE_PATTERN = r'^unmatchable$' + + LINK_FREE_PATTERN = r'(/download/redirect/[^"\']+)' + LINK_PREMIUM_PATTERN = r'href=[\'"](.+?/download/redirect/[^"\']+)' + + LIMIT_WAIT_PATTERN = r'<div id=\'timeout\'>(\d+)<' - return rtUpdate - def getDownloadUrl(self, rtUpdate): - self.req.http.lastURL = self.url + def handle_free(self, pyfile): + self.free_url = "https://turbobit.net/download/free/%s" % self.info['pattern']['ID'] + self.data = self.load(self.free_url) - found = re.search("(/\w+/timeout\.js\?\w+=)([^\"\'<>]+)", self.html) - url = "http://turbobit.net%s%s" % (found.groups() if found else ( - '/files/timeout.js?ver=', ''.join(random.choice('0123456789ABCDEF') for x in range(32)))) - fun = self.load(url) + m = re.search(self.LIMIT_WAIT_PATTERN, self.data) + if m is not None: + self.retry(wait=m.group(1)) - self.setWait(65, False) + self.solve_captcha() - for b in [1, 3]: - self.jscode = "var id = \'%s\';var b = %d;var inn = \'%s\';%sout" % ( - self.file_info['ID'], b, quote(fun), rtUpdate) + m = re.search(r'minLimit : (.+?),', self.data) + if m is None: + self.retry_captcha() + + self.captcha.correct() + + wait_time = self.js.eval(m.group(1)) + self.wait(wait_time) + + self.req.http.c.setopt(pycurl.HTTPHEADER, ["X-Requested-With: XMLHttpRequest"]) + self.data = self.load("https://turbobit.net/download/getLinkTimeout/%s" % self.info['pattern']['ID'], + ref=self.free_url) + self.req.http.c.setopt(pycurl.HTTPHEADER, ["X-Requested-With:"]) + + if "/download/started/" in self.data: + self.data = self.load("https://turbobit.net/download/started/%s" % self.info['pattern']['ID']) + + m = re.search(self.LINK_FREE_PATTERN, self.data) + if m is not None: + self.link = "https://turbobit.net%s" % m.group(1) + + def solve_captcha(self): + action, inputs = self.parse_html_form("id='form-captcha'") + if inputs is None: + self.fail(_("Captcha form not found")) - try: - out = self.js.eval(self.jscode) - self.logDebug("URL", self.js.engine, out) - if out.startswith('/download/'): - return "http://turbobit.net%s" % out.strip() - except Exception, e: - self.logError(e) else: - if self.retries >= 2: - # retry with updated js - self.delStorage("rtUpdate") - self.retry() - - def decrypt(self, data): - cipher = ARC4.new(hexlify('E\x15\xa1\x9e\xa3M\xa0\xc6\xa0\x84\xb6H\x83\xa8o\xa0')) - return unhexlify(cipher.encrypt(unhexlify(data))) - - def getLocalTimeString(self): - lt = time.localtime() - tz = time.altzone if lt.tm_isdst else time.timezone - return "%s GMT%+03d%02d" % (time.strftime("%a %b %d %Y %H:%M:%S", lt), -tz // 3600, tz % 3600) - - def handlePremium(self): - self.logDebug("Premium download as user %s" % self.user) - self.html = self.load(self.pyfile.url) # Useless in 0.5 - self.downloadFile() - - def downloadFile(self): - found = re.search(self.DOWNLOAD_URL_PATTERN, self.html) - if not found: - self.parseError("download link") - self.url = "http://turbobit.net" + found.group('url') - self.logDebug(self.url) - self.download(self.url) - - -getInfo = create_getInfo(TurbobitNet) + recaptcha = ReCaptcha(self.pyfile) + captcha_key = recaptcha.detect_key() + if captcha_key: + self.captcha = recaptcha + response = recaptcha.challenge(captcha_key) + inputs["g-recaptcha-response"] = response + + else: + hcaptcha = HCaptcha(self.pyfile) + captcha_key = hcaptcha.detect_key() + if captcha_key: + self.captcha = hcaptcha + response = hcaptcha.challenge(captcha_key) + inputs["g-recaptcha-response"] = inputs["h-captcha-response"] = response + + if captcha_key: + self.data = self.load(self.free_url, post=inputs, ref=self.free_url) + + else: + self.fail(_("Could not detect captcha type")) + + def handle_premium(self, pyfile): + m = re.search(self.LINK_PREMIUM_PATTERN, self.data) + if m is not None: + self.link = m.group(1) diff --git a/module/plugins/hoster/TurbouploadCom.py b/module/plugins/hoster/TurbouploadCom.py deleted file mode 100644 index 1c60c2c876..0000000000 --- a/module/plugins/hoster/TurbouploadCom.py +++ /dev/null @@ -1,32 +0,0 @@ -# -*- 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: zoidberg -""" - -from module.plugins.internal.DeadHoster import DeadHoster, create_getInfo - - -class TurbouploadCom(DeadHoster): - __name__ = "TurbouploadCom" - __type__ = "hoster" - __pattern__ = r"http://(?:\w*\.)?turboupload.com/(\w+).*" - __version__ = "0.03" - __description__ = """turboupload.com""" - __author_name__ = ("zoidberg") - __author_mail__ = ("zoidberg@mujmail.cz") - - -getInfo = create_getInfo(TurbouploadCom) diff --git a/module/plugins/hoster/TusfilesNet.py b/module/plugins/hoster/TusfilesNet.py index 4db551ee42..a1c4009009 100644 --- a/module/plugins/hoster/TusfilesNet.py +++ b/module/plugins/hoster/TusfilesNet.py @@ -1,29 +1,38 @@ # -*- coding: utf-8 -*- -from module.plugins.hoster.XFileSharingPro import XFileSharingPro, create_getInfo +from ..internal.XFSHoster import XFSHoster -class TusfilesNet(XFileSharingPro): + +class TusfilesNet(XFSHoster): __name__ = "TusfilesNet" __type__ = "hoster" - __pattern__ = r"http://(?:www\.)?tusfiles\.net/(?P<ID>[a-zA-Z0-9]{12})" - __version__ = "0.02" + __version__ = "0.20" + __status__ = "testing" + + __pattern__ = r'https?://(?:www\.)?tusfiles\.(?:net|com)/\w{12}' + __config__ = [("activated", "bool", "Activated", True), + ("use_premium", "bool", "Use premium account if available", True), + ("fallback", "bool", + "Fallback to free download if premium fails", True), + ("chk_filesize", "bool", "Check file size", True), + ("max_wait", "int", "Reconnect if waiting time is greater than minutes", 10)] + __description__ = """Tusfiles.net hoster plugin""" - __author_name__ = ("stickell", "Walter Purcaro") - __author_mail__ = ("l.stickell@yahoo.it", "vuolter@gmail.com") + __license__ = "GPLv3" + __authors__ = [("Walter Purcaro", "vuolter@gmail.com"), + ("guidobelix", "guidobelix@hotmail.it"), + ("GammaC0de", "nitzo2001[AT]yahoo[DOT]com")] - FILE_INFO_PATTERN = r'<li>(?P<N>[^<]+)</li>\s+<li><b>Size:</b> <small>(?P<S>[\d.]+) (?P<U>\w+)</small></li>' - FILE_OFFLINE_PATTERN = r'The file you were looking for could not be found' - HOSTER_NAME = "tusfiles.net" + PLUGIN_DOMAIN = "tusfiles.com" + URL_REPLACEMENTS = [(r"tusfiles\.net", "tusfiles.com")] - def setup(self): - self.chunkLimit = 1 - self.resumeDownload = self.multiDL = True - if self.premium: - self.limitDL = 5 - elif self.account: - self.limitDL = 3 - else: - self.limitDL = 2 + NAME_PATTERN = r'fa-file-o"></i>\s*(?P<N>.+) <' + SIZE_PATTERN = r"<b>\((?P<S>[\d.,]+) (?P<U>[\w^_]+)\)</b></small>" + OFFLINE_PATTERN = r"The file you are trying to download is no longer available!" -getInfo = create_getInfo(TusfilesNet) + def setup(self): + self.resume_download = True + self.chunk_limit = 1 + self.multi_dl = True + self.limitDL = 2 diff --git a/module/plugins/hoster/TwoSharedCom.py b/module/plugins/hoster/TwoSharedCom.py index 5d1cd835b0..7af4e33135 100644 --- a/module/plugins/hoster/TwoSharedCom.py +++ b/module/plugins/hoster/TwoSharedCom.py @@ -1,36 +1,33 @@ -#!/usr/bin/env python # -*- coding: utf-8 -*- -import re -from module.plugins.internal.SimpleHoster import SimpleHoster, create_getInfo +from ..internal.SimpleHoster import SimpleHoster class TwoSharedCom(SimpleHoster): __name__ = "TwoSharedCom" __type__ = "hoster" - __pattern__ = r"http://[\w\.]*?2shared.com/(account/)?(download|get|file|document|photo|video|audio)/.*" - __version__ = "0.11" - __description__ = """2Shared Download Hoster""" - __author_name__ = ("zoidberg") - __author_mail__ = ("zoidberg@mujmail.cz") + __version__ = "0.19" + __status__ = "testing" - FILE_NAME_PATTERN = r'<h1>(?P<N>.*)</h1>' - FILE_SIZE_PATTERN = r'<span class="dtitle">File size:</span>\s*(?P<S>[0-9,.]+) (?P<U>[kKMG])i?B' - FILE_OFFLINE_PATTERN = r'The file link that you requested is not valid\.|This file was deleted\.' - DOWNLOAD_URL_PATTERN = r"window.location ='([^']+)';" + __pattern__ = r'http://(?:www\.)?2shared\.com/(account/)?(download|get|file|document|photo|video|audio)/.+' + __config__ = [("activated", "bool", "Activated", True), + ("use_premium", "bool", "Use premium account if available", True), + ("fallback", "bool", + "Fallback to free download if premium fails", True), + ("chk_filesize", "bool", "Check file size", True), + ("max_wait", "int", "Reconnect if waiting time is greater than minutes", 10)] - def setup(self): - self.resumeDownload = self.multiDL = True - - def handleFree(self): - found = re.search(self.DOWNLOAD_URL_PATTERN, self.html) - if not found: - self.parseError('Download link') - link = found.group(1) - self.logDebug("Download URL %s" % link) + __description__ = """2Shared.com hoster plugin""" + __license__ = "GPLv3" + __authors__ = [("zoidberg", "zoidberg@mujmail.cz")] - self.download(link) + NAME_PATTERN = r'<h1>(?P<N>.*)</h1>' + SIZE_PATTERN = r'<span class="dtitle">File size:</span>\s*(?P<S>[\d.,]+) (?P<U>[\w^_]+)' + OFFLINE_PATTERN = r'The file link that you requested is not valid\.|This file was deleted\.' + LINK_FREE_PATTERN = r'window.location =\'(.+?)\';' -getInfo = create_getInfo(TwoSharedCom) + def setup(self): + self.resume_download = True + self.multiDL = True diff --git a/module/plugins/hoster/TwojlimitPl.py b/module/plugins/hoster/TwojlimitPl.py new file mode 100644 index 0000000000..da7063d94d --- /dev/null +++ b/module/plugins/hoster/TwojlimitPl.py @@ -0,0 +1,98 @@ +# -*- coding: utf-8 -*- + +from ..internal.misc import json +from ..internal.MultiHoster import MultiHoster + + +class TwojlimitPl(MultiHoster): + __name__ = "TwojlimitPl" + __type__ = "hoster" + __version__ = "0.02" + __status__ = "testing" + + __pattern__ = r'^unmatchable$' + __config__ = [("activated", "bool", "Activated", True), + ("use_premium", "bool", "Use premium account if available", True), + ("fallback", "bool", "Fallback to free download if premium fails", False), + ("chk_filesize", "bool", "Check file size", True), + ("max_wait", "int", "Reconnect if waiting time is greater than minutes", 10), + ("revert_failed", "bool", "Revert to standard download if fails", True)] + + __description__ = """Twojlimit.pl multi-hoster plugin""" + __license__ = "GPLv3" + __authors__ = [("synweap15", "pawel@twojlimit.pl")] + + API_URL = "https://crypt.twojlimit.pl" + + API_QUERY = {'site': "newtl", + 'output': "json", + 'username': "", + 'password': "", + 'url': ""} + + ERROR_CODES = {0: "Incorrect login credentials", + 1: "Not enough transfer to download - top-up your account", + 2: "Incorrect / dead link", + 3: "Error connecting to hosting, try again later", + 9: "Premium account has expired", + 15: "Hosting no longer supported", + 80: "Too many incorrect login attempts, account blocked for 24h"} + + def setup(self): + self.multiDL = True + self.resume_download = True + self.chunk_limit = -1 + + def handle_free(self, pyfile): + try: + data = self.run_file_query(pyfile.url, 'fileinfo') + + except Exception, e: + self.log_error(e) + self.temp_offline("Query error #1") + + try: + json_data = json.loads(data) + + except Exception: + self.temp_offline("Data not found") + + if "errno" in json_data: + if json_data['errno'] in self.ERROR_CODES: + #: Error code in known + self.fail(self.ERROR_CODES[json_data['errno']]) + + else: + #: Error code isn't yet added to plugin + self.fail(json_data['errstring'] or + _("Unknown error (code: %s)") % json_data['errno']) + + if "sdownload" in json_data: + if json_data['sdownload'] == "1": + self.fail(_("Download from %s is possible only using TwojLimit.pl website directly") % + json_data['hosting']) + + pyfile.name = json_data['filename'] + pyfile.size = json_data['filesize'] + + try: + self.download(self.run_file_query(pyfile.url, 'filedownload')) + + except Exception, e: + self.log_error(e) + self.temp_offline("Query error #2") + + def run_file_query(self, url, mode=None): + query = self.API_QUERY.copy() + + query['username'] = self.account.user + query['password'] = self.account.info['data']['hash_password'] + query['url'] = url + + if mode == "fileinfo": + query['check'] = 2 + query['loc'] = 1 + + return self.load(self.API_URL, + post=query, + redirect=20) diff --git a/module/plugins/hoster/UlozTo.py b/module/plugins/hoster/UlozTo.py index 5dc6f7f005..e0b732272b 100644 --- a/module/plugins/hoster/UlozTo.py +++ b/module/plugins/hoster/UlozTo.py @@ -1,154 +1,268 @@ # -*- 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. +import re +import urlparse - 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. +import pycurl - You should have received a copy of the GNU General Public License - along with this program; if not, see <http://www.gnu.org/licenses/>. +from ..captcha.ReCaptcha import ReCaptcha +from ..internal.misc import json, parse_name, timestamp +from ..internal.SimpleHoster import SimpleHoster - @author: zoidberg -""" -import re -import time -from module.plugins.internal.SimpleHoster import SimpleHoster, create_getInfo -from module.common.json_layer import json_loads +def convert_decimal_prefix(m): + #: Decimal prefixes used in filesize and traffic + return ("%%.%df" % {'k': 3, 'M': 6, 'G': 9}[ + m.group(2)] % float(m.group(1))).replace('.', '') -def convertDecimalPrefix(m): - # decimal prefixes used in filesize and traffic - return ("%%.%df" % {'k': 3, 'M': 6, 'G': 9}[m.group(2)] % float(m.group(1))).replace('.', '') class UlozTo(SimpleHoster): __name__ = "UlozTo" __type__ = "hoster" - __pattern__ = r"http://(\w*\.)?(uloz\.to|ulozto\.(cz|sk|net)|bagruj.cz|zachowajto.pl)/(?:live/)?(?P<id>\w+/[^/?]*)" - __version__ = "0.95" - __description__ = """uloz.to""" - __author_name__ = ("zoidberg") - - FILE_NAME_PATTERN = r'<a href="#download" class="jsShowDownload">(?P<N>[^<]+)</a>' - FILE_SIZE_PATTERN = r'<span id="fileSize">.*?(?P<S>[0-9.]+\s[kMG]?B)</span>' - FILE_INFO_PATTERN = r'<p>File <strong>(?P<N>[^<]+)</strong> is password protected</p>' - FILE_OFFLINE_PATTERN = r'<title>404 - Page not found|

    File (has been deleted|was banned)

    ' - FILE_SIZE_REPLACEMENTS = [('([0-9.]+)\s([kMG])B', convertDecimalPrefix)] - FILE_URL_REPLACEMENTS = [(r"(?<=http://)([^/]+)", "www.ulozto.net")] + __version__ = "1.52" + __status__ = "testing" + + __pattern__ = r'https?://(?:www\.)?(uloz\.to|ulozto\.(cz|sk|net)|bagruj\.cz|zachowajto\.pl|pornfile\.cz|pinkfile\.cz)/(?:live/)?(?P[!\w]+/[^/?]*)' + + __config__ = [("activated", "bool", "Activated", True), + ("use_premium", "bool", "Use premium account if available", True), + ("fallback", "bool", + "Fallback to free download if premium fails", True), + ("chk_filesize", "bool", "Check file size", True), + ("captcha", "Image;Sound", "Captcha recognition", "Image"), + ("max_wait", "int", "Reconnect if waiting time is greater than minutes", 10)] + + __description__ = """Uloz.to hoster plugin""" + __license__ = "GPLv3" + __authors__ = [("zoidberg", "zoidberg@mujmail.cz"), + ("ondrej", "git@ondrej.it"), + ("astran", "martin.hromadko@gmail.com")] + + NAME_PATTERN = r'(

    File |)(?P<N>.+?)(<| \|)' + SIZE_PATTERN = r'<span id="fileSize">.*?(?P<S>[\d.,]+\s[kMG]?B)</span>' + OFFLINE_PATTERN = r'<title>404 - Page not found|

    File (has been deleted|was banned)

    ' + TEMP_OFFLINE_PATTERN = r"500 - Internal Server Error" + URL_REPLACEMENTS = [("http://", "https://"), + (r'(uloz\.to|ulozto\.(cz|sk|net)|bagruj\.cz|zachowajto\.pl|pornfile\.cz|pinkfile\.cz)', "ulozto.net")] + + SIZE_REPLACEMENTS = [(r'([\d.]+)\s([kMG])B', convert_decimal_prefix)] + + CHECK_TRAFFIC = True + + ADULT_PATTERN = r'PINKfile.cz' PASSWD_PATTERN = r'
    ' - VIPLINK_PATTERN = r'' - FREE_URL_PATTERN = r'
    ' + TOKEN_PATTERN = r' 1.4.2016') + + inputs.update({'do': inputs['do'], '_token_': inputs['_token_'], + 'ts': inputs['ts'], 'cid': inputs['cid'], + 'adi': inputs['adi'], 'sign_a': inputs['sign_a'], 'sign': inputs['sign']}) + + else: + self.error(_("CAPTCHA form changed")) - data = json_loads(xapca) - captcha_value = self.decryptCaptcha(str(data['image'])) - self.logDebug('CAPTCHA HASH: ' + data['hash'] + ', CAPTCHA SALT: ' + str(data['salt']) + ', CAPTCHA VALUE: ' + captcha_value) + jsvars = self.get_json_response(domain + action, inputs) - inputs.update({'timestamp': data['timestamp'], 'salt': data['salt'], 'hash': data['hash'], 'captcha_value': captcha_value}) else: - self.parseError("CAPTCHA form changed") + jsvars = json.loads(self.data) + redirect = jsvars.get('redirectDialogContent') + if redirect: + self.data = self.load(domain + redirect) + action, inputs = self.parse_html_form('id="frm-rateLimitingCaptcha-form') + if inputs is None: + self.error(_("Captcha form not found")) - self.multiDL = True - self.download("http://www.ulozto.net" + action, post=inputs, cookies=True, disposition=True) - - def handlePremium(self): - self.download(self.pyfile.url + "?do=directDownload", disposition=True) - #parsed_url = self.findDownloadURL(premium=True) - #self.download(parsed_url, post={"download": "Download"}) - - def findDownloadURL(self, premium=False): - msg = "%s link" % ("Premium" if premium else "Free") - found = re.search(self.PREMIUM_URL_PATTERN if premium else self.FREE_URL_PATTERN, self.html) - if not found: - self.parseError(msg) - parsed_url = "http://www.ulozto.net" + found.group(1) - self.logDebug("%s: %s" % (msg, parsed_url)) - return parsed_url - - def doCheckDownload(self): - check = self.checkDownload({ - "wrong_captcha": re.compile(r'
      \s*
    • Error rewriting the text.
    • '), - "offline": re.compile(self.FILE_OFFLINE_PATTERN), - "passwd": self.PASSWD_PATTERN, - "server_error": 'src="http://img.ulozto.cz/error403/vykricnik.jpg"', # paralell dl, server overload etc. - "not_found": "Ulož.to" + recaptcha = ReCaptcha(pyfile) + + captcha_key = recaptcha.detect_key() + if captcha_key is None: + self.error(_("ReCaptcha key not found")) + + self.captcha = recaptcha + response = recaptcha.challenge(captcha_key) + + inputs['g-recaptcha-response'] = response + + jsvars = self.get_json_response(domain + action, inputs) + if 'slowDownloadLink' not in jsvars: + self.retry_captcha() + + self.download(jsvars['slowDownloadLink']) + + def handle_premium(self, pyfile): + m = re.search("/file/(.+)/", pyfile.url) + if not m: + self.error(_("Premium link not found")) + + premium_url = urlparse.urljoin("https://ulozto.net/quickDownload/", m.group(1)) + self.download(premium_url) + + def check_errors(self): + if self.PASSWD_PATTERN in self.data: + password = self.get_password() + + if password: + self.log_info(_("Password protected link, trying ") + password) + self.data = self.load(self.pyfile.url, + get={'do': "passwordProtectedForm-submit"}, + post={'password': password, + 'password_send': 'Send'}) + + if self.PASSWD_PATTERN in self.data: + self.fail(_("Wrong password")) + else: + self.fail(_("No password found")) + + if re.search(self.VIPLINK_PATTERN, self.data): + self.data = self.load(self.pyfile.url, get={'disclaimer': "1"}) + + return SimpleHoster.check_errors(self) + + def check_download(self): + check = self.scan_download({ + 'wrong_captcha': ">An error ocurred while verifying the user", + 'offline': re.compile(self.OFFLINE_PATTERN), + 'passwd': self.PASSWD_PATTERN, + #: Paralell dl, server overload etc. + 'server_error': "

      Z Tvého počítače se již stahuje", + 'not_found': "Ulož.to" }) if check == "wrong_captcha": - #self.delStorage("captcha_id") - #self.delStorage("captcha_text") - self.invalidCaptcha() - self.retry(reason="Wrong captcha code") + self.captcha.invalid() + self.retry(msg=_("Wrong captcha code")) + elif check == "offline": self.offline() + elif check == "passwd": - self.fail("Wrong password") + self.fail(_("Wrong password")) + elif check == "server_error": - self.logError("Server error, try downloading later") + self.log_error(_("Server error, try downloading later")) self.multiDL = False - self.setWait(3600, True) - self.wait() + self.wait(1 * 60 * 60, True) self.retry() + elif check == "not_found": - self.fail("Server error - file not downloadable") + self.fail(_("Server error, file not downloadable")) + + return SimpleHoster.check_download(self) + + def get_json_response(self, url, inputs): + self.req.http.c.setopt(pycurl.HTTPHEADER, ["X-Requested-With: XMLHttpRequest"]) + + res = self.load(url, post=inputs, ref=self.pyfile.url) + self.req.http.c.setopt(pycurl.HTTPHEADER, ["X-Requested-With:"]) + + if not res.startswith('{'): + self.retry(msg=_("Something went wrong")) + jsonres = json.loads(res) + if 'formErrorContent' in jsonres: + self.retry_captcha() -getInfo = create_getInfo(UlozTo) + self.log_debug(url, res) + return jsonres diff --git a/module/plugins/hoster/UloziskoSk.py b/module/plugins/hoster/UloziskoSk.py index 7060dff65a..db0f698473 100644 --- a/module/plugins/hoster/UloziskoSk.py +++ b/module/plugins/hoster/UloziskoSk.py @@ -1,80 +1,72 @@ # -*- 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 . - - @author: zoidberg -""" import re -from module.plugins.internal.SimpleHoster import SimpleHoster, create_getInfo, PluginParseError +import urlparse + +from ..internal.SimpleHoster import SimpleHoster class UloziskoSk(SimpleHoster): __name__ = "UloziskoSk" __type__ = "hoster" - __pattern__ = r"http://(\w*\.)?ulozisko.sk/.*" - __version__ = "0.23" - __description__ = """Ulozisko.sk""" - __author_name__ = ("zoidberg") - - URL_PATTERN = r'' - ID_PATTERN = r'' - FILE_NAME_PATTERN = r'
      (?P[^<]+)
      ' - FILE_SIZE_PATTERN = ur'Veľkosť súboru: (?P[0-9.]+) (?P[kKMG])i?B
      ' - CAPTCHA_PATTERN = r'' - FILE_OFFLINE_PATTERN = ur'Zadaný súbor neexistuje z jedného z nasledujúcich dôvodov:' - IMG_PATTERN = ur'PRE ZVÄČŠENIE KLIKNITE NA OBRÁZOK
      ' + __version__ = "0.31" + __status__ = "testing" + + __pattern__ = r'http://(?:www\.)?ulozisko\.sk/.+' + __config__ = [("activated", "bool", "Activated", True), + ("use_premium", "bool", "Use premium account if available", True), + ("fallback", "bool", + "Fallback to free download if premium fails", True), + ("chk_filesize", "bool", "Check file size", True), + ("max_wait", "int", "Reconnect if waiting time is greater than minutes", 10)] + + __description__ = """Ulozisko.sk hoster plugin""" + __license__ = "GPLv3" + __authors__ = [("zoidberg", "zoidberg@mujmail.cz")] + + NAME_PATTERN = r'
      (?P.+?)
      ' + SIZE_PATTERN = ur'Veľkosť súboru: (?P[\d.,]+) (?P[\w^_]+)
      ' + OFFLINE_PATTERN = ur'Zadaný súbor neexistuje z jedného z nasledujúcich dôvodov:' + + LINK_FREE_PATTERN = r'' + ID_PATTERN = r'' + CAPTCHA_PATTERN = r'' + IMG_PATTERN = ur'PRE ZVÄČŠENIE KLIKNITE NA OBRÁZOK
      ' def process(self, pyfile): - self.html = self.load(pyfile.url, decode=True) - self.getFileInfo() + self.data = self.load(pyfile.url) + self.get_fileInfo() - found = re.search(self.IMG_PATTERN, self.html) - if found: - url = "http://ulozisko.sk" + found.group(1) - self.download(url) + m = re.search(self.IMG_PATTERN, self.data) + if m is not None: + self.link = "http://ulozisko.sk" + m.group(1) else: - self.handleFree() - - def handleFree(self): - found = re.search(self.URL_PATTERN, self.html) - if found is None: - raise PluginParseError('URL') - parsed_url = 'http://www.ulozisko.sk' + found.group(1) - - found = re.search(self.ID_PATTERN, self.html) - if found is None: - raise PluginParseError('ID') - id = found.group(1) + self.handle_free(pyfile) - self.logDebug('URL:' + parsed_url + ' ID:' + id) + def handle_free(self, pyfile): + m = re.search(self.LINK_FREE_PATTERN, self.data) + if m is None: + self.error(_("LINK_FREE_PATTERN not found")) + parsed_url = 'http://www.ulozisko.sk' + m.group(1) - found = re.search(self.CAPTCHA_PATTERN, self.html) - if found is None: - raise PluginParseError('CAPTCHA') - captcha_url = 'http://www.ulozisko.sk' + found.group(1) + m = re.search(self.ID_PATTERN, self.data) + if m is None: + self.error(_("ID_PATTERN not found")) + id = m.group(1) - captcha = self.decryptCaptcha(captcha_url, cookies=True) + self.log_debug("URL:" + parsed_url + ' ID:' + id) - self.logDebug('CAPTCHA_URL:' + captcha_url + ' CAPTCHA:' + captcha) + m = re.search(self.CAPTCHA_PATTERN, self.data) + if m is None: + self.error(_("CAPTCHA_PATTERN not found")) - self.download(parsed_url, post={ - "antispam": captcha, - "id": id, - "name": self.pyfile.name, - "but": "++++STIAHNI+S%DABOR++++" - }) + captcha_url = urlparse.urljoin("http://www.ulozisko.sk/", m.group(1)) + captcha = self.captcha.decrypt(captcha_url, cookies=True) + self.log_debug("CAPTCHA_URL:" + captcha_url + ' CAPTCHA:' + captcha) -getInfo = create_getInfo(UloziskoSk) + self.download(parsed_url, + post={'antispam': captcha, + 'id': id, + 'name': pyfile.name, + 'but': "++++STIAHNI+S%DABOR++++"}) diff --git a/module/plugins/hoster/UnibytesCom.py b/module/plugins/hoster/UnibytesCom.py index d13f01ef31..72f8d79e23 100644 --- a/module/plugins/hoster/UnibytesCom.py +++ b/module/plugins/hoster/UnibytesCom.py @@ -1,82 +1,71 @@ # -*- 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 . - - @author: zoidberg -""" import re -from pycurl import FOLLOWLOCATION -from module.plugins.internal.SimpleHoster import SimpleHoster, create_getInfo +import urlparse + +from ..internal.SimpleHoster import SimpleHoster class UnibytesCom(SimpleHoster): __name__ = "UnibytesCom" __type__ = "hoster" - __pattern__ = r"http://(www\.)?unibytes\.com/[a-zA-Z0-9-._ ]{11}B" - __version__ = "0.1" - __description__ = """UniBytes.com""" - __author_name__ = ("zoidberg") + __version__ = "0.21" + __status__ = "testing" + + __pattern__ = r'https?://(?:www\.)?unibytes\.com/[\w\- .]{11}B' + __config__ = [("activated", "bool", "Activated", True), + ("use_premium", "bool", "Use premium account if available", True), + ("fallback", "bool", + "Fallback to free download if premium fails", True), + ("chk_filesize", "bool", "Check file size", True), + ("max_wait", "int", "Reconnect if waiting time is greater than minutes", 10)] - FILE_INFO_PATTERN = r']*?id="fileName"[^>]*>(?P[^>]+)\s*\((?P\d.*?)\)' - DOMAIN = 'http://www.unibytes.com' + __description__ = """UniBytes.com hoster plugin""" + __license__ = "GPLv3" + __authors__ = [("zoidberg", "zoidberg@mujmail.cz")] + + PLUGIN_DOMAIN = "unibytes.com" + + INFO_PATTERN = r']*?id="fileName".*?>(?P.+?)\s*\((?P\d.*?)\)' WAIT_PATTERN = r'Wait for (\d+) sec' - DOWNLOAD_LINK_PATTERN = r'Download' - - def handleFree(self): - action, post_data = self.parseHtmlForm('id="startForm"') - self.req.http.c.setopt(FOLLOWLOCATION, 0) - - for i in range(8): - self.logDebug(action, post_data) - self.html = self.load(self.DOMAIN + action, post=post_data) - - found = re.search(r'location:\s*(\S+)', self.req.http.header, re.I) - if found: - url = found.group(1) - break - - if '>Somebody else is already downloading using your IP-address<' in self.html: - self.setWait(600, True) - self.wait() - self.retry() - - if post_data['step'] == 'last': - found = re.search(self.DOWNLOAD_LINK_PATTERN, self.html) - if found: - url = found.group(1) - self.correctCaptcha() + LINK_FREE_PATTERN = r'Download' + + def handle_free(self, pyfile): + domain = "http://www.%s/" % self.PLUGIN_DOMAIN + action, post_data = self.parse_html_form('id="startForm"') + + for _i in range(3): + self.log_debug(action, post_data) + self.data = self.load(urlparse.urljoin(domain, action), + post=post_data, + redirect=False) + + location = self.last_header.get('location') + if location: + self.link = location + return + + if '>Somebody else is already downloading using your IP-address<' in self.data: + self.wait(10 * 60, True) + self.restart() + + if post_data['step'] == "last": + m = re.search(self.LINK_FREE_PATTERN, self.data) + if m is not None: + self.captcha.correct() + self.link = m.group(1) break else: - self.invalidCaptcha() + self.retry_captcha() last_step = post_data['step'] - action, post_data = self.parseHtmlForm('id="stepForm"') - - if last_step == 'timer': - found = re.search(self.WAIT_PATTERN, self.html) - self.setWait(int(found.group(1)) if found else 60, False) - self.wait() - elif last_step in ('captcha', 'last'): - post_data['captcha'] = self.decryptCaptcha(self.DOMAIN + '/captcha.jpg') - else: - self.fail("No valid captcha code entered") - - self.logDebug('Download link: ' + url) - self.req.http.c.setopt(FOLLOWLOCATION, 1) - self.download(url) + action, post_data = self.parse_html_form('id="stepForm"') + if last_step == "timer": + m = re.search(self.WAIT_PATTERN, self.data) + self.wait(m.group(1) if m else 60, False) -getInfo = create_getInfo(UnibytesCom) + elif last_step in ("captcha", "last"): + post_data['captcha'] = self.captcha.decrypt( + urlparse.urljoin(domain, "captcha.jpg")) diff --git a/module/plugins/hoster/UnrestrictLi.py b/module/plugins/hoster/UnrestrictLi.py deleted file mode 100644 index a3fe5ff5d0..0000000000 --- a/module/plugins/hoster/UnrestrictLi.py +++ /dev/null @@ -1,101 +0,0 @@ -# -*- coding: utf-8 -*- - -############################################################################ -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU Affero 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 Affero General Public License for more details. # -# # -# You should have received a copy of the GNU Affero General Public License # -# along with this program. If not, see . # -############################################################################ - -import re -from datetime import datetime, timedelta - -from module.plugins.Hoster import Hoster -from module.common.json_layer import json_loads - - -def secondsToMidnight(): - # Seconds until 00:10 GMT+2 - now = datetime.utcnow() + timedelta(hours=2) - if now.hour is 0 and now.minute < 10: - midnight = now - else: - midnight = now + timedelta(days=1) - midnight = midnight.replace(hour=0, minute=10, second=0, microsecond=0) - return int((midnight - now).total_seconds()) - - -class UnrestrictLi(Hoster): - __name__ = "UnrestrictLi" - __version__ = "0.11" - __type__ = "hoster" - __pattern__ = r"https?://.*(unrestrict|unr)\.li" - __description__ = """Unrestrict.li hoster plugin""" - __author_name__ = ("stickell") - __author_mail__ = ("l.stickell@yahoo.it") - - def setup(self): - self.chunkLimit = 16 - self.resumeDownload = True - - def process(self, pyfile): - if re.match(self.__pattern__, pyfile.url): - new_url = pyfile.url - elif not self.account: - self.logError(_("Please enter your %s account or deactivate this plugin") % "Unrestrict.li") - self.fail("No Unrestrict.li account provided") - else: - self.logDebug("Old URL: %s" % pyfile.url) - for i in xrange(5): - page = self.req.load('https://unrestrict.li/unrestrict.php', - post={'link': pyfile.url, 'domain': 'long'}) - self.logDebug("JSON data: " + page) - if page != '': - break - else: - self.logInfo("Unable to get API data, waiting 1 minute and retry") - self.retry(5, 60, "Unable to get API data") - - if 'Expired session' in page or ("You are not allowed to " - "download from this host" in page and self.premium): - self.account.relogin(self.user) - self.retry() - elif "File offline" in page: - self.offline() - elif "You are not allowed to download from this host" in page: - self.fail("You are not allowed to download from this host") - elif "You have reached your daily limit for this host" in page: - self.logInfo("Reached daily limit for this host. Waiting until 00:10 GMT+2") - self.retry(5, secondsToMidnight(), "Daily limit for this host reached") - elif "ERROR_HOSTER_TEMPORARILY_UNAVAILABLE" in page: - self.logInfo("Hoster temporarily unavailable, waiting 1 minute and retry") - self.retry(5, 60, "Hoster is temporarily unavailable") - page = json_loads(page) - new_url = page.keys()[0] - self.api_data = page[new_url] - - if new_url != pyfile.url: - self.logDebug("New URL: " + new_url) - - if hasattr(self, 'api_data'): - self.setNameSize() - - self.download(new_url, disposition=True) - - if self.getConfig("history"): - self.load("https://unrestrict.li/history/&delete=all") - self.logInfo("Download history deleted") - - def setNameSize(self): - if 'name' in self.api_data: - self.pyfile.name = self.api_data['name'] - if 'size' in self.api_data: - self.pyfile.size = self.api_data['size'] diff --git a/module/plugins/hoster/UpfileVn.py b/module/plugins/hoster/UpfileVn.py new file mode 100644 index 0000000000..119442f34e --- /dev/null +++ b/module/plugins/hoster/UpfileVn.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- + +import hashlib + +from ..internal.misc import json +from ..internal.SimpleHoster import SimpleHoster + + +class UpfileVn(SimpleHoster): + __name__ = "UpfileVn" + __type__ = "hoster" + __version__ = "0.01" + __status__ = "testing" + + __pattern__ = r'http://(?:www\.)?upfile\.vn/(?P.+?)/.+?\.html' + __config__ = [("activated", "bool", "Activated", True), + ("use_premium", "bool", "Use premium account if available", True), + ("fallback", "bool", "Fallback to free download if premium fails", True), + ("chk_filesize", "bool", "Check file size", True), + ("max_wait", "int", "Reconnect if waiting time is greater than minutes", 10)] + + __description__ = """Upfile.Vn hoster plugin""" + __license__ = "GPLv3" + __authors__ = [("GammaC0de", "nitzo2001[AT]yahoo[DOT]com")] + + INFO_PATTERN = r'

      (?P.+?) \((?P[\d.,]+)(?P[\w^_]+)\)

      ' + + WAIT_PATTERN = r'data-count=\'(\d+)\'' + + def handle_free(self, pyfile): + token = hashlib.sha256(self.info['pattern']['ID'] + "7891").hexdigest().upper() + + self.data = self.load(pyfile.url, + post={'Token': token}) + + json_data = json.loads(self.data) + + if json_data['Status'] is True: + self.link = json_data['Link'] + + else: + self.log_debug("Download failed: %s" % json_data) diff --git a/module/plugins/hoster/UpleaCom.py b/module/plugins/hoster/UpleaCom.py new file mode 100644 index 0000000000..db29b88afd --- /dev/null +++ b/module/plugins/hoster/UpleaCom.py @@ -0,0 +1,84 @@ +# -*- coding: utf-8 -*- + +import re +import urlparse + +from ..internal.SimpleHoster import SimpleHoster + + +def decode_cloudflare_email(value): + email = "" + + key = int(value[:2], 16) + for i in range(2, len(value), 2): + email += chr(int(value[i:i + 2], 16) ^ key) + + return email + + +class UpleaCom(SimpleHoster): + __name__ = "UpleaCom" + __type__ = "hoster" + __version__ = "0.21" + __status__ = "testing" + + __pattern__ = r'https?://(?:www\.)?uplea\.com/dl/\w{15}' + __config__ = [("activated", "bool", "Activated", True), + ("use_premium", "bool", "Use premium account if available", True), + ("fallback", "bool", + "Fallback to free download if premium fails", True), + ("chk_filesize", "bool", "Check file size", True), + ("max_wait", "int", "Reconnect if waiting time is greater than minutes", 10)] + + __description__ = """Uplea.com hoster plugin""" + __license__ = "GPLv3" + __authors__ = [("Redleon", None), + ("GammaC0de", None)] + + PLUGIN_DOMAIN = "uplea.com" + + SIZE_REPLACEMENTS = [('ko', 'KB'), ('mo', 'MB'), + ('go', 'GB'), ('Ko', 'KB'), ('Mo', 'MB'), ('Go', 'GB')] + + NAME_PATTERN = r'(?P.+?)' + SIZE_PATTERN = r'(?P[\d.,]+) (?P[\w^_]+?)' + OFFLINE_PATTERN = r'>You followed an invalid or expired link' + + LINK_PATTERN = r'"(https?://\w+\.uplea\.com/anonym/.*?)"' + + PREMIUM_ONLY_PATTERN = r'You need to have a Premium subscription to download this file' + WAIT_PATTERN = r'timeText: ?(\d+),' + STEP_PATTERN = r'' + + NAME_REPLACEMENTS = [(r'([A-Za-z0-9]+)" - __version__ = "0.52" - __description__ = """UploadStation.Com File Download Hoster""" - __author_name__ = ("fragonib", "zoidberg") - __author_mail__ = ("fragonib[AT]yahoo[DOT]es", "zoidberg@mujmail.cz") - - -getInfo = create_getInfo(UploadStationCom) diff --git a/module/plugins/hoster/UploadedTo.py b/module/plugins/hoster/UploadedTo.py deleted file mode 100644 index 88a8edebb3..0000000000 --- a/module/plugins/hoster/UploadedTo.py +++ /dev/null @@ -1,231 +0,0 @@ -# -*- coding: utf-8 -*- - -# Test links (random.bin): -# http://ul.to/044yug9o -# http://ul.to/gzfhd0xs - -import re -from time import sleep - -from module.utils import html_unescape, parseFileSize - -from module.plugins.Hoster import Hoster -from module.network.RequestFactory import getURL -from module.plugins.Plugin import chunks -from module.plugins.internal.CaptchaService import ReCaptcha - -key = "bGhGMkllZXByd2VEZnU5Y2NXbHhYVlZ5cEE1bkEzRUw=".decode('base64') - - -def getID(url): - """ returns id from file url""" - m = re.match(UploadedTo.__pattern__, url) - return m.group('ID') - - -def getAPIData(urls): - post = {"apikey": key} - - idMap = {} - - for i, url in enumerate(urls): - id = getID(url) - post["id_%s" % i] = id - idMap[id] = url - - for i in xrange(5): - api = unicode(getURL("http://uploaded.net/api/filemultiple", post=post, decode=False), 'iso-8859-1') - if api != "can't find request": - break - else: - sleep(3) - - result = {} - - if api: - for line in api.splitlines(): - data = line.split(",", 4) - if data[1] in idMap: - result[data[1]] = (data[0], data[2], data[4], data[3], idMap[data[1]]) - - return result - - -def parseFileInfo(self, url='', html=''): - if not html and hasattr(self, "html"): - html = self.html - name, size, status, found, fileid = url, 0, 3, None, None - - if re.search(self.FILE_OFFLINE_PATTERN, html): - # File offline - status = 1 - else: - found = re.search(self.FILE_INFO_PATTERN, html) - if found: - name, fileid = html_unescape(found.group('N')), found.group('ID') - size = parseFileSize(found.group('S')) - status = 2 - - return name, size, status, fileid - - -def getInfo(urls): - for chunk in chunks(urls, 80): - result = [] - - api = getAPIData(chunk) - - for data in api.itervalues(): - if data[0] == "online": - result.append((html_unescape(data[2]), data[1], 2, data[4])) - - elif data[0] == "offline": - result.append((data[4], 0, 1, data[4])) - - yield result - - -class UploadedTo(Hoster): - __name__ = "UploadedTo" - __type__ = "hoster" - __pattern__ = r"https?://[\w\.-]*?(uploaded\.(to|net)|ul\.to)(/file/|/?\?id=|.*?&id=|/)(?P\w+)" - __version__ = "0.72" - __description__ = """Uploaded.net Download Hoster""" - __author_name__ = ("spoob", "mkaay", "zoidberg", "netpok", "stickell") - __author_mail__ = ("spoob@pyload.org", "mkaay@mkaay.de", "zoidberg@mujmail.cz", - "netpok@gmail.com", "l.stickell@yahoo.it") - - FILE_INFO_PATTERN = r'(?P[^<]+)  \s*]*>(?P[^<]+)' - FILE_OFFLINE_PATTERN = r'Error: 404' - DL_LIMIT_PATTERN = "You have reached the max. number of possible free downloads for this hour" - - def setup(self): - self.multiDL = self.resumeDownload = self.premium - self.chunkLimit = 1 # critical problems with more chunks - - self.fileID = getID(self.pyfile.url) - self.pyfile.url = "http://uploaded.net/file/%s" % self.fileID - - def process(self, pyfile): - self.load("http://uploaded.net/language/en", just_header=True) - - api = getAPIData([pyfile.url]) - - # TODO: fallback to parse from site, because api sometimes delivers wrong status codes - - if not api: - self.logWarning("No response for API call") - - self.html = unicode(self.load(pyfile.url, decode=False), 'iso-8859-1') - name, size, status, self.fileID = parseFileInfo(self) - self.logDebug(name, size, status, self.fileID) - if status == 1: - self.offline() - elif status == 2: - pyfile.name, pyfile.size = name, size - else: - self.fail('Parse error - file info') - elif api == 'Access denied': - self.fail(_("API key invalid")) - - else: - if self.fileID not in api: - self.offline() - - self.data = api[self.fileID] - if self.data[0] != "online": - self.offline() - - pyfile.name = html_unescape(self.data[2]) - - # self.pyfile.name = self.get_file_name() - - if self.premium: - self.handlePremium() - else: - self.handleFree() - - def handlePremium(self): - info = self.account.getAccountInfo(self.user, True) - self.logDebug("%(name)s: Use Premium Account (%(left)sGB left)" % {"name": self.__name__, - "left": info["trafficleft"] / 1024 / 1024}) - if int(self.data[1]) / 1024 > info["trafficleft"]: - self.logInfo(_("%s: Not enough traffic left" % self.__name__)) - self.account.empty(self.user) - self.resetAccount() - self.fail(_("Traffic exceeded")) - - header = self.load("http://uploaded.net/file/%s" % self.fileID, just_header=True) - if "location" in header: - #Direct download - print "Direct Download: " + header['location'] - self.download(header['location']) - else: - #Indirect download - self.html = self.load("http://uploaded.net/file/%s" % self.fileID) - found = re.search(r'
      (\d+) seconds", self.html) - if not found: - self.fail("File not downloadable for free users") - self.setWait(int(found.group(1))) - - js = self.load("http://uploaded.net/js/download.js", decode=True) - - challengeId = re.search(r'Recaptcha\.create\("([^"]+)', js) - - url = "http://uploaded.net/io/ticket/captcha/%s" % self.fileID - downloadURL = "" - - for i in range(5): - re_captcha = ReCaptcha(self) - challenge, result = re_captcha.challenge(challengeId.group(1)) - options = {"recaptcha_challenge_field": challenge, "recaptcha_response_field": result} - self.wait() - - result = self.load(url, post=options) - self.logDebug("result: %s" % result) - - if "limit-size" in result: - self.fail("File too big for free download") - elif "limit-slot" in result: # Temporary restriction so just wait a bit - self.setWait(30 * 60, True) - self.wait() - self.retry() - elif "limit-parallel" in result: - self.fail("Cannot download in parallel") - elif self.DL_LIMIT_PATTERN in result: # limit-dl - self.setWait(3 * 60 * 60, True) - self.wait() - self.retry() - elif 'err:"captcha"' in result: - self.logError("ul.net captcha is disabled") - self.invalidCaptcha() - elif "type:'download'" in result: - self.correctCaptcha() - downloadURL = re.search("url:'([^']+)", result).group(1) - break - else: - self.fail("Unknown error '%s'") - - if not downloadURL: - self.fail("No Download url retrieved/all captcha attempts failed") - - self.download(downloadURL, disposition=True) - check = self.checkDownload({"limit-dl": self.DL_LIMIT_PATTERN}) - if check == "limit-dl": - self.setWait(3 * 60 * 60, True) - self.wait() - self.retry() diff --git a/module/plugins/hoster/UploadgigCom.py b/module/plugins/hoster/UploadgigCom.py new file mode 100644 index 0000000000..277d144172 --- /dev/null +++ b/module/plugins/hoster/UploadgigCom.py @@ -0,0 +1,87 @@ +# -*- coding: utf-8 -*- + +import re + +from ..captcha.ReCaptcha import ReCaptcha +from ..internal.misc import json +from ..internal.SimpleHoster import SimpleHoster + + +class UploadgigCom(SimpleHoster): + __name__ = "UploadgigCom" + __type__ = "hoster" + __version__ = "0.10" + __status__ = "testing" + + __pattern__ = r'https?://(?:www\.)?uploadgig.com/file/download/\w+' + __config__ = [("activated", "bool", "Activated", True), + ("use_premium", "bool", "Use premium account if available", True), + ("fallback", "bool", + "Fallback to free download if premium fails", True), + ("chk_filesize", "bool", "Check file size", True), + ("max_wait", "int", "Reconnect if waiting time is greater than minutes", 10)] + + __description__ = """Uploadgig.com hoster plugin""" + __license__ = "GPLv3" + __authors__ = [("GammaC0de", "nitzo2001[AT]yahoo[DOT]com")] + + URL_REPLACEMENTS = [("http://", "https://")] + + NAME_PATTERN = r'(?P.+?)<' + SIZE_PATTERN = r'\[(?P[\d.,]+) (?P[\w^_]+)\]<' + + OFFLINE_PATTERN = r'File not found' + + def handle_free(self, pyfile): + m = re.search( + r"", + self.data, + ) + if m is None: + self.error(_("f pattern not found")) + + f = self.js.eval(m.group(1)) + + url, inputs = self.parse_html_form('id="dl_captcha_form"') + if inputs is None: + self.error(_("Free download form not found")) + + recaptcha = ReCaptcha(pyfile) + + captcha_key = recaptcha.detect_key() + if captcha_key is None: + self.error(_("ReCaptcha key not found")) + + self.captcha = recaptcha + response = recaptcha.challenge(captcha_key) + + inputs['g-recaptcha-response'] = response + self.data = self.load(self.fixurl(url), + post=inputs) + + if self.data == "m": + self.log_warning(_("Max downloads for this hour reached")) + self.retry(wait=60*60) + + elif self.data in ("fl", "rfd"): + self.fail(_("File can be downloaded by premium users only")) + + elif self.data == "e": + self.retry() + + elif self.data == "0": + self.retry_captcha() + + else: + try: + res = json.loads(self.data) + + except: + self.fail(_("Illegal response from the server")) + + if any([_x not in res for _x in ('cd', 'sp', 'q', 'id')]): + self.fail(_("Illegal response from the server")) + + self.wait(res['cd']) + + self.link = res['sp'] + "id=" + str(res['id'] - int(f)) + "&" + res['q'] diff --git a/module/plugins/hoster/UploadheroCom.py b/module/plugins/hoster/UploadheroCom.py deleted file mode 100644 index 7b047e028e..0000000000 --- a/module/plugins/hoster/UploadheroCom.py +++ /dev/null @@ -1,90 +0,0 @@ -# -*- 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 . - - @author: zoidberg -""" - -# Test link (random.bin): -# http://uploadhero.co/dl/wQBRAVSM - -import re -from module.plugins.internal.SimpleHoster import SimpleHoster, create_getInfo - - -class UploadheroCom(SimpleHoster): - __name__ = "UploadheroCom" - __type__ = "hoster" - __pattern__ = r"http://(?:www\.)?uploadhero\.com?/dl/\w+" - __version__ = "0.15" - __description__ = """UploadHero.co plugin""" - __author_name__ = ("mcmyst", "zoidberg") - __author_mail__ = ("mcmyst@hotmail.fr", "zoidberg@mujmail.cz") - - SH_COOKIES = [("http://uploadhero.co", "lang", "en")] - FILE_NAME_PATTERN = r'
      (?P.*?)
      ' - FILE_SIZE_PATTERN = r'Taille du fichier :
      (?P.*?)' - FILE_OFFLINE_PATTERN = r'

      |

      Le lien du fichier ci-dessus n\'existe plus.' - - DOWNLOAD_URL_PATTERN = r'(\d+).*\s*(\d+)' - - CAPTCHA_PATTERN = r'"(/captchadl\.php\?[a-z0-9]+)"' - FREE_URL_PATTERN = r'var magicomfg = \'"/]+)"' - - def handleFree(self): - self.checkErrors() - - found = re.search(self.CAPTCHA_PATTERN, self.html) - if not found: - self.parseError("Captcha URL") - captcha_url = "http://uploadhero.co" + found.group(1) - - for i in range(5): - captcha = self.decryptCaptcha(captcha_url) - self.html = self.load(self.pyfile.url, get={"code": captcha}) - found = re.search(self.FREE_URL_PATTERN, self.html) - if found: - self.correctCaptcha() - download_url = found.group(1) or found.group(2) - break - else: - self.invalidCaptcha() - else: - self.fail("No valid captcha code entered") - - self.download(download_url) - - def handlePremium(self): - self.logDebug("%s: Use Premium Account" % self.__name__) - self.html = self.load(self.pyfile.url) - link = re.search(self.DOWNLOAD_URL_PATTERN, self.html).group(1) - self.logDebug("Downloading link : '%s'" % link) - self.download(link) - - def checkErrors(self): - found = re.search(self.IP_BLOCKED_PATTERN, self.html) - if found: - self.html = self.load("http://uploadhero.co%s" % found.group(1)) - - found = re.search(self.IP_WAIT_PATTERN, self.html) - wait_time = (int(found.group(1)) * 60 + int(found.group(2))) if found else 300 - self.setWait(wait_time, True) - self.wait() - self.retry() - - -getInfo = create_getInfo(UploadheroCom) diff --git a/module/plugins/hoster/UploadingCom.py b/module/plugins/hoster/UploadingCom.py deleted file mode 100644 index 1da5714600..0000000000 --- a/module/plugins/hoster/UploadingCom.py +++ /dev/null @@ -1,113 +0,0 @@ -#!/usr/bin/env python -# -*- 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 . - - @author: jeix -""" - -import re -from pycurl import HTTPHEADER -from module.plugins.internal.SimpleHoster import SimpleHoster, create_getInfo, timestamp -from module.common.json_layer import json_loads - - -class UploadingCom(SimpleHoster): - __name__ = "UploadingCom" - __type__ = "hoster" - __pattern__ = r"http://(?:www\.)?uploading\.com/files/(?:get/)?(?P[\w\d]+)" - __version__ = "0.33" - __description__ = """Uploading.Com File Download Hoster""" - __author_name__ = ("jeix", "mkaay", "zoidberg") - __author_mail__ = ("jeix@hasnomail.de", "mkaay@mkaay.de", "zoidberg@mujmail.cz") - - FILE_NAME_PATTERN = r'Download (?P<N>.*?) for free on uploading.com' - FILE_SIZE_PATTERN = r'File size: (?P.*?)' - FILE_OFFLINE_PATTERN = r'The requested file is not found

      ' - - def process(self, pyfile): - # set lang to english - self.req.cj.setCookie("uploading.com", "lang", "1") - self.req.cj.setCookie("uploading.com", "language", "1") - self.req.cj.setCookie("uploading.com", "setlang", "en") - self.req.cj.setCookie("uploading.com", "_lang", "en") - - if not "/get/" in self.pyfile.url: - self.pyfile.url = self.pyfile.url.replace("/files", "/files/get") - - self.html = self.load(pyfile.url, decode=True) - self.file_info = self.getFileInfo() - - if self.premium: - self.handlePremium() - else: - self.handleFree() - - def handlePremium(self): - postData = {'action': 'get_link', - 'code': self.file_info['ID'], - 'pass': 'undefined'} - - self.html = self.load('http://uploading.com/files/get/?JsHttpRequest=%d-xml' % timestamp(), post=postData) - url = re.search(r'"link"\s*:\s*"(.*?)"', self.html) - if url: - url = url.group(1).replace("\\/", "/") - self.download(url) - - raise Exception("Plugin defect.") - - def handleFree(self): - found = re.search('

      ((Daily )?Download Limit)

      ', self.html) - if found: - self.pyfile.error = found.group(1) - self.logWarning(self.pyfile.error) - self.retry(max_tries=6, wait_time=21600 if found.group(2) else 900, reason=self.pyfile.error) - - ajax_url = "http://uploading.com/files/get/?ajax" - self.req.http.c.setopt(HTTPHEADER, ["X-Requested-With: XMLHttpRequest"]) - self.req.http.lastURL = self.pyfile.url - - response = json_loads(self.load(ajax_url, post={'action': 'second_page', 'code': self.file_info['ID']})) - if 'answer' in response and 'wait_time' in response['answer']: - wait_time = int(response['answer']['wait_time']) - self.logInfo("%s: Waiting %d seconds." % (self.__name__, wait_time)) - self.setWait(wait_time) - self.wait() - else: - self.pluginParseError("AJAX/WAIT") - - response = json_loads( - self.load(ajax_url, post={'action': 'get_link', 'code': self.file_info['ID'], 'pass': 'false'})) - if 'answer' in response and 'link' in response['answer']: - url = response['answer']['link'] - else: - self.pluginParseError("AJAX/URL") - - self.html = self.load(url) - found = re.search(r'The file was removed' + TEMP_OFFLINE_PATTERN = '' + LINK_PATTERN = r'(https?://(?:www\.)?(?:[^/]*?uploadrocket\.net|\d+\.\d+\.\d+\.\d+)(?:\:\d+)?(?:/d/|(?:/files)?/\d+/\w+/).+?)["\'<]' + + def setup(self): + self.resume_download = True + self.multiDL = True + + def _post_parameters(self): + inputs = XFSHoster._post_parameters(self) + + # Remove parameters added by XFSHoster (method_free/method_premium) and + # remove the inappropriate inputs + if not self.premium: + if 'method_isfree' in inputs: + inputs.pop('method_free', None) + inputs.pop('method_ispremium', None) + elif 'method_ispremium' in inputs: + inputs.pop('method_isfree', None) + inputs.pop('method_premium', None) + + return inputs + + def process(self, pyfile): + # If filename present in url, remove it (else hoster will redirect) + m = re.search("(.*)/.*.html", pyfile.url, re.S) + + if m is not None and m.group(1): + self.pyfile.url = m.group(1) + + XFSHoster.process(self, pyfile) + + def download(self, url, get={}, post={}, ref=True, cookies=True, + disposition=True, resume=None, chunks=None): + # Read filename from download url as only present there + m = re.search(".*/(.*)", url, re.S) + + if m is not None and m.group(1): + self.pyfile.name = m.group(1) + + XFSHoster.download( + self, url, get, post, ref, cookies, disposition, resume, chunks) diff --git a/module/plugins/hoster/UploadshipCom.py b/module/plugins/hoster/UploadshipCom.py new file mode 100644 index 0000000000..d6b8d49a78 --- /dev/null +++ b/module/plugins/hoster/UploadshipCom.py @@ -0,0 +1,54 @@ +# -*- coding: utf-8 -*- + +import re + +from ..internal.XFSHoster import XFSHoster + + +class UploadshipCom(XFSHoster): + __name__ = "UploadshipCom" + __type__ = "hoster" + __version__ = "0.02" + __status__ = "testing" + + __pattern__ = r'https?://(?:www\.)?uploadship\.com/\w{16}' + __config__ = [("activated", "bool", "Activated", True), + ("use_premium", "bool", "Use premium account if available", True), + ("fallback", "bool", "Fallback to free download if premium fails", True), + ("chk_filesize", "bool", "Check file size", True), + ("max_wait", "int", "Reconnect if waiting time is greater than minutes", 10)] + + __description__ = """Uploadship.com hoster plugin""" + __license__ = "GPLv3" + __authors__ = [("OzzieIsaacs", "ozzie.fernandez.isaacs@gmail.com")] + + PLUGIN_DOMAIN = "uploadship.com" + + NAME_PATTERN = r'(?P.+?)' + SIZE_PATTERN = r'
      \s*\n\s*(?P[\d.,]+) (?P[\w^_]+)' + OFFLINE_PATTERN = r'File (?:not found|was deleted).*' + + PREMIUM_ONLY_PATTERN = r'available only for Premium' + LINK_FREE_PATTERN = r'' + + URL_REPLACEMENTS = [(__pattern__ + ".*", r'https://upstore.net/\g')] + + DL_LIMIT_PATTERN = r'Please wait .+? before downloading next' + WAIT_PATTERN = r'var sec = (\d+)' + + COOKIES = [("upstore.net", "lang", "en")] + + def handle_free(self, pyfile): + #: STAGE 1: get link to continue + self.data = self.load(pyfile.url, + post={'hash': self.info['pattern']['ID'], + 'free': 'Slow download'}) + + #: STAGE 2: solve captcha and wait + #: First get the infos we need: self.captcha key and wait time + m = re.search(self.WAIT_PATTERN, self.data) + if m is None: + self.error(_("Wait pattern not found")) + + #: prepare the waiting + wait_time = int(m.group(1)) + self.set_wait(wait_time) + + #: then, handle the captcha + hcaptcha = HCaptcha(self.pyfile) + + captcha_key = hcaptcha.detect_key() + if captcha_key is None: + self.fail(_("captcha key not found")) + + self.captcha = hcaptcha + + post_data = {'hash': self.info['pattern']['ID'], + 'free': 'Get download link', + 'antispam': "spam", + 'kpw': "spam"} + post_data['h-captcha-response'] = post_data['g-recaptcha-response'] = hcaptcha.challenge(captcha_key) + + #: then, do the waiting + self.wait() + + self.data = self.load(pyfile.url, + post=post_data, + ref=pyfile.url) + + #: check whether the captcha was wrong + if "Captcha check failed" in self.data: + self.captcha.invalid() + + else: + self.captcha.correct() + + # STAGE 3: get direct link or wait time + self.check_errors() + + m = re.search(self.LINK_FREE_PATTERN, self.data) + if m is not None: + self.link = m.group(1) + + def handle_premium(self, pyfile): + self.data = self.load("https://upstore.net/load/premium", + post={'hash': self.info['pattern']['ID'], + 'antispam': "spam", + 'js': "1"}) + json_data = json.loads(self.data) + self.link = json_data['ok'] diff --git a/module/plugins/hoster/UptoboxCom.py b/module/plugins/hoster/UptoboxCom.py index fe05bf9162..d00efdb38b 100644 --- a/module/plugins/hoster/UptoboxCom.py +++ b/module/plugins/hoster/UptoboxCom.py @@ -1,19 +1,54 @@ # -*- coding: utf-8 -*- -from module.plugins.hoster.XFileSharingPro import XFileSharingPro, create_getInfo +import re -class UptoboxCom(XFileSharingPro): +from ..internal.SimpleHoster import SimpleHoster + + +class UptoboxCom(SimpleHoster): __name__ = "UptoboxCom" __type__ = "hoster" - __pattern__ = r"http://(?:\w*\.)*?uptobox.com/\w{12}" - __version__ = "0.06" + __version__ = "0.41" + __status__ = "testing" + + __pattern__ = r'https?://(?:www\.)?(uptobox|uptostream)\.(?:com|eu|link)/(?P\w{12})' + __config__ = [("activated", "bool", "Activated", True), + ("use_premium", "bool", "Use premium account if available", True), + ("fallback", "bool", + "Fallback to free download if premium fails", True), + ("chk_filesize", "bool", "Check file size", True), + ("max_wait", "int", "Reconnect if waiting time is greater than minutes", 10)] + __description__ = """Uptobox.com hoster plugin""" - __author_name__ = ("zoidberg") - __author_mail__ = ("zoidberg@mujmail.cz") + __license__ = "GPLv3" + __authors__ = [("Walter Purcaro", "vuolter@gmail.com"), + ("GammaC0de", "nitzo2001[AT]yahoo[DOT]com")] + + PLUGIN_DOMAIN = "uptobox.link" + + INFO_PATTERN = r"""(?:"para_title">|)(?P.+) \((?P[\d.,]+) (?P[\w^_]+)\)""" + OFFLINE_PATTERN = r"""(File not found|Access Denied|404 Not Found)""" + TEMP_OFFLINE_PATTERN = r""">Service Unavailable""" + WAIT_PATTERN = r"""data-remaining-time=["'](\d+)["']""" + + LINK_PATTERN = r"""['"](https?://(?:obwp\d+\.uptobox\.(?:com|eu|link)|\w+\.uptobox\.(?:com|eu|link)/dl?)/.+?)['"]""" + + DL_LIMIT_PATTERN = r"""or you can wait (.+) to launch a new download""" + URL_REPLACEMENTS = [(__pattern__ + ".*", r"https://uptobox.link/\g")] + + def setup(self): + self.multiDL = self.premium + self.chunk_limit = 1 + self.resume_download = True - FILE_INFO_PATTERN = r'

      \s*Download File\s*]*>(?P[^>]+)

      \s*[^\(]*\((?P[^\)]+)\)' - FILE_OFFLINE_PATTERN = r'
      File Not Found
      ' - HOSTER_NAME = "uptobox.com" + def handle_free(self, pyfile): + m = re.search(r"""\w{12})' + __config__ = [("activated", "bool", "Activated", True), + ("use_premium", "bool", "Use premium account if available", True), + ("fallback", "bool", "Fallback to free download if premium fails", True), + ("chk_filesize", "bool", "Check file size", True), + ("max_wait", "int", "Reconnect if waiting time is greater than minutes", 10)] + + __description__ = """Userscloud.com hoster plugin""" + __license__ = "GPLv3" + __authors__ = [("GammaC0de", "nitzo2001[AT]yahoo[DOT]com")] + + PLUGIN_DOMAIN = "userscloud.com" + + INFO_PATTERN = r'
      (?P.+?) - (?P[\d.,]+) (?P[\w^_]+)' + OFFLINE_PATTERN = r'The file you are trying to download is no longer available' + LINK_FREE_PATTERN = r'')] + + def setup(self): + self.multiDL = True + self.resume_download = False + self.chunk_limit = 1 + + try: + self.req.http.close() + except Exception: + pass + + self.req.http = BIGHTTPRequest( + cookies=self.req.cj, + options=self.pyload.requestFactory.getOptions(), + limit=2000000) diff --git a/module/plugins/hoster/UseruploadNet.py b/module/plugins/hoster/UseruploadNet.py new file mode 100644 index 0000000000..953fa6d0bd --- /dev/null +++ b/module/plugins/hoster/UseruploadNet.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- + +import re + +from ..internal.XFSHoster import XFSHoster + + +class UseruploadNet(XFSHoster): + __name__ = "UseruploadNet" + __type__ = "hoster" + __version__ = "0.02" + __status__ = "testing" + + __pattern__ = r'https?://(?:www\.)?userupload\.net/\w{12}' + __config__ = [("activated", "bool", "Activated", True), + ("use_premium", "bool", + "Use premium account if available", True), + ("fallback", "bool", + "Fallback to free download if premium fails", True), + ("chk_filesize", "bool", "Check file size", True), + ("max_wait", "int", + "Reconnect if waiting time is greater than minutes", 10)] + + __description__ = """Userupload.net hoster plugin""" + __license__ = "GPLv3" + __authors__ = [("OzzieIsaacs", "ozzie.fernandez.isaacs@gmail.com")] + + PLUGIN_DOMAIN = "userupload.net" + + NAME_PATTERN = r'Download (?P<N>.+?)' + SIZE_PATTERN = r'File Size : (?P[\d.,]+) (?P[\w^_]+)' + + LINK_PATTERN = r'' + + def handle_free(self, pyfile): + self.check_errors() + + self.data = self.load(pyfile.url, + post=self._post_parameters(), + ref=self.pyfile.url, + redirect=False) + + m = re.search(self.LINK_PATTERN, self.data) + if m is not None: + self.link = m.group(1) + pyfile.name = self.link.split('/')[-1] diff --git a/module/plugins/hoster/VeehdCom.py b/module/plugins/hoster/VeehdCom.py index 3648ef4ec4..4a9c1a6243 100644 --- a/module/plugins/hoster/VeehdCom.py +++ b/module/plugins/hoster/VeehdCom.py @@ -1,24 +1,24 @@ -#!/usr/bin/env python # -*- coding: utf-8 -*- + import re -from module.plugins.Hoster import Hoster + +from ..internal.Hoster import Hoster class VeehdCom(Hoster): - __name__ = 'VeehdCom' - __type__ = 'hoster' + __name__ = "VeehdCom" + __type__ = "hoster" + __version__ = "0.29" + __status__ = "testing" + __pattern__ = r'http://veehd\.com/video/\d+_\S+' - __config__ = [ - ('filename_spaces', 'bool', "Allow spaces in filename", 'False'), - ('replacement_char', 'str', "Filename replacement character", '_'), - ] - __version__ = '0.23' - __description__ = """Veehd.com Download Hoster""" - __author_name__ = ('cat') - __author_mail__ = ('cat@pyload') - - def _debug(self, msg): - self.logDebug('[%s] %s' % (self.__name__, msg)) + __config__ = [("activated", "bool", "Activated", True), + ("filename_spaces", "bool", "Allow spaces in filename", False), + ("replacement_char", "str", "Filename replacement character", "_")] + + __description__ = """Veehd.com hoster plugin""" + __license__ = "GPLv3" + __authors__ = [("cat", "cat@pyload")] def setup(self): self.multiDL = True @@ -34,46 +34,46 @@ def process(self, pyfile): def download_html(self): url = self.pyfile.url - self._debug("Requesting page: %s" % (repr(url),)) - self.html = self.load(url) + self.log_debug("Requesting page: %s" % url) + self.data = self.load(url) def file_exists(self): - if self.html is None: + if not self.data: self.download_html() - if 'Veehd' in self.html: + if 'Veehd' in self.data: return False return True def get_file_name(self): - if self.html is None: + if not self.data: self.download_html() - match = re.search(r']*>([^<]+) on Veehd', self.html) - if not match: - self.fail("video title not found") - name = match.group(1) + m = re.search(r'(.+?) on Veehd', self.data) + if m is None: + self.error(_("Video title not found")) + + name = m.group(1) - # replace unwanted characters in filename - if self.getConfig('filename_spaces'): - pattern = '[^0-9A-Za-z\.\ ]+' + #: Replace unwanted characters in filename + if self.config.get('filename_spaces'): + pattern = '[^\w ]+' else: - pattern = '[^0-9A-Za-z\.]+' + pattern = '[^\w.]+' - name = re.sub(pattern, self.getConfig('replacement_char'), - name) - return name + '.avi' + return re.sub(pattern, self.config.get( + 'replacement_char'), name) + '.avi' def get_file_url(self): - """ returns the absolute downloadable filepath """ - if self.html is None: + Returns the absolute downloadable filepath + """ + if not self.data: self.download_html() - match = re.search(r'v\w+)' + __config__ = [("activated", "bool", "Activated", True), + ("use_premium", "bool", "Use premium account if available", True), + ("fallback", "bool", + "Fallback to free download if premium fails", True), + ("chk_filesize", "bool", "Check file size", True), + ("max_wait", "int", "Reconnect if waiting time is greater than minutes", 10)] + + __description__ = """Veoh.com hoster plugin""" + __license__ = "GPLv3" + __authors__ = [("Walter Purcaro", "vuolter@gmail.com"), + ("GammaC0de", "nitzo2001[AT]yahoo[DOT]com")] + + NAME_PATTERN = r'Sorry, we couldn\'t find the video you were looking for' + + URL_REPLACEMENTS = [ + (__pattern__ + ".*", + r'https://www.veoh.com/watch/\g') + ] + + COOKIES = [("veoh.com", "lassieLocale", "en")] + + def setup(self): + self.resume_download = True + self.multiDL = True + self.chunk_limit = -1 + + def handle_free(self, pyfile): + video_id = self.info['pattern']['ID'] + video_data = json.loads(self.load(r"https://www.veoh.com/watch/getVideo/%s" % video_id)) + pyfile.name = video_data["video"]['title'] + ".mp4" + self.link = video_data["video"]['src']['HQ'] diff --git a/module/plugins/hoster/VidPlayNet.py b/module/plugins/hoster/VidPlayNet.py new file mode 100644 index 0000000000..5f09da4fe3 --- /dev/null +++ b/module/plugins/hoster/VidPlayNet.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +# +# Test links: +# BigBuckBunny_320x180.mp4 - 61.7 Mb - http://vidplay.net/38lkev0h3jv0 + +from ..internal.XFSHoster import XFSHoster + + +class VidPlayNet(XFSHoster): + __name__ = "VidPlayNet" + __type__ = "hoster" + __version__ = "0.11" + __status__ = "testing" + + __pattern__ = r'https?://(?:www\.)?vidplay\.net/\w{12}' + __config__ = [("activated", "bool", "Activated", True), + ("use_premium", "bool", "Use premium account if available", True), + ("fallback", "bool", + "Fallback to free download if premium fails", True), + ("chk_filesize", "bool", "Check file size", True), + ("max_wait", "int", "Reconnect if waiting time is greater than minutes", 10)] + + __description__ = """VidPlay.net hoster plugin""" + __license__ = "GPLv3" + __authors__ = [("t4skforce", "t4skforce1337[AT]gmail[DOT]com")] + + PLUGIN_DOMAIN = "vidplay.net" + + NAME_PATTERN = r'Password:
      \s*(?P.+?)' diff --git a/module/plugins/hoster/VimeoCom.py b/module/plugins/hoster/VimeoCom.py new file mode 100644 index 0000000000..52c5895983 --- /dev/null +++ b/module/plugins/hoster/VimeoCom.py @@ -0,0 +1,103 @@ +# -*- coding: utf-8 -*- + +import re +import urlparse + +from ..internal.misc import json +from ..internal.SimpleHoster import SimpleHoster + + +class VimeoCom(SimpleHoster): + __name__ = "VimeoCom" + __type__ = "hoster" + __version__ = "0.14" + __status__ = "testing" + + __pattern__ = r'https?://(?:www\.)?(player\.)?vimeo\.com/(video/)?(?P\d+)' + __config__ = [("activated", "bool", "Activated", True), + ("use_premium", "bool", "Use premium account if available", True), + ("fallback", "bool", "Fallback to free download if premium fails", True), + ("chk_filesize", "bool", "Check file size", True), + ("max_wait", "int", "Reconnect if waiting time is greater than minutes", 10), + ("quality", "Lowest;360p;540p;720p;1080p;Highest", "Quality", "Highest")] + + __description__ = """Vimeo.com hoster plugin""" + __license__ = "GPLv3" + __authors__ = [("Walter Purcaro", "vuolter@gmail.com"), + ("GammaC0de", "nitzo2001[AT]yahoo[DOT]com")] + + NAME_PATTERN = r'(?P<N>.+?) on Vimeo<' + OFFLINE_PATTERN = r'class="exception_header"' + TEMP_OFFLINE_PATTERN = r'Please try again in a few minutes.<' + + URL_REPLACEMENTS = [(__pattern__ + ".*", r'https://vimeo.com/\g<ID>')] + + COOKIES = [("vimeo.com", "language", "en")] + + def get_info(self, url="", html=""): + info = super(VimeoCom, self).get_info(url, html) + # Unfortunately, NAME_PATTERN does not include file extension so we blindly add '.mp4' as an extension. + # (hopefully all links are '.mp4' files) + if 'name' in info: + info['name'] += ".mp4" + + return info + + def setup(self): + self.resume_download = True + self.multiDL = True + self.chunk_limit = -1 + + def handle_free(self, pyfile): + url, inputs = self.parse_html_form('id="pw_form"') + if url: + password = self.get_password() + + if not password: + self.fail(_("Video is password protected")) + + token = re.search(r'"vimeo":{"xsrft":"(.+?)"}', self.data).group(1) + inputs['token'] = token + inputs['password'] = password + + self.data = self.load(urlparse.urljoin(pyfile.url, url), + post=inputs) + + if "Sorry, that password was incorrect. Please try again." in self.data: + self.fail(_("Wrong password")) + + m = re.search(r'clip_page_config = ({.+?});', self.data) + if m is None: + self.fail("Clip config pattern not found") + + player_config_url = json.loads(m.group(1))['player']['config_url'] + + json_data = self.load(player_config_url) + + if not json_data.startswith('{'): + self.fail(_("Unexpected response, expected JSON data")) + + json_data = json.loads(json_data) + + videos = dict((v['quality'], v['url']) + for v in json_data['request']['files']['progressive']) + + quality = self.config.get('quality') + if quality == "Highest": + qlevel = ("1080p", "720p", "540p", "360p") + + elif quality == "Lowest": + qlevel = ("360p", "540p", "720p", "1080p") + + else: + qlevel = quality + + for q in qlevel: + if q in videos.keys(): + self.download(videos[q]) + return + + else: + self.log_info(_("No %s quality video found") % q) + else: + self.fail(_("No video found!")) diff --git a/module/plugins/hoster/VkCom.py b/module/plugins/hoster/VkCom.py new file mode 100644 index 0000000000..2c5a043e14 --- /dev/null +++ b/module/plugins/hoster/VkCom.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- +# +# Test links: +# http://vk.com/video_ext.php?oid=166335015&id=162608895&hash=b55affa83774504b&hd=1 + +import re + +from ..internal.SimpleHoster import SimpleHoster + + +class VkCom(SimpleHoster): + __name__ = "VkCom" + __type__ = "hoster" + __version__ = "0.06" + __status__ = "testing" + + __pattern__ = r'https?://(?:www\.)?vk\.com/video_ext\.php/\?.+' + __config__ = [("activated", "bool", "Activated", True), + ("use_premium", "bool", "Use premium account if available", True), + ("fallback", "bool", + "Fallback to free download if premium fails", True), + ("chk_filesize", "bool", "Check file size", True), + ("max_wait", "int", "Reconnect if waiting time is greater than minutes", 10)] + + __description__ = """Vk.com hoster plugin""" + __license__ = "GPLv3" + __authors__ = [("Walter Purcaro", "vuolter@gmail.com")] + + NAME_PATTERN = r'"md_title":"(?P<N>.+?)"' + OFFLINE_PATTERN = r'<div id="video_ext_msg">' + + LINK_FREE_PATTERN = r'url\d+":"(.+?)"' + + def handle_free(self, pyfile): + self.link = re.findall(self.LINK_FREE_PATTERN, self.data)[ + 0 if self.config.get('quality') == "Low" else -1] diff --git a/module/plugins/hoster/WarserverCz.py b/module/plugins/hoster/WarserverCz.py deleted file mode 100644 index ee580fbdd5..0000000000 --- a/module/plugins/hoster/WarserverCz.py +++ /dev/null @@ -1,75 +0,0 @@ -# -*- 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: zoidberg -""" - -#similar to coolshare.cz (down) - -import re -from module.plugins.internal.SimpleHoster import SimpleHoster, create_getInfo -from module.network.HTTPRequest import BadHeader -from module.utils import html_unescape - - -class WarserverCz(SimpleHoster): - __name__ = "WarserverCz" - __type__ = "hoster" - __pattern__ = r"http://(?:\w*\.)?warserver.cz/stahnout/(?P<ID>\d+)/.+" - __version__ = "0.12" - __description__ = """Warserver.cz""" - __author_name__ = ("zoidberg") - - FILE_NAME_PATTERN = r'<h1.*?>(?P<N>[^<]+)</h1>' - FILE_SIZE_PATTERN = r'<li>Velikost: <strong>(?P<S>[^<]+)</strong>' - FILE_OFFLINE_PATTERN = r'<h1>Soubor nenalezen</h1>' - - PREMIUM_URL_PATTERN = r'href="(http://[^/]+/dwn-premium.php.*?)"' - DOMAIN = "http://csd01.coolshare.cz" - - DOMAIN = "http://s01.warserver.cz" - - def handleFree(self): - try: - self.download("%s/dwn-free.php?fid=%s" % (self.DOMAIN, self.file_info['ID'])) - except BadHeader, e: - self.logError(e) - if e.code == 403: - self.longWait(60, 60) - else: - raise - self.checkDownloadedFile() - - def handlePremium(self): - found = re.search(self.PREMIUM_URL_PATTERN, self.html) - if not found: - self.parseError("Premium URL") - url = html_unescape(found.group(1)) - self.logDebug("Premium URL: " + url) - if not url.startswith("http://"): - self.resetAccount() - self.download(url) - self.checkDownloadedFile() - - def checkDownloadedFile(self): - check = self.checkDownload({ - "offline": ">404 Not Found<" - }) - - if check == "offline": - self.offline() - - -getInfo = create_getInfo(WarserverCz) diff --git a/module/plugins/hoster/WebshareCz.py b/module/plugins/hoster/WebshareCz.py index 1c9ddb290b..5db26491fa 100644 --- a/module/plugins/hoster/WebshareCz.py +++ b/module/plugins/hoster/WebshareCz.py @@ -1,46 +1,67 @@ # -*- 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: zoidberg -""" import re -from module.plugins.internal.SimpleHoster import SimpleHoster, create_getInfo + +from ..internal.SimpleHoster import SimpleHoster class WebshareCz(SimpleHoster): __name__ = "WebshareCz" __type__ = "hoster" - __pattern__ = r"http://(\w+\.)?webshare.cz/(stahnout/)?(?P<ID>\w{10})-.+" - __version__ = "0.12" - __description__ = """WebShare.cz""" - __author_name__ = ("zoidberg") + __version__ = "0.27" + __status__ = "testing" + + __pattern__ = r'https?://(?:www\.)?(?:en\.)?webshare\.cz/(?:#/)?(?:file/)(?P<ID>\w+)' + __config__ = [("activated", "bool", "Activated", True), + ("use_premium", "bool", "Use premium account if available", True), + ("fallback", "bool", "Fallback to free download if premium fails", True), + ("chk_filesize", "bool", "Check file size", True), + ("max_wait", "int", "Reconnect if waiting time is greater than minutes", 10)] + + __description__ = """Webshare.cz hoster plugin""" + __license__ = "GPLv3" + __authors__ = [("stickell", "l.stickell@yahoo.it"), + ("rush", "radek.senfeld@gmail.com"), + ("ondrej", "git@ondrej.it"), + ("GammaC0de", "nitzo2001[AT]yahoo[DOT]com")] + + API_URL = "https://webshare.cz/api/" + + def api_response(self, method, **kwargs): + return self.load(self.API_URL + method + "/", + post=kwargs) + + def api_info(self, url): + info = {} + api_data = self.api_response("file_info", ident=re.match(self.__pattern__, url).group('ID'), wst="") + + if re.search(r'<status>OK', api_data): + info['status'] = 2 + info['name'] = re.search(r'<name>(.+?)<', api_data).group(1) + info['size'] = re.search(r'<size>(.+?)<', api_data).group(1) + + elif re.search(r'<status>FATAL', api_data): + info['status'] = 1 + + else: + info['status'] = 8 + info['error'] = _("Could not find required xml data") - FILE_NAME_PATTERN = r'<h3>Stahujete soubor: </h3>\s*<div class="textbox">(?P<N>[^<]+)</div>' - FILE_SIZE_PATTERN = r'<h3>Velikost souboru je: </h3>\s*<div class="textbox">(?P<S>[^<]+)</div>' - FILE_OFFLINE_PATTERN = r'<h3>Soubor ".*?" nebyl nalezen.</h3>' + return info - DOWNLOAD_LINK_PATTERN = r'id="download_link" href="(?P<url>.*?)"' + def setup(self): + self.multiDL = self.premium + self.resume_download = True + self.chunk_limit = 2 - def handleFree(self): - url_a = re.search(r"(var l.*)", self.html).group(1) - url_b = re.search(r"(var keyStr.*)", self.html).group(1) - url = self.js.eval("%s\n%s\ndec(l)" % (url_a, url_b)) + def handle_free(self, pyfile): + wst = self.account.get_data('wst') if self.account else "" - self.logDebug('Download link: ' + url) - self.download(url) + api_data = self.api_response("file_link", ident=self.info['pattern']['ID'], wst=wst) + m = re.search('<link>(.+?)</link>', api_data) + if m is not None: + self.link = m.group(1) -getInfo = create_getInfo(WebshareCz) + def handle_premium(self, pyfile): + return self.handle_free(pyfile) diff --git a/module/plugins/hoster/WetransferCom.py b/module/plugins/hoster/WetransferCom.py new file mode 100644 index 0000000000..9745cb1171 --- /dev/null +++ b/module/plugins/hoster/WetransferCom.py @@ -0,0 +1,103 @@ +# -*- coding: utf-8 -*- + +import re + +import pycurl +from module.network.HTTPRequest import BadHeader + +from ..internal.misc import json +from ..internal.SimpleHoster import SimpleHoster + + +class WetransferCom(SimpleHoster): + __name__ = "WetransferCom" + __type__ = "hoster" + __version__ = "0.01" + __status__ = "testing" + + __pattern__ = r'https?://(?:www\.)?wetransfer.com/downloads/(?P<ID>[0-9a-f]+)/(?:(?P<RID>[0-9a-f]+)/)?(?P<SHASH>[0-9a-f]{6})$' + __config__ = [("activated", "bool", "Activated", True), + ("use_premium", "bool", "Use premium account if available", True), + ("fallback", "bool", "Fallback to free download if premium fails", True), + ("chk_filesize", "bool", "Check file size", True), + ("max_wait", "int", "Reconnect if waiting time is greater than minutes", 10)] + + __description__ = """Wetransfer.com hoster plugin""" + __license__ = "GPLv3" + __authors__ = [("GammaC0de", "nitzo2001[AT]yahoo[DOT]com")] + + URL_REPLACEMENTS = [("https?://(?:www\.)?wetransfer.com/", "https://wetransfer.com/")] + DIRECT_LINK = False + + API_URL = "https://wetransfer.com/api/v4/" + + def api_request(self, method, file_id, **kwargs): + self.req.http.c.setopt(pycurl.HTTPHEADER, + ["X-Requested-With: XMLHttpRequest", "Content-Type: application/json"]) + try: + json_data = self.load("%s%s/%s/%s" % (self.API_URL, "transfers", file_id, method), + post=json.dumps(kwargs)) + except BadHeader, e: + json_data = e.content + + api_data = json.loads(json_data) + + return api_data + + def api_info(self, url): + info = {} + m = re.search(self.__pattern__, url) + file_id = m.group('ID') + recipient_id = m.group('RID') + security_hash = m.group('SHASH') + + if recipient_id is not None: + api_data = self.api_request("prepare-download", + file_id, + security_hash=security_hash, + recipient_id=recipient_id) + else: + api_data = self.api_request("prepare-download", + file_id, + security_hash=security_hash) + + if 'message' in api_data: + message = api_data['message'] + if message == "Transfer not found": + info['status'] = 1 + else: + info['error'] = message + info['status'] = 8 + + else: + info['status'] = 2 if api_data['state'] == 'downloadable' else 1 + info['name'] = api_data['recommended_filename'] + info['size'] = api_data['size'] + + return info + + def setup(self): + self.multiDL = True + self.chunk_limit = -1 + self.resume_download = True + + def handle_free(self, pyfile): + file_id = self.info['pattern']['ID'] + recipient_id = self.info['pattern']['RID'] + security_hash = self.info['pattern']['SHASH'] + if recipient_id is not None: + api_data = self.api_request("download", + file_id, + intent="entire_transfer", + security_hash=security_hash, + recipient_id=recipient_id) + else: + api_data = self.api_request("download", + file_id, + intent="entire_transfer", + security_hash=security_hash) + + if 'message' in api_data: + self.fail(api_data['message']) + + self.link = api_data.get('direct_link') diff --git a/module/plugins/hoster/WorkuploadCom.py b/module/plugins/hoster/WorkuploadCom.py new file mode 100644 index 0000000000..d0b4224395 --- /dev/null +++ b/module/plugins/hoster/WorkuploadCom.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- + +from ..internal.misc import json +from ..internal.SimpleHoster import SimpleHoster + + +class WorkuploadCom(SimpleHoster): + __name__ = "WorkuploadCom" + __type__ = "hoster" + __version__ = "0.02" + __status__ = "testing" + + __pattern__ = r"https?://workupload\.com/(?:file|start)/(?P<ID>\w+)" + __config__ = [ + ("activated", "bool", "Activated", True), + ("use_premium", "bool", "Use premium account if available", True), + ("fallback", "bool", "Fallback to free download if premium fails", True), + ("chk_filesize", "bool", "Check file size", True), + ("max_wait", "int", "Reconnect if waiting time is greater than minutes", 10), + ] + + __description__ = """Workupload.com hoster plugin""" + __license__ = "GPLv3" + __authors__ = [("GammaC0de", "nitzo2001[AT]yahoo[DOT]com")] + + API_URL = "https://workupload.com/api/" + + INFO_PATTERN = ur"<td>Filename:\xa0</td><td [^>]+>(?P<N>.+?)</td></tr><tr><td>Filesize:\xa0</td><td>(?P<S>\d+) \((?P<U>[\w^_]+)\)</td></tr><tr><td>Checksum:\xa0</td><td [^>]+>(?P<D>\w+) \((?P<H>\w+)\)<" + + URL_REPLACEMENTS = [(__pattern__ + ".*", r"https://workupload.com/file/\g<ID>")] + + def api_request(self, method, **kwargs): + json_data = self.load(self.API_URL + method) + return json.loads(json_data) + + def setup(self): + self.multi_dl = True + + def handle_free(self, pyfile): + api_data = self.api_request("file/getDownloadServer/" + self.info["pattern"]["ID"]) + if api_data["success"]: + self.link = api_data["data"]["url"] diff --git a/module/plugins/hoster/WorldbytezCom.py b/module/plugins/hoster/WorldbytezCom.py new file mode 100644 index 0000000000..001ce2d9f6 --- /dev/null +++ b/module/plugins/hoster/WorldbytezCom.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- + +from ..internal.XFSHoster import XFSHoster + + +class WorldbytezCom(XFSHoster): + __name__ = "WorldbytezCom" + __type__ = "hoster" + __version__ = "0.01" + __status__ = "testing" + + __pattern__ = r"https?://(?:www\.)?worldbytez\.com/\w{12}" + __config__ = [ + ("activated", "bool", "Activated", True), + ("use_premium", "bool", "Use premium account if available", True), + ("fallback", "bool", "Fallback to free download if premium fails", True), + ("chk_filesize", "bool", "Check file size", True), + ("max_wait", "int", "Reconnect if waiting time is greater than minutes", 10), + ] + + __description__ = """Worldbytez.com hoster plugin""" + __license__ = "GPLv3" + __authors__ = [("GammaC0de", "nitzo2001[AT]yahoo[DOT]com")] + + PLUGIN_DOMAIN = "worldbytez.com" + + PLUGIN_URL = "https://worldbytez.com/download" + + WAIT_PATTERN = r'<span class="seconds">(\d+)</span>' + SIZE_LIMIT_PATTERN = r'Upgrade your account to download bigger files' + + LINK_PATTERN = r'<a href="(https://[^/]+/d/[^"]+)"' diff --git a/module/plugins/hoster/WrzucTo.py b/module/plugins/hoster/WrzucTo.py index 861e7f11e0..4eae22d4c6 100644 --- a/module/plugins/hoster/WrzucTo.py +++ b/module/plugins/hoster/WrzucTo.py @@ -1,62 +1,64 @@ # -*- 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: zoidberg -""" import re -from pycurl import HTTPHEADER -from module.plugins.internal.SimpleHoster import SimpleHoster, create_getInfo +import pycurl + +from ..internal.SimpleHoster import SimpleHoster class WrzucTo(SimpleHoster): __name__ = "WrzucTo" __type__ = "hoster" - __pattern__ = r"http://(?:\w+\.)*?wrzuc\.to/([a-zA-Z0-9]+(\.wt|\.html)|(\w+/?linki/[a-zA-Z0-9]+))" - __version__ = "0.01" - __description__ = """Wrzuc.to plugin - free only""" - __author_name__ = ("zoidberg") - __author_mail__ = ("zoidberg@mujmail.cz") + __version__ = "0.09" + __status__ = "testing" - SH_COOKIES = [("http://www.wrzuc.to", "language", "en")] - FILE_SIZE_PATTERN = r'class="info">\s*<tr>\s*<td>(?P<S>.*?)</td>' - FILE_NAME_PATTERN = r'id="file_info">\s*<strong>(?P<N>.*?)</strong>' + __pattern__ = r'http://(?:www\.)?wrzuc\.to/(\w+(\.wt|\.html)|(\w+/?linki/\w+))' + __config__ = [("activated", "bool", "Activated", True), + ("use_premium", "bool", "Use premium account if available", True), + ("fallback", "bool", + "Fallback to free download if premium fails", True), + ("chk_filesize", "bool", "Check file size", True), + ("max_wait", "int", "Reconnect if waiting time is greater than minutes", 10)] - def setup(self): - self.multiDL = True + __description__ = """Wrzuc.to hoster plugin""" + __license__ = "GPLv3" + __authors__ = [("zoidberg", "zoidberg@mujmail.cz")] - def handleFree(self): - data = dict(re.findall(r'(md5|file): "(.*?)"', self.html)) - if len(data) != 2: - self.parseError('File ID') + NAME_PATTERN = r'id="file_info">\s*<strong>(?P<N>.*?)</strong>' + SIZE_PATTERN = r'class="info">\s*<tr>\s*<td>(?P<S>.*?)</td>' - self.req.http.c.setopt(HTTPHEADER, ["X-Requested-With: XMLHttpRequest"]) - self.req.http.lastURL = self.pyfile.url - self.load("http://www.wrzuc.to/ajax/server/prepair", post={"md5": data['md5']}) + COOKIES = [("wrzuc.to", "language", "en")] - self.req.http.lastURL = self.pyfile.url - self.html = self.load("http://www.wrzuc.to/ajax/server/download_link", post={"file": data['file']}) + def setup(self): + self.multiDL = True - data.update(re.findall(r'"(download_link|server_id)":"(.*?)"', self.html)) + def handle_free(self, pyfile): + data = dict(re.findall(r'(md5|file): "(.*?)"', self.data)) + if len(data) != 2: + self.error(_("No file ID")) + + self.req.http.c.setopt( + pycurl.HTTPHEADER, + ["X-Requested-With: XMLHttpRequest"]) + self.req.http.lastURL = pyfile.url + self.load( + "http://www.wrzuc.to/ajax/server/prepair", + post={ + 'md5': data['md5']}) + + self.req.http.lastURL = pyfile.url + self.data = self.load( + "http://www.wrzuc.to/ajax/server/download_link", + post={ + 'file': data['file']}) + + data.update( + re.findall( + r'"(download_link|server_id)":"(.*?)"', + self.data)) if len(data) != 4: - self.parseError('Download URL') - - download_url = "http://%s.wrzuc.to/pobierz/%s" % (data['server_id'], data['download_link']) - self.logDebug("Download URL: %s" % download_url) - self.download(download_url) - + self.error(_("No download URL")) -getInfo = create_getInfo(WrzucTo) + self.link = "http://%s.wrzuc.to/pobierz/%s" % ( + data['server_id'], data['download_link']) diff --git a/module/plugins/hoster/WuploadCom.py b/module/plugins/hoster/WuploadCom.py deleted file mode 100644 index 68e83e2286..0000000000 --- a/module/plugins/hoster/WuploadCom.py +++ /dev/null @@ -1,17 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -from module.plugins.internal.DeadHoster import DeadHoster, create_getInfo - - -class WuploadCom(DeadHoster): - __name__ = "WuploadCom" - __type__ = "hoster" - __pattern__ = r"http://[\w\.]*?wupload\..*?/file/(([a-z][0-9]+/)?[0-9]+)(/.*)?" - __version__ = "0.23" - __description__ = """Wupload com""" - __author_name__ = ("jeix", "paulking") - __author_mail__ = ("jeix@hasnomail.de", "") - - -getInfo = create_getInfo(WuploadCom) diff --git a/module/plugins/hoster/X7To.py b/module/plugins/hoster/X7To.py deleted file mode 100644 index 950cbd164b..0000000000 --- a/module/plugins/hoster/X7To.py +++ /dev/null @@ -1,16 +0,0 @@ -# -*- coding: utf-8 -*- - -from module.plugins.internal.DeadHoster import DeadHoster, create_getInfo - - -class X7To(DeadHoster): - __name__ = "X7To" - __type__ = "hoster" - __pattern__ = r"http://(?:www.)?x7.to/" - __version__ = "0.41" - __description__ = """X7.To File Download Hoster""" - __author_name__ = ("ernieb") - __author_mail__ = ("ernieb") - - -getInfo = create_getInfo(X7To) diff --git a/module/plugins/hoster/XDCC.py b/module/plugins/hoster/XDCC.py new file mode 100644 index 0000000000..a40847b66b --- /dev/null +++ b/module/plugins/hoster/XDCC.py @@ -0,0 +1,766 @@ +# -*- coding: utf-8 -*- + +import os +import re +import select +import socket +import struct +import sys +import threading +import time + +from module.plugins.internal.Hoster import Hoster +from module.plugins.internal.misc import encode, exists, format_time, fsjoin, lock, threaded +from module.plugins.Plugin import Abort + + +def decode_text(text): + try: + decoded = unicode(text, 'utf-8') + except UnicodeDecodeError: + decoded = unicode(text, 'latin1', 'replace') + + return decoded + + +class IRC(object): + def __init__(self, plugin, nick, ident ,realname): + self.plugin = plugin + self.lock = threading.RLock() + + self.nick = "pyload-%04d" % (time.time() % 10000) if nick == "pyload" else nick #: last 4 digits + self.ident = ident + self.realname = realname + + self.irc_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.receive_buffer = "" + self.lines = [] + + self.connected = False + self.host = "" + self.port = 0 + + self.bot_host = {} + + self.xdcc_request_time = 0 + self.xdcc_queue_query_time = 0 + + def _data_available(self): + fdset = select.select([self.irc_sock], [], [], 0) + return True if self.irc_sock in fdset[0] else False + + def _get_response_line(self, timeout=5): + start_time = time.time() + while time.time() - start_time < timeout: + if self._data_available(): + self.receive_buffer += self.irc_sock.recv(1024) + self.lines += self.receive_buffer.split("\r\n") + self.receive_buffer = self.lines.pop() + + if self.lines: + return self.lines.pop(0) + + else: + time.sleep(0.1) + + return None + + def _parse_irc_msg(self, line): + """ + Breaks a message from an IRC server into its origin, command, and arguments. + """ + origin = '' + if not line: + return None, None, None + + if line[0] == ':': + origin, line = line[1:].split(' ', 1) + + if line.find(' :') != -1: + line, trailing = line.split(' :', 1) + args = line.split() + args.append(trailing) + + else: + args = line.split() + + command = args.pop(0) + + return origin, command, args + + @lock + def connect_server(self, host, port): + """ + Connect to the IRC server and wait for RPL_WELCOME message + """ + if self.connected: + self.plugin.log_warning(_("Already connected to server, not connecting")) + return False + + self.plugin.log_info(_("Connecting to: %s:%s") % (host, port)) + + self.irc_sock.settimeout(30) + self.irc_sock.connect((host, port)) + self.irc_sock.settimeout(None) + + self.irc_sock.send("NICK %s\r\n" % self.nick) + self.irc_sock.send("USER %s %s bla :%s\r\n" % (self.ident, host, self.realname)) + + start_time = time.time() + while time.time() - start_time < 30: + origin, command, args = self.get_irc_command() + if command == "001": #: RPL_WELCOME + self.connected = True + self.host = host + self.port = port + + start_time = time.time() + while self._get_response_line() and time.time() - start_time < 30: #: Skip MOTD + pass + + self.plugin.log_debug(_("Successfully connected to %s:%s") % (host, port)) + + return True + + elif command == "432": #: ERR_ERRONEUSNICKNAME + self.plugin.log_error(_("Illegal nickname: %s") % self.nick) + break + + elif command == "433": #: ERR_NICKNAMEINUSE + self.plugin.log_error(_("Nickname %s is already in use") % self.nick) + break + + self.plugin.log_error(_("Connection to %s:%s failed.") % (host, port)) + return False + + @lock + def disconnect_server(self): + if self.connected: + self.plugin.log_info(_("Disconnecting from %s:%s") % (self.host, self.port)) + self.irc_sock.send("QUIT :byebye\r\n") + self.plugin.log_debug(_("Disconnected")) + self.connected = False + + else: + self.plugin.log_warning(_("Not connected to server, cannot disconnect")) + + self.irc_sock.close() + + @lock + def get_irc_command(self): + origin, command, args = None, None, None + while True: + line = self._get_response_line() + origin, command, args = self._parse_irc_msg(line) + + if command == "PING": + self.plugin.log_debug(_("[%s] Ping? Pong!") % args[0]) + self.irc_sock.send("PONG :%s\r\n" % args[0]) + + elif origin and command == "PRIVMSG": + sender_nick = origin.split('@')[0].split('!')[0] + recipient = args[0] + text = args[1] + + if text[0] == '\x01' and text[-1] == '\x01': #: CTCP + ctcp_data = text[1:-1].split(' ', 1) + ctcp_command = ctcp_data[0] + ctcp_args = ctcp_data[1] if len(ctcp_data) > 1 else "" + + if recipient[0:len(self.nick)] == self.nick: + if ctcp_command == "VERSION": + self.plugin.log_debug(_("[%s] CTCP VERSION") % sender_nick) + self.irc_sock.send("NOTICE %s :\x01VERSION %s\x01\r\n" % (sender_nick, "pyLoad! IRC Interface")) + + elif ctcp_command == "TIME": + self.plugin.log_debug(_("[%s] CTCP TIME") % sender_nick) + self.irc_sock.send("NOTICE %s :\x01%s\x01\r\n" % (sender_nick, time.strftime("%a %b %d %H:%M:%S %Y"))) + + elif ctcp_command == "PING": + self.plugin.log_debug(_("[%s] Ping? Pong!") % sender_nick) + self.irc_sock.send("NOTICE %s :\x01PING %s\x01\r\n" % (sender_nick, ctcp_args)) #@NOTE: PING is not a typo + + else: + break + + else: + break + + else: + break + + return origin, command, args + + @lock + def join_channel(self, chan): + chan = "#" + chan if chan[0] != '#' else chan + + self.plugin.log_info(_("Joining channel %s") % chan) + self.irc_sock.send("JOIN %s\r\n" % chan) + + start_time = time.time() + while time.time() - start_time < 30: + origin, command, args = self.get_irc_command() + + #: ERR_KEYSET, ERR_CHANNELISFULL, ERR_INVITEONLYCHAN, ERR_BANNEDFROMCHAN, ERR_BADCHANNELKEY + if command in ("467", "471", "473", "474", "475") and args[1].lower() == chan.lower(): + self.plugin.log_error(_("Cannot join channel %s (error %s: '%s')") % (chan, command, args[2])) + return False + + elif command == "353" and args[2].lower() == chan.lower(): #: RPL_NAMREPLY + self.plugin.log_debug(_("Successfully joined channel %s") % chan) + return True + + return False + + @lock + def nickserv_identify(self, password): + self.plugin.log_info(_("Authenticating nickname")) + + bot = "nickserv" + bot_host = self.get_bot_host(bot) + + if not bot_host: + self.plugin.log_warning(_("Server does not seems to support nickserv commands")) + return + + self.irc_sock.send("PRIVMSG %s :identify %s\r\n" % (bot, password)) + + start_time = time.time() + while time.time() - start_time < 30: + origin, command, args = self.get_irc_command() + + if origin is None \ + or command is None \ + or args is None: + return + + # Private message from bot to us? + if '@' not in origin \ + or (origin[0:len(bot)] != bot and origin.split('@')[1] != bot_host) \ + or args[0][0:len(self.nick)] != self.nick \ + or command not in ("PRIVMSG", "NOTICE"): + continue + + text = decode_text(args[1]) + sender_nick = origin.split('@')[0].split('!')[0] + self.plugin.log_info(_("PrivMsg: <%s> %s") % (sender_nick, text)) + break + + else: + self.plugin.log_warning(_("'%s' did not respond to the request") % bot) + + @lock + def send_invite_request(self, bot, chan, password): + bot_host = self.get_bot_host(bot) + if bot_host: + self.plugin.log_info(_("Sending invite request for #%s to '%s'") % (chan, bot)) + else: + self.plugin.log_warning(_("Cannot send invite request")) + return + + self.irc_sock.send("PRIVMSG %s :enter #%s %s %s\r\n" % (bot, chan, self.nick, password)) + start_time = time.time() + while time.time() - start_time < 30: + origin, command, args = self.get_irc_command() + + if origin is None \ + or command is None \ + or args is None: + return + + # Private message from bot to us? + if '@' not in origin \ + or (origin[0:len(bot)] != bot and origin.split('@')[1] != bot_host) \ + or args[0][0:len(self.nick)] != self.nick \ + or command not in ("PRIVMSG", "NOTICE", "INVITE"): + continue + + text = decode_text(args[1]) + sender_nick = origin.split('@')[0].split('!')[0] + if command == "INVITE": + self.plugin.log_info(_("Got invite to #%s") % chan) + + else: + self.plugin.log_info(_("PrivMsg: <%s> %s") % (sender_nick, text)) + + break + + else: + self.plugin.log_warning(_("'%s' did not respond to the request") % bot) + + @lock + def is_bot_online(self, bot): + self.plugin.log_info(_("Checking if bot '%s' is online") % bot) + self.irc_sock.send("WHOIS %s\r\n" % bot) + + start_time = time.time() + while time.time() - start_time < 30: + origin, command, args = self.get_irc_command() + if command == '401' and args[0] == self.nick and args[1].lower() == bot.lower(): #: ERR_NOSUCHNICK + self.plugin.log_debug(_("Bot '%s' is offline") % bot) + return False + + elif command == "311" and args[0] == self.nick and args[1].lower() == bot.lower(): #: RPL_WHOISUSER + self.plugin.log_debug(_("Bot '%s' is online") % bot) + self.bot_host[bot] = args[3] # bot host + return True + + else: + time.sleep(0.1) + + else: + self.plugin.log_error(_("Server did not respond in a reasonable time")) + return False + + @lock + def get_bot_host(self, bot): + bot_host = self.bot_host.get(bot) + if bot_host: + return bot_host + + else: + if self.is_bot_online(bot): + return self.bot_host.get(bot) + + else: + return None + + @lock + def xdcc_request_pack(self, bot, pack): + self.plugin.log_info(_("Requesting pack #%s") % pack) + self.xdcc_request_time = time.time() + self.irc_sock.send("PRIVMSG %s :xdcc send #%s\r\n" % (bot, pack)) + + @lock + def xdcc_cancel_pack(self, bot): + if self.xdcc_request_time: + self.plugin.log_info(_("Requesting XDCC cancellation")) + self.xdcc_request_time = 0 + self.irc_sock.send("PRIVMSG %s :xdcc cancel\r\n" % bot) + + else: + self.plugin.log_warning(_("No XDCC request pending, cannot cancel")) + + @lock + def xdcc_remove_queued(self, bot): + if self.xdcc_request_time: + self.plugin.log_info(_("Requesting XDCC remove from queue")) + self.xdcc_request_time = 0 + self.irc_sock.send("PRIVMSG %s :xdcc remove\r\n" % bot) + + else: + self.plugin.log_warning(_("No XDCC request pending, cannot remove from queue")) + + @lock + def xdcc_query_queue_status(self, bot): + if self.xdcc_request_time: + self.plugin.log_info(_("Requesting XDCC queue status")) + self.xdcc_queue_query_time = time.time() + self.irc_sock.send("PRIVMSG %s :xdcc queue\r\n" % bot) + + else: + self.plugin.log_warning(_("No XDCC request pending, cannot query queue status")) + + @lock + def xdcc_request_resume(self, bot, dcc_port, file_name, resume_position): + if self.xdcc_request_time: + bot_host = self.get_bot_host(bot) + + self.plugin.log_info(_("Requesting XDCC resume of '%s' at position %s") % (file_name, resume_position)) + + self.irc_sock.send("PRIVMSG %s :\x01DCC RESUME \"%s\" %s %s\x01\r\n" % (bot, encode(file_name,'utf-8'), dcc_port, resume_position)) + + start_time = time.time() + while time.time() - start_time < 30: + origin, command, args = self.get_irc_command() + + # Private message from bot to us? + if origin and command and args \ + and '@' in origin \ + and (origin[0:len(bot)] == bot or bot_host and origin.split('@')[1] == bot_host) \ + and args[0][0:len(self.nick)] == self.nick \ + and command in ("PRIVMSG", "NOTICE"): + + text = decode_text(args[1]) + sender_nick = origin.split('@')[0].split('!')[0] + self.plugin.log_debug(_("PrivMsg: <%s> %s") % (sender_nick, text)) + + m = re.match(r'\x01DCC ACCEPT .*? %s (?P<RESUME_POS>\d+)\x01' % dcc_port, text) + if m: + self.plugin.log_debug(_("Bot '%s' acknowledged resume at position %s") % (sender_nick, m.group('RESUME_POS'))) + return int(m.group('RESUME_POS')) + + else: + time.sleep(0.1) + + self.plugin.log_warning(_("Timeout while waiting for resume acknowledge, not resuming")) + + else: + self.plugin.log_error(_("No XDCC request pending, cannot resume")) + + return 0 + + @lock + def xdcc_get_pack_info(self, bot, pack): + bot_host = self.get_bot_host(bot) + + self.plugin.log_info(_("Requesting pack #%s info") % pack) + self.irc_sock.send("PRIVMSG %s :xdcc info #%s\r\n" % (bot, pack)) + + info ={} + start_time = time.time() + while time.time() - start_time < 90: + origin, command, args = self.get_irc_command() + + # Private message from bot to us? + if origin and command and args \ + and (origin[0:len(bot)] == bot or bot_host and origin.split('@')[1] == bot_host) \ + and args[0][0:len(self.nick)] == self.nick \ + and command in ("PRIVMSG", "NOTICE"): + + text = decode_text(args[1]) + pack_info = text.split() + if pack_info[0].lower() == "filename": + self.plugin.log_debug(_("Filename: '%s'") % pack_info[1]) + info.update({'status': "online", 'name': pack_info[1]}) + + elif pack_info[0].lower() == "filesize": + self.plugin.log_debug(_("Filesize: '%s'") % pack_info[1]) + info.update({'status': "online", 'size': pack_info[1]}) + + else: + sender_nick = origin.split('@')[0].split('!')[0] + self.plugin.log_debug(_("PrivMsg: <%s> %s") % (sender_nick, text)) + + else: + if len(info) > 2: #: got both name and size + break + + time.sleep(0.1) + + else: + if len(info) == 0: + self.plugin.log_error(_("XDCC Bot '%s' did not answer") % bot) + return {'status': "offline", 'msg': "XDCC Bot did not answer"} + + return info + + +class XDCC(Hoster): + __name__ = "XDCC" + __type__ = "hoster" + __version__ = "0.55" + __status__ = "testing" + + __pattern__ = r'xdcc://(?P<SERVER>.*?)/#?(?P<CHAN>.*?)/(?P<BOT>.*?)/#?(?P<PACK>\d+)/?' + __config__ = [("nick", "str", "Nickname", "pyload"), + ("ident", "str", "Ident", "pyloadident"), + ("realname", "str", "Realname", "pyloadreal"), + ("try_resume", "bool", "Request XDCC resume?", True), + ("nick_pw", "str", "Registered nickname password (optional)", ""), + ("queued_timeout", "int", "Time to wait before failing if queued (minutes, 0 = infinite)", 300), + ("queue_query_interval", "int", "Interval to query queue position when queued (minutes, 0 = disabled)", 3), + ("response_timeout", "int", "XDCC Bot response timeout (seconds, minimum 60)", 300), + ("waiting_opts", "str", "Time to wait before requesting pack from the XDCC Bot (format: ircserver/channel/wait_seconds, ...)", 0), + ("invite_opts", "str", "Invite bots options (format: ircserver/channel/invitebot/password, ...)", ""), + ("channel_opts", "str", "Join custom channel before joining channel (format: ircserver/channel/customchannel, ...)", "")] + + __description__ = """Download from IRC XDCC bot""" + __license__ = "GPLv3" + __authors__ = [("jeix", "jeix@hasnomail.com"), + ("GammaC0de", "nitzo2001[AT]yahoo[DOT]com")] + + RE_QUEUED = re.compile(r'Added you to the (?:main|idle) queue for pack \d+ \("[^"]+"\) in position (\d+)') + RE_QUEUE_STAT = re.compile(r'^Queued \w+ for ".+?", in position (\d+).+?([\dhm]+) or less remaining') + RE_XDCC = re.compile(r'\x01DCC SEND "?(?P<NAME>.*?)"? (?P<IP>\d+) (?P<PORT>\d+)(?: (?P<SIZE>\d+))?\x01') + + def setup(self): + self.dl_started = False + self.dl_finished = False + self.last_response_time = 0 + self.queued_time = 0 + + self.irc_client = None + self.exc_info = None + + self.dcc_port = 0 + self.dcc_file_name = "" + self.dcc_sender_bot = None + self.bot_host = None + + self.multiDL = False + + def xdcc_send_resume(self, resume_position): + if not self.config.get('try_resume') or not self.dcc_sender_bot: + return 0 + + else: + return self.irc_client.xdcc_request_resume(self.dcc_sender_bot, self.dcc_port, self.dcc_file_name, resume_position) + + def process(self, pyfile): + server = self.info['pattern']['SERVER'] + chan = self.info['pattern']['CHAN'] + bot = self.info['pattern']['BOT'] + pack = self.info['pattern']['PACK'] + + temp = server.split(':') + ln = len(temp) + if ln == 2: + host, port = temp[0], int(temp[1]) + + elif ln == 1: + host, port = temp[0], 6667 + + else: + self.fail(_("Invalid hostname for IRC Server: %s") % server) + + nick = self.config.get('nick') + nick_pw = self.config.get('nick_pw') + ident = self.config.get('ident') + realname = self.config.get('realname') + + queued_timeout = self.config.get('queued_timeout') * 60 + queue_query_interval = self.config.get('queue_query_interval') * 60 + response_timeout = max(self.config.get('response_timeout'), 60) + self.config.set('response_timeout', response_timeout) + + waiting_opts = [_x.split('/') + for _x in self.config.get('waiting_opts').strip().split(',') + if len(_x.split('/')) == 3 and unicode(_x.split('/')[2]).isnumeric()] + + #: Remove leading '#' from channel name + for opt in waiting_opts: + opt[1] = opt[1][1:] if opt[1].startswith('#') else opt[1] + opt[2] = int(opt[2]) + + invite_opts = [_x.split('/') + for _x in self.config.get('invite_opts').strip().split(',') + if len(_x.split('/')) == 4] + + #: Remove leading '#' from channel name + for opt in invite_opts: + opt[1] = opt[1][1:] if opt[1].startswith('#') else opt[1] + + channel_opts = [_x.split('/') + for _x in self.config.get('channel_opts').strip().split(',') + if len(_x.split('/')) == 3] + + #: Remove leading '#' from channel name and custom channel name + for opt in channel_opts: + opt[1] = opt[1][1:] if opt[1].startswith('#') else opt[1] + opt[2] = opt[2][1:] if opt[2].startswith('#') else opt[2] + + #: Change request type + self.req.close() + self.req = self.pyload.requestFactory.getRequest(self.classname, type="XDCC") + + self.pyfile.setCustomStatus("connect irc") + + self.irc_client = IRC(self, nick, ident, realname) + for _i in range(3): + try: + if self.irc_client.connect_server(host, port): + try: + if nick_pw: + self.irc_client.nickserv_identify(nick_pw) + + for opt in invite_opts: + if opt[0].lower() == host.lower() and opt[1].lower() == chan.lower(): + self.irc_client.send_invite_request(opt[2], opt[1], opt[3]) + + for opt in channel_opts: + if opt[0].lower() == host.lower() and opt[1].lower() == chan.lower(): + if not self.irc_client.join_channel(opt[2]): + self.fail(_("Cannot join channel")) + + if not self.irc_client.join_channel(chan): + self.fail(_("Cannot join channel")) + + if not self.irc_client.is_bot_online(bot): + self.fail(_("Bot is offline")) + + for opt in waiting_opts: + if opt[0].lower() == host.lower() and opt[1].lower() == chan.lower() and opt[2] > 0: + self.wait(opt[2], reconnect=False) + + self.pyfile.setStatus("waiting") + + self.irc_client.xdcc_request_pack(bot, pack) + + # Main IRC loop + while (not self.pyfile.abort or self.dl_started) and not self.dl_finished: + if not self.dl_started: + if self.queued_time: + if queued_timeout and time.time() - self.queued_time > queued_timeout: + self.irc_client.xdcc_remove_queued(bot) + self.queued_time = False + self.irc_client.disconnect_server() + self.log_error(_("Timed out while waiting in the XDCC queue (%s seconds)") % queued_timeout) + self.retry(3, 60, _("Timed out while waiting in the XDCC queue (%s seconds)") % queued_timeout) + + elif queue_query_interval and time.time() - self.irc_client.xdcc_queue_query_time > queue_query_interval: + self.irc_client.xdcc_query_queue_status(bot) + + elif self.last_response_time: + if time.time() - self.last_response_time > response_timeout: + self.irc_client.xdcc_request_pack(bot, pack) + self.last_response_time = 0 + + else: + if self.irc_client.xdcc_request_time and time.time() - self.irc_client.xdcc_request_time > response_timeout: + self.irc_client.disconnect_server() + self.log_error(_("XDCC Bot did not answer")) + self.retry(3, 60, _("XDCC Bot did not answer")) + + origin, command, args = self.irc_client.get_irc_command() + self.process_irc_command(origin, command, args) + + if self.exc_info: + raise self.exc_info[1], None, self.exc_info[2] + + finally: + if self.pyfile.abort and not self.dl_started and self.queued_time: + self.irc_client.xdcc_remove_queued(bot) + self.irc_client.disconnect_server() + + return + + except socket.error, e: + if hasattr(e, "errno") and e.errno is not None: + err_no = e.errno + + if err_no in (10054, 10061): + self.log_warning("Server blocked our ip, retry in 5 min") + self.wait(300) + continue + + else: + self.log_error(_("Failed due to socket errors. Code: %s") % err_no) + self.fail(_("Failed due to socket errors. Code: %s") % err_no) + + else: + err_msg = e.args[0] + self.log_error(_("Failed due to socket errors: '%s'") % err_msg) + self.fail(_("Failed due to socket errors: '%s'") % err_msg) + + self.log_error(_("Server blocked our ip, retry again later manually")) + self.fail(_("Server blocked our ip, retry again later manually")) + + def process_irc_command(self, origin, command, args): + bot = self.info['pattern']['BOT'] + nick = self.config.get('nick') + + if origin is None\ + or command is None\ + or args is None: + return + + #: ERR_CANTSENDTOUSER + if command == "531" and args[0] == nick and args[1] == bot: + text = decode_text(args[2]) + self.log_error("<%s> %s" % (bot, text)) + self.fail(text) + + # Private message from bot to us? + bot_host = self.irc_client.get_bot_host(bot) + if '@' not in origin \ + or (origin[0:len(bot)] != bot and bot_host and origin.split('@')[1] != bot_host) \ + or args[0][0:len(nick)] != nick \ + or command not in ("PRIVMSG", "NOTICE"): + return + + text = decode_text(args[1]) + sender_nick = origin.split('@')[0].split('!')[0] + self.log_debug(_("PrivMsg: <%s> %s") % (sender_nick, text)) + + if not self.queued_time and text in ("You already requested that pack", "All Slots Full", "You have a DCC pending"): + self.last_response_time = time.time() + + elif "you must be on a known channel to request a pack" in text: + self.log_error(_("Invalid channel")) + self.fail(_("Invalid channel")) + + elif "Invalid Pack Number" in text: + self.log_error(_("Invalid Pack Number")) + self.fail(_("Invalid Pack Number")) + + else: + m = self.RE_XDCC.match(text) #: XDCC? + if m: + ip = socket.inet_ntoa(struct.pack('!I', int(m.group('IP')))) + self.dcc_port = int(m.group('PORT')) + self.dcc_file_name = m.group('NAME') + self.dcc_sender_bot = origin.split('@')[0].split('!')[0] + file_size = int(m.group('SIZE')) if m.group('SIZE') else 0 + + self.do_download(ip, self.dcc_port, self.dcc_file_name, file_size) + + else: + m = self.RE_QUEUED.search(text) + if m: + self.queued_time = self.last_response_time = time.time() + self.log_info(_("Got queued at position %s") % m.group(1)) + + else: + m = self.RE_QUEUE_STAT.search(text) + if m: + self.log_info(_("Waiting in the queue for about %s, current position %s, estimated remaining wait time: %s") % + (format_time(time.time() - self.queued_time), m.group(1), m.group(2))) + + def _on_notification(self, notification): + if 'progress' in notification: + self.pyfile.setProgress(notification['progress']) + + @threaded + def do_download(self, ip, port, file_name, file_size): + if self.dl_started: + return + + self.dl_started = True + + try: + self.pyfile.name = file_name + self.req.filesize = file_size + + self.pyfile.setStatus("downloading") + + dl_folder = fsjoin(self.pyload.config.get('general', 'download_folder'), + self.pyfile.package().folder if self.pyload.config.get("general", + "folder_per_package") else "") + if not exists(dl_folder): + try: + os.makedirs(dl_folder) + + except OSError, e: + self.fail(e.strerror) + + self.set_permissions(dl_folder) + + self.check_duplicates() + + dl_file = fsjoin(dl_folder, self.pyfile.name) + + self.log_debug(_("DOWNLOAD XDCC '%s' from %s:%d") % (file_name, ip, port)) + + self.pyload.hookManager.dispatchEvent("download_start", self.pyfile, "%s:%s" % (ip, port), dl_file) + + newname = self.req.download(ip, port, dl_file, status_notify=self._on_notification, resume=self.xdcc_send_resume) + + if newname and newname != dl_file: + self.log_info(_("%(name)s saved as %(newname)s") % {'name': self.pyfile.name, 'newname': newname}) + dl_file = newname + + self.last_download = dl_file + + except Abort: + pass + + except Exception, e: + bot = self.info['pattern']['BOT'] + self.irc_client.xdcc_cancel_pack(bot) + + if not self.exc_info: + self.exc_info = sys.exc_info() #: pass the exception to the main thread + + self.dl_finished = True diff --git a/module/plugins/hoster/XFileSharing.py b/module/plugins/hoster/XFileSharing.py new file mode 100644 index 0000000000..abda7dd3fe --- /dev/null +++ b/module/plugins/hoster/XFileSharing.py @@ -0,0 +1,66 @@ +# -*- coding: utf-8 -*- + +import re + +from ..internal.XFSHoster import XFSHoster + + +class XFileSharing(XFSHoster): + __name__ = "XFileSharing" + __type__ = "hoster" + __version__ = "0.67" + __status__ = "testing" + + __pattern__ = r'^unmatchable$' + __config__ = [("activated", "bool", "Activated", False), + ("use_premium", "bool", "Use premium account if available", True), + ("fallback", "bool", + "Fallback to free download if premium fails", True), + ("chk_filesize", "bool", "Check file size", True), + ("max_wait", "int", "Reconnect if waiting time is greater than minutes", 10)] + + __description__ = """XFileSharing dummy hoster plugin for hook""" + __license__ = "GPLv3" + __authors__ = [("Walter Purcaro", "vuolter@gmail.com")] + + URL_REPLACEMENTS = [("/embed-", "/")] + + def _log(self, level, plugintype, pluginname, messages, tbframe=None): + messages = (self.PLUGIN_NAME,) + messages + return XFSHoster._log(self, level, plugintype, pluginname, messages, tbframe=tbframe) + + def init(self): + self.__pattern__ = self.pyload.pluginManager.hosterPlugins[ + self.classname]['pattern'] + + self.PLUGIN_DOMAIN = re.match( + self.__pattern__, + self.pyfile.url).group("DOMAIN").lower() + self.PLUGIN_NAME = "".join( + part.capitalize() for part in re.split( + r'\.|\d+|-', self.PLUGIN_DOMAIN) if part != '.') + + def setup(self): + self.chunk_limit = -1 if self.premium else 1 + self.multiDL = True + self.resume_download = self.premium + + #@TODO: Recheck in 0.4.10 + def setup_base(self): + XFSHoster.setup_base(self) + + if self.account: + self.req = self.pyload.requestFactory.getRequest( + self.PLUGIN_NAME, self.account.user) + # @NOTE: Don't call get_info here to reduce overhead + self.premium = self.account.info['data']['premium'] + else: + self.req = self.pyload.requestFactory.getRequest(self.classname) + self.premium = False + + #@TODO: Recheck in 0.4.10 + def load_account(self): + class_name = self.classname + self.__class__.__name__ = str(self.PLUGIN_NAME) + XFSHoster.load_account(self) + self.__class__.__name__ = class_name diff --git a/module/plugins/hoster/XFileSharingPro.py b/module/plugins/hoster/XFileSharingPro.py deleted file mode 100644 index d6fb313078..0000000000 --- a/module/plugins/hoster/XFileSharingPro.py +++ /dev/null @@ -1,334 +0,0 @@ -# -*- 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: zoidberg -""" - -import re -from random import random -from urllib import unquote -from urlparse import urlparse -from pycurl import FOLLOWLOCATION, LOW_SPEED_TIME -from module.plugins.internal.SimpleHoster import SimpleHoster, create_getInfo, PluginParseError -from module.plugins.internal.CaptchaService import ReCaptcha, SolveMedia -from module.utils import html_unescape -from module.network.RequestFactory import getURL - - -class XFileSharingPro(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__ = "XFileSharingPro" - __type__ = "hoster" - __pattern__ = r"^unmatchable$" - __version__ = "0.23" - __description__ = """XFileSharingPro common hoster base""" - __author_name__ = ("zoidberg", "stickell") - __author_mail__ = ("zoidberg@mujmail.cz", "l.stickell@yahoo.it") - - FILE_NAME_PATTERN = r'<input type="hidden" name="fname" value="(?P<N>[^"]+)"' - FILE_SIZE_PATTERN = r'You have requested <font color="red">[^<]+</font> \((?P<S>[^<]+)\)</font>' - FILE_INFO_PATTERN = r'<tr><td align=right><b>Filename:</b></td><td nowrap>(?P<N>[^<]+)</td></tr>\s*.*?<small>\((?P<S>[^<]+)\)</small>' - FILE_OFFLINE_PATTERN = r'<(b|h[1-6])>File Not Found</(b|h[1-6])>' - - WAIT_PATTERN = r'<span id="countdown_str">.*?>(\d+)</span>' - LONG_WAIT_PATTERN = r'(?P<H>\d+(?=\s*hour))?.*?(?P<M>\d+(?=\s*minute))?.*?(?P<S>\d+(?=\s*second))?' - OVR_DOWNLOAD_LINK_PATTERN = r'<h2>Download Link</h2>\s*<textarea[^>]*>([^<]+)' - OVR_KILL_LINK_PATTERN = r'<h2>Delete Link</h2>\s*<textarea[^>]*>([^<]+)' - CAPTCHA_URL_PATTERN = r'(http://[^"\']+?/captchas?/[^"\']+)' - RECAPTCHA_URL_PATTERN = r'http://[^"\']+?recaptcha[^"\']+?\?k=([^"\']+)"' - CAPTCHA_DIV_PATTERN = r'<b>Enter code.*?<div.*?>(.*?)</div>' - SOLVEMEDIA_PATTERN = r'http:\/\/api\.solvemedia\.com\/papi\/challenge\.script\?k=(.*?)"' - ERROR_PATTERN = r'class=["\']err["\'][^>]*>(.*?)</' - - def setup(self): - if self.__name__ == "XFileSharingPro": - self.__pattern__ = self.core.pluginManager.hosterPlugins[self.__name__]['pattern'] - self.multiDL = True - else: - self.resumeDownload = self.multiDL = self.premium - - self.chunkLimit = 1 - - def process(self, pyfile): - self.prepare() - - if not re.match(self.__pattern__, self.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.5 as the cookies bug has been fixed. - self.html = getURL(pyfile.url, decode=True) - 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 prepare(self): - """ Initialize important variables """ - if not hasattr(self, "HOSTER_NAME"): - self.HOSTER_NAME = re.search(self.__pattern__, self.pyfile.url).group(1) - if not hasattr(self, "DIRECT_LINK_PATTERN"): - self.DIRECT_LINK_PATTERN = r'(http://([^/]*?%s|\d+\.\d+\.\d+\.\d+)(:\d+/d/|(?:/files)?/\d+/\w+/)[^"\'<]+)' % self.HOSTER_NAME - - self.captcha = self.errmsg = None - self.passwords = self.getPassword().splitlines() - - 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, cookies=True, decode=True) - self.header = self.req.http.header - self.req.http.c.setopt(FOLLOWLOCATION, 1) - - location = None - found = re.search(r"Location\s*:\s*(.*)", self.header, re.I) - if found and re.match(self.DIRECT_LINK_PATTERN, found.group(1)): - location = found.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 range(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) - - found = re.search(r"Location\s*:\s*(.*)", self.header, re.I) - if found: - break - - found = re.search(self.DIRECT_LINK_PATTERN, self.html, re.S) - if found: - 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 found.group(1) - - def handlePremium(self): - self.html = self.load(self.pyfile.url, post=self.getPostParameters()) - found = re.search(self.DIRECT_LINK_PATTERN, self.html) - if not found: - self.parseError('DIRECT LINK') - self.startDownload(found.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') - 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=180, reason=inputs['st']) - else: - self.fail(inputs['st']) - - #get easybytez.com link for uploaded file - found = re.search(self.OVR_DOWNLOAD_LINK_PATTERN, self.html) - if not found: - self.parseError('DIRECT LINK (OVR)') - self.pyfile.url = found.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): - found = re.search(self.ERROR_PATTERN, self.html) - if found: - self.errmsg = found.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.setWait(wait_time, True) - self.wait() - 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.setWait(3600, True) - self.wait() - self.retry(25) - elif 'countdown' in self.errmsg or 'Expired session' in self.errmsg: - self.retry(3) - 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 range(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: - found = re.search(self.WAIT_PATTERN, self.html) - if found: - wait_time = int(found.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): - found = re.search(self.RECAPTCHA_URL_PATTERN, self.html) - if found: - recaptcha_key = unquote(found.group(1)) - self.logDebug("RECAPTCHA KEY: %s" % recaptcha_key) - recaptcha = ReCaptcha(self) - inputs['recaptcha_challenge_field'], inputs['recaptcha_response_field'] = recaptcha.challenge(recaptcha_key) - return 1 - else: - found = re.search(self.CAPTCHA_URL_PATTERN, self.html) - if found: - captcha_url = found.group(1) - inputs['code'] = self.decryptCaptcha(captcha_url) - return 2 - else: - found = re.search(self.CAPTCHA_DIV_PATTERN, self.html, re.S) - if found: - captcha_div = found.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 3 - else: - found = re.search(self.SOLVEMEDIA_PATTERN, self.html) - if found: - captcha_key = found.group(1) - captcha = SolveMedia(self) - inputs['adcopy_challenge'], inputs['adcopy_response'] = captcha.challenge(captcha_key) - return 4 - return 0 - - -getInfo = create_getInfo(XFileSharingPro) diff --git a/module/plugins/hoster/XHamsterCom.py b/module/plugins/hoster/XHamsterCom.py index d2c55e8bca..2317806b43 100644 --- a/module/plugins/hoster/XHamsterCom.py +++ b/module/plugins/hoster/XHamsterCom.py @@ -1,28 +1,38 @@ -#!/usr/bin/env python # -*- coding: utf-8 -*- import re -from urllib import unquote -from module.plugins.Hoster import Hoster -from module.common.json_layer import json_loads +from ..internal.Hoster import Hoster +from ..internal.misc import json -def clean_json(json_expr): - json_expr = re.sub('[\n\r]', '', json_expr) - json_expr = re.sub(' +', '', json_expr) - json_expr = re.sub('\'', '"', json_expr) +def quality_fallback(desired, available): + result = available.get(desired, None) + if result is None: + if desired == "720p": + return quality_fallback("480p", available) + elif desired == "480p": + return quality_fallback("240p", available) + else: + # Return the entry starting with the lowest digit (shoud be 240p) + (quality, result) = sorted(available.iteritems(), key=lambda x: x[0], reverse=True)[0] - return json_expr + return result class XHamsterCom(Hoster): __name__ = "XHamsterCom" __type__ = "hoster" - __pattern__ = r"http://(www\.)?xhamster\.com/movies/.+" - __version__ = "0.12" - __config__ = [("type", ".mp4;.flv", "Preferred type", ".mp4")] - __description__ = """XHamster.com Video Download Hoster""" + __version__ = "0.19" + __status__ = "testing" + + __pattern__ = r'https?://(?:\w+\.)?xhamster\.com/videos/.+' + __config__ = [("activated", "bool", "Activated", True), + ("quality", "720p;480p;240p", "Preferred quality", "480p")] + + __description__ = """XHamster.com hoster plugin""" + __license__ = "GPLv3" + __authors__ = [] def process(self, pyfile): self.pyfile = pyfile @@ -30,89 +40,62 @@ def process(self, pyfile): if not self.file_exists(): self.offline() - if self.getConfig("type"): - self.desired_fmt = self.getConfig("type") + quality = self.config.get('quality') + self.desired_quality = quality if quality is not None else '480p' - self.pyfile.name = self.get_file_name() + self.desired_fmt + pyfile.name = self.get_file_name() + '.' + self.desired_quality + '.mp4' self.download(self.get_file_url()) def download_html(self): url = self.pyfile.url - self.html = self.load(url) + self.data = self.load(url) def get_file_url(self): - """ returns the absolute downloadable filepath """ - if self.html is None: + Returns the absolute downloadable filepath + """ + if not self.data: self.download_html() - flashvar_pattern = re.compile('flashvars = ({.*?});', re.DOTALL) - json_flashvar = flashvar_pattern.search(self.html) + video_data_re = r'(?ms)<script\s+id="initials-script"\s*>.*?window\.initials\s*=\s*({.*?});\s*<\/script>' + video_data_search = re.search(video_data_re, self.data) - if json_flashvar is None: - self.fail("Parse error (flashvars)") + if not video_data_search: + self.error(_("video data not found")) - j = clean_json(json_flashvar.group(1)) - flashvars = json_loads(j) + video_data = json.loads(video_data_search.group(1)) - if flashvars["srv"]: - srv_url = flashvars["srv"] + '/' - else: - self.fail("Parse error (srv_url)") + video_model = video_data.get('videoModel', None) + if video_model is None: + self.error(_("Could not find video model!")) - if flashvars["url_mode"]: - url_mode = flashvars["url_mode"] - else: - self.fail("Parse error (url_mode)") - - if self.desired_fmt == ".mp4": - file_url = re.search(r"<a href=\"" + srv_url + "(.+?)\"", self.html) - if file_url is None: - self.fail("Parse error (file_url)") - file_url = file_url.group(1) - long_url = srv_url + file_url - self.logDebug("long_url: %s" % long_url) - else: - if flashvars["file"]: - file_url = unquote(flashvars["file"]) - else: - self.fail("Parse error (file_url)") - - if url_mode == '3': - long_url = file_url - self.logDebug("long_url: %s" % long_url) - else: - long_url = srv_url + "key=" + file_url - self.logDebug("long_url: %s" % long_url) + sources = video_model.get('sources', None) + if sources is None: + self.error(_("Could not find sources!")) + + mp4_sources = sources.get('mp4', None) + if mp4_sources is None: + self.error(_("Could not find mp4 sources!")) + + long_url = quality_fallback(self.desired_quality, mp4_sources) return long_url def get_file_name(self): - if self.html is None: + if not self.data: self.download_html() - file_name_pattern = r"<title>(.*?) - xHamster\.com" - file_name = re.search(file_name_pattern, self.html) - if file_name is None: - file_name_pattern = r"

      (.*)

      " - file_name = re.search(file_name_pattern, self.html) - if file_name is None: - file_name_pattern = r"http://[www.]+xhamster\.com/movies/.*/(.*?)\.html?" - file_name = re.search(file_name_pattern, self.pyfile.url) - if file_name is None: - file_name_pattern = r"
      (.*)
      " - file_name = re.search(file_name_pattern, self.html) - if file_name is None: - return "Unknown" - - return file_name.group(1) + pattern = r'([^<]+)Filename:\s*
      \s*(?P.*?)\n' + SIZE_PATTERN = r'\s*
      \s*(?P[\d.,]+)(?P[\w^_]+)' + OFFLINE_PATTERN = r' Device Filter' + + def setup(self): + self.multiDL = True + self.resume_download = True + self.chunk_limit = 1 + + def handle_free(self, pyfile): + # @TODO: Revert to `get={'task': "get"}` in 0.4.10 + self.link = pyfile.url + "&task=get" diff --git a/module/plugins/hoster/Xdcc.py b/module/plugins/hoster/Xdcc.py deleted file mode 100644 index bd8c6025a2..0000000000 --- a/module/plugins/hoster/Xdcc.py +++ /dev/null @@ -1,224 +0,0 @@ -# -*- 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 . - - @author: jeix -""" - -from os.path import join -from os.path import exists -from os import makedirs -import re -import sys -import time -import socket -import struct -from select import select - -from module.utils import save_join -from module.plugins.Hoster import Hoster - - -class Xdcc(Hoster): - __name__ = "Xdcc" - __version__ = "0.32" - __pattern__ = r'xdcc://.*?(/#?.*?)?/.*?/#?\d+/?' # xdcc://irc.Abjects.net/#channel/[XDCC]|Shit/#0004/ - __type__ = "hoster" - __config__ = [ - ("nick", "str", "Nickname", "pyload"), - ("ident", "str", "Ident", "pyloadident"), - ("realname", "str", "Realname", "pyloadreal") - ] - __description__ = """A Plugin that allows you to download from an IRC XDCC bot""" - __author_name__ = ("jeix") - __author_mail__ = ("jeix@hasnomail.com") - - def setup(self): - self.debug = 0 # 0,1,2 - self.timeout = 30 - self.multiDL = False - - def process(self, pyfile): - # change request type - self.req = pyfile.m.core.requestFactory.getRequest(self.__name__, type="XDCC") - - self.pyfile = pyfile - for i in range(0, 3): - try: - nmn = self.doDownload(pyfile.url) - self.logDebug("%s: Download of %s finished." % (self.__name__, nmn)) - return - except socket.error, e: - if hasattr(e, "errno"): - errno = e.errno - else: - errno = e.args[0] - - if errno in (10054,): - self.logDebug("XDCC: Server blocked our ip, retry in 5 min") - self.setWait(300) - self.wait() - continue - - self.fail("Failed due to socket errors. Code: %d" % errno) - - self.fail("Server blocked our ip, retry again later manually") - - def doDownload(self, url): - self.pyfile.setStatus("waiting") # real link - - download_folder = self.config['general']['download_folder'] - location = join(download_folder, self.pyfile.package().folder.decode(sys.getfilesystemencoding())) - if not exists(location): - makedirs(location) - - m = re.search(r'xdcc://(.*?)/#?(.*?)/(.*?)/#?(\d+)/?', url) - server = m.group(1) - chan = m.group(2) - bot = m.group(3) - pack = m.group(4) - nick = self.getConfig('nick') - ident = self.getConfig('ident') - real = self.getConfig('realname') - - temp = server.split(':') - ln = len(temp) - if ln == 2: - host, port = temp - elif ln == 1: - host, port = temp[0], 6667 - else: - self.fail("Invalid hostname for IRC Server (%s)" % server) - - ####################### - # CONNECT TO IRC AND IDLE FOR REAL LINK - dl_time = time.time() - - sock = socket.socket() - sock.connect((host, int(port))) - if nick == "pyload": - nick = "pyload-%d" % (time.time() % 1000) # last 3 digits - sock.send("NICK %s\r\n" % nick) - sock.send("USER %s %s bla :%s\r\n" % (ident, host, real)) - time.sleep(3) - sock.send("JOIN #%s\r\n" % chan) - sock.send("PRIVMSG %s :xdcc send #%s\r\n" % (bot, pack)) - - # IRC recv loop - readbuffer = "" - done = False - retry = None - m = None - while True: - - # done is set if we got our real link - if done: - break - - if retry: - if time.time() > retry: - retry = None - dl_time = time.time() - sock.send("PRIVMSG %s :xdcc send #%s\r\n" % (bot, pack)) - - else: - if (dl_time + self.timeout) < time.time(): # todo: add in config - sock.send("QUIT :byebye\r\n") - sock.close() - self.fail("XDCC Bot did not answer") - - fdset = select([sock], [], [], 0) - if sock not in fdset[0]: - continue - - readbuffer += sock.recv(1024) - temp = readbuffer.split("\n") - readbuffer = temp.pop() - - for line in temp: - if self.debug is 2: - print "*> " + unicode(line, errors='ignore') - line = line.rstrip() - first = line.split() - - if first[0] == "PING": - sock.send("PONG %s\r\n" % first[1]) - - if first[0] == "ERROR": - self.fail("IRC-Error: %s" % line) - - msg = line.split(None, 3) - if len(msg) != 4: - continue - - msg = { - "origin": msg[0][1:], - "action": msg[1], - "target": msg[2], - "text": msg[3][1:] - } - - if nick == msg["target"][0:len(nick)] and "PRIVMSG" == msg["action"]: - if msg["text"] == "\x01VERSION\x01": - self.logDebug("XDCC: Sending CTCP VERSION.") - sock.send("NOTICE %s :%s\r\n" % (msg['origin'], "pyLoad! IRC Interface")) - elif msg["text"] == "\x01TIME\x01": - self.logDebug("Sending CTCP TIME.") - sock.send("NOTICE %s :%d\r\n" % (msg['origin'], time.time())) - elif msg["text"] == "\x01LAG\x01": - pass # don't know how to answer - - if not (bot == msg["origin"][0:len(bot)] - and nick == msg["target"][0:len(nick)] - and msg["action"] in ("PRIVMSG", "NOTICE")): - continue - - if self.debug is 1: - print "%s: %s" % (msg["origin"], msg["text"]) - - if "You already requested that pack" in msg["text"]: - retry = time.time() + 300 - - if "you must be on a known channel to request a pack" in msg["text"]: - self.fail("Wrong channel") - - m = re.match('\x01DCC SEND (.*?) (\d+) (\d+)(?: (\d+))?\x01', msg["text"]) - if m: - done = True - - # get connection data - ip = socket.inet_ntoa(struct.pack('L', socket.ntohl(int(m.group(2))))) - port = int(m.group(3)) - packname = m.group(1) - - if len(m.groups()) > 3: - self.req.filesize = int(m.group(4)) - - self.pyfile.name = packname - filename = save_join(location, packname) - self.logInfo("XDCC: Downloading %s from %s:%d" % (packname, ip, port)) - - self.pyfile.setStatus("downloading") - newname = self.req.download(ip, port, filename, sock, self.pyfile.setProgress) - if newname and newname != filename: - self.logInfo("%(name)s saved as %(newname)s" % {"name": self.pyfile.name, "newname": newname}) - filename = newname - - # kill IRC socket - # sock.send("QUIT :byebye\r\n") - sock.close() - - self.lastDownload = filename - return self.lastDownload diff --git a/module/plugins/hoster/YadiSk.py b/module/plugins/hoster/YadiSk.py new file mode 100644 index 0000000000..f7683d3c7a --- /dev/null +++ b/module/plugins/hoster/YadiSk.py @@ -0,0 +1,88 @@ +# -*- coding: utf-8 -*- + +import random +import re + +from ..internal.misc import json +from ..internal.SimpleHoster import SimpleHoster + + +class YadiSk(SimpleHoster): + __name__ = "YadiSk" + __type__ = "hoster" + __version__ = "0.12" + __status__ = "testing" + + __pattern__ = r'https?://yadi\.sk/d/[\w\-]+' + __config__ = [("activated", "bool", "Activated", True), + ("use_premium", "bool", "Use premium account if available", True), + ("fallback", "bool", + "Fallback to free download if premium fails", True), + ("chk_filesize", "bool", "Check file size", True), + ("max_wait", "int", "Reconnect if waiting time is greater than minutes", 10)] + + __description__ = """Yadi.sk hoster plugin""" + __license__ = "GPLv3" + __authors__ = [("GammaC0de", None)] + + OFFLINE_PATTERN = r'Nothing found' + + def get_info(self, url="", html=""): + info = SimpleHoster.get_info(url, html) + + if html: + if 'idclient' not in info: + info['idclient'] = "" + for _i in range(32): + info['idclient'] += random.choice('0123456abcdef') + + m = re.search( + r'', html) + if m is not None: + api_data = json.loads(m.group(1)) + try: + for sect in api_data: + if 'model' in sect: + if sect['model'] == "config": + info['version'] = sect['data']['version'] + info['sk'] = sect['data']['sk'] + + elif sect['model'] == "resource": + info['id'] = sect['data']['id'] + info['size'] = sect['data']['meta']['size'] + info['name'] = sect['data']['name'] + + except Exception, e: + info['status'] = 8 + info['error'] = _( + "Unexpected server response: %s") % e.message + + else: + info['status'] = 8 + info['error'] = _("could not find required json data") + + return info + + def setup(self): + self.resume_download = False + self.multiDL = False + self.chunk_limit = 1 + + def handle_free(self, pyfile): + if any(True for _k in ['id', 'sk', 'version', + 'idclient'] if _k not in self.info): + self.error(_("Missing JSON data")) + + try: + self.data = self.load("https://yadi.sk/models/", + get={'_m': "do-get-resource-url"}, + post={'idClient': self.info['idclient'], + 'version': self.info['version'], + '_model.0': "do-get-resource-url", + 'sk': self.info['sk'], + 'id.0': self.info['id']}) + + self.link = json.loads(self.data)['models'][0]['data']['file'] + + except Exception: + pass diff --git a/module/plugins/hoster/YesPornPleaseCom.py b/module/plugins/hoster/YesPornPleaseCom.py new file mode 100644 index 0000000000..c36c55b878 --- /dev/null +++ b/module/plugins/hoster/YesPornPleaseCom.py @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- + +import re + +from ..internal.Hoster import Hoster + + +class YesPornPleaseCom(Hoster): + __name__ = "YesPornPleaseCom" + __type__ = "hoster" + __version__ = "0.02" + __status__ = "testing" + + __pattern__ = r'https?://(?:www\.)?yespornplease\.com/view/(\d+)' + __config__ = [("activated", "bool", "Activated", True), + ("quality", "240p;360p;480p;720p", "Quality", "720p")] + + __description__ = """YesPornPlease.Com hoster plugin""" + __license__ = "GPLv3" + __authors__ = [("ondrej", "git@ondrej.it")] + + NAME_PATTERN = r'(.+) watch online for free' + + def setup(self): + self.resume_download = True + self.multiDL = True + + def process(self, pyfile): + resp = self.load(pyfile.url) + iframe_url = re.findall(r'<iframe src="([^"]+)', resp) + if not iframe_url: + self.error(_("Iframe url not found")) + + iframe_resp = self.load("http:" + iframe_url[0]) + video_url = re.findall(r'<source src="([^"]+)', iframe_resp) + if not video_url: + self.error(_("Video url not found")) + + self.pyfile.name = re.findall(self.NAME_PATTERN, resp)[0] + self.pyfile.name += "." + video_url[0].split(".")[-1] + + self.log_info(_("Downloading file...")) + + quality = self.config.get("quality") + quality_index = ["720p", "480p", "360p", "240p"] + q = quality_index.index(quality) + self.download(video_url[q]) diff --git a/module/plugins/hoster/YibaishiwuCom.py b/module/plugins/hoster/YibaishiwuCom.py index 37eaa17e5d..f39345dbaa 100644 --- a/module/plugins/hoster/YibaishiwuCom.py +++ b/module/plugins/hoster/YibaishiwuCom.py @@ -1,58 +1,70 @@ # -*- 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: zoidberg -""" import re -from module.plugins.internal.SimpleHoster import SimpleHoster, create_getInfo -from module.common.json_layer import json_loads +import urlparse + +from ..internal.misc import json +from ..internal.SimpleHoster import SimpleHoster class YibaishiwuCom(SimpleHoster): __name__ = "YibaishiwuCom" __type__ = "hoster" - __pattern__ = r"http://(?:www\.)?(?:u\.)?115.com/file/(?P<ID>\w+)" - __version__ = "0.12" - __description__ = """115.com""" - __author_name__ = ("zoidberg") - - FILE_NAME_PATTERN = r"file_name: '(?P<N>[^']+)'" - FILE_SIZE_PATTERN = r"file_size: '(?P<S>[^']+)'" - FILE_OFFLINE_PATTERN = ur'<h3><i style="color:red;">哎呀!提取码不存在!不妨搜搜看吧!</i></h3>' - - AJAX_URL_PATTERN = r'(/\?ct=(pickcode|download)[^"\']+)' - - def handleFree(self): - found = re.search(self.AJAX_URL_PATTERN, self.html) - if not found: - self.parseError("AJAX URL") - url = found.group(1) - self.logDebug(('FREEUSER' if found.group(2) == 'download' else 'GUEST') + ' URL', url) - - response = json_loads(self.load("http://115.com" + url, decode=False)) - for mirror in (response['urls'] if 'urls' in response else response['data'] if 'data' in response else []): - try: - url = mirror['url'].replace('\\', '') - self.logDebug("Trying URL: " + url) - self.download(url) - break - except: - continue + __version__ = "0.19" + __status__ = "testing" + + __pattern__ = r'http://(?:www\.)?(?:u\.)?115\.com/file/(?P<ID>\w+)' + __config__ = [("activated", "bool", "Activated", True), + ("use_premium", "bool", "Use premium account if available", True), + ("fallback", "bool", + "Fallback to free download if premium fails", True), + ("chk_filesize", "bool", "Check file size", True), + ("max_wait", "int", "Reconnect if waiting time is greater than minutes", 10)] + + __description__ = """115.com hoster plugin""" + __license__ = "GPLv3" + __authors__ = [("zoidberg", "zoidberg@mujmail.cz")] + + NAME_PATTERN = r'file_name: \'(?P<N>.+?)\'' + SIZE_PATTERN = r'file_size: \'(?P<S>.+?)\'' + OFFLINE_PATTERN = ur'<h3><i style="color:red;">哎呀!提取码不存在!不妨搜搜看吧!</i></h3>' + + LINK_FREE_PATTERN = r'(/\?ct=(pickcode|download)[^"\']+)' + + def handle_free(self, pyfile): + m = re.search(self.LINK_FREE_PATTERN, self.data) + if m is None: + self.error(_("LINK_FREE_PATTERN not found")) + + url = m.group(1) + + self.log_debug( + ('FREEUSER' if m.group(2) == "download" else 'GUEST') + + ' URL', + url) + + html = self.load( + urlparse.urljoin( + "http://115.com/", + url), + decode=False) + res = json.loads(html) + if "urls" in res: + mirrors = res['urls'] + + elif "data" in res: + mirrors = res['data'] + else: - self.fail('No working link found') + mirrors = None + for mr in mirrors: + try: + self.link = mr['url'].replace("\\", "") + self.log_debug("Trying URL: " + self.link) + break -getInfo = create_getInfo(YibaishiwuCom) + except Exception: + pass + else: + self.fail(_("No working link found")) diff --git a/module/plugins/hoster/YoupornCom.py b/module/plugins/hoster/YoupornCom.py index b0d594ca5a..b00e404d64 100644 --- a/module/plugins/hoster/YoupornCom.py +++ b/module/plugins/hoster/YoupornCom.py @@ -1,18 +1,22 @@ -#!/usr/bin/env python # -*- coding: utf-8 -*- import re -from module.plugins.Hoster import Hoster + +from ..internal.Hoster import Hoster class YoupornCom(Hoster): __name__ = "YoupornCom" __type__ = "hoster" - __pattern__ = r"http://(www\.)?youporn\.com/watch/.+" - __version__ = "0.2" - __description__ = """Youporn.com Video Download Hoster""" - __author_name__ = ("willnix") - __author_mail__ = ("willnix@pyload.org") + __version__ = "0.26" + __status__ = "testing" + + __pattern__ = r'http://(?:www\.)?youporn\.com/watch/.+' + __config__ = [("activated", "bool", "Activated", True)] + + __description__ = """Youporn.com hoster plugin""" + __license__ = "GPLv3" + __authors__ = [("willnix", "willnix@pyload.net")] def process(self, pyfile): self.pyfile = pyfile @@ -20,35 +24,42 @@ def process(self, pyfile): if not self.file_exists(): self.offline() - self.pyfile.name = self.get_file_name() + pyfile.name = self.get_file_name() self.download(self.get_file_url()) def download_html(self): url = self.pyfile.url - self.html = self.load(url, post={"user_choice": "Enter"}, cookies=False) + self.data = self.load( + url, + post={ + 'user_choice': "Enter"}, + cookies=False) def get_file_url(self): - """ returns the absolute downloadable filepath """ - if self.html is None: + Returns the absolute downloadable filepath + """ + if not self.data: self.download_html() - file_url = re.search(r'(http://download\.youporn\.com/download/\d+\?save=1)">', self.html).group(1) - return file_url + return re.search( + r'(http://download\.youporn\.com/download/\d+\?save=1)">', self.data).group(1) def get_file_name(self): - if self.html is None: + if not self.data: self.download_html() - file_name_pattern = r"<title>(.*) - Free Porn Videos - YouPorn" - return re.search(file_name_pattern, self.html).group(1).replace("&", "&").replace("/", "") + '.flv' + file_name_pattern = r'(.+) - ' + return re.search(file_name_pattern, self.data).group( + 1).replace("&", "&").replace("/", "") + '.flv' def file_exists(self): - """ returns True or False """ - if self.html is None: + Returns True or False + """ + if not self.data: self.download_html() - if re.search(r"(.*invalid video_id.*)", self.html) is not None: + if re.search(r'(.*invalid video_id.*)', self.data): return False else: return True diff --git a/module/plugins/hoster/YourfilesTo.py b/module/plugins/hoster/YourfilesTo.py index 0fd094f612..5d56aeec59 100644 --- a/module/plugins/hoster/YourfilesTo.py +++ b/module/plugins/hoster/YourfilesTo.py @@ -1,19 +1,24 @@ -#!/usr/bin/env python # -*- coding: utf-8 -*- import re import urllib -from module.plugins.Hoster import Hoster + +from ..internal.Hoster import Hoster class YourfilesTo(Hoster): __name__ = "YourfilesTo" __type__ = "hoster" - __pattern__ = r"(http://)?(www\.)?yourfiles\.(to|biz)/\?d=[a-zA-Z0-9]+" - __version__ = "0.21" - __description__ = """Youfiles.to Download Hoster""" - __author_name__ = ("jeix", "skydancer") - __author_mail__ = ("jeix@hasnomail.de", "skydancer@hasnomail.de") + __version__ = "0.28" + __status__ = "testing" + + __pattern__ = r'http://(?:www\.)?yourfiles\.(to|biz)/\?d=\w+' + __config__ = [("activated", "bool", "Activated", True)] + + __description__ = """Youfiles.to hoster plugin""" + __license__ = "GPLv3" + __authors__ = [("jeix", "jeix@hasnomail.de"), + ("skydancer", "skydancer@hasnomail.de")] def process(self, pyfile): self.pyfile = pyfile @@ -26,18 +31,15 @@ def prepare(self): self.pyfile.name = self.get_file_name() - wait_time = self.get_waiting_time() - self.setWait(wait_time) - self.logDebug("%s: Waiting %d seconds." % (self.__name__, wait_time)) - self.wait() + self.wait(self.get_waiting_time()) def get_waiting_time(self): - if self.html is None: + if not self.data: self.download_html() - #var zzipitime = 15; - m = re.search(r'var zzipitime = (\d+);', self.html) - if m: + #: var zzipitime = 15 + m = re.search(r'var zzipitime = (\d+);', self.data) + if m is not None: sec = int(m.group(1)) else: sec = 0 @@ -46,32 +48,35 @@ def get_waiting_time(self): def download_html(self): url = self.pyfile.url - self.html = self.load(url) + self.data = self.load(url) def get_file_url(self): - """ returns the absolute downloadable filepath """ - url = re.search(r"var bla = '(.*?)';", self.html) + Returns the absolute downloadable filepath + """ + url = re.search(r"var bla = '(.*?)';", self.data) if url: url = url.group(1) - url = urllib.unquote(url.replace("http://http:/http://", "http://").replace("dumdidum", "")) + url = urllib.unquote(url.replace( + "http://http:/http://", "http://").replace("dumdidum", "")) return url else: - self.fail("absolute filepath could not be found. offline? ") + self.error(_("Absolute filepath not found")) def get_file_name(self): - if self.html is None: + if not self.data: self.download_html() - return re.search("<title>(.*)", self.html).group(1) + return re.search("(.*)", self.data).group(1) def file_exists(self): - """ returns True or False """ - if self.html is None: + Returns True or False + """ + if not self.data: self.download_html() - if re.search(r"HTTP Status 404", self.html) is not None: + if re.search(r'HTTP Status 404', self.data): return False else: return True diff --git a/module/plugins/hoster/YoutubeCom.py b/module/plugins/hoster/YoutubeCom.py index 319eb36e61..3d12045174 100644 --- a/module/plugins/hoster/YoutubeCom.py +++ b/module/plugins/hoster/YoutubeCom.py @@ -1,168 +1,1110 @@ -#!/usr/bin/env python # -*- coding: utf-8 -*- +import operator +import os import re import subprocess -import os -from urllib import unquote +import time +import urllib +import urlparse +from xml.dom.minidom import parseString as parse_xml + +from ..internal.Hoster import Hoster +from ..internal.misc import ( + BIGHTTPRequest, Popen, decode, exists, fs_encode, fsjoin, isexecutable, json, reduce, renice, replace_patterns, + safename, uniqify, which) +from ..internal.Plugin import Skip + + +def try_get(data, *path): + def get_one(src, what): + if isinstance(src, dict) and isinstance(what, basestring): + return src.get(what, None) + elif isinstance(src, list) and type(what) is int: + try: + return src[what] + except IndexError: + return None + elif callable(what): + try: + return what(src) + except Exception: + return None + else: + return None + + res = get_one(data, path[0]) + for item in path[1:]: + if res is None: + break + res = get_one(res, item) + + return res + + +class Ffmpeg(object): + _RE_DURATION = re.compile(r'Duration: (\d{2}):(\d{2}):(\d{2})\.(\d{2}),') + _RE_TIME = re.compile(r'time=(\d{2}):(\d{2}):(\d{2})\.(\d{2})') + _RE_VERSION = re.compile((r'ffmpeg version (.+?) ')) + + CMD = None + priority = 0 + streams = [] + start_time = (0, 0) + output_filename = None + error_message = "" + + def __init__(self, priority, plugin=None): + self.plugin = plugin + self.priority = priority + + self.streams = [] + self.start_time = (0, 0) + self.output_filename = None + self.error_message = "" + + self.find() + + @classmethod + def find(cls): + """ + Check for ffmpeg + """ + if cls.CMD is not None: + return True + + try: + if os.name == "nt": + ffmpeg = os.path.join(pypath, "ffmpeg.exe") if isexecutable(os.path.join(pypath, "ffmpeg.exe")) \ + else "ffmpeg.exe" + + else: + ffmpeg = "ffmpeg" + + cmd = which(ffmpeg) or ffmpeg + + p = Popen([cmd, "-version"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + out, err = (_r.strip() if _r else "" for _r in p.communicate()) + except OSError: + return False + + m = cls._RE_VERSION.search(out) + if m is not None: + cls.VERSION = m.group(1) + + cls.CMD = cmd + + return True + + @property + def found(self): + return self.CMD is not None + + def add_stream(self, streams): + if isinstance(streams, list): + self.streams.extend(streams) + else: + self.streams.append(streams) + + def set_start_time(self, start_time): + self.start_time = start_time + + def set_output_filename(self, output_filename): + self.output_filename = output_filename + + def run(self): + if self.CMD is None or self.output_filename is None: + return False + + maps = [] + args = [] + meta = [] + for i, stream in enumerate(self.streams): + args.extend(["-i", stream[1]]) + maps.extend(["-map", "%s:%s:0" % (i, stream[0])]) + if stream[0] == 's': + meta.extend(["-metadata:s:s:0:%s" % i, "language=%s" % stream[2]]) -from module.utils import html_unescape -from module.plugins.Hoster import Hoster + args.extend(maps) + args.extend(meta) + args.extend(["-y", + "-vcodec", "copy", + "-acodec", "copy", + "-scodec", "copy", + "-ss", "00:%s:%s.00" % (self.start_time[0], self.start_time[1]), + "-sub_charenc", "utf8"]) + call = [self.CMD] + args + [self.output_filename] + self.plugin.log_debug("EXECUTE " + " ".join(call)) -def which(program): - """Works exactly like the unix command which + p = Popen( + call, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) - Courtesy of http://stackoverflow.com/a/377028/675646""" + renice(p.pid, self.priority) - def is_exe(fpath): - return os.path.isfile(fpath) and os.access(fpath, os.X_OK) + duration = self._find_duration(p) + if duration: + last_line = self._progress(p, duration) + else: + last_line = "" + + out, err = (_r.strip() if _r else "" for _r in p.communicate()) + if err or p.returncode: + self.error_message = last_line + return False + + else: + self.error_message = "" + return True + + def _find_duration(self, process): + duration = 0 + while True: + line = process.stderr.readline() #: ffmpeg writes to stderr + + #: Quit loop on eof + if not line: + break + + m = self._RE_DURATION.search(line) + if m is not None: + duration = sum(int(v) * [60 * 60 * 100, 60 * 100, 100, 1][i] + for i, v in enumerate(m.groups())) + break - fpath, fname = os.path.split(program) - if fpath: - if is_exe(program): - return program - else: - for path in os.environ["PATH"].split(os.pathsep): - path = path.strip('"') - exe_file = os.path.join(path, program) - if is_exe(exe_file): - return exe_file + return duration - return None + def _progress(self, process, duration): + line = "" + last_line = "" + while True: + c = process.stderr.read(1) #: ffmpeg writes to stderr + + #: Quit loop on eof + if not c: + break + + elif c == "\r": + last_line = line.strip('\r\n') + line = "" + m = self._RE_TIME.search(last_line) + if m is not None: + current_time = sum(int(v) * [60 * 60 * 100, 60 * 100, 100, 1][i] + for i, v in enumerate(m.groups())) + if self.plugin: + progress = current_time * 100 / duration + self.plugin.pyfile.setProgress(progress) + + else: + line += c + continue + + return last_line #: Last line may contain error message class YoutubeCom(Hoster): __name__ = "YoutubeCom" __type__ = "hoster" - __pattern__ = r"https?://(?:[^/]*?)youtube\.com/watch.*?[?&]v=.*" - __version__ = "0.38" - __config__ = [("quality", "sd;hd;fullhd;240p;360p;480p;720p;1080p;3072p", "Quality Setting", "hd"), - ("fmt", "int", "FMT/ITAG Number (5-102, 0 for auto)", 0), + __version__ = "0.90" + __status__ = "broken" + + __pattern__ = r'https?://(?:[^/]*\.)?(?:youtu\.be/|youtube\.com/watch\?(?:.*&)?v=)[\w\-]+' + __config__ = [("activated", "bool", "Activated", True), + ("quality", "sd;hd;fullhd;240p;360p;480p;720p;1080p;1440p;2160p;3072p;4320p", "Quality Setting", "hd"), + ("vfmt", "int", "Video FMT/ITAG Number (0 for auto)", 0), + ("afmt", "int", "Audio FMT/ITAG Number (0 for auto)", 0), (".mp4", "bool", "Allow .mp4", True), (".flv", "bool", "Allow .flv", True), - (".webm", "bool", "Allow .webm", False), + (".webm", "bool", "Allow .webm", True), + (".mkv", "bool", "Allow .mkv", True), (".3gp", "bool", "Allow .3gp", False), - ("3d", "bool", "Prefer 3D", False)] - __description__ = """Youtube.com Video Download Hoster""" - __author_name__ = ("spoob", "zoidberg") - __author_mail__ = ("spoob@pyload.org", "zoidberg@mujmail.cz") - - # name, width, height, quality ranking, 3D - formats = {5: (".flv", 400, 240, 1, False), - 6: (".flv", 640, 400, 4, False), - 17: (".3gp", 176, 144, 0, False), - 18: (".mp4", 480, 360, 2, False), - 22: (".mp4", 1280, 720, 8, False), - 43: (".webm", 640, 360, 3, False), - 34: (".flv", 640, 360, 4, False), - 35: (".flv", 854, 480, 6, False), - 36: (".3gp", 400, 240, 1, False), - 37: (".mp4", 1920, 1080, 9, False), - 38: (".mp4", 4096, 3072, 10, False), - 44: (".webm", 854, 480, 5, False), - 45: (".webm", 1280, 720, 7, False), - 46: (".webm", 1920, 1080, 9, False), - 82: (".mp4", 640, 360, 3, True), - 83: (".mp4", 400, 240, 1, True), - 84: (".mp4", 1280, 720, 8, True), - 85: (".mp4", 1920, 1080, 9, True), - 100: (".webm", 640, 360, 3, True), - 101: (".webm", 640, 360, 4, True), - 102: (".webm", 1280, 720, 8, True)} + ("aac", "bool", "Allow aac audio (DASH video only)", True), + ("vorbis", "bool", "Allow vorbis audio (DASH video only)", True), + ("opus", "bool", "Allow opus audio (DASH video only)", True), + ("ac3", "bool", "Allow ac3 audio (DASH video only)", True), + ("dts", "bool", "Allow dts audio (DASH video only)", True), + ("3d", "bool", "Prefer 3D", False), + ("subs_dl", "off;all_specified;first_available", "Download subtitles", "off"), + ("subs_dl_langs", "str", "Subtitle language codes to download (comma separated)", ""), + ("auto_subs", "bool", "Allow machine generated subtitles", True), + ("subs_translate", "str", "Translate subtitles to language (forces first_available)" , ""), + ("subs_embed", "bool", "Embed subtitles inside the output file (.mp4 and .mkv only)", False), + ("priority", "int", "ffmpeg process priority", 0)] + + __description__ = """Youtube.com hoster plugin""" + __license__ = "GPLv3" + __authors__ = [("spoob", "spoob@pyload.net"), + ("zoidberg", "zoidberg@mujmail.cz"), + ("GammaC0de", "nitzo2001[AT]yahoo[DOT]com")] + + URL_REPLACEMENTS = [(r'youtu\.be/', 'youtube.com/watch?v=')] + + #: name, width, height, quality ranking, 3D, type + formats = { + # 3gp + 17: {'ext': ".3gp", 'width': 176, 'height': 144, 'qi': 0, '3d': False, 'type': "av"}, + 36: {'ext': ".3gp", 'width': 400, 'height': 240, 'qi': 1, '3d': False, 'type': "av"}, + # flv + 5: {'ext': ".flv", 'width': 400, 'height': 240, 'qi': 1, '3d': False, 'type': "av"}, + 6: {'ext': ".flv", 'width': 640, 'height': 400, 'qi': 4, '3d': False, 'type': "av"}, + 34: {'ext': ".flv", 'width': 640, 'height': 360, 'qi': 4, '3d': False, 'type': "av"}, + 35: {'ext': ".flv", 'width': 854, 'height': 480, 'qi': 6, '3d': False, 'type': "av"}, + # mp4 + 83: {'ext': ".mp4", 'width': 400, 'height': 240, 'qi': 1, '3d': True, 'type': "av"}, + 18: {'ext': ".mp4", 'width': 480, 'height': 360, 'qi': 2, '3d': False, 'type': "av"}, + 82: {'ext': ".mp4", 'width': 640, 'height': 360, 'qi': 3, '3d': True, 'type': "av"}, + 22: {'ext': ".mp4", 'width': 1280, 'height': 720, 'qi': 8, '3d': False, 'type': "av"}, + 136: {'ext': ".mp4", 'width': 1280, 'height': 720, 'qi': 8, '3d': False, 'type': "v"}, + 84: {'ext': ".mp4", 'width': 1280, 'height': 720, 'qi': 8, '3d': True, 'type': "av"}, + 37: {'ext': ".mp4", 'width': 1920, 'height': 1080, 'qi': 9, '3d': False, 'type': "av"}, + 137: {'ext': ".mp4", 'width': 1920, 'height': 1080, 'qi': 9, '3d': False, 'type': "v"}, + 85: {'ext': ".mp4", 'width': 1920, 'height': 1080, 'qi': 9, '3d': True, 'type': "av"}, + 264: {'ext': ".mp4", 'width': 2560, 'height': 1440, 'qi': 10, '3d': False, 'type': "v"}, + 266: {'ext': ".mp4", 'width': 3840, 'height': 2160, 'qi': 11, '3d': False, 'type': "v"}, + 38: {'ext': ".mp4", 'width': 4096, 'height': 3072, 'qi': 12 , '3d': False, 'type': "av"}, + # webm + 43: {'ext': ".webm", 'width': 640, 'height': 360, 'qi': 3, '3d': False, 'type': "av"}, + 100: {'ext': ".webm", 'width': 640, 'height': 360, 'qi': 3, '3d': True, 'type': "av"}, + 101: {'ext': ".webm", 'width': 640, 'height': 360, 'qi': 4, '3d': True, 'type': "av"}, + 44: {'ext': ".webm", 'width': 854, 'height': 480, 'qi': 5, '3d': False, 'type': "av"}, + 45: {'ext': ".webm", 'width': 1280, 'height': 720, 'qi': 7, '3d': False, 'type': "av"}, + 247: {'ext': ".webm", 'width': 1280, 'height': 720, 'qi': 7, '3d': False, 'type': "v"}, + 102: {'ext': ".webm", 'width': 1280, 'height': 720, 'qi': 8, '3d': True, 'type': "av"}, + 46: {'ext': ".webm", 'width': 1920, 'height': 1080, 'qi': 9, '3d': False, 'type': "av"}, + 248: {'ext': ".webm", 'width': 1920, 'height': 1080, 'qi': 9, '3d': False, 'type': "v"}, + 271: {'ext': ".webm", 'width': 2560, 'height': 1440, 'qi': 10, '3d': False, 'type': "v"}, + 313: {'ext': ".webm", 'width': 3840, 'height': 2160, 'qi': 11, '3d': False, 'type': "v"}, + 272: {'ext': ".webm", 'width': 7680, 'height': 4320, 'qi': 13, '3d': False, 'type': "v"}, + # audio + 139: {'ext': ".mp4", 'qi': 1, 'acodec': "aac", 'type': "a"}, + 140: {'ext': ".mp4", 'qi': 2, 'acodec': "aac", 'type': "a"}, + 141: {'ext': ".mp4", 'qi': 3, 'acodec': "aac", 'type': "a"}, + 256: {'ext': ".mp4", 'qi': 4, 'acodec': "aac", 'type': "a"}, + 258: {'ext': ".mp4", 'qi': 5, 'acodec': "aac", 'type': "a"}, + 325: {'ext': ".mp4", 'qi': 6, 'acodec': "dts", 'type': "a"}, + 328: {'ext': ".mp4", 'qi': 7, 'acodec': "ac3", 'type': "a"}, + 171: {'ext': ".webm", 'qi': 1, 'acodec': "vorbis", 'type': 'a'}, + 172: {'ext': ".webm", 'qi': 2, 'acodec': "vorbis", 'type': 'a'}, + 249: {'ext': ".webm", 'qi': 3, 'acodec': "opus", 'type': 'a'}, + 250: {'ext': ".webm", 'qi': 4, 'acodec': "opus", 'type': 'a'}, + 251: {'ext': ".webm", 'qi': 5, 'acodec': "opus", 'type': 'a'} + } + + def _decrypt_signature(self, encrypted_sig): + """Turn the encrypted 's' field into a working signature""" + sig_cache_id = self.player_url + "_" + ".".join(str(len(part)) for part in encrypted_sig.split('.')) + + cache_info = self.db.retrieve("cache") + cache_dirty = False + + if cache_info is None or cache_info.get('version') != self.__version__: + cache_info = {'version': self.__version__, + 'cache': {}} + cache_dirty = True + + if sig_cache_id in cache_info['cache'] and time.time() < cache_info['cache'][sig_cache_id]['time'] + 24 * 60 * 60: + self.log_debug("Using cached decode function to decrypt the URL") + decrypt_func = lambda s: ''.join(s[_i] for _i in cache_info['cache'][sig_cache_id]['decrypt_map']) + decrypted_sig = decrypt_func(encrypted_sig) + + else: + player_data = self.load(self.player_url) + + m = re.search(r'\b[cs]\s*&&\s*[adf]\.set\([^,]+\s*,\s*encodeURIComponent\s*\(\s*(?P[a-zA-Z0-9$]+)\(', player_data) or \ + re.search(r'\b[a-zA-Z0-9]+\s*&&\s*[a-zA-Z0-9]+\.set\([^,]+\s*,\s*encodeURIComponent\s*\(\s*(?P[a-zA-Z0-9$]+)\(', player_data) or \ + re.search(r'\b(?P[a-zA-Z0-9$]{2})\s*=\s*function\(\s*a\s*\)\s*{\s*a\s*=\s*a\.split\(\s*""\s*\)', player_data) or \ + re.search(r'(?P[a-zA-Z0-9$]+)\s*=\s*function\(\s*a\s*\)\s*{\s*a\s*=\s*a\.split\(\s*""\s*\)', player_data) or \ + re.search(r'\.sig\|\|(?P[a-zA-Z0-9$]+)\(', player_data) or \ + re.search(r'\bc\s*&&\s*d\.set\([^,]+\s*,\s*\([^)]*\)\s*\(\s*(?P[a-zA-Z0-9$]+)\(', player_data) or \ + re.search(r'(["\'])signature\1\s*,\s*(?P[a-zA-Z0-9$]+)\(', player_data) + + try: + function_name = m.group('sig') + + except (AttributeError, IndexError): + self.fail(_("Signature decode function name not found")) + + try: + jsi = JSInterpreter(player_data) + decrypt_func = lambda s: jsi.extract_function(function_name)([s]) + + #: Since Youtube just scrambles the order of the characters in the signature + #: and does not change any byte value, we can store just a transformation map as a cached function + decrypt_map = [ord(c) for c in decrypt_func(''.join(map(unichr, range(len(encrypted_sig)))))] + cache_info['cache'][sig_cache_id] = {'decrypt_map': decrypt_map, + 'time': time.time()} + cache_dirty = True + + decrypted_sig = decrypt_func(encrypted_sig) + + except (JSInterpreterError, AssertionError), e: + self.log_error(_("Signature decode failed"), e) + self.fail(e.args[0]) + + #: Remove old records from cache + for _k in list(cache_info['cache'].keys()): + if time.time() >= cache_info['cache'][_k]['time'] + 24 * 60 * 60: + cache_info['cache'].pop(_k, None) + cache_dirty = True + + if cache_dirty: + self.db.store("cache", cache_info) + + return decrypted_sig + + def _handle_video(self): + use3d = self.config.get('3d') + + if use3d: + quality = {'sd': 82, 'hd': 84, 'fullhd': 85, '240p': 83, '360p': 82, '480p': 82, '720p': 84, + '1080p': 85, '1440p': 85, '2160p': 85, '3072p': 85, '4320p': 85} + else: + quality = {'sd': 18, 'hd': 22, 'fullhd': 37, '240p': 5, '360p': 18, '480p': 35, '720p': 22, + '1080p': 37, '1440p': 264, '2160p': 266, '3072p': 38, '4320p': 272} + + desired_fmt = self.config.get('vfmt') or quality.get(self.config.get('quality'), 0) + + is_video = lambda x: 'v' in self.formats[x]['type'] + if desired_fmt not in self.formats or not is_video(desired_fmt): + self.log_warning(_("VIDEO ITAG %d unknown, using default") % desired_fmt) + desired_fmt = 22 + + #: Build dictionary of supported itags (3D/2D) + allowed_suffix = lambda x: self.config.get(self.formats[x]['ext']) + video_streams = dict([(_s[0], _s[1:]) for _s in self.streams + if _s[0] in self.formats and allowed_suffix(_s[0]) and + is_video(_s[0]) and self.formats[_s[0]]['3d'] == use3d]) + + if not video_streams: + self.fail(_("No available video stream meets your preferences")) + + self.log_debug("DESIRED VIDEO STREAM: ITAG:%d (%s %dx%d Q:%d 3D:%s) %sfound, %sallowed" % + (desired_fmt, self.formats[desired_fmt]['ext'], self.formats[desired_fmt]['width'], + self.formats[desired_fmt]['height'], self.formats[desired_fmt]['qi'], + self.formats[desired_fmt]['3d'], "" if desired_fmt in video_streams else "NOT ", + "" if allowed_suffix(desired_fmt) else "NOT ")) + + #: Return fmt nearest to quality index + if desired_fmt in video_streams and allowed_suffix(desired_fmt): + chosen_fmt = desired_fmt + else: + quality_index = lambda x: self.formats[x]['qi'] #: Select quality index + quality_distance = lambda x, y: abs(quality_index(x) - quality_index(y)) + + self.log_debug("Choosing nearest stream: %s" % [(_s, allowed_suffix(_s), quality_distance(_s, desired_fmt)) + for _s in video_streams.keys()]) + + chosen_fmt = reduce(lambda x, y: x if quality_distance(x, desired_fmt) <= quality_distance(y, desired_fmt) + and quality_index(x) > quality_index(y) else y, video_streams.keys()) + + self.log_debug("CHOSEN VIDEO STREAM: ITAG:%d (%s %dx%d Q:%d 3D:%s)" % + (chosen_fmt, self.formats[chosen_fmt]['ext'], self.formats[chosen_fmt]['width'], + self.formats[chosen_fmt]['height'], self.formats[chosen_fmt]['qi'], + self.formats[chosen_fmt]['3d'])) + + url = video_streams[chosen_fmt][0] + + if video_streams[chosen_fmt][1]: + if video_streams[chosen_fmt][2]: + signature = self._decrypt_signature(video_streams[chosen_fmt][1]) + + else: + signature = video_streams[chosen_fmt][1] + + url += "&%s=%s" % (video_streams[chosen_fmt][3], signature) + + if "&ratebypass=" not in url: + url += "&ratebypass=yes" + + file_suffix = self.formats[chosen_fmt]['ext'] if chosen_fmt in self.formats else ".flv" + + if 'a' not in self.formats[chosen_fmt]['type']: + file_suffix = ".video" + file_suffix + + self.pyfile.name = self.file_name + file_suffix + + try: + filename = self.download(url, disposition=False) + except Skip, e: + filename = os.path.join(self.pyload.config.get("general", "download_folder"), + self.pyfile.package().folder, + self.pyfile.name) + self.log_info(_("Download skipped: %s due to %s") % (self.pyfile.name, e.args[0])) + + return filename, chosen_fmt + + def _handle_audio(self, video_fmt): + desired_fmt = self.config.get('afmt') or 141 + + is_audio = lambda x: self.formats[x]['type'] == "a" + if desired_fmt not in self.formats or not is_audio(desired_fmt): + self.log_warning(_("AUDIO ITAG %d unknown, using default") % desired_fmt) + desired_fmt = 141 + + #: Build dictionary of supported audio itags + allowed_codec = lambda x: self.config.get(self.formats[x]['acodec']) + allowed_suffix = lambda x: self.config.get(".mkv") or \ + self.config.get(self.formats[x]['ext']) and \ + self.formats[x]['ext'] == self.formats[video_fmt]['ext'] + + audio_streams = dict([(_s[0], _s[1:]) for _s in self.streams + if _s[0] in self.formats and is_audio(_s[0]) and + allowed_codec(_s[0]) and allowed_suffix(_s[0])]) + + if not audio_streams: + self.fail(_("No available audio stream meets your preferences")) + + if desired_fmt in audio_streams and allowed_suffix(desired_fmt): + chosen_fmt = desired_fmt + else: + quality_index = lambda x: self.formats[x]['qi'] #: Select quality index + quality_distance = lambda x, y: abs(quality_index(x) - quality_index(y)) + + self.log_debug("Choosing nearest stream: %s" % [(_s, allowed_suffix(_s), quality_distance(_s, desired_fmt)) + for _s in audio_streams.keys()]) + + chosen_fmt = reduce(lambda x, y: x if quality_distance(x, desired_fmt) <= quality_distance(y, desired_fmt) + and quality_index(x) > quality_index(y) else y, audio_streams.keys()) + + self.log_debug("CHOSEN AUDIO STREAM: ITAG:%d (%s %s Q:%d)" % + (chosen_fmt, self.formats[chosen_fmt]['ext'], self.formats[chosen_fmt]['acodec'], + self.formats[chosen_fmt]['qi'])) + + url = audio_streams[chosen_fmt][0] + + if audio_streams[chosen_fmt][1]: + if audio_streams[chosen_fmt][2]: + signature = self._decrypt_signature(audio_streams[chosen_fmt][1]) + + else: + signature = audio_streams[chosen_fmt][1] + + url += "&%s=%s" % (audio_streams[chosen_fmt][3], signature) + + if "&ratebypass=" not in url: + url += "&ratebypass=yes" + + file_suffix = ".audio" + self.formats[chosen_fmt]['ext'] if chosen_fmt in self.formats else ".m4a" + + self.pyfile.name = self.file_name + file_suffix + + try: + filename = self.download(url, disposition=False) + except Skip, e: + filename = os.path.join(self.pyload.config.get("general", "download_folder"), + self.pyfile.package().folder, + self.pyfile.name) + self.log_info(_("Download skipped: %s due to %s") % (self.pyfile.name, e.args[0])) + + return filename, chosen_fmt + + def _handle_subtitles(self): + def timedtext_to_srt(timedtext): + def _format_srt_time(millisec): + sec, milli = divmod(millisec, 1000) + m, s = divmod(int(sec), 60) + h, m = divmod(m, 60) + return "%02d:%02d:%02d,%s" % (h, m, s, milli) + + srt = "" + dom = parse_xml(timedtext) + body = dom.getElementsByTagName("body")[0] + paras = body.getElementsByTagName("p") + subtitles = [] + for para in paras: + try: + start_time = int(para.attributes['t'].value) + end_time = int(para.attributes['t'].value) + int(para.attributes['d'].value) + except KeyError: + continue + + subtitle_text = "" + words = para.getElementsByTagName("s") + if words: + subtitle_text = "".join([unicode(word.firstChild.data) for word in words]) + + else: + for child in para.childNodes: + if child.nodeName == 'br': + subtitle_text += "\n" + elif child.nodeName == '#text': + subtitle_text += unicode(child.data) + + if subtitle_text.strip(): + subtitles.append({'start': start_time, + 'end': end_time, + 'text': subtitle_text}) + else: + continue + + for line_num in range(len(subtitles)): + start_time = subtitles[line_num]['start'] + try: + end_time = min(subtitles[line_num]['end'], subtitles[line_num + 1]['start']) + except IndexError: + end_time = subtitles[line_num]['end'] + + subtitle_text = subtitles[line_num]['text'] + + subtitle_element = str(line_num + 1) + "\n" \ + + _format_srt_time(start_time) + ' --> ' + _format_srt_time(end_time) + "\n" \ + + subtitle_text + "\n\n" + srt += subtitle_element + + return srt + + srt_files =[] + try: + subs = self.player_response['captions']['playerCaptionsTracklistRenderer']['captionTracks'] + subtitles_info = dict([(_subtitle['languageCode'], + (urllib.unquote(_subtitle['baseUrl']).decode('unicode-escape') + "&fmt=3", + _subtitle['vssId'].startswith("a."), + _subtitle['isTranslatable'])) + for _subtitle in subs]) + self.log_debug("AVAILABLE SUBTITLES: %s" % subtitles_info.keys() or "None") + + except KeyError: + self.log_debug("AVAILABLE SUBTITLES: None") + return srt_files + + subs_dl = self.config.get('subs_dl') + if subs_dl != "off": + subs_translate = self.config.get('subs_translate').strip() + auto_subs = self.config.get('auto_subs') + subs_dl = "first_available" if subs_translate != "" else subs_dl + subs_dl_langs = [_x.strip() for _x in self.config.get('subs_dl_langs', "").split(',') if _x.strip()] + + if subs_dl_langs: + # Download only listed subtitles (`subs_dl_langs` config gives the priority) + for _lang in subs_dl_langs: + if _lang in subtitles_info: + subtitle_code = _lang if subs_translate == "" else subs_translate + + if auto_subs is False and subtitles_info[_lang][1] is True: + self.log_warning(_("Skipped machine generated subtitle: %s") % _lang) + continue + + subtitle_url = subtitles_info[_lang][0] + if subs_translate: + if subtitles_info[_lang][2]: #: Translatable? + subtitle_url += "&tlang=%s" % subs_translate + else: + self.log_warning(_("Skipped non translatable subtitle: %s") % _lang) + continue #: No, try next one + + srt_filename = fsjoin(self.pyload.config.get("general", "download_folder"), + self.pyfile.package().folder, + self.file_name + "." + subtitle_code + ".srt") + + if self.pyload.config.get('download', 'skip_existing') and \ + exists(srt_filename) and os.stat(srt_filename).st_size != 0: + self.log_info("Download skipped: %s due to File exists" % os.path.basename(srt_filename)) + srt_files.append((srt_filename, subtitle_code)) + continue + + timed_text = self.load(subtitle_url, decode=False) + srt = timedtext_to_srt(timed_text) + + with open(srt_filename, "w") as f: + f.write(srt.encode('utf-8')) + self.set_permissions(srt_filename) + self.log_debug("Saved subtitle: %s" % os.path.basename(srt_filename)) + srt_files.append((srt_filename, _lang)) + if subs_dl == "first_available": + break + + else: + # Download any available subtitle + for _subtitle in subtitles_info.items(): + if auto_subs is False and _subtitle[1][1] is True: + self.log_warning(_("Skipped machine generated subtitle: %s") % _subtitle[0]) + continue + + subtitle_code = _subtitle[0] if subs_translate == "" else subs_translate + + subtitle_url = _subtitle[1][0] + if subs_translate: + if _subtitle[1][2]: #: Translatable? + subtitle_url += "&tlang=%s" % subs_translate + else: + self.log_warning(_("Skipped non translatable subtitle: %s") % _subtitle[0]) + continue #: No, try next one + + srt_filename = fsjoin(self.pyload.config.get("general", "download_folder"), + self.pyfile.package().folder, + os.path.splitext(self.file_name)[0] + "." + subtitle_code + ".srt") + + if self.pyload.config.get('download', 'skip_existing') and \ + exists(srt_filename) and os.stat(srt_filename).st_size != 0: + self.log_info("Download skipped: %s due to File exists" % os.path.basename(srt_filename)) + srt_files.append((srt_filename, subtitle_code)) + continue + + timed_text = self.load(subtitle_url, decode=False) + srt = timedtext_to_srt(timed_text) + + with open(srt_filename, "w") as f: + f.write(srt.encode('utf-8')) + self.set_permissions(srt_filename) + + self.log_debug("Saved subtitle: %s" % os.path.basename(srt_filename)) + srt_files.append((srt_filename, subtitle_code)) + if subs_dl == "first_available": + break + + return srt_files + + def _postprocess(self, video_filename, audio_filename, subtitles_files): + final_filename = video_filename + subs_embed = self.config.get("subs_embed") + + self.pyfile.setCustomStatus("postprocessing") + self.pyfile.setProgress(0) + + if self.ffmpeg.found: + if audio_filename is not None: + video_suffix = os.path.splitext(video_filename)[1] + final_filename = os.path.join(os.path.dirname(video_filename), + self.file_name + + (video_suffix if video_suffix == os.path.splitext(audio_filename)[1] + else ".mkv")) + + self.ffmpeg.add_stream(('v', video_filename)) + self.ffmpeg.add_stream(('a', audio_filename)) + + if subtitles_files and subs_embed: + for subtitle in subtitles_files: + self.ffmpeg.add_stream(('s',) + subtitle) + + self.ffmpeg.set_start_time(self.start_time) + self.ffmpeg.set_output_filename(final_filename) + + self.pyfile.name = os.path.basename(final_filename) + self.pyfile.size = os.path.getsize(fs_encode(video_filename)) + \ + os.path.getsize(fs_encode(audio_filename)) #: Just an estimate + + if self.ffmpeg.run(): + self.remove(video_filename, trash=False) + self.remove(audio_filename, trash=False) + if subtitles_files and subs_embed: + for subtitle in subtitles_files: + self.remove(subtitle[0]) + + else: + self.log_warning(_("ffmpeg error"), self.ffmpeg.error_message) + final_filename = video_filename + + elif self.start_time[0] != 0 or self.start_time[1] != 0 or subtitles_files and subs_embed: + inputfile = video_filename + "_" + final_filename = video_filename + os.rename(video_filename, inputfile) + + self.ffmpeg.add_stream(('v', video_filename)) + self.ffmpeg.set_start_time(self.start_time) + + if subtitles_files and subs_embed: + for subtitle in subtitles_files: + self.ffmpeg.add_stream(('s', subtitle)) + + self.pyfile.name = os.path.basename(final_filename) + self.pyfile.size = os.path.getsize(inputfile) #: Just an estimate + + if self.ffmpeg.run(): + self.remove(inputfile, trash=False) + if subtitles_files and subs_embed: + for subtitle in subtitles_files: + self.remove(subtitle[0]) + + else: + self.log_warning(_("ffmpeg error"), self.ffmpeg.error_message) + + else: + if audio_filename is not None: + self.log_warning("ffmpeg is not installed, video and audio files will not be merged") + + if subtitles_files and self.config.get("subs_embed"): + self.log_warning("ffmpeg is not installed, subtitles files will not be embedded") + + self.pyfile.setProgress(100) + + self.set_permissions(final_filename) + + return final_filename def setup(self): - self.resumeDownload = self.multiDL = True + self.resume_download = True + self.chunk_limit = -1 + self.multiDL = True + + try: + self.req.http.close() + except Exception: + pass + + self.req.http = BIGHTTPRequest( + cookies=self.req.cj, + options=self.pyload.requestFactory.getOptions(), + limit=5000000) def process(self, pyfile): - html = self.load(pyfile.url, decode=True) + pyfile.url = replace_patterns(pyfile.url, self.URL_REPLACEMENTS) + self.data = self.load(pyfile.url) - if re.search(r'
      ', html): - self.offline() + url, inputs = self.parse_html_form('action="https://consent.youtube.com/s"') + if url is not None: + self.data = self.load(url, post=inputs) - if "We have been receiving a large volume of requests from your network." in html: - self.tempOffline() + m = re.search(r'"playabilityStatus":{"status":"(\w+)",(:?"(?:reason":|messages":\[)"([^"]+))?', self.data) + if m is None: + self.log_warning(_("Playability status pattern not found")) - #get config - use3d = self.getConfig("3d") - if use3d: - quality = {"sd": 82, "hd": 84, "fullhd": 85, "240p": 83, "360p": 82, - "480p": 82, "720p": 84, "1080p": 85, "3072p": 85} else: - quality = {"sd": 18, "hd": 22, "fullhd": 37, "240p": 5, "360p": 18, - "480p": 35, "720p": 22, "1080p": 37, "3072p": 38} - desired_fmt = self.getConfig("fmt") - if desired_fmt and desired_fmt not in self.formats: - self.logWarning("FMT %d unknown - using default." % desired_fmt) - desired_fmt = 0 - if not desired_fmt: - desired_fmt = quality.get(self.getConfig("quality"), 18) - - #parse available streams - streams = re.search(r'"url_encoded_fmt_stream_map": "(.*?)",', html).group(1) - streams = [x.split('\u0026') for x in streams.split(',')] - streams = [dict((y.split('=', 1)) for y in x) for x in streams] - streams = [(int(x['itag']), "%s&signature=%s" % (unquote(x['url']), x['sig'])) for x in streams] - #self.logDebug("Found links: %s" % streams) - self.logDebug("AVAILABLE STREAMS: %s" % [x[0] for x in streams]) - - #build dictionary of supported itags (3D/2D) - allowed = lambda x: self.getConfig(self.formats[x][0]) - streams = [x for x in streams if x[0] in self.formats and allowed(x[0])] - if not streams: - self.fail("No available stream meets your preferences") - fmt_dict = dict([x for x in streams if self.formats[x[0]][4] == use3d] or streams) - - self.logDebug("DESIRED STREAM: ITAG:%d (%s) %sfound, %sallowed" % - (desired_fmt, "%s %dx%d Q:%d 3D:%s" % self.formats[desired_fmt], - "" if desired_fmt in fmt_dict else "NOT ", "" if allowed(desired_fmt) else "NOT ")) - - #return fmt nearest to quality index - if desired_fmt in fmt_dict and allowed(desired_fmt): - fmt = desired_fmt + if m.group(1) != "OK": + if m.group(2): + self.log_error(m.group(2)) + self.offline() + + if "We have been receiving a large volume of requests from your network." in self.data: + self.temp_offline() + + m = re.search(r'ytplayer.config = ({.+?});', self.data) + if m is not None: + self.player_config = json.loads(m.group(1)) + self.player_response = json.loads(self.player_config['args']['player_response']) + else: - sel = lambda x: self.formats[x][3] # select quality index - comp = lambda x, y: abs(sel(x) - sel(y)) - - self.logDebug("Choosing nearest fmt: %s" % [(x, allowed(x), comp(x, desired_fmt)) for x in fmt_dict.keys()]) - fmt = reduce(lambda x, y: x if comp(x, desired_fmt) <= comp(y, desired_fmt) and - sel(x) > sel(y) else y, fmt_dict.keys()) - - self.logDebug("Chosen fmt: %s" % fmt) - url = fmt_dict[fmt] - self.logDebug("URL: %s" % url) - - #set file name - file_suffix = self.formats[fmt][0] if fmt in self.formats else ".flv" - file_name_pattern = '' - name = re.search(file_name_pattern, html).group(1).replace("/", "") - - # Cleaning invalid characters from the file name - name = name.encode('ascii', 'replace') - - pyfile.name = html_unescape(name) - - time = re.search(r"t=((\d+)m)?(\d+)s", pyfile.url) - ffmpeg = which("ffmpeg") - if ffmpeg and time: - m, s = time.groups()[1:] - if not m: - m = "0" - - pyfile.name += " (starting at %s:%s)" % (m, s) - pyfile.name += file_suffix - - filename = self.download(url) - - if ffmpeg and time: - inputfile = filename + "_" - os.rename(filename, inputfile) - - subprocess.call([ - ffmpeg, - "-ss", "00:%s:%s" % (m, s), - "-i", inputfile, - "-vcodec", "copy", - "-acodec", "copy", - filename]) - os.remove(inputfile) + m =re.search(r'ytInitialPlayerResponse = ({.+?});', self.data) + if m is not None: + self.player_config = json.loads(m.group(1)) + self.player_response = self.player_config + + else: + self.fail(_("Player config pattern not found")) + + m = re.search(r'"jsUrl"\s*:\s*"(.+?)"', self.data) or re.search(r'"assets":.+?"js":\s*"(.+?)"', self.data) + if m is None: + self.fail(_("Player URL pattern not found")) + + self.player_url = self.fixurl(m.group(1)) + + if not self.player_url.endswith(".js"): + self.fail(_("Unsupported player type %s") % self.player_url) + + self.ffmpeg = Ffmpeg(self.config.get('priority'), self) + + #: Set file name + self.file_name = decode(self.player_response['videoDetails']['title']) + + #: Check for start time + self.start_time = (0, 0) + m = re.search(r't=(?:(\d+)m)?(\d+)s', pyfile.url) + if self.ffmpeg and m: + self.start_time = tuple(map(lambda _x: 0 if _x is None else int(_x), m.groups())) + self.file_name += " (starting at %sm%ss)" % (self.start_time[0], self.start_time[1]) + + #: Cleaning invalid characters from the file name + self.file_name = safename(self.file_name) + + #: Parse available streams + streams = [] + for path in [('args', 'url_encoded_fmt_stream_map'), + ('args', 'adaptive_fmts')]: + item = try_get(self.player_config, *path) + if item is not None: + strms = [urlparse.parse_qs(_s) for _s in item.split(',')] + strms = [dict((k, v[0]) for k,v in _d.items()) for _d in strms] + streams.extend(strms) + streams.extend(try_get(self.player_response, 'streamingData', 'formats') or []) + streams.extend(try_get(self.player_response, 'streamingData', 'adaptiveFormats') or []) + + self.streams = [] + for _s in streams: + itag = int(_s['itag']) + url_data = _s + url = _s.get('url', None) + if url is None: + cipher = _s.get('cipher', None) + if cipher is not None: + url_data = urlparse.parse_qs(cipher) + url_data = dict((k, v[0]) for k,v in url_data.items()) + url = url_data.get('url') + if url is None: + continue + + else: + cipher = _s.get('signatureCipher') + if cipher is not None: + url_data = urlparse.parse_qs(cipher) + url = try_get(url_data, 'url', 0) + if url is None: + continue + + self.streams.append((itag, + url, + try_get(url_data, 's', 0) or url_data.get('s', url_data.get('sig', None)), + 's' in url_data, + try_get(url_data, 'sp', 0) or url_data.get('sp', "signature"))) + + self.streams = uniqify(self.streams) + + self.log_debug("AVAILABLE STREAMS: %s" % [_s[0] for _s in self.streams]) + + video_filename, video_itag = self._handle_video() + + has_audio = 'a' in self.formats[video_itag]['type'] + if not has_audio: + audio_filename, audio_itag = self._handle_audio(video_itag) + + else: + audio_filename = None + + subtitles_files = self._handle_subtitles() + + final_filename = self._postprocess(video_filename, + audio_filename, + subtitles_files) + + #: Everything is finished and final name can be set + pyfile.name = os.path.basename(fs_encode(final_filename)) + pyfile.size = os.path.getsize(fs_encode(final_filename)) + self.last_download = final_filename + + +"""Credit to this awesome piece of code below goes to the 'youtube_dl' project, kudos!""" + +class JSInterpreterError(Exception): + pass + + +class JSInterpreter(object): + + def __init__(self, code, objects=None): + self._OPERATORS = [ + ('|', operator.or_), + ('^', operator.xor), + ('&', operator.and_), + ('>>', operator.rshift), + ('<<', operator.lshift), + ('-', operator.sub), + ('+', operator.add), + ('%', operator.mod), + ('/', operator.truediv), + ('*', operator.mul), + ] + self._ASSIGN_OPERATORS = [(op + '=', opfunc) + for op, opfunc in self._OPERATORS] + self._ASSIGN_OPERATORS.append(('=', lambda cur, right: right)) + self._VARNAME_PATTERN = r'[a-zA-Z_$][a-zA-Z_$0-9]*' + + if objects is None: + objects = {} + self.code = code + self._functions = {} + self._objects = objects + + def interpret_statement(self, stmt, local_vars, allow_recursion=100): + if allow_recursion < 0: + raise JSInterpreterError('Recursion limit reached') + + should_abort = False + stmt = stmt.lstrip() + stmt_m = re.match(r'var\s', stmt) + if stmt_m: + expr = stmt[len(stmt_m.group(0)):] + + else: + return_m = re.match(r'return(?:\s+|$)', stmt) + if return_m: + expr = stmt[len(return_m.group(0)):] + should_abort = True + else: + # Try interpreting it as an expression + expr = stmt + + v = self.interpret_expression(expr, local_vars, allow_recursion) + return v, should_abort + + def interpret_expression(self, expr, local_vars, allow_recursion): + expr = expr.strip() + + if expr == '': # Empty expression + return None + + if expr.startswith('('): + parens_count = 0 + for m in re.finditer(r'[()]', expr): + if m.group(0) == '(': + parens_count += 1 + else: + parens_count -= 1 + if parens_count == 0: + sub_expr = expr[1:m.start()] + sub_result = self.interpret_expression(sub_expr, local_vars, allow_recursion) + remaining_expr = expr[m.end():].strip() + if not remaining_expr: + return sub_result + else: + expr = json.dumps(sub_result) + remaining_expr + break + else: + raise JSInterpreterError('Premature end of parens in %r' % expr) + + for op, opfunc in self._ASSIGN_OPERATORS: + m = re.match(r'(?x)(?P%s)(?:\[(?P[^\]]+?)\])?\s*%s(?P.*)$' % + (self._VARNAME_PATTERN, re.escape(op)), expr) + if m is None: + continue + right_val = self.interpret_expression(m.group('expr'), local_vars, allow_recursion - 1) + + if m.groupdict().get('index'): + lvar = local_vars[m.group('out')] + idx = self.interpret_expression(m.group('index'), local_vars, allow_recursion) + assert isinstance(idx, int) + cur = lvar[idx] + val = opfunc(cur, right_val) + lvar[idx] = val + return val + else: + cur = local_vars.get(m.group('out')) + val = opfunc(cur, right_val) + local_vars[m.group('out')] = val + return val + + if expr.isdigit(): + return int(expr) + + var_m = re.match(r'(?!if|return|true|false)(?P%s)$' % self._VARNAME_PATTERN, expr) + if var_m: + return local_vars[var_m.group('name')] + + try: + return json.loads(expr) + except ValueError: + pass + + m = re.match(r'(?P%s)\.(?P[^(]+)(?:\(+(?P[^()]*)\))?$' % self._VARNAME_PATTERN, expr) + if m is not None: + variable = m.group('var') + member = m.group('member') + arg_str = m.group('args') + + if variable in local_vars: + obj = local_vars[variable] + else: + if variable not in self._objects: + self._objects[variable] = self.extract_object(variable) + obj = self._objects[variable] + + if arg_str is None: + # Member access + if member == 'length': + return len(obj) + return obj[member] + + assert expr.endswith(')') + # Function call + if arg_str == '': + argvals = tuple() + else: + argvals = tuple(self.interpret_expression(v, local_vars, allow_recursion) for v in arg_str.split(',')) + + if member == 'split': + assert argvals == ('',) + return list(obj) + + if member == 'join': + assert len(argvals) == 1 + return argvals[0].join(obj) + + if member == 'reverse': + assert len(argvals) == 0 + obj.reverse() + return obj + + if member == 'slice': + assert len(argvals) == 1 + return obj[argvals[0]:] + + if member == 'splice': + assert isinstance(obj, list) + index, howMany = argvals + res = [] + for i in range(index, min(index + howMany, len(obj))): + res.append(obj.pop(index)) + return res + + return obj[member](argvals) + + m = re.match(r'(?P%s)\[(?P.+)\]$' % self._VARNAME_PATTERN, expr) + if m is not None: + val = local_vars[m.group('in')] + idx = self.interpret_expression(m.group('idx'), local_vars, allow_recursion - 1) + return val[idx] + + for op, opfunc in self._OPERATORS: + m = re.match(r'(?P.+?)%s(?P.+)' % re.escape(op), expr) + if m is None: + continue + + x, abort = self.interpret_statement(m.group('x'), local_vars, allow_recursion - 1) + if abort: + raise JSInterpreterError('Premature left-side return of %s in %r' % (op, expr)) + + y, abort = self.interpret_statement(m.group('y'), local_vars, allow_recursion - 1) + if abort: + raise JSInterpreterError('Premature right-side return of %s in %r' % (op, expr)) + + return opfunc(x, y) + + m = re.match(r'^(?P%s)\((?P[a-zA-Z0-9_$,]+)\)$' % self._VARNAME_PATTERN, expr) + if m is not None: + fname = m.group('func') + argvals = tuple(int(v) if v.isdigit() else local_vars[v] + for v in m.group('args').split(',')) + if fname not in self._functions: + self._functions[fname] = self.extract_function(fname) + return self._functions[fname](argvals) + + raise JSInterpreterError('Unsupported JS expression %r' % expr) + + def extract_object(self, objname): + obj = {} + obj_m = re.search(r'(?:var\s+)?%s\s*=\s*\{\s*(?P([a-zA-Z$0-9]+\s*:\s*function\(.*?\)\s*\{.*?\}(?:,\s*)?)*)\}\s*;' + % re.escape(objname), self.code) + fields = obj_m.group('fields') + # Currently, it only supports function definitions + fields_m = re.finditer(r'(?P[a-zA-Z$0-9]+)\s*:\s*function\((?P[a-z,]+)\){(?P[^}]+)}', fields) + for f in fields_m: + argnames = f.group('args').split(',') + obj[f.group('key')] = self.build_function(argnames, f.group('code')) + + return obj + + def extract_function(self, function_name): + func_m = re.search(r'(?x)(?:function\s+%s|[{;,]\s*%s\s*=\s*function|var\s+%s\s*=\s*function)\s*\((?P[^)]*)\)\s*\{(?P[^}]+)\}' + % (re.escape(function_name), re.escape(function_name), re.escape(function_name)), self.code) + if func_m is None: + raise JSInterpreterError('Could not find JS function %r' % function_name) + + argnames = func_m.group('args').split(',') + + return self.build_function(argnames, func_m.group('code')) + + def call_function(self, function_name, *args): + f = self.extract_function(function_name) + return f(args) + + def build_function(self, argnames, code): + def resf(argvals): + local_vars = dict(zip(argnames, argvals)) + for stmt in code.split(';'): + res, abort = self.interpret_statement(stmt, local_vars) + if abort: + break + return res + + return resf diff --git a/module/plugins/hoster/ZDF.py b/module/plugins/hoster/ZDF.py index 9940fd078e..07436df70d 100644 --- a/module/plugins/hoster/ZDF.py +++ b/module/plugins/hoster/ZDF.py @@ -1,47 +1,64 @@ +# -*- coding: utf-8 -*- + +import json +import os import re -from xml.etree.ElementTree import fromstring -from module.plugins.Hoster import Hoster +import pycurl -XML_API = "http://www.zdf.de/ZDFmediathek/xmlservice/web/beitragsDetails?id=%i" +from ..internal.Hoster import Hoster +# Based on zdfm by Roland Beermann (http://github.com/enkore/zdfm/) class ZDF(Hoster): - # Based on zdfm by Roland Beermann - # http://github.com/enkore/zdfm/ __name__ = "ZDF Mediathek" - __version__ = "0.7" - __pattern__ = r"http://www\.zdf\.de/ZDFmediathek/[^0-9]*([0-9]+)[^0-9]*" - __config__ = [] - - @staticmethod - def video_key(video): - return ( - int(video.findtext("videoBitrate", "0")), - any(f.text == "progressive" for f in video.iter("facet")), - ) + __type__ = "hoster" + __version__ = "0.93" + __status__ = "testing" - @staticmethod - def video_valid(video): - return video.findtext("url").startswith("http") and video.findtext("url").endswith(".mp4") + __pattern__ = r"https://(?:www\.)?zdf\.de/(?P[/\w-]+)\.html" + __config__ = [("activated", "bool", "Activated", True), + ("use_premium", "bool", "Use premium account if available", True), + ("fallback", "bool", "Fallback to free download if premium fails", True), + ("chk_filesize", "bool", "Check file size", True), + ("max_wait", "int", "Reconnect if waiting time is greater than minutes", 10)] - @staticmethod - def get_id(url): - return int(re.search(r"[^0-9]*([0-9]+)[^0-9]*", url).group(1)) + __description__ = """ZDF.de downloader plugin""" + __license__ = "GPLv3" + __authors__ = [] def process(self, pyfile): - xml = fromstring(self.load(XML_API % self.get_id(pyfile.url))) + self.data = self.load(pyfile.url) + try: + api_token = re.search( + r'window\.zdfsite\.player\.apiToken = "([\d\w]+)";', self.data + ).group(1) - status = xml.findtext("./status/statuscode") - if status != "ok": - self.fail("Error retrieving manifest.") + self.req.http.c.setopt(pycurl.HTTPHEADER, ["Api-Auth: Bearer " + api_token]) + id = re.match(self.__pattern__, pyfile.url).group("ID") - video = xml.find("video") - title = video.findtext("information/title") + filename = json.loads( + self.load( + "https://api.zdf.de/content/documents/zdf/" + id + ".json", + get={"profile": "player-3"}, + ) + ) + stream_list = filename["mainVideoContent"]["http://zdf.de/rels/target"][ + "streams" + ]["default"]["extId"] - pyfile.name = title + streams = json.loads( + self.load( + "https://api.zdf.de/tmd/2/ngplayer_2_4/vod/ptmd/mediathek/" + + stream_list + ) + ) + download_name = streams["priorityList"][0]["formitaeten"][0]["qualities"][ + 0 + ]["audio"]["tracks"][0]["uri"] - target_url = sorted((v for v in video.iter("formitaet") if self.video_valid(v)), - key=self.video_key)[-1].findtext("url") + self.pyfile.name = os.path.basename(id) + os.path.splitext(download_name)[1] + self.download(download_name) - self.download(target_url) + except Exception as exc: + self.log_error(exc) diff --git a/module/plugins/hoster/ZbigzCom.py b/module/plugins/hoster/ZbigzCom.py new file mode 100644 index 0000000000..1c5d0eb792 --- /dev/null +++ b/module/plugins/hoster/ZbigzCom.py @@ -0,0 +1,181 @@ +# -*- coding: utf-8 -*- + +import os +import time +import urllib +import urlparse + +from ..internal.Hoster import Hoster +from ..internal.misc import json, safejoin + +try: + from module.network.HTTPRequest import FormFile +except ImportError: + pass + + + +class ZbigzCom(Hoster): + __name__ = "ZbigzCom" + __type__ = "hoster" + __version__ = "0.05" + __status__ = "testing" + + __pattern__ = r'^unmatchable$' + __config__ = [("activated", "bool", "Activated", False)] + + __description__ = """Zbigz.com hoster plugin""" + __license__ = "GPLv3" + __authors__ = [("GammaC0de", "nitzo2001[AT}yahoo[DOT]com")] + + API_URL = "https://api.zbigz.com/v1/" + + def load_json(self, url, **kwargs): + json_data = self.load(url, **kwargs) + return json.loads(json_data) + + def api_call(self, method, **kwargs): + return self.load_json(self.API_URL + method, **kwargs) + + def sleep(self, sec): + for _ in range(sec): + if self.pyfile.abort: + break + time.sleep(1) + + def exit_error(self, msg): + if self.tmp_file: + os.remove(self.tmp_file) + + self.fail(msg) + + def send_request_to_server(self): + """ Send torrent/magnet to the server """ + + if self.pyfile.url.startswith("magnet:"): + #: magnet URL, send it to the server + api_data = self.api_call( + "torrent/add", + post={'url': self.pyfile.url}, + multipart=True + ) + + else: + #: torrent URL + if self.pyfile.url.startswith("http"): + #: remote URL, download the torrent to tmp directory + torrent_content = self.load(self.pyfile.url, decode=False) + torrent_filename = safejoin(self.pyload.tempdir, "tmp_{}.torrent".format(self.pyfile.package().name)) + with open(torrent_filename, "wb") as f: + f.write(torrent_content) + + else: + #: URL is a local torrent file (uploaded container) + torrent_filename = urllib.url2pathname(self.pyfile.url[7:]) #: trim the starting `file://` + if not os.path.exists(torrent_filename): + self.fail(_("Torrent file does not exist")) + + self.tmp_file = torrent_filename + + #: Check if the torrent file path is inside pyLoad's temp directory + if os.path.abspath(torrent_filename).startswith(self.pyload.tempdir + os.sep): + #: send the torrent content to the server + api_data = self.api_call( + "torrent/add", + post={'file': FormFile(torrent_filename, mimetype="application/octet-stream")}, + multipart=True + ) + + else: + self.fail(_("Illegal URL")) #: We don't allow files outside pyLoad's temp directory + + if api_data['error']: + self.fail(api_data['message']) + + api_data = self.api_call("storage/list") + if api_data['error']: + self.fail(api_data['error_msg']) + + torrent_id = api_data["0"]["hash"] + server = api_data["0"]["server"] + + if self.tmp_file: + os.remove(self.tmp_file) + self.tmp_file = None + + return torrent_id, server + + def wait_for_server_dl(self, torrent_id, server): + """ Show progress while the server does the download """ + + self.pyfile.setCustomStatus("torrent") + self.pyfile.setProgress(0) + + while True: + api_data = self.load_json( + "https://%s/gate/status" % server, + get={"hash": torrent_id} + ) + + if api_data["error"] == 404 and api_data["result"] == "not found +init": + pass + + elif api_data["error"]: + self.exit_error(api_data["result"]) + + if api_data.get("has_metadata", False): + self.pyfile.name = api_data["name"] + self.pyfile.size = api_data["size"] + break + + self.sleep(5) + + while True: + api_data = self.load_json( + "https://%s/gate/status" % server, + get={"hash": torrent_id} + ) + if api_data["error"]: + self.exit_error(api_data["result"]) + + progress = api_data["progress"] + self.pyfile.setProgress(progress) + if progress >= 100: + break + + self.sleep(5) + + self.pyfile.setProgress(100) + + def download_from_server(self, torrent_id, server): + api_data = self.api_call("storage/download/%s" % torrent_id) + if api_data['error']: + self.fail(api_data['error_msg']) + + if "state" in api_data: + zip_status_url = urlparse.urljoin("https://", api_data["state"]) + self.pyfile.setCustomStatus("zipping") + self.pyfile.setProgress(0) + + while True: + zip_status = self.load_json(zip_status_url) + progress = zip_status["proc"] + self.pyfile.setProgress(progress) + if progress >= 100: + break + + self.sleep(2) + + self.pyfile.setProgress(100) + + download_url = api_data["link"] + self.download(download_url) + + def process(self, pyfile): + self.tmp_file = None + torrent_id, server = self.send_request_to_server() + self.wait_for_server_dl(torrent_id, server) + self.download_from_server(torrent_id, server) + + if self.tmp_file: + os.remove(self.tmp_file) diff --git a/module/plugins/hoster/ZeveraCom.py b/module/plugins/hoster/ZeveraCom.py index e8b832a139..dc2aa3c6ac 100644 --- a/module/plugins/hoster/ZeveraCom.py +++ b/module/plugins/hoster/ZeveraCom.py @@ -1,106 +1,49 @@ -#!/usr/bin/env python # -*- coding: utf-8 -*- -from module.plugins.Hoster import Hoster +from ..internal.misc import json +from ..internal.MultiHoster import MultiHoster -class ZeveraCom(Hoster): + +class ZeveraCom(MultiHoster): __name__ = "ZeveraCom" - __version__ = "0.21" __type__ = "hoster" - __pattern__ = r"http://zevera.com/.*" - __description__ = """zevera.com hoster plugin""" - __author_name__ = ("zoidberg") - __author_mail__ = ("zoidberg@mujmail.cz") + __version__ = "0.39" + __status__ = "testing" + + __pattern__ = r'https?://(?:www\.)zevera\.com/(getFiles\.ashx|Members/download\.ashx)\?.*ourl=.+' + __config__ = [("activated", "bool", "Activated", True), + ("use_premium", "bool", "Use premium account if available", True), + ("fallback", "bool", "Fallback to free download if premium fails", False), + ("chk_filesize", "bool", "Check file size", True), + ("max_wait", "int","Reconnect if waiting time is greater than minutes", 10), + ("revert_failed", "bool", "Revert to standard download if fails", True)] - def setup(self): - self.resumeDownload = self.multiDL = True - self.chunkLimit = 1 + __description__ = """Zevera.com multi-hoster plugin""" + __license__ = "GPLv3" + __authors__ = [("zoidberg", "zoidberg@mujmail.cz"), + ("Walter Purcaro", "vuolter@gmail.com"), + ("GammaC0de", "nitzo2001[AT]yahoo[DOT]com")] - def process(self, pyfile): - if not self.account: - self.logError(_("Please enter your %s account or deactivate this plugin") % "zevera.com") - self.fail("No zevera.com account provided") + API_URL = "https://www.zevera.com/api/" - self.logDebug("zevera.com: Old URL: %s" % pyfile.url) + def api_response(self, method, api_key, **kwargs): + get_data = {'client_id': "452508742", + 'apikey': api_key} - if self.account.getAPIData(self.req, cmd="checklink", olink=pyfile.url) != "Alive": - self.fail("Offline or not downloadable - contact Zevera support") + get_data.update(kwargs) - header = self.account.getAPIData(self.req, just_header=True, cmd="generatedownloaddirect", olink=pyfile.url) - if not "location" in header: - self.fail("Unable to initialize download - contact Zevera support") + res = self.load(self.API_URL + method, + get=get_data) - self.download(header['location'], disposition=True) + return json.loads(res) - check = self.checkDownload({"error": 'action="ErrorDownload.aspx'}) - if check == "error": - self.fail("Error response received - contact Zevera support") + def handle_premium(self, pyfile): + res = self.api_response("transfer/directdl", self.account.info['login']['password'], src=pyfile.url) + if res['status'] == "success": + self.link = res['location'] + pyfile.name = res['filename'] + pyfile.size = res['filesize'] - # BitAPI not used - defunct, probably abandoned by Zevera - # - # api_url = "http://zevera.com/API.ashx" - # - # def process(self, pyfile): - # if not self.account: - # self.logError(_("Please enter your zevera.com account or deactivate this plugin")) - # self.fail("No zevera.com account provided") - # - # self.logDebug("zevera.com: Old URL: %s" % pyfile.url) - # - # last_size = retries = 0 - # olink = self.pyfile.url #quote(self.pyfile.url.encode('utf_8')) - # - # for i in range(100): - # self.retData = self.account.loadAPIRequest(self.req, cmd = 'download_request', olink = olink) - # self.checkAPIErrors(self.retData) - # - # if self.retData['FileInfo']['StatusID'] == 100: - # break - # elif self.retData['FileInfo']['StatusID'] == 99: - # self.fail('Failed to initialize download (99)') - # else: - # if self.retData['FileInfo']['Progress']['BytesReceived'] <= last_size: - # if retries >= 6: - # self.fail('Failed to initialize download (%d)' % self.retData['FileInfo']['StatusID'] ) - # retries += 1 - # else: - # retries = 0 - # - # last_size = self.retData['FileInfo']['Progress']['BytesReceived'] - # - # self.setWait(self.retData['Update_Wait']) - # self.wait() - # - # pyfile.name = self.retData['FileInfo']['RealFileName'] - # pyfile.size = self.retData['FileInfo']['FileSizeInBytes'] - # - # self.retData = self.account.loadAPIRequest(self.req, cmd = 'download_start', - # FileID = self.retData['FileInfo']['FileID']) - # self.checkAPIErrors(self.retData) - # - # self.download(self.api_url, get = { - # 'cmd': "open_stream", - # 'login': self.account.loginname, - # 'pass': self.account.password, - # 'FileID': self.retData['FileInfo']['FileID'], - # 'startBytes': 0 - # } - # ) - # - # def checkAPIErrors(self, retData): - # if not retData: - # self.fail('Unknown API response') - # - # if retData['ErrorCode']: - # self.logError(retData['ErrorCode'], retData['ErrorMessage']) - # #self.fail('ERROR: ' + retData['ErrorMessage']) - # - # if self.pyfile.size / 1024000 > retData['AccountInfo']['AvailableTODAYTrafficForUseInMBytes']: - # self.logWarning("Not enough data left to download the file") - # - # def crazyDecode(self, ustring): - # # accepts decoded ie. unicode string - API response is double-quoted, double-utf8-encoded - # # no idea what the proper order of calling these functions would be :-/ - # return html_unescape(unquote(unquote(ustring.replace( - # '@DELIMITER@','#'))).encode('raw_unicode_escape').decode('utf-8')) + else: + self.fail(res['message']) diff --git a/module/plugins/hoster/ZippyshareCom.py b/module/plugins/hoster/ZippyshareCom.py deleted file mode 100644 index e4f2befa01..0000000000 --- a/module/plugins/hoster/ZippyshareCom.py +++ /dev/null @@ -1,224 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -# Test links (random.bin): -# http://www29.zippyshare.com/v/55578602/file.html - -import re -import subprocess -import tempfile -import os - -from module.plugins.internal.SimpleHoster import SimpleHoster, create_getInfo, timestamp -from module.plugins.internal.CaptchaService import ReCaptcha -from module.common.json_layer import json_loads - - -class ZippyshareCom(SimpleHoster): - __name__ = "ZippyshareCom" - __type__ = "hoster" - __pattern__ = r"(?Phttp://www\d{0,2}\.zippyshare.com)/v(?:/|iew.jsp.*key=)(?P\d+)" - __version__ = "0.42" - __description__ = """Zippyshare.com Download Hoster""" - __author_name__ = ("spoob", "zoidberg", "stickell") - __author_mail__ = ("spoob@pyload.org", "zoidberg@mujmail.cz", "l.stickell@yahoo.it") - __config__ = [("swfdump_path", "string", "Path to swfdump", "")] - - FILE_NAME_PATTERN = r'>Name:\s*]*>(?P[^<]+)
      ' - FILE_SIZE_PATTERN = r'>Size:\s*]*>(?P[0-9.,]+) (?P[kKMG]+)i?B
      ' - FILE_INFO_PATTERN = r'document\.getElementById\(\'dlbutton\'\)\.href = "[^;]*/(?P[^"]+)";' - FILE_OFFLINE_PATTERN = r'>File does not exist on this server
      ' - - SH_COOKIES = [('zippyshare.com', 'ziplocale', 'en')] - - DOWNLOAD_URL_PATTERN = r" + {% endblock %} @@ -63,7 +63,7 @@ {% endblock %} {% block hidden %}
      - +

      {{ _("Change Password") }}

      {{ _("Enter your current and desired Password.") }}

      diff --git a/module/web/templates/classic/base.html b/module/web/templates/classic/base.html new file mode 100644 index 0000000000..b76daf16ed --- /dev/null +++ b/module/web/templates/classic/base.html @@ -0,0 +1,292 @@ + + + + + + + + + + + + + + + + + + + + +{% block title %}pyLoad {{_("Webinterface")}}{% endblock %} + +{% block head %} +{% endblock %} + + + + +
      + +
      + {% block headpanel %} + + {% if user.is_authenticated %} + +{% if update %} + +{{_("pyLoad Update available!")}} + +{% endif %} + +{% if plugins %} + +{{_("Plugins updated, please restart!")}} + +{% endif %} + + +Captcha: +{{_("Captcha waiting")}} + + + User:{{user.name}} + +{% else %} + {{_("Please Login!")}} +{% endif %} + + {% endblock %} +
      + + + +
      + +
      + +
      +
      + +{% if perms.STATUS %} + +{% endif %} + +{% if perms.LIST %} + +{% endif %} + +{% block pageactions %} +{% endblock %} +
      + +
      + +
      + +

      {% block subtitle %}pyLoad - {{_("Webinterface")}}{% endblock %}

      + +{% block statusbar %} +{% endblock %} + +
      + +
      +
      + + +{% for message in messages %} +

      {{message}}

      +{% endfor %} + +
      + + {{_("loading")}} +
      + +{% block content %} +{% endblock content %} + +
      + + +
      +
      + +
      + {% include "classic/window.html" %} + {% include "classic/captcha.html" %} + {% block hidden %} + {% endblock %} +
      + + diff --git a/module/web/templates/classic/captcha.html b/module/web/templates/classic/captcha.html new file mode 100644 index 0000000000..3a3f5d1ed5 --- /dev/null +++ b/module/web/templates/classic/captcha.html @@ -0,0 +1,55 @@ + +
      + +

      {{_("Captcha reading")}}

      +

      {{_("Please read the text on the captcha.")}}

      + + +
      + + + + + + +
      + + + + + +
      + + + + +
      + +
      + + + +
      \ No newline at end of file diff --git a/module/web/templates/default/downloads.html b/module/web/templates/classic/downloads.html similarity index 94% rename from module/web/templates/default/downloads.html rename to module/web/templates/classic/downloads.html index 450b8a1025..e64d2a27b9 100644 --- a/module/web/templates/default/downloads.html +++ b/module/web/templates/classic/downloads.html @@ -1,4 +1,4 @@ -{% extends 'default/base.html' %} +{% extends 'classic/base.html' %} {% block title %}Downloads - {{super()}} {% endblock %} diff --git a/module/web/templates/default/filemanager.html b/module/web/templates/classic/filemanager.html similarity index 81% rename from module/web/templates/default/filemanager.html rename to module/web/templates/classic/filemanager.html index 97095c13e6..cf5976dd31 100644 --- a/module/web/templates/default/filemanager.html +++ b/module/web/templates/classic/filemanager.html @@ -1,8 +1,8 @@ -{% extends 'default/base.html' %} +{% extends 'classic/base.html' %} {% block head %} - + - +
      diff --git a/module/web/templates/default/queue.html b/module/web/templates/classic/queue.html similarity index 83% rename from module/web/templates/default/queue.html rename to module/web/templates/classic/queue.html index 046abbe498..bc0f6b513f 100644 --- a/module/web/templates/default/queue.html +++ b/module/web/templates/classic/queue.html @@ -1,7 +1,7 @@ -{% extends 'default/base.html' %} +{% extends 'classic/base.html' %} {% block head %} - + - - + + + {% endblock %} @@ -81,7 +81,7 @@

         {{ _("Choose a section from the menu") }}

      -
      + @@ -172,7 +172,7 @@

         {{ _("Choose a section from the menu") }}

      {% endblock %} {% block hidden %}
      - +

      {{_("Add Account")}}

      {{_("Enter your account data to use premium features.")}}

      + style="color:#424242;">{{_(option.desc)}}:
      {% if option.type == "bool" %} {% elif ";" in option.type %} - - - - - - - - - - - -
      - -
      - -
      - - - - -
      - -
      - - - - \ No newline at end of file diff --git a/module/web/templates/modern/admin.html b/module/web/templates/modern/admin.html new file mode 100644 index 0000000000..f75c1076b8 --- /dev/null +++ b/module/web/templates/modern/admin.html @@ -0,0 +1,157 @@ +{% extends 'modern/base.html' %} + +{% block head %} + +{% endblock %} + +{% block footer %} + +{% endblock %} + + +{% block title %}{{_('Administrate')}} - {{super()}} {% endblock %} +{% block subtitle %}{{_('Administrate')}}{% endblock %} + +{% block content %} +
      + + +
      +
      +
      + +
      +
      +
      +

      {{_('To add user or change passwords use:')}} python pyLoadCore.py -u

      +

      {{_('Important:')}} {{_('Admin user have always all permissions!')}}

      +
      +
      +
      + +
      +
      + + + + + + + + + + + {% for name, data in users.iteritems() %} + + + + + + + {% endfor %} + + +
      {{_('Name')}}{{_('Change Password')}}{{_('Admin')}}{{_('Permissions')}}
      {{name}}
      {{_('change')}}
      + +
      + + +
      +{% endblock %} + +{% block dialog %} + + + + + + + + + +{% endblock %} diff --git a/module/web/templates/modern/base.html b/module/web/templates/modern/base.html new file mode 100644 index 0000000000..e377511880 --- /dev/null +++ b/module/web/templates/modern/base.html @@ -0,0 +1,253 @@ + + + + + + + + + + + + + + + + +{% block title %}pyLoad {{_('Webinterface')}}{% endblock %} + +{% block head %} +{% endblock %} + + + + +{% macro selected(name, right=False) -%} + {% if name in url -%}class="{% if right -%}right {% endif %}selected"{%- endif %} + {% if not name in url and right -%}class="right"{%- endif %} +{%- endmacro %} + + +
      +
      + {% block headpanel %} + {% if user.is_authenticated %} + {% if update %} + + {{_('pyLoad Update available!')}} + + {% endif %} + {% if plugins %} + + {{_('Plugins updated, please restart!')}} + + {% endif %} + {% endif %} + {% endblock %} +
      + + + +
      +
      + + + +
      +
      +
      +
      +

      {% block subtitle %}{{_(' ')}}{% endblock %}

      +
      +
      + {% block statusbar %} + {% endblock %} + {% for message in messages %} +

      {{message}}

      + {% endfor %} +
      +
      + + {{_('loading')}} +
      +
      + + {% block content %} + {% endblock content %} +
      +
      + +
      + + + + +
      + {% block hidden %} + {% endblock %} +
      +{% block dialog %} +{% endblock %} + +{% include "modern/window.html" %} +{% include "modern/captcha.html" %} + + + + + + + + +{% block footer %} +{% endblock %} + + + + diff --git a/module/web/templates/modern/captcha.html b/module/web/templates/modern/captcha.html new file mode 100644 index 0000000000..825346f588 --- /dev/null +++ b/module/web/templates/modern/captcha.html @@ -0,0 +1,53 @@ + + + diff --git a/module/web/templates/modern/downloads.html b/module/web/templates/modern/downloads.html new file mode 100644 index 0000000000..35cb0db274 --- /dev/null +++ b/module/web/templates/modern/downloads.html @@ -0,0 +1,29 @@ +{% extends 'modern/base.html' %} + +{% block title %}Downloads - {{super()}} {% endblock %} + +{% block subtitle %} +{{_('Downloads')}} +{% endblock %} + +{% block content %} + +
        + {% for folder in files.folder %} +
      • + {{folder.name}} +
          + {% for file in folder.files %} +
        • {{file}}
        • + {% endfor %} +
        +
      • + {% endfor %} + + {% for file in files.files %} +
      • {{file}}
      • + {% endfor %} + +
      + +{% endblock %} diff --git a/module/web/templates/modern/home.html b/module/web/templates/modern/home.html new file mode 100644 index 0000000000..a5bb892eaa --- /dev/null +++ b/module/web/templates/modern/home.html @@ -0,0 +1,29 @@ +{% extends 'modern/base.html' %} +{% block head %} + +{% endblock %} + +{% block footer %} + +{% endblock %} + +{% block pageactions %} +{% endblock %} + +{% block subtitle %}{{_('Active Downloads')}}{% endblock %} +{% block content %} + + + + + + + + + + + + + +
      {{_('Name')}}{{_('Information')}}
      +{% endblock %} diff --git a/module/web/templates/modern/info.html b/module/web/templates/modern/info.html new file mode 100644 index 0000000000..2843ad9a89 --- /dev/null +++ b/module/web/templates/modern/info.html @@ -0,0 +1,59 @@ +{% extends 'modern/base.html' %} + +{% block title %}{{_('Support')}} - {{super()}} {% endblock %} +{% block subtitle %}{{_('Support')}}{% endblock %} + +{% block head %} + +{% endblock %} + +{% block footer %} + +{% endblock %} + +{% block content %} + + +

      {{_('System')}}

      +
      +
      {{_('Python:')}}
      +
      {{python}}
      +
      {{_('OS:')}}
      +
      {{os}}
      +
      {{_('pyLoad version:')}}
      +
      {{version}}
      +
      {{_('Installation Folder:')}}
      +
      {{folder}}
      +
      {{_('Config Folder:')}}
      +
      {{config}}
      +
      {{_('Download Folder:')}}
      +
      {{download}}
      +
      {{_('Free Space:')}}
      +
      {{freespace}}
      +
      {{_('Language:')}}
      +
      {{language}}
      +
      {{_('Webinterface Theme:')}}
      +
      modern by GammaC0de based on work of Marco Fernandes and Dogan Bagci
      +
      {{_('Webinterface Port:')}}
      +
      {{webif}}
      +
      {{_('Remote Interface Port:')}}
      +
      {{remote}}
      +
      +

      {{_('News')}}

      +
      {{_('Loading...')}}
      +{% endblock %} + +{% block pageactions %} + +{% endblock %} diff --git a/module/web/templates/modern/login.html b/module/web/templates/modern/login.html new file mode 100644 index 0000000000..c138a67b1e --- /dev/null +++ b/module/web/templates/modern/login.html @@ -0,0 +1,72 @@ +{% extends 'modern/base.html' %} +{% block title %}{{_('Login')}} - {{super()}} {% endblock %} + +{% block head %} + +{% endblock %} + +{% block content %} +
      +
      + + + + + {% if errors %} +
      +
      +

      {{_('Incorrect username/email or password.')}}

      +
      +
      + {% endif %} + +
      +
      + + +
      +
      + + +
      + + +
      +
      +
      +
      + +{% endblock %} diff --git a/module/web/templates/modern/logout.html b/module/web/templates/modern/logout.html new file mode 100644 index 0000000000..f2c31dae82 --- /dev/null +++ b/module/web/templates/modern/logout.html @@ -0,0 +1,19 @@ +{% extends 'modern/base.html' %} + +{% block head %} + + + +{% endblock %} + +{% block content %} +
      +
      +

      {{_('You were successfully logged out.')}}

      +
      +
      +{% endblock %} diff --git a/module/web/templates/modern/logs.html b/module/web/templates/modern/logs.html new file mode 100644 index 0000000000..8e24a82a86 --- /dev/null +++ b/module/web/templates/modern/logs.html @@ -0,0 +1,117 @@ +{% extends 'modern/base.html' %} + +{% block title %}{{_('Logs')}} - {{super()}} {% endblock %} +{% block subtitle %}{{_('Logs')}}{% endblock %} + +{% block head %} + +{% endblock %} + +{% block content %} +
      + +
      +
      +
      + +   +   + +
      +
      +
      {{warning}}
      +
      +
      + + {% for line in log %} + + {% endfor %} +
      {{line.line}}{{line.date}}{{line.level}}{{line.message}}
      +
      +
      +
      + + + +
      +
      + + +{% endblock %} diff --git a/module/web/templates/modern/pathchooser.html b/module/web/templates/modern/pathchooser.html new file mode 100644 index 0000000000..0d726c1177 --- /dev/null +++ b/module/web/templates/modern/pathchooser.html @@ -0,0 +1,121 @@ + + + + + + + + + + + + + + + + + + {% if parentdir %} + + + + + + {% endif %} + {% for file in files %} + {% if type == 'folder' %} {# browsing for folder #} + {% if file.type == 'dir' %} + + + {% else %} + + + {% endif %} + {% else %} {# browsing for file #} + {% if file.type == 'dir' %} + + + {% else %} + + + {% endif %} + {% endif %} + + + + + + {% endfor %} + +
      {{_('name')}}{{_('size')}}{{_('type')}}{{_('last modified')}}
      ..<DIR>
      {{file.name}}
      {{file.name}}
      {{file.name}}
      {{file.name}}{% if file.type == 'file' %} + {% if file.unit == 'Byte' %}1 KB{% else %}{{(file.size|round(precision=0, method='common'))|int}} {{file.unit|replace('Byte', 'B')}}{% endif %} + {% endif %}{% if file.type == 'dir' %}<DIR>{% else %}{{file.ext|default('file', True)}}{% endif %}{{file.modified|date('d.m.Y - H:i:s')}}
      + + diff --git a/module/web/templates/modern/queue.html b/module/web/templates/modern/queue.html new file mode 100644 index 0000000000..f1d6b8dd23 --- /dev/null +++ b/module/web/templates/modern/queue.html @@ -0,0 +1,104 @@ +{% extends 'modern/base.html' %} +{% block head %} +{% endblock %} + +{% block footer %} + + +{% endblock %} + +{% if target %} + {% set name = _("Queue") %} +{% else %} + {% set name = _("Collector") %} +{% endif %} + +{% block pageactions %} +
      + + +
      +{% endblock %} + +{% block title %}{{name}} - {{super()}} {% endblock %} +{% block subtitle %}{{name}}{% endblock %} + +{% block content %} +{% autoescape true %} + +
        +{% for package in content %} +
      • +
        + + +
        + + {{package.name}} + + + + + + + +
        + {% set progress = (package.linksdone * 100) / package.linkstotal %} + +
        +
        + + +
        + + +
        +
      • +{% endfor %} +
      +{% endautoescape %} +{% endblock %} + +{% block dialog %} + +{% endblock %} diff --git a/module/web/templates/modern/settings.html b/module/web/templates/modern/settings.html new file mode 100644 index 0000000000..1777374fb7 --- /dev/null +++ b/module/web/templates/modern/settings.html @@ -0,0 +1,226 @@ +{% extends 'modern/base.html' %} + +{% block title %}{{_('Config')}} - {{super()}} {% endblock %} +{% block subtitle %}{{_('Config')}}{% endblock %} + +{% block head %} + + + +{% endblock %} + +{% block footer %} + +{% endblock %} + +{% block content %} + + +
      +
      +
      +
      +
        + {% for entry,name in conf.general %} + +
      • {{_(name)}}
      • +
        + {% endfor %} +
      +
      +
      +
      + +
      +

      {{_('Choose a section from the menu')}}

      +
      +
      + +
      +
      +
      +
      +
      +
      + + + +
      +
        + + +
      +
      +
      +
      + +
      +

      {{_('Choose a section from the menu')}}

      +
      +
      + +
      +
      +
      +
      + + + + + + + + + + + + + + + + + {% for account in conf.accs %} + {% set plugin = account.type %} + + + + + + + + + + + + + + {% endfor %} +
      {{_('Plugin')}}{{_('Name')}}{{_('Password')}}{{_('Status')}}{{_('Premium')}}{{_('Valid until')}}{{_('Traffic left')}}{{_('Time')}}{{_('Max Parallel')}}{{_('Delete?')}}
      + {{plugin}} + + + + + + {% if account.valid %}{{_('valid')}}{% else %}{{_('not valid')}}{% endif %} + + {% if account.premium %} + {{_('yes')}}{% else %}{{_('no')}}{% endif %} + + {{account.validuntil}} + + {{account.trafficleft}} + + + + + + +
      + + +
      +
      +
      +{% endblock %} + +{% block dialog %} + + + + + + +{% endblock %} diff --git a/module/web/templates/modern/settings_item.html b/module/web/templates/modern/settings_item.html new file mode 100644 index 0000000000..f1d020b591 --- /dev/null +++ b/module/web/templates/modern/settings_item.html @@ -0,0 +1,50 @@ + + {% if section.outline %} + + + + {% endif %} + {% for okey, option in section.iteritems() %} + {% if okey not in ("desc","outline") %} + + + + + {% endif %} + {% endfor %} +
      {{section.outline}}
      + {% if option.type == "bool" %} + + {% elif ";" in option.type %} + + {% elif option.type == "folder" %} +
      + + + + +
      + {% elif option.type == "file" %} +
      + + + + +
      + {% elif option.type == "password" %} + + {% else %} + + {% endif %} +
      diff --git a/module/web/templates/modern/window.html b/module/web/templates/modern/window.html new file mode 100644 index 0000000000..538707e8d6 --- /dev/null +++ b/module/web/templates/modern/window.html @@ -0,0 +1,53 @@ + + diff --git a/module/web/templates/pyplex/admin.html b/module/web/templates/pyplex/admin.html new file mode 100644 index 0000000000..9eddeed955 --- /dev/null +++ b/module/web/templates/pyplex/admin.html @@ -0,0 +1,161 @@ +{% extends 'pyplex/base.html' %} + +{% block head %} + +{% endblock %} + +{% block footer %} + +{% endblock %} + + +{% block title %}{{_('Administrate')}} - {{super()}} {% endblock %} +{% block subtitle %}{{_('Administrate')}}{% endblock %} + +{% block content %} +
      + + +
      +
      +
      + +
      +
      +
      +

      {{_('To add user or change passwords use:')}} python pyLoadCore.py -u

      +

      {{_('Important:')}} {{_('Admin user have always all permissions!')}}

      +
      +
      +
      + +
      +
      + + + + + + + + + + + {% for name, data in users.iteritems() %} + + + + + + + {% endfor %} + + +
      {{_('Name')}}{{_('Change Password')}}{{_('Admin')}}{{_('Permissions')}}
      {{name}}
      {{_('change')}}
      + +
      + + +
      +{% endblock %} + +{% block dialog %} + + + + + + + + + +{% endblock %} diff --git a/module/web/templates/pyplex/base.html b/module/web/templates/pyplex/base.html new file mode 100644 index 0000000000..c78d111ba6 --- /dev/null +++ b/module/web/templates/pyplex/base.html @@ -0,0 +1,253 @@ + + + + + + + + + + + + + + + + +{% block title %}pyLoad {{_('Webinterface')}}{% endblock %} + +{% block head %} +{% endblock %} + + + + +{% macro selected(name, right=False) -%} + {% if name in url -%}class="{% if right -%}right {% endif %}selected"{%- endif %} + {% if not name in url and right -%}class="right"{%- endif %} +{%- endmacro %} + + +
      +
      + {% block headpanel %} + {% if user.is_authenticated %} + {% if update %} + + {{_('pyLoad Update available!')}} + + {% endif %} + {% if plugins %} + + {{_('Plugins updated, please restart!')}} + + {% endif %} + {% endif %} + {% endblock %} +
      + + + +
      +
      + + + +
      +
      +
      +
      +

      {% block subtitle %}{{_(' ')}}{% endblock %}

      +
      +
      + {% block statusbar %} + {% endblock %} + {% for message in messages %} +

      {{message}}

      + {% endfor %} +
      +
      + + {{_('loading')}} +
      +
      + + {% block content %} + {% endblock content %} +
      +
      + +
      + + + + +
      + {% block hidden %} + {% endblock %} +
      +{% block dialog %} +{% endblock %} + +{% include "pyplex/window.html" %} +{% include "pyplex/captcha.html" %} + + + + + + + + +{% block footer %} +{% endblock %} + + + + diff --git a/module/web/templates/pyplex/captcha.html b/module/web/templates/pyplex/captcha.html new file mode 100644 index 0000000000..5b29f53da7 --- /dev/null +++ b/module/web/templates/pyplex/captcha.html @@ -0,0 +1,52 @@ + + + diff --git a/module/web/templates/pyplex/downloads.html b/module/web/templates/pyplex/downloads.html new file mode 100644 index 0000000000..9cea2ff4ed --- /dev/null +++ b/module/web/templates/pyplex/downloads.html @@ -0,0 +1,29 @@ +{% extends 'pyplex/base.html' %} + +{% block title %}Downloads - {{super()}} {% endblock %} + +{% block subtitle %} +{{_('Downloads')}} +{% endblock %} + +{% block content %} + +
        + {% for folder in files.folder %} +
      • + {{folder.name}} +
          + {% for file in folder.files %} +
        • {{file}}
        • + {% endfor %} +
        +
      • + {% endfor %} + + {% for file in files.files %} +
      • {{file}}
      • + {% endfor %} + +
      + +{% endblock %} diff --git a/module/web/templates/pyplex/home.html b/module/web/templates/pyplex/home.html new file mode 100644 index 0000000000..1b338611de --- /dev/null +++ b/module/web/templates/pyplex/home.html @@ -0,0 +1,29 @@ +{% extends 'pyplex/base.html' %} +{% block head %} + +{% endblock %} + +{% block footer %} + +{% endblock %} + +{% block pageactions %} +{% endblock %} + +{% block subtitle %}{{_('Active Downloads')}}{% endblock %} +{% block content %} + + + + + + + + + + + + + +
      {{_('Name')}}{{_('Information')}}
      +{% endblock %} diff --git a/module/web/templates/pyplex/info.html b/module/web/templates/pyplex/info.html new file mode 100644 index 0000000000..0f664dd3bc --- /dev/null +++ b/module/web/templates/pyplex/info.html @@ -0,0 +1,59 @@ +{% extends 'pyplex/base.html' %} + +{% block title %}{{_('Support')}} - {{super()}} {% endblock %} +{% block subtitle %}{{_('Support')}}{% endblock %} + +{% block head %} + +{% endblock %} + +{% block footer %} + +{% endblock %} + +{% block content %} + + +

      {{_('System')}}

      +
      +
      {{_('Python:')}}
      +
      {{python}}
      +
      {{_('OS:')}}
      +
      {{os}}
      +
      {{_('pyLoad version:')}}
      +
      {{version}}
      +
      {{_('Installation Folder:')}}
      +
      {{folder}}
      +
      {{_('Config Folder:')}}
      +
      {{config}}
      +
      {{_('Download Folder:')}}
      +
      {{download}}
      +
      {{_('Free Space:')}}
      +
      {{freespace}}
      +
      {{_('Language:')}}
      +
      {{language}}
      +
      {{_('Webinterface Theme:')}}
      +
      pyplex by GammaC0de based on work of Marco Fernandes and Dogan Bagci
      +
      {{_('Webinterface Port:')}}
      +
      {{webif}}
      +
      {{_('Remote Interface Port:')}}
      +
      {{remote}}
      +
      +

      {{_('News')}}

      +
      {{_('Loading...')}}
      +{% endblock %} + +{% block pageactions %} + +{% endblock %} diff --git a/module/web/templates/pyplex/login.html b/module/web/templates/pyplex/login.html new file mode 100644 index 0000000000..eb3d49c46a --- /dev/null +++ b/module/web/templates/pyplex/login.html @@ -0,0 +1,72 @@ +{% extends 'pyplex/base.html' %} +{% block title %}{{_('Login')}} - {{super()}} {% endblock %} + +{% block head %} + +{% endblock %} + +{% block content %} +
      +
      + + + + + {% if errors %} +
      +
      +

      {{_('Incorrect username/email or password.')}}

      +
      +
      + {% endif %} + +
      +
      + + +
      +
      + + +
      + + +
      +
      +
      +
      + +{% endblock %} diff --git a/module/web/templates/pyplex/logout.html b/module/web/templates/pyplex/logout.html new file mode 100644 index 0000000000..1516471537 --- /dev/null +++ b/module/web/templates/pyplex/logout.html @@ -0,0 +1,19 @@ +{% extends 'pyplex/base.html' %} + +{% block head %} + + + +{% endblock %} + +{% block content %} +
      +
      +

      {{_('You were successfully logged out.')}}

      +
      +
      +{% endblock %} diff --git a/module/web/templates/pyplex/logs.html b/module/web/templates/pyplex/logs.html new file mode 100644 index 0000000000..6272551896 --- /dev/null +++ b/module/web/templates/pyplex/logs.html @@ -0,0 +1,117 @@ +{% extends 'pyplex/base.html' %} + +{% block title %}{{_('Logs')}} - {{super()}} {% endblock %} +{% block subtitle %}{{_('Logs')}}{% endblock %} + +{% block head %} + +{% endblock %} + +{% block content %} +
      + +
      +
      +
      + +   +   + +
      +
      +
      {{warning}}
      +
      +
      + + {% for line in log %} + + {% endfor %} +
      {{line.line}}{{line.date}}{{line.level}}{{line.message}}
      +
      +
      +
      + + + +
      +
      + + +{% endblock %} diff --git a/module/web/templates/pyplex/pathchooser.html b/module/web/templates/pyplex/pathchooser.html new file mode 100644 index 0000000000..7a0f6a5c99 --- /dev/null +++ b/module/web/templates/pyplex/pathchooser.html @@ -0,0 +1,121 @@ + + + + + + + + + + + + + + + + + + {% if parentdir %} + + + + + + {% endif %} + {% for file in files %} + {% if type == 'folder' %} {# browsing for folder #} + {% if file.type == 'dir' %} + + + {% else %} + + + {% endif %} + {% else %} {# browsing for file #} + {% if file.type == 'dir' %} + + + {% else %} + + + {% endif %} + {% endif %} + + + + + + {% endfor %} + +
      {{_('name')}}{{_('size')}}{{_('type')}}{{_('last modified')}}
      ..<DIR>
      {{file.name}}
      {{file.name}}
      {{file.name}}
      {{file.name}}{% if file.type == 'file' %} + {% if file.unit == 'Byte' %}1 KB{% else %}{{(file.size|round(precision=0, method='common'))|int}} {{file.unit|replace('Byte', 'B')}}{% endif %} + {% endif %}{% if file.type == 'dir' %}<DIR>{% else %}{{file.ext|default('file', True)}}{% endif %}{{file.modified|date('d.m.Y - H:i:s')}}
      + + diff --git a/module/web/templates/pyplex/queue.html b/module/web/templates/pyplex/queue.html new file mode 100644 index 0000000000..1df5fc836f --- /dev/null +++ b/module/web/templates/pyplex/queue.html @@ -0,0 +1,104 @@ +{% extends 'pyplex/base.html' %} +{% block head %} +{% endblock %} + +{% block footer %} + + +{% endblock %} + +{% if target %} + {% set name = _("Queue") %} +{% else %} + {% set name = _("Collector") %} +{% endif %} + +{% block pageactions %} +
      + + +
      +{% endblock %} + +{% block title %}{{name}} - {{super()}} {% endblock %} +{% block subtitle %}{{name}}{% endblock %} + +{% block content %} +{% autoescape true %} + +
        +{% for package in content %} +
      • +
        + + +
        + + {{package.name}} + + + + + + + +
        + {% set progress = (package.linksdone * 100) / package.linkstotal %} + +
        +
        + + +
        + + +
        +
      • +{% endfor %} +
      +{% endautoescape %} +{% endblock %} + +{% block dialog %} + +{% endblock %} diff --git a/module/web/templates/pyplex/settings.html b/module/web/templates/pyplex/settings.html new file mode 100644 index 0000000000..c9d5561092 --- /dev/null +++ b/module/web/templates/pyplex/settings.html @@ -0,0 +1,226 @@ +{% extends 'pyplex/base.html' %} + +{% block title %}{{_('Config')}} - {{super()}} {% endblock %} +{% block subtitle %}{{_('Config')}}{% endblock %} + +{% block head %} + + + +{% endblock %} + +{% block footer %} + +{% endblock %} + +{% block content %} + + +
      +
      +
      +
      +
        + {% for entry,name in conf.general %} + +
      • {{_(name)}}
      • +
        + {% endfor %} +
      +
      +
      +
      + +
      +

      {{_('Choose a section from the menu')}}

      +
      +
      + +
      +
      +
      +
      +
      +
      + + + +
      +
        + + +
      +
      +
      +
      + +
      +

      {{_('Choose a section from the menu')}}

      +
      +
      + +
      +
      +
      +
      + + + + + + + + + + + + + + + + + {% for account in conf.accs %} + {% set plugin = account.type %} + + + + + + + + + + + + + + {% endfor %} +
      {{_('Plugin')}}{{_('Name')}}{{_('Password')}}{{_('Status')}}{{_('Premium')}}{{_('Valid until')}}{{_('Traffic left')}}{{_('Time')}}{{_('Max Parallel')}}{{_('Delete?')}}
      + {{plugin}} + + + + + + {% if account.valid %}{{_('valid')}}{% else %}{{_('not valid')}}{% endif %} + + {% if account.premium %} + {{_('yes')}}{% else %}{{_('no')}}{% endif %} + + {{account.validuntil}} + + {{account.trafficleft}} + + + + + + +
      + + +
      +
      +
      +{% endblock %} + +{% block dialog %} + + + + + + +{% endblock %} diff --git a/module/web/templates/pyplex/settings_item.html b/module/web/templates/pyplex/settings_item.html new file mode 100644 index 0000000000..45761709fd --- /dev/null +++ b/module/web/templates/pyplex/settings_item.html @@ -0,0 +1,50 @@ + + {% if section.outline %} + + + + {% endif %} + {% for okey, option in section.iteritems() %} + {% if okey not in ("desc","outline") %} + + + + + {% endif %} + {% endfor %} +
      {{section.outline}}
      + {% if option.type == "bool" %} + + {% elif ";" in option.type %} + + {% elif option.type == "folder" %} +
      + + + + +
      + {% elif option.type == "file" %} +
      + + + + +
      + {% elif option.type == "password" %} + + {% else %} + + {% endif %} +
      diff --git a/module/web/templates/pyplex/window.html b/module/web/templates/pyplex/window.html new file mode 100644 index 0000000000..25681c683c --- /dev/null +++ b/module/web/templates/pyplex/window.html @@ -0,0 +1,53 @@ + + diff --git a/module/web/utils.py b/module/web/utils.py index a89c87558b..5dc5d680e9 100644 --- a/module/web/utils.py +++ b/module/web/utils.py @@ -16,11 +16,10 @@ @author: RaNaN """ -from bottle import request, HTTPError, redirect, ServerAdapter +from bottle import HTTPError, ServerAdapter, redirect, request, response +from module.Api import PERMS, ROLE, has_permission +from webinterface import PREFIX, PYLOAD, TEMPLATE, env -from webinterface import env, TEMPLATE - -from module.Api import has_permission, PERMS, ROLE def render_to_response(name, args={}, proc=[]): for p in proc: @@ -108,14 +107,27 @@ def _view(*args, **kwargs): if request.headers.get('X-Requested-With') == 'XMLHttpRequest': return HTTPError(403, "Forbidden") else: - return redirect("/nopermission") + return redirect(PREFIX + "/nopermission") return func(*args, **kwargs) else: - if request.headers.get('X-Requested-With') == 'XMLHttpRequest': - return HTTPError(403, "Forbidden") + if PYLOAD.getConfigValue("webinterface", "basicauth") == "True": + user, password = request.auth or (None, None) + if user is None: + response.headers['WWW-Authenticate'] = 'Basic realm="pyLoad"' + return HTTPError(401, "Access denied") + else: + info = PYLOAD.checkAuth(user, password) + if not info: + return HTTPError(403, "Forbidden") + else: + set_session(request, info) + return func(*args, **kwargs) else: - return redirect("/login") + if request.headers.get('X-Requested-With') == 'XMLHttpRequest': + return HTTPError(403, "Forbidden") + else: + return redirect(PREFIX + "/login") return _view @@ -129,9 +141,12 @@ def toDict(obj): return ret -class CherryPyWSGI(ServerAdapter): +class WSGI(ServerAdapter): def run(self, handler): - from wsgiserver import CherryPyWSGIServer + try: + from module.lib.wsgiserver.wsgi import Server as WSGIServer + except ImportError: + from module.lib.wsgiserver import CherryPyWSGIServer as WSGIServer - server = CherryPyWSGIServer((self.host, self.port), handler) + server = WSGIServer((self.host, self.port), handler) server.start() diff --git a/module/web/webinterface.py b/module/web/webinterface.py index ec8b2e56ce..8d9ffdbd58 100644 --- a/module/web/webinterface.py +++ b/module/web/webinterface.py @@ -17,12 +17,15 @@ @author: RaNaN """ +from module.common.json_layer import json + import sys import module.common.pylgettext as gettext import os from os.path import join, abspath, dirname, exists from os import makedirs +from socket import error as socket_error PROJECT_DIR = abspath(dirname(__file__)) PYLOAD_DIR = abspath(join(PROJECT_DIR, "..", "..")) @@ -57,7 +60,14 @@ JS = JsEngine() +TEMPLATES = [t for t in os.listdir(os.path.join(pypath, "module", "web", "templates")) + if os.path.isdir(os.path.join(pypath, "module", "web", "templates", t))] TEMPLATE = config.get('webinterface', 'template') +if TEMPLATE not in TEMPLATES: + TEMPLATE = TEMPLATES[0] +config.config['webinterface']['template']['type'] = ';'.join(TEMPLATES) +config.set('webinterface', 'template', TEMPLATE) + DL_ROOT = config.get('general', 'download_folder') LOG_ROOT = config.get('log', 'log_folder') PREFIX = config.get('webinterface', 'prefix') @@ -75,16 +85,19 @@ makedirs(cache) bcc = FileSystemBytecodeCache(cache, '%s.cache') -loader = PrefixLoader({ - "default": FileSystemLoader(join(PROJECT_DIR, "templates", "default")), - 'js': FileSystemLoader(join(PROJECT_DIR, 'media', 'js')) -}) + +mapping = {'js': FileSystemLoader(join(PROJECT_DIR, 'media', 'js'))} +for template in TEMPLATES: + mapping[template] = FileSystemLoader(join(PROJECT_DIR, "templates", template)) + +loader = PrefixLoader(mapping) env = Environment(loader=loader, extensions=['jinja2.ext.i18n', 'jinja2.ext.autoescape'], trim_blocks=True, auto_reload=False, bytecode_cache=bcc) from filters import quotepath, path_make_relative, path_make_absolute, truncate, date +env.filters["tojson"] = json.dumps env.filters["quotepath"] = quotepath env.filters["truncate"] = truncate env.filters["date"] = date @@ -94,10 +107,7 @@ env.filters["type"] = lambda x: str(type(x)) env.filters["formatsize"] = formatSize env.filters["getitem"] = lambda x, y: x.__getitem__(y) -if PREFIX: - env.filters["url"] = lambda x: x -else: - env.filters["url"] = lambda x: PREFIX + x if x.startswith("/") else x +env.filters["url"] = lambda x: PREFIX + x if x.startswith("/") else x gettext.setpaths([join(os.sep, "usr", "share", "pyload", "locale"), None]) translation = gettext.translation("django", join(PYLOAD_DIR, "locale"), @@ -133,18 +143,32 @@ def run_lightweight(host="0.0.0.0", port="8000"): run(app=web, host=host, port=port, quiet=True, server="bjoern") -def run_threaded(host="0.0.0.0", port="8000", theads=3, cert="", key=""): - from wsgiserver import CherryPyWSGIServer +def run_threaded(host="0.0.0.0", port="8000", threads=3, cert="", key="", cert_chain=None): + try: + from module.lib.wsgiserver.wsgi import Server as WSGIServer + from module.lib.wsgiserver.ssl.builtin import BuiltinSSLAdapter + except ImportError: + from module.lib.wsgiserver import CherryPyWSGIServer as WSGIServer + from module.lib.wsgiserver.ssl_builtin import BuiltinSSLAdapter if cert and key: - CherryPyWSGIServer.ssl_certificate = cert - CherryPyWSGIServer.ssl_private_key = key + WSGIServer.ssl_adapter = BuiltinSSLAdapter(cert, key, cert_chain) + + WSGIServer.numthreads = threads + + from utils import WSGI + + try: + run(app=web, host=host, port=port, server=WSGI, quiet=True) - CherryPyWSGIServer.numthreads = theads + except socket_error, e: + if '10048' in e.args[0]: #: Unfortunately, CherryPy raises socket.error without setting errno :( + PYLOAD.core.log.fatal("** FATAL ERROR ** Could not start web server - Address Already in Use | Exiting pyLoad") + PYLOAD.core.api.kill() - from utils import CherryPyWSGI + else: + raise - run(app=web, host=host, port=port, server=CherryPyWSGI, quiet=True) def run_fcgi(host="0.0.0.0", port="8000"): diff --git a/pavement.py b/pavement.py index ac9a6fa1a8..2d5b12b5df 100644 --- a/pavement.py +++ b/pavement.py @@ -23,15 +23,15 @@ setup( name="pyload", - version="0.4.9", + version="0.4.20", description='Fast, lightweight and full featured download manager.', long_description=open(PROJECT_DIR / "README").read(), keywords = ('pyload', 'download-manager', 'one-click-hoster', 'download'), - url="http://pyload.org", - download_url='http://pyload.org/download', + url="http://pyload.net", + download_url='http://pyload.net/download', license='GPL v3', author="pyLoad Team", - author_email="support@pyload.org", + author_email="support@pyload.net", platforms = ('Any',), #package_dir={'pyload': 'src'}, packages=['pyload'], @@ -92,7 +92,7 @@ # xgettext args xargs = ["--from-code=utf-8", "--copyright-holder=pyLoad Team", "--package-name=pyLoad", - "--package-version=%s" % options.version, "--msgid-bugs-address='bugs@pyload.org'"] + "--package-version=%s" % options.version, "--msgid-bugs-address='bugs@pyload.net'"] @task @needs('cog') @@ -136,7 +136,7 @@ def get_source(options): file.chmod(0755) (pyload / ".hgtags").remove() - (pyload / ".hgignore").remove() + (pyload / ".gitignore").remove() #(pyload / "docs").rmtree() f = open(pyload / "__init__.py", "wb") diff --git a/pyLoadCore.py b/pyLoadCore.py index 35cac46821..2276058dfd 100755 --- a/pyLoadCore.py +++ b/pyLoadCore.py @@ -18,9 +18,10 @@ @author: sebnapi @author: RaNaN @author: mkaay - @version: v0.4.9 + @author: GammaC0de + @version: v0.4.20 """ -CURRENT_VERSION = '0.4.9' +CURRENT_VERSION = '0.4.20' import __builtin__ @@ -64,6 +65,16 @@ # - configurable auth system ldap/mysql # - cron job like sheduler + +def exceptHook(exc_type, exc_value, exc_traceback): + logger = logging.getLogger("log") + if issubclass(exc_type, KeyboardInterrupt): + sys.__excepthook__(exc_type, exc_value, exc_traceback) + return + + logger.error("<<< UNCAUGHT EXCEPTION >>>", exc_info=(exc_type, exc_value, exc_traceback)) + + class Core(object): """pyLoad Core, one tool to rule them all... (the filehosters) :D""" @@ -129,7 +140,7 @@ def __init__(self): print pid exit(0) else: - print "false" + print "false" exit(1) elif option == "--clean": self.cleanTree() @@ -208,13 +219,59 @@ def checkPidFile(self): def isAlreadyRunning(self): pid = self.checkPidFile() - if not pid or os.name == "nt": return False - try: - os.kill(pid, 0) # 0 - default signal (does nothing) - except: + if not pid: return 0 - return pid + if os.name == "nt": + ret = 0 + import ctypes + import ctypes.wintypes + + TH32CS_SNAPPROCESS = 2 + INVALID_HANDLE_VALUE = -1 + + class PROCESSENTRY32(ctypes.Structure): + _fields_ = [('dwSize', ctypes.wintypes.DWORD), + ('cntUsage', ctypes.wintypes.DWORD), + ('th32ProcessID', ctypes.wintypes.DWORD), + ('th32DefaultHeapID', ctypes.wintypes.LPVOID), + ('th32ModuleID', ctypes.wintypes.DWORD), + ('cntThreads', ctypes.wintypes.DWORD), + ('th32ParentProcessID', ctypes.wintypes.DWORD), + ('pcPriClassBase', ctypes.wintypes.LONG), + ('dwFlags', ctypes.wintypes.DWORD), + ('szExeFile', ctypes.c_char * 260)] + + kernel32 = ctypes.windll.kernel32 + + processInfo = PROCESSENTRY32() + processInfo.dwSize = ctypes.sizeof(PROCESSENTRY32) + hProcessSnapshot = kernel32.CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS , 0) + if hProcessSnapshot != INVALID_HANDLE_VALUE: + found = False + status = kernel32.Process32First(hProcessSnapshot , ctypes.pointer(processInfo)) + while status: + if processInfo.th32ProcessID == pid: + found = True + break + status = kernel32.Process32Next(hProcessSnapshot, ctypes.pointer(processInfo)) + + kernel32.CloseHandle(hProcessSnapshot) + if found and processInfo.szExeFile.decode().lower() in ("python.exe", "pythonw.exe"): + ret = pid + + else: + print "Unhandled error in CreateToolhelp32Snapshot: %s" % kernel32.GetLastError() + + return ret + + else: + try: + os.kill(pid, 0) # 0 - default signal (does nothing) + except: + return 0 + + return pid def quitInstance(self): if os.name == "nt": @@ -227,7 +284,7 @@ def quitInstance(self): return try: - os.kill(pid, 3) #SIGUIT + os.kill(pid, 3) #: SIGQUIT t = time() print "waiting for pyLoad to quit" @@ -333,6 +390,9 @@ def start(self, rpc=True, web=True): else: self.init_logger(logging.INFO) # logging level + if not self.startedInGui: + sys.excepthook = exceptHook + self.do_kill = False self.do_restart = False self.shuttedDown = False @@ -455,10 +515,16 @@ def start(self, rpc=True, web=True): locals().clear() while True: - sleep(2) + try: + sleep(2) + except IOError, e: + if e.errno != 4: # errno.EINTR + raise + if self.do_restart: self.log.info(_("restarting pyLoad")) self.restart() + if self.do_kill: self.shutdown() self.log.info(_("pyLoad quits")) @@ -592,9 +658,7 @@ def shutdown(self): self.hookManager.coreExiting() except: - if self.debug: - print_exc() - self.log.info(_("error while shutting down")) + self.log.info(_("error while shutting down"), exc_info=self.debug) finally: self.files.syncSave() diff --git a/systemCheck.py b/systemCheck.py index 60fe0313b8..538a5c772e 100644 --- a/systemCheck.py +++ b/systemCheck.py @@ -24,7 +24,6 @@ def main(): except: print("py-crypto:", "missing") - try: import OpenSSL print("OpenSSL:", OpenSSL.version.__version__) @@ -32,10 +31,14 @@ def main(): print("OpenSSL:", "missing") try: - import Image - print("image libary:", Image.VERSION) + from PIL import Image + print("image library:", Image.VERSION) except: - print("image libary:", "missing") + try: + import Image + print("image library:", Image.VERSION) + except: + print("image library:", "missing") try: import PyQt4.QtCore @@ -43,6 +46,11 @@ def main(): except: print("pyqt:", "missing") + from module.common import JsEngine + js = JsEngine.ENGINE if JsEngine.ENGINE else "missing" + print("JS engine:", js) + + print("\n\n##### System Status #####") print("\n## pyLoadCore ##") @@ -60,16 +68,18 @@ def main(): except: core_err.append("Please install py-curl to use pyLoad.") - try: from pycurl import AUTOREFERER except: core_err.append("Your py-curl version is to old, please upgrade!") try: - import Image + from PIL import Image except: - core_err.append("Please install py-imaging/pil to use Hoster, which uses captchas.") + try: + import Image + except: + core_err.append("Please install py-imaging/pil/pillow to use Hoster, which uses captchas.") pipe = subprocess.PIPE try: @@ -82,6 +92,10 @@ def main(): except: core_info.append("Install OpenSSL if you want to create a secure connection to the core.") + if not js: + print("no JavaScript engine found") + print("You will need this for some Click'N'Load links. Install Spidermonkey, ossp-js, pyv8 or rhino") + if core_err: print("The system check has detected some errors:\n") for err in core_err: @@ -94,7 +108,6 @@ def main(): for line in core_info: print(line) - print("\n## pyLoadGui ##") gui_err = [] @@ -111,7 +124,6 @@ def main(): else: print("No Problems detected, pyLoadGui should work fine.") - print("\n## Webinterface ##") web_err = [] @@ -122,7 +134,6 @@ def main(): except: web_info.append("Install Flup to use FastCGI or optional webservers.") - if web_err: print("The system check has detected some errors:\n") for err in web_err: @@ -134,7 +145,6 @@ def main(): print("\nPossible improvements for webinterface:\n") for line in web_info: print(line) - if __name__ == "__main__": main()