mirror of
https://github.com/rembo10/headphones.git
synced 2026-03-21 12:19:27 +00:00
Merge pull request #2822 from dsm1212/qbittorrent
Add qbittorrent downloader support
This commit is contained in:
@@ -312,6 +312,7 @@
|
||||
<input type="radio" name="torrent_downloader" id="torrent_downloader_transmission" value="1" ${config['torrent_downloader_transmission']}> Transmission
|
||||
<input type="radio" name="torrent_downloader" id="torrent_downloader_utorrent" value="2" ${config['torrent_downloader_utorrent']}> uTorrent (Beta)
|
||||
<input type="radio" name="torrent_downloader" id="torrent_downloader_deluge" value="3" ${config['torrent_downloader_deluge']}> Deluge (Beta)
|
||||
<input type="radio" name="torrent_downloader" id="torrent_downloader_qbittorrent" value="4" ${config['torrent_downloader_qbittorrent']}> QBitTorrent
|
||||
</fieldset>
|
||||
<fieldset id="torrent_blackhole_options">
|
||||
<div class="row">
|
||||
@@ -386,6 +387,26 @@
|
||||
<input type="text" name="utorrent_label" value="${config['utorrent_label']}" size="30">
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset id="qbittorrent_options">
|
||||
<small class="heading"><i class="fa fa-info-circle"></i> Note: Works with WebAPI Rev 11 and later (QBitTorrent 3.4.0 and later) </small>
|
||||
<div class="row">
|
||||
<label>QBitTorrent Host</label>
|
||||
<input type="text" name="qbittorrent_host" value="${config['qbittorrent_host']}" size="30">
|
||||
<small>usually http://localhost:8081</small>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label>QBitTorrent Username</label>
|
||||
<input type="text" name="qbittorrent_username" value="${config['qbittorrent_username']}" size="30">
|
||||
</div>
|
||||
<div class="row">
|
||||
<label>QBitTorrent Password</label>
|
||||
<input type="password" name="qbittorrent_password" value="${config['qbittorrent_password']}" size="30">
|
||||
</div>
|
||||
<div class="row">
|
||||
<label>QBitTorrent Label</label>
|
||||
<input type="text" name="qbittorrent_label" value="${config['qbittorrent_label']}" size="30">
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset id="deluge_options">
|
||||
<div class="row">
|
||||
<label>Deluge WebUI Host and Port</label>
|
||||
@@ -2275,26 +2296,30 @@
|
||||
|
||||
if ($("#torrent_downloader_blackhole").is(":checked"))
|
||||
{
|
||||
$("#transmission_options,#utorrent_options,#deluge_options").hide();
|
||||
$("#transmission_options,#utorrent_options,#deluge_options,#qbittorrent_options").hide();
|
||||
$("#torrent_blackhole_options").show();
|
||||
}
|
||||
if ($("#torrent_downloader_transmission").is(":checked"))
|
||||
{
|
||||
$("#torrent_blackhole_options,#utorrent_options,#deluge_options").hide();
|
||||
$("#torrent_blackhole_options,#utorrent_options,#deluge_options,#qbittorrent_options").hide();
|
||||
$("#transmission_options").show();
|
||||
}
|
||||
if ($("#torrent_downloader_utorrent").is(":checked"))
|
||||
{
|
||||
$("#torrent_blackhole_options,#transmission_options,#deluge_options").hide();
|
||||
$("#torrent_blackhole_options,#transmission_options,#deluge_options,#qbittorrent_options").hide();
|
||||
$("#utorrent_options").show();
|
||||
}
|
||||
if ($("#torrent_downloader_qbittorrent").is(":checked"))
|
||||
{
|
||||
$("#torrent_blackhole_options,#transmission_options,#utorrent_options,#deluge_options").hide();
|
||||
$("#qbittorrent_options").show();
|
||||
}
|
||||
if ($("#torrent_downloader_deluge").is(":checked"))
|
||||
{
|
||||
$("#torrent_blackhole_options,#transmission_options,#utorrent_options").hide();
|
||||
$("#torrent_blackhole_options,#transmission_options,#utorrent_options,#qbittorent_options").hide();
|
||||
$("#deluge_options").show();
|
||||
}
|
||||
|
||||
|
||||
$('input[type=radio]').change(function(){
|
||||
if ($("#preferred_bitrate").is(":checked"))
|
||||
{
|
||||
@@ -2330,19 +2355,23 @@
|
||||
}
|
||||
if ($("#torrent_downloader_blackhole").is(":checked"))
|
||||
{
|
||||
$("#transmission_options,#utorrent_options,#deluge_options").fadeOut("fast", function() { $("#torrent_blackhole_options").fadeIn() });
|
||||
$("#transmission_options,#utorrent_options,#deluge_options,#qbittorrent_options").fadeOut("fast", function() { $("#torrent_blackhole_options").fadeIn() });
|
||||
}
|
||||
if ($("#torrent_downloader_transmission").is(":checked"))
|
||||
{
|
||||
$("#torrent_blackhole_options,#utorrent_options,#deluge_options").fadeOut("fast", function() { $("#transmission_options").fadeIn() });
|
||||
$("#torrent_blackhole_options,#utorrent_options,#deluge_options,#qbittorrent_options").fadeOut("fast", function() { $("#transmission_options").fadeIn() });
|
||||
}
|
||||
if ($("#torrent_downloader_utorrent").is(":checked"))
|
||||
{
|
||||
$("#torrent_blackhole_options,#transmission_options,#deluge_options").fadeOut("fast", function() { $("#utorrent_options").fadeIn() });
|
||||
$("#torrent_blackhole_options,#transmission_options,#deluge_options,#qbittorrent_options").fadeOut("fast", function() { $("#utorrent_options").fadeIn() });
|
||||
}
|
||||
if ($("#torrent_downloader_qbittorrent").is(":checked"))
|
||||
{
|
||||
$("#torrent_blackhole_options,#transmission_options,#utorrent_options,#deluge_options").fadeOut("fast", function() { $("#qbittorrent_options").fadeIn() });
|
||||
}
|
||||
if ($("#torrent_downloader_deluge").is(":checked"))
|
||||
{
|
||||
$("#torrent_blackhole_options,#utorrent_options,#transmission_options").fadeOut("fast", function() { $("#deluge_options").fadeIn() });
|
||||
$("#torrent_blackhole_options,#utorrent_options,#transmission_options,#qbittorrent_options").fadeOut("fast", function() { $("#deluge_options").fadeIn() });
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -232,6 +232,10 @@ _CONFIG_DEFINITIONS = {
|
||||
'PUSHOVER_KEYS': (str, 'Pushover', ''),
|
||||
'PUSHOVER_ONSNATCH': (int, 'Pushover', 0),
|
||||
'PUSHOVER_PRIORITY': (int, 'Pushover', 0),
|
||||
'QBITTORRENT_HOST': (str, 'QBitTorrent', ''),
|
||||
'QBITTORRENT_LABEL': (str, 'QBitTorrent', ''),
|
||||
'QBITTORRENT_PASSWORD': (str, 'QBitTorrent', ''),
|
||||
'QBITTORRENT_USERNAME': (str, 'QBitTorrent', ''),
|
||||
'RENAME_FILES': (int, 'General', 0),
|
||||
'RENAME_UNPROCESSED': (bool_int, 'General', 1),
|
||||
'RENAME_FROZEN': (bool_int, 'General', 1),
|
||||
|
||||
@@ -26,7 +26,7 @@ from beets import autotag
|
||||
from beets import config as beetsconfig
|
||||
from beets.mediafile import MediaFile, FileTypeError, UnreadableFileError
|
||||
from beetsplug import lyrics as beetslyrics
|
||||
from headphones import notifiers, utorrent, transmission, deluge
|
||||
from headphones import notifiers, utorrent, transmission, deluge, qbittorrent
|
||||
from headphones import db, albumart, librarysync
|
||||
from headphones import logger, helpers, request, mb, music_encoder
|
||||
from headphones import metadata
|
||||
@@ -452,7 +452,7 @@ def doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list,
|
||||
[albumid])
|
||||
|
||||
# Check if torrent has finished seeding
|
||||
if headphones.CONFIG.TORRENT_DOWNLOADER == 1 or headphones.CONFIG.TORRENT_DOWNLOADER == 2:
|
||||
if headphones.CONFIG.TORRENT_DOWNLOADER != 0:
|
||||
seed_snatched = myDB.action(
|
||||
'SELECT * from snatched WHERE Status="Seed_Snatched" and AlbumID=?',
|
||||
[albumid]).fetchone()
|
||||
@@ -465,8 +465,10 @@ def doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list,
|
||||
torrent_removed = transmission.removeTorrent(hash, True)
|
||||
elif headphones.CONFIG.TORRENT_DOWNLOADER == 3: # Deluge
|
||||
torrent_removed = deluge.removeTorrent(hash, True)
|
||||
else:
|
||||
elif headphones.CONFIG.TORRENT_DOWNLOADER == 2:
|
||||
torrent_removed = utorrent.removeTorrent(hash, True)
|
||||
else:
|
||||
torrent_removed = qbittorrent.removeTorrent(hash, True)
|
||||
|
||||
# Torrent removed, delete the snatched record, else update Status for scheduled job to check
|
||||
if torrent_removed:
|
||||
|
||||
286
headphones/qbittorrent.py
Normal file
286
headphones/qbittorrent.py
Normal file
@@ -0,0 +1,286 @@
|
||||
# This file is part of Headphones.
|
||||
#
|
||||
# Headphones 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.
|
||||
#
|
||||
# Headphones 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 Headphones. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import urllib
|
||||
import urllib2
|
||||
import cookielib
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
import mimetypes
|
||||
import random
|
||||
import string
|
||||
|
||||
import headphones
|
||||
|
||||
from headphones import logger
|
||||
from collections import namedtuple
|
||||
|
||||
|
||||
class qbittorrentclient(object):
|
||||
|
||||
TOKEN_REGEX = "<div id='token' style='display:none;'>([^<>]+)</div>"
|
||||
UTSetting = namedtuple("UTSetting", ["name", "int", "str", "access"])
|
||||
|
||||
def __init__(self, base_url=None, username=None, password=None,):
|
||||
|
||||
host = headphones.CONFIG.QBITTORRENT_HOST
|
||||
if not host.startswith('http'):
|
||||
host = 'http://' + host
|
||||
|
||||
if host.endswith('/'):
|
||||
host = host[:-1]
|
||||
|
||||
if host.endswith('/gui'):
|
||||
host = host[:-4]
|
||||
|
||||
self.base_url = host
|
||||
self.username = headphones.CONFIG.QBITTORRENT_USERNAME
|
||||
self.password = headphones.CONFIG.QBITTORRENT_PASSWORD
|
||||
self.cookiejar = cookielib.CookieJar()
|
||||
self.opener = self._make_opener()
|
||||
self._get_sid(self.base_url, self.username, self.password)
|
||||
|
||||
def _make_opener(self):
|
||||
# create opener with cookie handler to carry QBitTorrent SID cookie
|
||||
cookie_handler = urllib2.HTTPCookieProcessor(self.cookiejar)
|
||||
handlers = [cookie_handler]
|
||||
return urllib2.build_opener(*handlers)
|
||||
|
||||
def _get_sid(self, base_url, username, password):
|
||||
# login so we can capture SID cookie
|
||||
login_data = urllib.urlencode({'username': username, 'password': password})
|
||||
try:
|
||||
self.opener.open(base_url + '/login', login_data)
|
||||
except urllib2.URLError as err:
|
||||
logger.debug('Error getting SID. qBittorrent responded with error: ' + str(err.reason))
|
||||
return
|
||||
for cookie in self.cookiejar:
|
||||
logger.debug('login cookie: ' + cookie.name + ', value: ' + cookie.value)
|
||||
return
|
||||
|
||||
def _command(self, command, args=None, content_type=None, files=None):
|
||||
logger.debug('QBittorrent WebAPI Command: %s' % command)
|
||||
|
||||
url = self.base_url + '/' + command
|
||||
|
||||
data = None
|
||||
headers = dict()
|
||||
if content_type == 'multipart/form-data':
|
||||
data, headers = encode_multipart(args, files)
|
||||
else:
|
||||
if args:
|
||||
data = urllib.urlencode(args)
|
||||
if content_type:
|
||||
headers['Content-Type'] = content_type
|
||||
|
||||
logger.debug('%s' % json.dumps(headers, indent=4))
|
||||
logger.debug('%s' % data)
|
||||
|
||||
request = urllib2.Request(url, data, headers)
|
||||
try:
|
||||
response = self.opener.open(request)
|
||||
info = response.info()
|
||||
if info:
|
||||
if info.getheader('content-type'):
|
||||
if info.getheader('content-type') == 'application/json':
|
||||
resp = ''
|
||||
for line in response:
|
||||
resp = resp + line
|
||||
logger.debug('response code: %s' % str(response.code))
|
||||
logger.debug('response: %s' % resp)
|
||||
return response.code, json.loads(resp)
|
||||
logger.debug('response code: %s' % str(response.code))
|
||||
return response.code, None
|
||||
except urllib2.URLError as err:
|
||||
logger.debug('Failed URL: %s' % url)
|
||||
logger.debug('QBitTorrent webUI raised the following error: %s' % str(err))
|
||||
return None, None
|
||||
|
||||
def _get_list(self, **args):
|
||||
return self._command('query/torrents', args)
|
||||
|
||||
def _get_settings(self):
|
||||
status, value = self._command('query/preferences')
|
||||
logger.debug('get_settings() returned %d items' % len(value))
|
||||
return value
|
||||
|
||||
def get_savepath(self, hash):
|
||||
logger.debug('qb.get_savepath(%s)' % hash)
|
||||
status, torrentList = self._get_list()
|
||||
for torrent in torrentList:
|
||||
if torrent['hash']:
|
||||
if torrent['hash'].upper() == hash.upper():
|
||||
return torrent['save_path']
|
||||
return None
|
||||
|
||||
def start(self, hash):
|
||||
logger.debug('qb.start(%s)' % hash)
|
||||
args = {'hash': hash}
|
||||
return self._command('command/resume', args, 'application/x-www-form-urlencoded')
|
||||
|
||||
def pause(self, hash):
|
||||
logger.debug('qb.pause(%s)' % hash)
|
||||
args = {'hash': hash}
|
||||
return self._command('command/pause', args, 'application/x-www-form-urlencoded')
|
||||
|
||||
def getfiles(self, hash):
|
||||
logger.debug('qb.getfiles(%s)' % hash)
|
||||
return self._command('query/propertiesFiles/' + hash)
|
||||
|
||||
def getprops(self, hash):
|
||||
logger.debug('qb.getprops(%s)' % hash)
|
||||
return self._command('query/propertiesGeneral/' + hash)
|
||||
|
||||
def setprio(self, hash, priority):
|
||||
logger.debug('qb.setprio(%s,%d)' % (hash, priority))
|
||||
args = {'hash': hash, 'priority': priority}
|
||||
return self._command('command/setFilePrio', args, 'application/x-www-form-urlencoded')
|
||||
|
||||
def remove(self, hash, remove_data=False):
|
||||
logger.debug('qb.remove(%s,%s)' % (hash, remove_data))
|
||||
|
||||
args = {'hashes': hash}
|
||||
if remove_data:
|
||||
command = 'command/deletePerm'
|
||||
else:
|
||||
command = 'command/delete'
|
||||
return self._command(command, args, 'application/x-www-form-urlencoded')
|
||||
|
||||
|
||||
def removeTorrent(hash, remove_data=False):
|
||||
|
||||
logger.debug('removeTorrent(%s,%s)' % (hash, remove_data))
|
||||
|
||||
qbclient = qbittorrentclient()
|
||||
status, torrentList = qbclient._get_list()
|
||||
for torrent in torrentList:
|
||||
if torrent['hash'].upper() == hash.upper():
|
||||
if torrent['state'] == 'uploading' or torrent['state'] == 'stalledUP':
|
||||
logger.info('%s has finished seeding, removing torrent and data' % torrent['name'])
|
||||
qbclient.remove(hash, remove_data)
|
||||
return True
|
||||
else:
|
||||
logger.info('%s has not finished seeding yet, torrent will not be removed, will try again on next run' % torrent['name'])
|
||||
return False
|
||||
return False
|
||||
|
||||
|
||||
def addTorrent(link):
|
||||
logger.debug('addTorrent(%s)' % link)
|
||||
|
||||
qbclient = qbittorrentclient()
|
||||
args = {'urls': link, 'savepath': headphones.CONFIG.DOWNLOAD_TORRENT_DIR}
|
||||
if headphones.CONFIG.QBITTORRENT_LABEL:
|
||||
args['category'] = headphones.CONFIG.QBITTORRENT_LABEL
|
||||
|
||||
return qbclient._command('command/download', args, 'multipart/form-data')
|
||||
|
||||
|
||||
def addFile(data):
|
||||
logger.debug('addFile(data)')
|
||||
|
||||
qbclient = qbittorrentclient()
|
||||
files = {'torrents': {'filename': '', 'content': data}}
|
||||
|
||||
return qbclient._command('command/upload', filelist=files)
|
||||
|
||||
|
||||
def getFolder(hash):
|
||||
logger.debug('getFolder(%s)' % hash)
|
||||
|
||||
qbclient = qbittorrentclient()
|
||||
|
||||
# Get Active Directory from settings
|
||||
settings = qbclient._get_settings()
|
||||
active_dir = settings['temp_path']
|
||||
|
||||
if not active_dir:
|
||||
logger.error('Could not get "Keep incomplete torrents in:" directory from QBitTorrent settings, please ensure it is set')
|
||||
return None
|
||||
|
||||
# Get Torrent Folder Name
|
||||
torrent_folder = qbclient.get_savepath(hash)
|
||||
|
||||
# If there's no folder yet then it's probably a magnet, try until folder is populated
|
||||
if torrent_folder == active_dir or not torrent_folder:
|
||||
tries = 1
|
||||
while (torrent_folder == active_dir or torrent_folder is None) and tries <= 10:
|
||||
tries += 1
|
||||
time.sleep(6)
|
||||
torrent_folder = qbclient.get_savepath(hash)
|
||||
|
||||
if torrent_folder == active_dir or not torrent_folder:
|
||||
torrent_folder = qbclient.get_savepath(hash)
|
||||
return torrent_folder
|
||||
else:
|
||||
if headphones.SYS_PLATFORM != "win32":
|
||||
torrent_folder = torrent_folder.replace('\\', '/')
|
||||
return os.path.basename(os.path.normpath(torrent_folder))
|
||||
|
||||
|
||||
_BOUNDARY_CHARS = string.digits + string.ascii_letters
|
||||
|
||||
|
||||
# Taken from http://code.activestate.com/recipes/578668-encode-multipart-form-data-for-uploading-files-via/
|
||||
# "MIT License" which is compatible with GPL
|
||||
def encode_multipart(args, files, boundary=None):
|
||||
logger.debug('encode_multipart()')
|
||||
|
||||
def escape_quote(s):
|
||||
return s.replace('"', '\\"')
|
||||
|
||||
if boundary is None:
|
||||
boundary = ''.join(random.choice(_BOUNDARY_CHARS) for i in range(30))
|
||||
lines = []
|
||||
|
||||
if args:
|
||||
for name, value in args.items():
|
||||
lines.extend((
|
||||
'--{0}'.format(boundary),
|
||||
'Content-Disposition: form-data; name="{0}"'.format(escape_quote(name)),
|
||||
'',
|
||||
str(value),
|
||||
))
|
||||
logger.debug(''.join(lines))
|
||||
|
||||
if files:
|
||||
for name, value in files.items():
|
||||
filename = value['filename']
|
||||
if 'mimetype' in value:
|
||||
mimetype = value['mimetype']
|
||||
else:
|
||||
mimetype = mimetypes.guess_type(filename)[0] or 'application/octet-stream'
|
||||
lines.extend((
|
||||
'--{0}'.format(boundary),
|
||||
'Content-Disposition: form-data; name="{0}"; filename="{1}"'.format(
|
||||
escape_quote(name), escape_quote(filename)),
|
||||
'Content-Type: {0}'.format(mimetype),
|
||||
'',
|
||||
value['content'],
|
||||
))
|
||||
|
||||
lines.extend((
|
||||
'--{0}--'.format(boundary),
|
||||
'',
|
||||
))
|
||||
body = '\r\n'.join(lines)
|
||||
|
||||
headers = {
|
||||
'Content-Type': 'multipart/form-data; boundary={0}'.format(boundary),
|
||||
'Content-Length': str(len(body)),
|
||||
}
|
||||
|
||||
return (body, headers)
|
||||
@@ -35,7 +35,7 @@ from pygazelle import format as gazelleformat
|
||||
import headphones
|
||||
from headphones.common import USER_AGENT
|
||||
from headphones import logger, db, helpers, classes, sab, nzbget, request
|
||||
from headphones import utorrent, transmission, notifiers, rutracker, deluge
|
||||
from headphones import utorrent, transmission, notifiers, rutracker, deluge, qbittorrent
|
||||
from bencode import bencode, bdecode
|
||||
|
||||
# Magnet to torrent services, for Black hole. Stolen from CouchPotato.
|
||||
@@ -981,7 +981,7 @@ def send_to_downloader(data, bestqual, album):
|
||||
except Exception as e:
|
||||
logger.error('Error sending torrent to Deluge: %s' % str(e))
|
||||
|
||||
else: # if headphones.CONFIG.TORRENT_DOWNLOADER == 2:
|
||||
elif headphones.CONFIG.TORRENT_DOWNLOADER == 2:
|
||||
logger.info("Sending torrent to uTorrent")
|
||||
|
||||
# Add torrent
|
||||
@@ -1012,6 +1012,28 @@ def send_to_downloader(data, bestqual, album):
|
||||
seed_ratio = get_seed_ratio(bestqual[3])
|
||||
if seed_ratio is not None:
|
||||
utorrent.setSeedRatio(torrentid, seed_ratio)
|
||||
else: # if headphones.CONFIG.TORRENT_DOWNLOADER == 4:
|
||||
logger.info("Sending torrent to QBiTorrent")
|
||||
|
||||
# Add torrent
|
||||
if bestqual[3] == 'rutracker.org':
|
||||
qbittorrent.addFile(data)
|
||||
else:
|
||||
qbittorrent.addTorrent(bestqual[2])
|
||||
|
||||
# Get hash
|
||||
torrentid = calculate_torrent_hash(bestqual[2], data)
|
||||
if not torrentid:
|
||||
logger.error('Torrent id could not be determined')
|
||||
return
|
||||
|
||||
# Get folder
|
||||
folder_name = qbittorrent.getFolder(torrentid)
|
||||
if folder_name:
|
||||
logger.info('Torrent folder name: %s' % folder_name)
|
||||
else:
|
||||
logger.error('Torrent folder name could not be determined')
|
||||
return
|
||||
|
||||
myDB = db.DBConnection()
|
||||
myDB.action('UPDATE albums SET status = "Snatched" WHERE AlbumID=?', [album['AlbumID']])
|
||||
|
||||
@@ -1157,6 +1157,10 @@ class WebInterface(object):
|
||||
"nzbget_password": headphones.CONFIG.NZBGET_PASSWORD,
|
||||
"nzbget_category": headphones.CONFIG.NZBGET_CATEGORY,
|
||||
"nzbget_priority": headphones.CONFIG.NZBGET_PRIORITY,
|
||||
"qbittorrent_host": headphones.CONFIG.QBITTORRENT_HOST,
|
||||
"qbittorrent_username": headphones.CONFIG.QBITTORRENT_USERNAME,
|
||||
"qbittorrent_password": headphones.CONFIG.QBITTORRENT_PASSWORD,
|
||||
"qbittorrent_label": headphones.CONFIG.QBITTORRENT_LABEL,
|
||||
"transmission_host": headphones.CONFIG.TRANSMISSION_HOST,
|
||||
"transmission_username": headphones.CONFIG.TRANSMISSION_USERNAME,
|
||||
"transmission_password": headphones.CONFIG.TRANSMISSION_PASSWORD,
|
||||
@@ -1177,6 +1181,7 @@ class WebInterface(object):
|
||||
"torrent_downloader_transmission": radio(headphones.CONFIG.TORRENT_DOWNLOADER, 1),
|
||||
"torrent_downloader_utorrent": radio(headphones.CONFIG.TORRENT_DOWNLOADER, 2),
|
||||
"torrent_downloader_deluge": radio(headphones.CONFIG.TORRENT_DOWNLOADER, 3),
|
||||
"torrent_downloader_qbittorrent": radio(headphones.CONFIG.TORRENT_DOWNLOADER, 4),
|
||||
"download_dir": headphones.CONFIG.DOWNLOAD_DIR,
|
||||
"use_blackhole": checked(headphones.CONFIG.BLACKHOLE),
|
||||
"blackhole_dir": headphones.CONFIG.BLACKHOLE_DIR,
|
||||
|
||||
Reference in New Issue
Block a user