Browse Source

Merge branch 'master' of github.com:CouchPotato/CouchPotatoServer

pull/6082/head
Ruud 9 years ago
parent
commit
28adab41a0
  1. 14
      README.md
  2. 4
      contributing.md
  3. 12
      couchpotato/core/_base/updater/main.py
  4. 34
      couchpotato/core/downloaders/qbittorrent_.py
  5. 2
      couchpotato/core/helpers/variable.py
  6. 8
      couchpotato/core/media/_base/providers/torrent/awesomehd.py
  7. 2
      couchpotato/core/media/_base/providers/torrent/torrentpotato.py
  8. 2
      couchpotato/core/media/movie/_base/static/movie.js
  9. 22
      couchpotato/core/media/movie/providers/automation/imdb.py
  10. 2
      couchpotato/core/notifications/base.py
  11. 2
      couchpotato/core/notifications/emby.py
  12. 2
      couchpotato/core/notifications/toasty.py
  13. 4
      couchpotato/core/plugins/log/static/log.js
  14. 2
      couchpotato/core/plugins/renamer.py
  15. 6
      couchpotato/core/settings.py
  16. 6
      couchpotato/static/scripts/combined.plugins.min.js
  17. 12
      couchpotato/static/style/combined.min.css
  18. 2
      libs/qbittorrent/__init__.py
  19. 62
      libs/qbittorrent/base.py
  20. 634
      libs/qbittorrent/client.py
  21. 15
      libs/qbittorrent/file.py
  22. 7
      libs/qbittorrent/helpers.py
  23. 96
      libs/qbittorrent/torrent.py

14
README.md

@ -1,9 +1,9 @@
CouchPotato
=====
[![Join the chat at https://gitter.im/RuudBurger/CouchPotatoServer](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/RuudBurger/CouchPotatoServer?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
[![Build Status](https://travis-ci.org/RuudBurger/CouchPotatoServer.svg?branch=master)](https://travis-ci.org/RuudBurger/CouchPotatoServer)
[![Coverage Status](https://coveralls.io/repos/RuudBurger/CouchPotatoServer/badge.svg?branch=master&service=github)](https://coveralls.io/github/RuudBurger/CouchPotatoServer?branch=master)
[![Join the chat at https://gitter.im/CouchPotato/CouchPotatoServer](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/CouchPotato/CouchPotatoServer?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
[![Build Status](https://travis-ci.org/CouchPotato/CouchPotatoServer.svg?branch=master)](https://travis-ci.org/CouchPotato/CouchPotatoServer)
[![Coverage Status](https://coveralls.io/repos/CouchPotato/CouchPotatoServer/badge.svg?branch=master&service=github)](https://coveralls.io/github/CouchPotato/CouchPotatoServer?branch=master)
CouchPotato (CP) is an automatic NZB and torrent downloader. You can keep a "movies I want"-list and it will search for NZBs/torrents of these movies every X hours.
Once a movie is found, it will send it to SABnzbd or download the torrent to a specified directory.
@ -19,7 +19,7 @@ Windows, see [the CP forum](http://couchpota.to/forum/showthread.php?tid=14) for
* Then install [PyWin32 2.7](http://sourceforge.net/projects/pywin32/files/pywin32/Build%20217/) and [GIT](http://git-scm.com/)
* If you come and ask on the forums 'why directory selection no work?', I will kill a kitten, also this is because you need PyWin32
* Open up `Git Bash` (or CMD) and go to the folder you want to install CP. Something like Program Files.
* Run `git clone https://github.com/RuudBurger/CouchPotatoServer.git`.
* Run `git clone https://github.com/CouchPotato/CouchPotatoServer.git`.
* You can now start CP via `CouchPotatoServer\CouchPotato.py` to start
* Your browser should open up, but if it doesn't go to `http://localhost:5050/`
@ -30,7 +30,7 @@ OS X:
* Install [LXML](http://lxml.de/installation.html) for better/faster website scraping
* Open up `Terminal`
* Go to your App folder `cd /Applications`
* Run `git clone https://github.com/RuudBurger/CouchPotatoServer.git`
* Run `git clone https://github.com/CouchPotato/CouchPotatoServer.git`
* Then do `python CouchPotatoServer/CouchPotato.py`
* Your browser should open up, but if it doesn't go to `http://localhost:5050/`
@ -41,7 +41,7 @@ Linux:
* Install [LXML](http://lxml.de/installation.html) for better/faster website scraping
* 'cd' to the folder of your choosing.
* Install [PyOpenSSL](https://pypi.python.org/pypi/pyOpenSSL) with `pip install --upgrade pyopenssl`
* Run `git clone https://github.com/RuudBurger/CouchPotatoServer.git`
* Run `git clone https://github.com/CouchPotato/CouchPotatoServer.git`
* Then do `python CouchPotatoServer/CouchPotato.py` to start
* (Ubuntu / Debian with upstart) To run on boot copy the init script `sudo cp CouchPotatoServer/init/ubuntu /etc/init.d/couchpotato`
* (Ubuntu / Debian with upstart) Copy the default paths file `sudo cp CouchPotatoServer/init/ubuntu.default /etc/default/couchpotato`
@ -66,7 +66,7 @@ FreeBSD:
* Install required tools `pkg install python py27-sqlite3 fpc-libcurl docbook-xml git-lite`
* For default install location and running as root `cd /usr/local`
* If running as root, expects python here `ln -s /usr/local/bin/python /usr/bin/python`
* Run `git clone https://github.com/RuudBurger/CouchPotatoServer.git`
* Run `git clone https://github.com/CouchPotato/CouchPotatoServer.git`
* Copy the startup script `cp CouchPotatoServer/init/freebsd /usr/local/etc/rc.d/couchpotato`
* Make startup script executable `chmod 555 /usr/local/etc/rc.d/couchpotato`
* Add startup to boot `echo 'couchpotato_enable="YES"' >> /etc/rc.conf`

4
contributing.md

@ -6,7 +6,7 @@
## Contributing
Thank you for your interest in contributing to CouchPotato. There are several ways to help out, even if you've never worked on an open source project before.
If you've found a bug or want to request a feature, you can report it by [posting an issue](https://github.com/RuudBurger/CouchPotatoServer/issues/new) - be sure to read the [guidelines](#issues) first!
If you've found a bug or want to request a feature, you can report it by [posting an issue](https://github.com/CouchPotato/CouchPotatoServer/issues/new) - be sure to read the [guidelines](#issues) first!
If you want to contribute your own work, please read the [guidelines](#pull-requests) for submitting a pull request.
Lastly, for anything related to CouchPotato, feel free to stop by the [forum](http://couchpota.to/forum/) or the [#couchpotato](http://webchat.freenode.net/?channels=couchpotato) IRC channel at irc.freenode.net.
@ -26,7 +26,7 @@ Before you submit an issue, please go through the following checklist:
* Give a short step by step of how to reproduce the error.
* What hardware / OS are you using and what are its limitations? For example: NAS can be slow and maybe have a different version of python installed than when you use CP on OS X or Windows.
* Your issue might be marked with the "can't reproduce" tag. Don't ask why your issue was closed if it says so in the tag.
* If you're running on a NAS (QNAP, Austor, Synology etc.) with pre-made packages, make sure these are set up to use our source repository (RuudBurger/CouchPotatoServer) and nothing else!
* If you're running on a NAS (QNAP, Austor, Synology etc.) with pre-made packages, make sure these are set up to use our source repository (CouchPotato/CouchPotatoServer) and nothing else!
* Do not "bump" issues with "Any updates on this" or whatever. Yes I've seen it, you don't have to remind me of it. There will be an update when the code is done or I need information. If you feel the need to do so, you'd better have more info on the issue.
The more relevant information you provide, the more likely that your issue will be resolved.

12
couchpotato/core/_base/updater/main.py

@ -155,7 +155,7 @@ class Updater(Plugin):
class BaseUpdater(Plugin):
repo_user = 'RuudBurger'
repo_user = 'CouchPotato'
repo_name = 'CouchPotatoServer'
branch = version.BRANCH
@ -188,9 +188,19 @@ class BaseUpdater(Plugin):
class GitUpdater(BaseUpdater):
old_repo = 'RuudBurger/CouchPotatoServer'
new_repo = 'CouchPotato/CouchPotatoServer'
def __init__(self, git_command):
self.repo = LocalRepository(Env.get('app_dir'), command = git_command)
remote_name = 'origin'
remote = self.repo.getRemoteByName(remote_name)
if self.old_repo in remote.url:
log.info('Changing repo to new github organization: %s -> %s', (self.old_repo, self.new_repo))
new_url = remote.url.replace(self.old_repo, self.new_repo)
self.repo._executeGitCommandAssertSuccess("remote set-url %s %s" % (remote_name, new_url))
def doUpdate(self):
try:

34
couchpotato/core/downloaders/qbittorrent_.py

@ -30,11 +30,8 @@ class qBittorrent(DownloaderBase):
url = cleanHost(self.conf('host'), protocol = True, ssl = False)
if self.conf('username') and self.conf('password'):
self.qb = QBittorrentClient(
url,
username = self.conf('username'),
password = self.conf('password')
)
self.qb = QBittorrentClient(url)
self.qb.login(username=self.conf('username'),password=self.conf('password'))
else:
self.qb = QBittorrentClient(url)
@ -45,10 +42,7 @@ class qBittorrent(DownloaderBase):
:return: bool
"""
if self.connect():
return True
return False
return self.qb._is_authenticated
def download(self, data = None, media = None, filedata = None):
""" Send a torrent/nzb file to the downloader
@ -95,7 +89,7 @@ class qBittorrent(DownloaderBase):
# Send request to qBittorrent
try:
self.qb.add_file(filedata)
self.qb.download_from_file(filedata, label=self.conf('label'))
return self.downloadReturnId(torrent_hash)
except Exception as e:
@ -127,14 +121,13 @@ class qBittorrent(DownloaderBase):
return []
try:
torrents = self.qb.get_torrents()
torrents = self.qb.torrents(label=self.conf('label'))
release_downloads = ReleaseDownloadList(self)
for torrent in torrents:
if torrent.hash in ids:
torrent.update_general() # get extra info
torrent_filelist = torrent.get_files()
torrent_filelist = self.qb.get_torrent_files(torrent.hash)
torrent_files = []
torrent_dir = os.path.join(torrent.save_path, torrent.name)
@ -179,8 +172,8 @@ class qBittorrent(DownloaderBase):
return False
if pause:
return torrent.pause()
return torrent.resume()
return self.qb.pause(release_download['id'])
return self.qb.resume(release_download['id'])
def removeFailed(self, release_download):
log.info('%s failed downloading, deleting...', release_download['name'])
@ -193,15 +186,15 @@ class qBittorrent(DownloaderBase):
if not self.connect():
return False
torrent = self.qb.find_torrent(release_download['id'])
torrent = self.qb.get_torrent(release_download['id'])
if torrent is None:
return False
if delete_files:
torrent.delete() # deletes torrent with data
self.qb.delete_permanently(release_download['id']) # deletes torrent with data
else:
torrent.remove() # just removes the torrent, doesn't delete data
self.qb.delete(release_download['id']) # just removes the torrent, doesn't delete data
return True
@ -236,6 +229,11 @@ config = [{
'type': 'password',
},
{
'name': 'label',
'label': 'Torrent Label',
'default': 'couchpotato',
},
{
'name': 'remove_complete',
'label': 'Remove torrent',
'default': False,

2
couchpotato/core/helpers/variable.py

@ -35,7 +35,7 @@ def symlink(src, dst):
import ctypes
if ctypes.windll.kernel32.CreateSymbolicLinkW(toUnicode(dst), toUnicode(src), 1 if os.path.isdir(src) else 0) in [0, 1280]: raise ctypes.WinError()
else:
os.link(toUnicode(src), toUnicode(dst))
os.symlink(toUnicode(src), toUnicode(dst))
def getUserDir():

8
couchpotato/core/media/_base/providers/torrent/awesomehd.py

@ -13,10 +13,10 @@ log = CPLog(__name__)
class Base(TorrentProvider):
urls = {
'test': 'https://awesome-hd.net/',
'detail': 'https://awesome-hd.net/torrents.php?torrentid=%s',
'search': 'https://awesome-hd.net/searchapi.php?action=imdbsearch&passkey=%s&imdb=%s&internal=%s',
'download': 'https://awesome-hd.net/torrents.php?action=download&id=%s&authkey=%s&torrent_pass=%s',
'test': 'https://awesome-hd.me/',
'detail': 'https://awesome-hd.me/torrents.php?torrentid=%s',
'search': 'https://awesome-hd.me/searchapi.php?action=imdbsearch&passkey=%s&imdb=%s&internal=%s',
'download': 'https://awesome-hd.me/torrents.php?action=download&id=%s&authkey=%s&torrent_pass=%s',
}
http_time_between_calls = 1

2
couchpotato/core/media/_base/providers/torrent/torrentpotato.py

@ -132,7 +132,7 @@ config = [{
'list': 'torrent_providers',
'name': 'TorrentPotato',
'order': 10,
'description': 'CouchPotato torrent provider. Checkout <a href="https://github.com/RuudBurger/CouchPotatoServer/wiki/CouchPotato-Torrent-Provider">the wiki page about this provider</a> for more info.',
'description': 'CouchPotato torrent provider. Checkout <a href="https://github.com/CouchPotato/CouchPotatoServer/wiki/CouchPotato-Torrent-Provider">the wiki page about this provider</a> for more info.',
'wizard': True,
'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAABnRSTlMAAAAAAABupgeRAAABSElEQVR4AZ2Nz0oCURTGv8t1YMpqUxt9ARFxoQ/gQtppgvUKcu/sxB5iBJkogspaBC6iVUplEC6kv+oiiKDNhAtt16roP0HQgdsMLgaxfvy4nHP4Pi48qE2g4v91JOqT1CH/UnA7w7icUlLawyEdj+ZI/7h6YluWbRiddHonHh9M70aj7VTKzuXuikUMci/EO/ACnAI15599oAk8AR/AgxBQNCzreD7bmpl+FOIVuAHqQDUcJo+AK+CZFKLt95/MpSmMt0TiW9POxse6UvYZ6zB2wFgjFiNpOGesR0rZ0PVPXf8KhUCl22CwClz4eN8weoZBb9c0bdPsOWvHx/cYu9Y0CoNoZTJrwAbn5DrnZc6XOV+igVbnsgo0IxEomlJuA1vUIYGyq3PZBChwmExCUSmVZgMBDIUCK4UCFIv5vHIhm/XUDeAf/ADbcpd5+aXSWQAAAABJRU5ErkJggg==',
'options': [

2
couchpotato/core/media/movie/_base/static/movie.js

@ -194,7 +194,7 @@ var Movie = new Class({
var rating, stars;
if(['suggested','chart'].indexOf(self.data.status) > -1 && self.data.info && self.data.info.rating && self.data.info.rating.imdb){
rating = self.data.info.rating.imdb;
rating = Array.prototype.slice.call(self.data.info.rating.imdb);
stars = [];

22
couchpotato/core/media/movie/providers/automation/imdb.py

@ -37,14 +37,8 @@ class IMDBBase(Automation, RSS):
'name': 'IMDB - Box Office',
'url': 'http://www.imdb.com/boxoffice/',
},
'rentals': {
'order': 3,
'name': 'IMDB - Top DVD rentals',
'url': 'http://www.imdb.com/boxoffice/rentals',
'type': 'json',
},
'top250': {
'order': 4,
'order': 3,
'name': 'IMDB - Top 250 Movies',
'url': 'http://www.imdb.com/chart/top',
},
@ -270,13 +264,6 @@ config = [{
'default': True,
},
{
'name': 'automation_charts_rentals',
'type': 'bool',
'label': 'DVD Rentals',
'description': 'Top DVD <a href="http://www.imdb.com/boxoffice/rentals" target="_blank">rentals</a> chart',
'default': True,
},
{
'name': 'automation_charts_top250',
'type': 'bool',
'label': 'TOP 250',
@ -319,13 +306,6 @@ config = [{
'default': False,
},
{
'name': 'chart_display_rentals',
'type': 'bool',
'label': 'DVD Rentals',
'description': 'Top DVD <a href="http://www.imdb.com/boxoffice/rentals" target="_blank">rentals</a> chart',
'default': True,
},
{
'name': 'chart_display_boxoffice',
'type': 'bool',
'label': 'Box office TOP 10',

2
couchpotato/core/notifications/base.py

@ -43,7 +43,7 @@ class Notification(Provider):
return notify
def getNotificationImage(self, size = 'small'):
return 'https://raw.github.com/RuudBurger/CouchPotatoServer/master/couchpotato/static/images/notify.couch.%s.png' % size
return 'https://raw.github.com/CouchPotato/CouchPotatoServer/master/couchpotato/static/images/notify.couch.%s.png' % size
def _notify(self, *args, **kwargs):
if self.isEnabled():

2
couchpotato/core/notifications/emby.py

@ -41,7 +41,7 @@ class Emby(Notification):
host = cleanHost(host)
url = '%semby/Notifications/Admin' % (host)
values = {'Name': 'CouchPotato', 'Description': message, 'ImageUrl': 'https://raw.githubusercontent.com/RuudBurger/CouchPotatoServer/master/couchpotato/static/images/notify.couch.small.png'}
values = {'Name': 'CouchPotato', 'Description': message, 'ImageUrl': 'https://raw.githubusercontent.com/CouchPotato/CouchPotatoServer/master/couchpotato/static/images/notify.couch.small.png'}
data = json.dumps(values)
try:

2
couchpotato/core/notifications/toasty.py

@ -23,7 +23,7 @@ class Toasty(Notification):
'title': self.default_title,
'text': toUnicode(message),
'sender': toUnicode("CouchPotato"),
'image': 'https://raw.github.com/RuudBurger/CouchPotatoServer/master/couchpotato/static/images/homescreen.png',
'image': 'https://raw.github.com/CouchPotato/CouchPotatoServer/master/couchpotato/static/images/homescreen.png',
}
try:

4
couchpotato/core/plugins/log/static/log.js

@ -250,7 +250,7 @@ Page.Log = new Class({
new Element('a.button', {
'target': '_blank',
'text': 'the contributing guide',
'href': 'https://github.com/RuudBurger/CouchPotatoServer/blob/develop/contributing.md'
'href': 'https://github.com/CouchPotato/CouchPotatoServer/blob/develop/contributing.md'
}),
new Element('span', {
'html': ' before posting, then copy the text below and <strong>FILL IN</strong> the dots.'
@ -262,7 +262,7 @@ Page.Log = new Class({
new Element('a.button', {
'target': '_blank',
'text': 'Create a new issue on GitHub with the text above',
'href': 'https://github.com/RuudBurger/CouchPotatoServer/issues/new',
'href': 'https://github.com/CouchPotato/CouchPotatoServer/issues/new',
'events': {
'click': function(e){
(e).stop();

2
couchpotato/core/plugins/renamer.py

@ -1363,7 +1363,7 @@ config = [{
'name': 'replace_doubles',
'type': 'bool',
'label': 'Clean Name',
'description': ('Attempt to clean up double separaters due to missing data for fields.','Sometimes this eliminates wanted white space (see <a href="https://github.com/RuudBurger/CouchPotatoServer/issues/2782">#2782</a>).'),
'description': ('Attempt to clean up double separaters due to missing data for fields.','Sometimes this eliminates wanted white space (see <a href="https://github.com/CouchPotato/CouchPotatoServer/issues/2782">#2782</a>).'),
'default': True
},
{

6
couchpotato/core/settings.py

@ -1,5 +1,6 @@
from __future__ import with_statement
import ConfigParser
import traceback
from hashlib import md5
from CodernityDB.hash_index import HashIndex
@ -415,8 +416,11 @@ class Settings(object):
try:
propert = db.get('property', identifier, with_doc = True)
prop = propert['doc']['value']
except ValueError:
propert = db.get('property', identifier)
fireEvent('database.delete_corrupted', propert.get('_id'))
except:
pass # self.log.debug('Property "%s" doesn\'t exist: %s', (identifier, traceback.format_exc(0)))
self.log.debug('Property "%s" doesn\'t exist: %s', (identifier, traceback.format_exc(0)))
return prop

6
couchpotato/static/scripts/combined.plugins.min.js

@ -3093,7 +3093,7 @@ Page.Log = new Class({
}), new Element("a.button", {
target: "_blank",
text: "the contributing guide",
href: "https://github.com/RuudBurger/CouchPotatoServer/blob/develop/contributing.md"
href: "https://github.com/CouchPotato/CouchPotatoServer/blob/develop/contributing.md"
}), new Element("span", {
html: " before posting, then copy the text below and <strong>FILL IN</strong> the dots."
})), textarea = new Element("textarea", {
@ -3101,7 +3101,7 @@ Page.Log = new Class({
}), new Element("a.button", {
target: "_blank",
text: "Create a new issue on GitHub with the text above",
href: "https://github.com/RuudBurger/CouchPotatoServer/issues/new",
href: "https://github.com/CouchPotato/CouchPotatoServer/issues/new",
events: {
click: function(e) {
e.stop();
@ -3804,4 +3804,4 @@ Page.Wizard = new Class({
self.el.getElement(".advanced_toggle").destroy();
self.el.getElement(".section_nzb").hide();
}
});
});

12
couchpotato/static/style/combined.min.css

@ -54,7 +54,7 @@
.search_form .results_container .results .media_result .options .add a{color:#FFF}
.search_form .results_container .results .media_result .options .button{display:block;background:#ac0000;text-align:center;margin:0}
.dark .search_form .results_container .results .media_result .options .button{background:#f85c22}
.search_form .results_container .results .media_result .options .message{font-size:20px;color:#FFF}
.search_form .results_container .results .media_result .options .message{font-size:20px;color:#fff}
.search_form .results_container .results .media_result .thumbnail{width:30px;min-height:100%;display:block;margin:0;vertical-align:top}
.search_form .results_container .results .media_result .data{position:absolute;height:100%;top:0;left:30px;right:0;cursor:pointer;border-top:1px solid rgba(255,255,255,.08);transition:all .4s cubic-bezier(.9,0,.1,1);will-change:transform;-webkit-transform:translateX(0) rotateZ(360deg);transform:translateX(0) rotateZ(360deg);background:#FFF}
.dark .search_form .results_container .results .media_result .data{background:#2d2d2d;border-color:rgba(255,255,255,.08)}
@ -95,15 +95,17 @@
.page.home .search_form .wrapper .input input{padding-right:44px;font-size:1em}
.page.home .search_form .wrapper .results_container{top:44px;min-height:44px}
}
@media (min-width:480px){.page.home .search_form .wrapper .results_container .results .media_result .data,.page.home .search_form .wrapper .results_container .results .media_result .options{left:40px}
.page.home .search_form .wrapper .results_container .results{max-height:400px}
@media (min-width:480px){.page.home .search_form .wrapper .results_container .results{max-height:400px}
.page.home .search_form .wrapper .results_container .results .media_result{height:66px}
.page.home .search_form .wrapper .results_container .results .media_result .thumbnail{width:40px}
.page.home .search_form .wrapper .results_container .results .media_result .options{left:40px}
.page.home .search_form .wrapper .results_container .results .media_result .options .title{margin-right:5px;width:320px}
}
@media (min-width:480px) and (max-width:480px){.page.home .search_form .wrapper .results_container .results .media_result{height:44px}
.page.home .search_form .wrapper .results_container .results .media_result .options .title{width:140px;margin-right:2px}
}
@media (min-width:480px){.page.home .search_form .wrapper .results_container .results .media_result .data{left:40px}
}
@media (max-width:480px){.page.home .search_form .wrapper .results_container .results .media_result{height:44px}
.page.home .search_form .wrapper .results_container .results .media_result .options .title{width:140px;margin-right:2px}
}
@ -809,7 +811,7 @@ input[type=text],textarea{-webkit-appearance:none}
.mask .message,.mask .spinner{position:absolute;top:50%;left:50%}
.mask .message{color:#FFF;text-align:center;width:320px;margin:-49px 0 0 -160px;font-size:16px}
.mask .message h1{font-size:1.5em}
.mask .spinner{width:22px;height:22px;display:block;background:#FFF;margin-top:-11px;margin-left:-11px;outline:transparent solid 1px;-webkit-animation:rotating 2.5s cubic-bezier(.9,0,.1,1) infinite normal;animation:rotating 2.5s cubic-bezier(.9,0,.1,1) infinite normal;-webkit-transform:scale(0);transform:scale(0)}
.mask .spinner{width:22px;height:22px;display:block;background:#fff;margin-top:-11px;margin-left:-11px;outline:transparent solid 1px;-webkit-animation:rotating 2.5s cubic-bezier(.9,0,.1,1) infinite normal;animation:rotating 2.5s cubic-bezier(.9,0,.1,1) infinite normal;-webkit-transform:scale(0);transform:scale(0)}
.mask.with_message .spinner{margin-top:-88px}
.mask.show{pointer-events:auto;opacity:1}
.mask.show .spinner{-webkit-transform:scale(1);transform:scale(1)}
@ -933,7 +935,7 @@ input[type=text],textarea{-webkit-appearance:none}
.page.settings .option_list h2 .hint{font-weight:300}
.page.settings .combined_table{margin-top:20px}
.page.settings .combined_table .head{margin:0 10px 0 46px;font-size:.8em}
.page.settings .combined_table .head abbr{display:inline-block;font-weight:700;border-bottom:1px dotted #FFF;line-height:140%;cursor:help;margin-right:10px;text-align:center}
.page.settings .combined_table .head abbr{display:inline-block;font-weight:700;border-bottom:1px dotted #fff;line-height:140%;cursor:help;margin-right:10px;text-align:center}
.page.settings .combined_table .head abbr:first-child{display:none}
.page.settings .combined_table input{min-width:0!important;display:inline-block;margin-right:10px}
.page.settings .combined_table .automation_ids,.page.settings .combined_table .automation_urls,.page.settings .combined_table .host{width:200px}

2
libs/qbittorrent/__init__.py

@ -1 +1 @@
__version__ = '0.1'
__version__ = '0.2'

62
libs/qbittorrent/base.py

@ -1,62 +0,0 @@
from urlparse import urljoin
import logging
log = logging.getLogger(__name__)
class Base(object):
properties = {}
def __init__(self, url, session, client=None):
self._client = client
self._url = url
self._session = session
@staticmethod
def _convert(response, response_type):
if response_type == 'json':
try:
return response.json()
except ValueError:
pass
return response
def _get(self, path='', response_type='json', **kwargs):
r = self._session.get(urljoin(self._url, path), **kwargs)
return self._convert(r, response_type)
def _post(self, path='', response_type='json', data=None, **kwargs):
r = self._session.post(urljoin(self._url, path), data, **kwargs)
return self._convert(r, response_type)
def _fill(self, data):
for key, value in data.items():
if self.set_property(self, key, value):
continue
log.debug('%s is missing item with key "%s" and value %s', self.__class__, key, repr(value))
@classmethod
def parse(cls, client, data):
obj = cls(client._url, client._session, client)
obj._fill(data)
return obj
@classmethod
def set_property(cls, obj, key, value):
prop = cls.properties.get(key, {})
if prop.get('key'):
key = prop['key']
if not hasattr(obj, key):
return False
if prop.get('parse'):
value = prop['parse'](value)
setattr(obj, key, value)
return True

634
libs/qbittorrent/client.py

@ -1,72 +1,610 @@
from qbittorrent.base import Base
from qbittorrent.torrent import Torrent
from requests import Session
from requests.auth import HTTPDigestAuth
import time
import requests
import json
class LoginRequired(Exception):
def __str__(self):
return 'Please login first.'
class QBittorrentClient(Base):
def __init__(self, url, username=None, password=None):
super(QBittorrentClient, self).__init__(url, Session())
if username and password:
self._session.auth = HTTPDigestAuth(username, password)
class QBittorrentClient(object):
"""class to interact with qBittorrent WEB API"""
def __init__(self, url):
if not url.endswith('/'):
url += '/'
self.url = url
def test_connection(self):
r = self._get(response_type='response')
session = requests.Session()
check_prefs = session.get(url+'query/preferences')
return r.status_code == 200
if check_prefs.status_code == 200:
self._is_authenticated = True
self.session = session
else:
self._is_authenticated = False
def add_file(self, file):
self._post('command/upload', files={'torrent': file})
def _get(self, endpoint, **kwargs):
"""
Method to perform GET request on the API.
:param endpoint: Endpoint of the API.
:param kwargs: Other keyword arguments for requests.
:return: Response of the GET request.
"""
return self._request(endpoint, 'get', **kwargs)
def _post(self, endpoint, data, **kwargs):
"""
Method to perform POST request on the API.
:param endpoint: Endpoint of the API.
:param data: POST DATA for the request.
:param kwargs: Other keyword arguments for requests.
:return: Response of the POST request.
"""
return self._request(endpoint, 'post', data, **kwargs)
def _request(self, endpoint, method, data=None, **kwargs):
"""
Method to hanle both GET and POST requests.
:param endpoint: Endpoint of the API.
:param method: Method of HTTP request.
:param data: POST DATA for the request.
:param kwargs: Other keyword arguments.
:return: Response for the request.
"""
final_url = self.url + endpoint
if not self._is_authenticated:
raise LoginRequired
rq = self.session
if method == 'get':
request = rq.get(final_url, **kwargs)
else:
request = rq.post(final_url, data, **kwargs)
request.raise_for_status()
if len(request.text) == 0:
data = json.loads('{}')
else:
try:
data = json.loads(request.text)
except ValueError:
data = request.text
return data
def login(self, username, password):
"""
Method to authenticate the qBittorrent Client.
Declares a class attribute named ``session`` which
stores the authenticated session if the login is correct.
Else, shows the login error.
:param username: Username.
:param password: Password.
:return: Response to login request to the API.
"""
self.session = requests.Session()
login = self.session.post(self.url+'login',
data={'username': username,
'password': password})
if login.text == 'Ok.':
self._is_authenticated = True
else:
return login.text
def logout(self):
"""
Logout the current session.
"""
response = self._get('logout')
self._is_authenticated = False
return response
@property
def qbittorrent_version(self):
"""
Get qBittorrent version.
"""
return self._get('version/qbittorrent')
@property
def api_version(self):
"""
Get WEB API version.
"""
return self._get('version/api')
@property
def api_min_version(self):
"""
Get minimum WEB API version.
"""
return self._get('version/api_min')
def shutdown(self):
"""
Shutdown qBittorrent.
"""
return self._get('command/shutdown')
def torrents(self, status='active', label='', sort='priority',
reverse=False, limit=10, offset=0):
"""
Returns a list of torrents matching the supplied filters.
:param status: Current status of the torrents.
:param label: Fetch all torrents with the supplied label.
:param sort: Sort torrents by.
:param reverse: Enable reverse sorting.
:param limit: Limit the number of torrents returned.
:param offset: Set offset (if less than 0, offset from end).
:return: list() of torrent with matching filter.
"""
STATUS_LIST = ['all', 'downloading', 'completed',
'paused', 'active', 'inactive']
if status not in STATUS_LIST:
raise ValueError("Invalid status.")
params = {
'filter': status,
'label': label,
'sort': sort,
'reverse': reverse,
'limit': limit,
'offset': offset
}
return self._get('query/torrents', params=params)
def get_torrent(self, infohash):
"""
Get details of the torrent.
:param infohash: INFO HASH of the torrent.
"""
return self._get('query/propertiesGeneral/' + infohash.lower())
def get_torrent_trackers(self, infohash):
"""
Get trackers for the torrent.
:param infohash: INFO HASH of the torrent.
"""
return self._get('query/propertiesTrackers/' + infohash.lower())
def get_torrent_webseeds(self, infohash):
"""
Get webseeds for the torrent.
:param infohash: INFO HASH of the torrent.
"""
return self._get('query/propertiesWebSeeds/' + infohash.lower())
def get_torrent_files(self, infohash):
"""
Get list of files for the torrent.
:param infohash: INFO HASH of the torrent.
"""
return self._get('query/propertiesFiles/' + infohash.lower())
@property
def global_transfer_info(self):
"""
Get JSON data of the global transfer info of qBittorrent.
"""
return self._get('query/transferInfo')
@property
def preferences(self):
"""
Get the current qBittorrent preferences.
Can also be used to assign individual preferences.
For setting multiple preferences at once,
see ``set_preferences`` method.
Note: Even if this is a ``property``,
to fetch the current preferences dict, you are required
to call it like a bound method.
Wrong::
qb.preferences
Right::
qb.preferences()
"""
prefs = self._get('query/preferences')
class Proxy(Client):
"""
Proxy class to to allow assignment of individual preferences.
this class overrides some methods to ease things.
Because of this, settings can be assigned like::
In [5]: prefs = qb.preferences()
In [6]: prefs['autorun_enabled']
Out[6]: True
In [7]: prefs['autorun_enabled'] = False
In [8]: prefs['autorun_enabled']
Out[8]: False
"""
def __init__(self, url, prefs, auth, session):
super(Proxy, self).__init__(url)
self.prefs = prefs
self._is_authenticated = auth
self.session = session
def __getitem__(self, key):
return self.prefs[key]
def __setitem__(self, key, value):
kwargs = {key: value}
return self.set_preferences(**kwargs)
def __call__(self):
return self.prefs
return Proxy(self.url, prefs, self._is_authenticated, self.session)
def sync(self, rid=0):
"""
Sync the torrents by supplied LAST RESPONSE ID.
Read more @ http://git.io/vEgXr
:param rid: Response ID of last request.
"""
return self._get('sync/maindata', params={'rid': rid})
def download_from_link(self, link,
save_path=None, label=''):
"""
Download torrent using a link.
:param link: URL Link or list of.
:param save_path: Path to download the torrent.
:param label: Label of the torrent(s).
:return: Empty JSON data.
"""
if not isinstance(link, list):
link = [link]
data = {'urls': link}
if save_path:
data.update({'savepath': save_path})
if label:
data.update({'label': label})
return self._post('command/download', data=data)
def download_from_file(self, file_buffer,
save_path=None, label=''):
"""
Download torrent using a file.
:param file_buffer: Single file() buffer or list of.
:param save_path: Path to download the torrent.
:param label: Label of the torrent(s).
:return: Empty JSON data.
"""
if isinstance(file_buffer, list):
torrent_files = {}
for i, f in enumerate(file_buffer):
torrent_files.update({'torrents%s' % i: f})
print torrent_files
else:
torrent_files = {'torrents': file_buffer}
def add_url(self, urls):
if type(urls) is not list:
urls = [urls]
data = {}
urls = '%0A'.join(urls)
if save_path:
data.update({'savepath': save_path})
if label:
data.update({'label': label})
return self._post('command/upload', data=data, files=torrent_files)
self._post('command/download', data={'urls': urls})
def add_trackers(self, infohash, trackers):
"""
Add trackers to a torrent.
:param infohash: INFO HASH of torrent.
:param trackers: Trackers.
"""
data = {'hash': infohash.lower(),
'urls': trackers}
return self._post('command/addTrackers', data=data)
@staticmethod
def process_infohash_list(infohash_list):
"""
Method to convert the infohash_list to qBittorrent API friendly values.
:param infohash_list: List of infohash.
"""
if isinstance(infohash_list, list):
data = {'hashes': '|'.join([h.lower() for h in infohash_list])}
else:
data = {'hashes': infohash_list.lower()}
return data
def pause(self, infohash):
"""
Pause a torrent.
:param infohash: INFO HASH of torrent.
"""
return self._post('command/pause', data={'hash': infohash.lower()})
def pause_all(self):
"""
Pause all torrents.
"""
return self._get('command/pauseAll')
def pause_multiple(self, infohash_list):
"""
Pause multiple torrents.
:param infohash_list: Single or list() of infohashes.
"""
data = self.process_infohash_list(infohash_list)
return self._post('command/pauseAll', data=data)
def resume(self, infohash):
"""
Resume a paused torrent.
:param infohash: INFO HASH of torrent.
"""
return self._post('command/resume', data={'hash': infohash.lower()})
def resume_all(self):
"""
Resume all torrents.
"""
return self._get('command/resumeAll')
def resume_multiple(self, infohash_list):
"""
Resume multiple paused torrents.
:param infohash_list: Single or list() of infohashes.
"""
data = self.process_infohash_list(infohash_list)
return self._post('command/resumeAll', data=data)
def delete(self, infohash_list):
"""
Delete torrents.
:param infohash_list: Single or list() of infohashes.
"""
data = self.process_infohash_list(infohash_list)
return self._post('command/delete', data=data)
def delete_permanently(self, infohash_list):
"""
Permanently delete torrents.
:param infohash_list: Single or list() of infohashes.
"""
data = self.process_infohash_list(infohash_list)
return self._post('command/deletePerm', data=data)
def recheck(self, infohash_list):
"""
Recheck torrents.
:param infohash_list: Single or list() of infohashes.
"""
data = self.process_infohash_list(infohash_list)
return self._post('command/recheck', data=data)
def increase_priority(self, infohash_list):
"""
Increase priority of torrents.
:param infohash_list: Single or list() of infohashes.
"""
data = self.process_infohash_list(infohash_list)
return self._post('command/increasePrio', data=data)
def decrease_priority(self, infohash_list):
"""
Decrease priority of torrents.
:param infohash_list: Single or list() of infohashes.
"""
data = self.process_infohash_list(infohash_list)
return self._post('command/decreasePrio', data=data)
def set_max_priority(self, infohash_list):
"""
Set torrents to maximum priority level.
:param infohash_list: Single or list() of infohashes.
"""
data = self.process_infohash_list(infohash_list)
return self._post('command/topPrio', data=data)
def set_min_priority(self, infohash_list):
"""
Set torrents to minimum priority level.
:param infohash_list: Single or list() of infohashes.
"""
data = self.process_infohash_list(infohash_list)
return self._post('command/bottomPrio', data=data)
def set_file_priority(self, infohash, file_id, priority):
"""
Set file of a torrent to a supplied priority level.
:param infohash: INFO HASH of torrent.
:param file_id: ID of the file to set priority.
:param priority: Priority level of the file.
"""
if priority not in [0, 1, 2, 7]:
raise ValueError("Invalid priority, refer WEB-UI docs for info.")
elif not isinstance(file_id, int):
raise TypeError("File ID must be an int")
data = {'hash': infohash.lower(),
'id': file_id,
'priority': priority}
return self._post('command/setFilePrio', data=data)
# Get-set global download and upload speed limits.
def get_global_download_limit(self):
"""
Get global download speed limit.
"""
return self._get('command/getGlobalDlLimit')
def set_global_download_limit(self, limit):
"""
Set global download speed limit.
:param limit: Speed limit in bytes.
"""
return self._post('command/setGlobalDlLimit', data={'limit': limit})
global_download_limit = property(get_global_download_limit,
set_global_download_limit)
def get_global_upload_limit(self):
"""
Get global upload speed limit.
"""
return self._get('command/getGlobalUpLimit')
def set_global_upload_limit(self, limit):
"""
Set global upload speed limit.
:param limit: Speed limit in bytes.
"""
return self._post('command/setGlobalUpLimit', data={'limit': limit})
global_upload_limit = property(get_global_upload_limit,
set_global_upload_limit)
# Get-set download and upload speed limits of the torrents.
def get_torrent_download_limit(self, infohash_list):
"""
Get download speed limit of the supplied torrents.
def get_torrents(self):
"""Fetch all torrents
:param infohash_list: Single or list() of infohashes.
"""
data = self.process_infohash_list(infohash_list)
return self._post('command/getTorrentsDlLimit', data=data)
def set_torrent_download_limit(self, infohash_list, limit):
"""
Set download speed limit of the supplied torrents.
:param infohash_list: Single or list() of infohashes.
:param limit: Speed limit in bytes.
"""
data = self.process_infohash_list(infohash_list)
data.update({'limit': limit})
return self._post('command/setTorrentsDlLimit', data=data)
def get_torrent_upload_limit(self, infohash_list):
"""
Get upoload speed limit of the supplied torrents.
:param infohash_list: Single or list() of infohashes.
"""
data = self.process_infohash_list(infohash_list)
return self._post('command/getTorrentsUpLimit', data=data)
:return: list of Torrent
def set_torrent_upload_limit(self, infohash_list, limit):
"""
r = self._get('json/torrents')
Set upload speed limit of the supplied torrents.
return [Torrent.parse(self, x) for x in r]
:param infohash_list: Single or list() of infohashes.
:param limit: Speed limit in bytes.
"""
data = self.process_infohash_list(infohash_list)
data.update({'limit': limit})
return self._post('command/setTorrentsUpLimit', data=data)
def get_torrent(self, hash, include_general=True, max_retries=5):
"""Fetch details for torrent by info_hash.
# setting preferences
def set_preferences(self, **kwargs):
"""
Set preferences of qBittorrent.
Read all possible preferences @ http://git.io/vEgDQ
:param info_hash: Torrent info hash
:param include_general: Include general torrent properties
:param max_retries: Maximum number of retries to wait for torrent to appear in client
:param kwargs: set preferences in kwargs form.
"""
json_data = "json={}".format(json.dumps(kwargs))
headers = {'content-type': 'application/x-www-form-urlencoded'}
return self._post('command/setPreferences', data=json_data,
headers=headers)
:rtype: Torrent or None
def get_alternative_speed_status(self):
"""
Get Alternative speed limits. (1/0)
"""
return self._get('command/alternativeSpeedLimitsEnabled')
torrent = None
retries = 0
alternative_speed_status = property(get_alternative_speed_status)
# Try find torrent in client
while retries < max_retries:
# TODO this wouldn't be very efficient with large numbers of torrents on the client
torrents = dict([(t.hash, t) for t in self.get_torrents()])
def toggle_alternative_speed(self):
"""
Toggle alternative speed limits.
"""
return self._get('command/toggleAlternativeSpeedLimits')
if hash in torrents:
torrent = torrents[hash]
break
def toggle_sequential_download(self, infohash_list):
"""
Toggle sequential download in supplied torrents.
retries += 1
time.sleep(1)
:param infohash_list: Single or list() of infohashes.
"""
data = self.process_infohash_list(infohash_list)
return self._post('command/toggleSequentialDownload', data=data)
def toggle_first_last_piece_priority(self, infohash_list):
"""
Toggle first/last piece priority of supplied torrents.
if torrent is None:
return None
:param infohash_list: Single or list() of infohashes.
"""
data = self.process_infohash_list(infohash_list)
return self._post('command/toggleFirstLastPiecePrio', data=data)
# Fetch general properties for torrent
if include_general:
torrent.update_general()
def force_start(self, infohash_list, value=True):
"""
Force start selected torrents.
return torrent
:param infohash_list: Single or list() of infohashes.
:param value: Force start value (bool)
"""
data = self.process_infohash_list(infohash_list)
data.update({'value': json.dumps(value)})
return self._post('command/setForceStart', data=data)

15
libs/qbittorrent/file.py

@ -1,15 +0,0 @@
from qbittorrent.base import Base
class File(Base):
def __init__(self, url, session, client=None):
super(File, self).__init__(url, session, client)
self.name = None
self.progress = None
self.priority = None
self.is_seed = None
self.size = None

7
libs/qbittorrent/helpers.py

@ -1,7 +0,0 @@
def try_convert(value, to_type, default=None):
try:
return to_type(value)
except ValueError:
return default
except TypeError:
return default

96
libs/qbittorrent/torrent.py

@ -1,96 +0,0 @@
from qbittorrent.base import Base
from qbittorrent.file import File
from qbittorrent.helpers import try_convert
class Torrent(Base):
properties = {
'num_seeds': {
'key': 'seeds',
'parse': lambda value: try_convert(value, int)
},
'num_leechs': {
'key': 'leechs',
'parse': lambda value: try_convert(value, int)
},
'ratio': {
'parse': lambda value: try_convert(value, float)
}
}
def __init__(self, url, session, client=None):
super(Torrent, self).__init__(url, session, client)
self.hash = None
self.name = None
self.state = None
self.ratio = None
self.progress = None
self.priority = None
self.seeds = None
self.leechs = None
# General properties
self.comment = None
self.save_path = None
self.eta = None
self.size = None
self.dlspeed = None
self.upspeed = None
self.nb_connections = None
self.share_ratio = None
self.piece_size = None
self.total_wasted = None
self.total_downloaded = None
self.total_uploaded = None
self.creation_date = None
self.time_elapsed = None
self.up_limit = None
self.dl_limit = None
#
# Commands
#
def pause(self):
self._post('command/pause', data={'hash': self.hash})
def resume(self):
self._post('command/resume', data={'hash': self.hash})
def remove(self):
self._post('command/delete', data={'hashes': self.hash})
def delete(self):
self._post('command/deletePerm', data={'hashes': self.hash})
def recheck(self):
self._post('command/recheck', data={'hash': self.hash})
#
# Fetch details
#
def get_files(self):
r = self._get('json/propertiesFiles/%s' % self.hash)
return [File.parse(self._client, x) for x in r]
def get_trackers(self):
pass
#
# Update torrent details
#
def update_general(self):
r = self._get('json/propertiesGeneral/%s' % self.hash)
if r:
self._fill(r)
return True
return False
Loading…
Cancel
Save