mirror of
https://github.com/clinton-hall/nzbToMedia.git
synced 2025-08-19 21:03:14 -07:00
Merge branch 'nightly' into dev
This commit is contained in:
commit
9ac74615af
14 changed files with 1185 additions and 29 deletions
|
@ -17,7 +17,7 @@ Failed download handling for SickBeard is available by using Tolstyak's fork [Si
|
|||
To use this feature, in autoProcessTV.cfg set the parameter "fork=failed". Default is "fork=default" and will work with the standard version of SickBeard and just ignores failed downloads.
|
||||
Development of Tolstyak's fork ended in 2013, but newer forks exist with significant feature updates such as [Mr-Orange TPB](https://github.com/coach0742/Sick-Beard) (discontinued), [SickRageTV](https://github.com/SiCKRAGETV/SickRage) and [SickRage](https://github.com/SickRage/SickRage) (active). See [SickBeard Forks](https://github.com/clinton-hall/nzbToMedia/wiki/Failed-Download-Handling-%28FDH%29#sick-beard-and-its-forks "SickBeard Forks") for a list of known forks.
|
||||
|
||||
Full support is provided for [SickRageTV](https://github.com/SiCKRAGETV/SickRage) and [SickRage](https://github.com/SickRage/SickRage).
|
||||
Full support is provided for [SickRageTV](https://github.com/SiCKRAGETV/SickRage), [SickRage](https://github.com/SickRage/SickRage), and [SickGear](https://github.com/SickGear/SickGear).
|
||||
|
||||
Torrent support has been added with the assistance of jkaberg and berkona. Currently supports uTorrent, Transmission, Deluge and possibly more.
|
||||
To enable Torrent extraction, on Windows, you need to install [7-zip](http://www.7-zip.org/ "7-zip") or on *nix you need to install the following packages/commands.
|
||||
|
|
|
@ -132,7 +132,7 @@ def processTorrent(inputDirectory, inputName, inputCategory, inputHash, inputID,
|
|||
|
||||
logger.debug("Scanning files in directory: {0}".format(inputDirectory))
|
||||
|
||||
if sectionName == 'HeadPhones':
|
||||
if sectionName in ['HeadPhones', 'Lidarr']:
|
||||
core.NOFLATTEN.extend(
|
||||
inputCategory) # Make sure we preserve folder structure for HeadPhones.
|
||||
|
||||
|
@ -142,6 +142,10 @@ def processTorrent(inputDirectory, inputName, inputCategory, inputHash, inputID,
|
|||
inputFiles = core.listMediaFiles(inputDirectory, archives=False, other=True, otherext=extensions)
|
||||
else:
|
||||
inputFiles = core.listMediaFiles(inputDirectory, other=True, otherext=extensions)
|
||||
if len(inputFiles) == 0 and os.path.isfile(inputDirectory):
|
||||
inputFiles = [inputDirectory]
|
||||
logger.debug("Found 1 file to process: {0}".format(inputDirectory))
|
||||
else:
|
||||
logger.debug("Found {0} files in {1}".format(len(inputFiles), inputDirectory))
|
||||
for inputFile in inputFiles:
|
||||
filePath = os.path.dirname(inputFile)
|
||||
|
@ -235,7 +239,7 @@ def processTorrent(inputDirectory, inputName, inputCategory, inputHash, inputID,
|
|||
inputHash = inputHash.upper()
|
||||
result = core.autoProcessTV().processEpisode(sectionName, outputDestination, inputName,
|
||||
status, clientAgent, inputHash, inputCategory)
|
||||
elif sectionName == 'HeadPhones':
|
||||
elif sectionName in ['HeadPhones', 'Lidarr']:
|
||||
result = core.autoProcessMusic().process(sectionName, outputDestination, inputName,
|
||||
status, clientAgent, inputCategory)
|
||||
elif sectionName == 'Mylar':
|
||||
|
|
|
@ -193,6 +193,33 @@
|
|||
##### Set to path where download client places completed downloads locally for this category
|
||||
watch_dir =
|
||||
|
||||
[Lidarr]
|
||||
#### autoProcessing for Movies
|
||||
#### raCategory - category that gets called for post-processing with Radarr
|
||||
[[music]]
|
||||
enabled = 0
|
||||
apikey =
|
||||
host = localhost
|
||||
port = 8686
|
||||
###### ADVANCED USE - ONLY EDIT IF YOU KNOW WHAT YOU'RE DOING ######
|
||||
web_root =
|
||||
ssl = 0
|
||||
delete_failed = 0
|
||||
# Enable/Disable linking for Torrents
|
||||
Torrent_NoLink = 0
|
||||
keep_archive = 1
|
||||
extract = 1
|
||||
nzbExtractionBy = Downloader
|
||||
wait_for = 6
|
||||
# Set this to minimum required size to consider a media file valid (in MB)
|
||||
minSize = 0
|
||||
# Enable/Disable deleting ignored files (samples and invalid media files)
|
||||
delete_ignored = 0
|
||||
##### Enable if NzbDrone is on a remote server for this category
|
||||
remote_path = 0
|
||||
##### Set to path where download client places completed downloads locally for this category
|
||||
watch_dir =
|
||||
|
||||
[Mylar]
|
||||
#### autoProcessing for Comics
|
||||
#### comics - category that gets called for post-processing with Mylar
|
||||
|
@ -252,7 +279,7 @@
|
|||
[Nzb]
|
||||
###### clientAgent - Supported clients: sabnzbd, nzbget
|
||||
clientAgent = sabnzbd
|
||||
###### SabNZBD (You must edit this if your using nzbToMedia.py with SabNZBD)
|
||||
###### SabNZBD (You must edit this if you're using nzbToMedia.py with SabNZBD)
|
||||
sabnzbd_host = http://localhost
|
||||
sabnzbd_port = 8080
|
||||
sabnzbd_apikey =
|
||||
|
@ -260,7 +287,7 @@
|
|||
default_downloadDirectory =
|
||||
|
||||
[Torrent]
|
||||
###### clientAgent - Supported clients: utorrent, transmission, deluge, rtorrent, vuze, other
|
||||
###### clientAgent - Supported clients: utorrent, transmission, deluge, rtorrent, vuze, qbittorrent, other
|
||||
clientAgent = other
|
||||
###### useLink - Set to hard for physical links, sym for symbolic links, move to move, move-sym to move and link back, and no to not use links (copy)
|
||||
useLink = hard
|
||||
|
@ -272,20 +299,25 @@
|
|||
categories = music_videos,pictures,software,manual
|
||||
###### A list of categories that you don't want to be flattened (i.e preserve the directory structure when copying/linking.
|
||||
noFlatten = pictures,manual
|
||||
###### uTorrent Hardlink solution (You must edit this if your using TorrentToMedia.py with uTorrent)
|
||||
###### uTorrent Hardlink solution (You must edit this if you're using TorrentToMedia.py with uTorrent)
|
||||
uTorrentWEBui = http://localhost:8090/gui/
|
||||
uTorrentUSR = your username
|
||||
uTorrentPWD = your password
|
||||
###### Transmission (You must edit this if your using TorrentToMedia.py with Transmission)
|
||||
###### Transmission (You must edit this if you're using TorrentToMedia.py with Transmission)
|
||||
TransmissionHost = localhost
|
||||
TransmissionPort = 9091
|
||||
TransmissionUSR = your username
|
||||
TransmissionPWD = your password
|
||||
#### Deluge (You must edit this if your using TorrentToMedia.py with deluge. Note that the host/port is for the deluge daemon, not the webui)
|
||||
#### Deluge (You must edit this if you're using TorrentToMedia.py with deluge. Note that the host/port is for the deluge daemon, not the webui)
|
||||
DelugeHost = localhost
|
||||
DelugePort = 58846
|
||||
DelugeUSR = your username
|
||||
DelugePWD = your password
|
||||
###### qBittorrent (You must edit this if you're using TorrentToMedia.py with qBittorrent)
|
||||
qBittorrenHost = localhost
|
||||
qBittorrentPort = 8080
|
||||
qBittorrentUSR = your username
|
||||
qBittorrentPWD = your password
|
||||
###### ADVANCED USE - ONLY EDIT IF YOU KNOW WHAT YOU'RE DOING ######
|
||||
deleteOriginal = 0
|
||||
chmodDirectory = 0
|
||||
|
|
|
@ -51,7 +51,7 @@ from core.databases import mainDB
|
|||
|
||||
# Client Agents
|
||||
NZB_CLIENTS = ['sabnzbd', 'nzbget', 'manual']
|
||||
TORRENT_CLIENTS = ['transmission', 'deluge', 'utorrent', 'rtorrent', 'other', 'manual']
|
||||
TORRENT_CLIENTS = ['transmission', 'deluge', 'utorrent', 'rtorrent', 'qbittorrent', 'other', 'manual']
|
||||
|
||||
# sabnzbd constants
|
||||
SABNZB_NO_OF_ARGUMENTS = 8
|
||||
|
@ -62,11 +62,11 @@ FORKS = {}
|
|||
FORK_DEFAULT = "default"
|
||||
FORK_FAILED = "failed"
|
||||
FORK_FAILED_TORRENT = "failed-torrent"
|
||||
FORK_SICKRAGETV = "sickragetv"
|
||||
FORK_SICKRAGE = "sickrage"
|
||||
FORK_SICKRAGE_API = "sickrage-api"
|
||||
FORK_MEDUSA = "medusa"
|
||||
FORK_SICKGEAR = "sickgear"
|
||||
FORK_SICKRAGETV = "SickRageTV"
|
||||
FORK_SICKRAGE = "SickRage"
|
||||
FORK_SICKRAGE_API = "SiCKRAGE-api"
|
||||
FORK_MEDUSA = "Medusa"
|
||||
FORK_SICKGEAR = "SickGear"
|
||||
FORKS[FORK_DEFAULT] = {"dir": None}
|
||||
FORKS[FORK_FAILED] = {"dirName": None, "failed": None}
|
||||
FORKS[FORK_FAILED_TORRENT] = {"dir": None, "failed": None, "process_method": None}
|
||||
|
@ -137,6 +137,11 @@ DELUGEPORT = None
|
|||
DELUGEUSR = None
|
||||
DELUGEPWD = None
|
||||
|
||||
QBITTORRENTHOST = None
|
||||
QBITTORRENTPORT = None
|
||||
QBITTORRENTUSR = None
|
||||
QBITTORRENTPWD = None
|
||||
|
||||
PLEXSSL = None
|
||||
PLEXHOST = None
|
||||
PLEXPORT = None
|
||||
|
@ -236,7 +241,7 @@ def initialize(section=None):
|
|||
DELETE_ORIGINAL, TORRENT_CHMOD_DIRECTORY, PASSWORDSFILE, USER_DELAY, USER_SCRIPT, USER_SCRIPT_CLEAN, USER_SCRIPT_MEDIAEXTENSIONS, \
|
||||
USER_SCRIPT_PARAM, USER_SCRIPT_RUNONCE, USER_SCRIPT_SUCCESSCODES, DOWNLOADINFO, CHECK_MEDIA, SAFE_MODE, \
|
||||
TORRENT_DEFAULTDIR, TORRENT_RESUME_ON_FAILURE, NZB_DEFAULTDIR, REMOTEPATHS, LOG_ENV, PID_FILE, MYAPP, ACHANNELS, ACHANNELS2, ACHANNELS3, \
|
||||
PLEXSSL, PLEXHOST, PLEXPORT, PLEXTOKEN, PLEXSEC, TORRENT_RESUME, PAR2CMD
|
||||
PLEXSSL, PLEXHOST, PLEXPORT, PLEXTOKEN, PLEXSEC, TORRENT_RESUME, PAR2CMD, QBITTORRENTHOST, QBITTORRENTPORT, QBITTORRENTUSR, QBITTORRENTPWD
|
||||
|
||||
if __INITIALIZED__:
|
||||
return False
|
||||
|
@ -359,7 +364,7 @@ def initialize(section=None):
|
|||
if GROUPS == ['']:
|
||||
GROUPS = None
|
||||
|
||||
TORRENT_CLIENTAGENT = CFG["Torrent"]["clientAgent"] # utorrent | deluge | transmission | rtorrent | vuze |other
|
||||
TORRENT_CLIENTAGENT = CFG["Torrent"]["clientAgent"] # utorrent | deluge | transmission | rtorrent | vuze | qbittorrent |other
|
||||
USELINK = CFG["Torrent"]["useLink"] # no | hard | sym
|
||||
OUTPUTDIRECTORY = CFG["Torrent"]["outputDirectory"] # /abs/path/to/complete/
|
||||
TORRENT_DEFAULTDIR = CFG["Torrent"]["default_downloadDirectory"]
|
||||
|
@ -387,6 +392,11 @@ def initialize(section=None):
|
|||
DELUGEUSR = CFG["Torrent"]["DelugeUSR"] # mysecretusr
|
||||
DELUGEPWD = CFG["Torrent"]["DelugePWD"] # mysecretpwr
|
||||
|
||||
QBITTORRENTHOST = CFG["Torrent"]["qBittorrenHost"] # localhost
|
||||
QBITTORRENTPORT = int(CFG["Torrent"]["qBittorrentPort"]) # 8080
|
||||
QBITTORRENTUSR = CFG["Torrent"]["qBittorrentUSR"] # mysecretusr
|
||||
QBITTORRENTPWD = CFG["Torrent"]["qBittorrentPWD"] # mysecretpwr
|
||||
|
||||
REMOTEPATHS = CFG["Network"]["mount_points"] or []
|
||||
if REMOTEPATHS:
|
||||
if isinstance(REMOTEPATHS, list):
|
||||
|
|
|
@ -75,7 +75,7 @@ class autoProcessMovie(object):
|
|||
if release['status'] not in ['snatched', 'downloaded', 'done']:
|
||||
continue
|
||||
if download_id:
|
||||
if download_id != release['download_info']['id']:
|
||||
if download_id.lower() != release['download_info']['id'].lower():
|
||||
continue
|
||||
|
||||
id = release['_id']
|
||||
|
@ -185,7 +185,7 @@ class autoProcessMovie(object):
|
|||
media_id = None
|
||||
downloader = None
|
||||
release_status_old = None
|
||||
if release and imdbid:
|
||||
if release:
|
||||
try:
|
||||
release_id = release.keys()[0]
|
||||
media_id = release[release_id]['media_id']
|
||||
|
|
|
@ -4,6 +4,7 @@ import os
|
|||
import time
|
||||
import requests
|
||||
import core
|
||||
import json
|
||||
|
||||
from core.nzbToMediaUtil import convert_to_ascii, remoteDir, listMediaFiles, server_responding
|
||||
from core.nzbToMediaSceneExceptions import process_all_exceptions
|
||||
|
@ -13,6 +14,39 @@ requests.packages.urllib3.disable_warnings()
|
|||
|
||||
|
||||
class autoProcessMusic(object):
|
||||
def command_complete(self, url, params, headers, section):
|
||||
try:
|
||||
r = requests.get(url, params=params, headers=headers, stream=True, verify=False, timeout=(30, 60))
|
||||
except requests.ConnectionError:
|
||||
logger.error("Unable to open URL: {0}".format(url), section)
|
||||
return None
|
||||
if r.status_code not in [requests.codes.ok, requests.codes.created, requests.codes.accepted]:
|
||||
logger.error("Server returned status {0}".format(r.status_code), section)
|
||||
return None
|
||||
else:
|
||||
try:
|
||||
return r.json()['state']
|
||||
except (ValueError, KeyError):
|
||||
# ValueError catches simplejson's JSONDecodeError and json's ValueError
|
||||
logger.error("{0} did not return expected json data.".format(section), section)
|
||||
return None
|
||||
|
||||
def CDH(self, url2, headers, section="MAIN"):
|
||||
try:
|
||||
r = requests.get(url2, params={}, headers=headers, stream=True, verify=False, timeout=(30, 60))
|
||||
except requests.ConnectionError:
|
||||
logger.error("Unable to open URL: {0}".format(url2), section)
|
||||
return False
|
||||
if r.status_code not in [requests.codes.ok, requests.codes.created, requests.codes.accepted]:
|
||||
logger.error("Server returned status {0}".format(r.status_code), section)
|
||||
return False
|
||||
else:
|
||||
try:
|
||||
return r.json().get("enableCompletedDownloadHandling", False)
|
||||
except ValueError:
|
||||
# ValueError catches simplejson's JSONDecodeError and json's ValueError
|
||||
return False
|
||||
|
||||
def get_status(self, url, apikey, dirName):
|
||||
logger.debug("Attempting to get current status for release:{0}".format(os.path.basename(dirName)))
|
||||
|
||||
|
@ -96,6 +130,9 @@ class autoProcessMusic(object):
|
|||
else:
|
||||
extract = int(cfg.get("extract", 0))
|
||||
|
||||
if section == "Lidarr":
|
||||
url = "{0}{1}:{2}{3}/api/v1".format(protocol, host, port, web_root)
|
||||
else:
|
||||
url = "{0}{1}:{2}{3}/api".format(protocol, host, port, web_root)
|
||||
if not server_responding(url):
|
||||
logger.error("Server did not respond. Exiting", section)
|
||||
|
@ -119,11 +156,11 @@ class autoProcessMusic(object):
|
|||
core.extractFiles(dirName)
|
||||
inputName, dirName = convert_to_ascii(inputName, dirName)
|
||||
|
||||
if listMediaFiles(dirName, media=False, audio=True, meta=False, archives=False) and status:
|
||||
logger.info("Status shown as failed from Downloader, but valid video files found. Setting as successful.", section)
|
||||
status = 0
|
||||
#if listMediaFiles(dirName, media=False, audio=True, meta=False, archives=False) and status:
|
||||
# logger.info("Status shown as failed from Downloader, but valid video files found. Setting as successful.", section)
|
||||
# status = 0
|
||||
|
||||
if status == 0:
|
||||
if status == 0 and section == "HeadPhones":
|
||||
|
||||
params = {
|
||||
'apikey': apikey,
|
||||
|
@ -149,6 +186,74 @@ class autoProcessMusic(object):
|
|||
logger.warning("The music album does not appear to have changed status after {0} minutes. Please check your Logs".format(wait_for), section)
|
||||
return [1, "{0}: Failed to post-process - No change in wanted status".format(section)]
|
||||
|
||||
elif status == 0 and section == "Lidarr":
|
||||
url = "{0}{1}:{2}{3}/api/v1/command".format(protocol, host, port, web_root)
|
||||
url2 = "{0}{1}:{2}{3}/api/v1/config/downloadClient".format(protocol, host, port, web_root)
|
||||
headers = {"X-Api-Key": apikey}
|
||||
if remote_path:
|
||||
logger.debug("remote_path: {0}".format(remoteDir(dirName)), section)
|
||||
data = {"name": "DownloadedAlbumScan", "path": remoteDir(dirName), "downloadClientId": download_id, "importMode": "Move"}
|
||||
else:
|
||||
logger.debug("path: {0}".format(dirName), section)
|
||||
data = {"name": "DownloadedAlbumScan", "path": dirName, "downloadClientId": download_id, "importMode": "Move"}
|
||||
if not download_id:
|
||||
data.pop("downloadClientId")
|
||||
data = json.dumps(data)
|
||||
try:
|
||||
logger.debug("Opening URL: {0} with data: {1}".format(url, data), section)
|
||||
r = requests.post(url, data=data, headers=headers, stream=True, verify=False, timeout=(30, 1800))
|
||||
except requests.ConnectionError:
|
||||
logger.error("Unable to open URL: {0}".format(url), section)
|
||||
return [1, "{0}: Failed to post-process - Unable to connect to {1}".format(section, section)]
|
||||
|
||||
Success = False
|
||||
Queued = False
|
||||
Started = False
|
||||
try:
|
||||
res = json.loads(r.content)
|
||||
scan_id = int(res['id'])
|
||||
logger.debug("Scan started with id: {0}".format(scan_id), section)
|
||||
Started = True
|
||||
except Exception as e:
|
||||
logger.warning("No scan id was returned due to: {0}".format(e), section)
|
||||
scan_id = None
|
||||
Started = False
|
||||
return [1, "{0}: Failed to post-process - Unable to start scan".format(section)]
|
||||
|
||||
n = 0
|
||||
params = {}
|
||||
url = "{0}/{1}".format(url, scan_id)
|
||||
while n < 6: # set up wait_for minutes to see if command completes..
|
||||
time.sleep(10 * wait_for)
|
||||
command_status = self.command_complete(url, params, headers, section)
|
||||
if command_status and command_status in ['completed', 'failed']:
|
||||
break
|
||||
n += 1
|
||||
if command_status:
|
||||
logger.debug("The Scan command return status: {0}".format(command_status), section)
|
||||
if not os.path.exists(dirName):
|
||||
logger.debug("The directory {0} has been removed. Renaming was successful.".format(dirName), section)
|
||||
return [0, "{0}: Successfully post-processed {1}".format(section, inputName)]
|
||||
elif command_status and command_status in ['completed']:
|
||||
logger.debug("The Scan command has completed successfully. Renaming was successful.", section)
|
||||
return [0, "{0}: Successfully post-processed {1}".format(section, inputName)]
|
||||
elif command_status and command_status in ['failed']:
|
||||
logger.debug("The Scan command has failed. Renaming was not successful.", section)
|
||||
# return [1, "%s: Failed to post-process %s" % (section, inputName) ]
|
||||
if self.CDH(url2, headers, section=section):
|
||||
logger.debug("The Scan command did not return status completed, but complete Download Handling is enabled. Passing back to {0}.".format(section), section)
|
||||
return [status, "{0}: Complete DownLoad Handling is enabled. Passing back to {1}".format(section, section)]
|
||||
else:
|
||||
logger.warning("The Scan command did not return a valid status. Renaming was not successful.", section)
|
||||
return [1, "{0}: Failed to post-process {1}".format(section, inputName)]
|
||||
|
||||
else:
|
||||
if section == "Lidarr":
|
||||
logger.postprocess("FAILED: The download failed. Sending failed download to {0} for CDH processing".format(section), section)
|
||||
return [1, "{0}: Download Failed. Sending back to {1}".format(section, section)] # Return as failed to flag this in the downloader.
|
||||
else:
|
||||
logger.warning("FAILED DOWNLOAD DETECTED", section)
|
||||
return [1, "{0}: Failed to post-process. {1} does not support failed downloads".format(section, section)]
|
||||
if delete_failed and os.path.isdir(dirName) and not os.path.dirname(dirName) == dirName:
|
||||
logger.postprocess("Deleting failed files and folder {0}".format(dirName), section)
|
||||
rmDir(dirName)
|
||||
return [1, "{0}: Failed to post-process. {1} does not support failed downloads".format(section, section)] # Return as failed to flag this in the downloader.
|
|
@ -285,6 +285,9 @@ class autoProcessTV(object):
|
|||
if not apikey and username and password:
|
||||
login = "{0}{1}:{2}{3}/login".format(protocol, host, port, web_root)
|
||||
login_params = {'username': username, 'password': password}
|
||||
r = s.get(login, verify=False, timeout=(30,60))
|
||||
if r.status_code == 401 and r.cookies.get('_xsrf'):
|
||||
login_params['_xsrf'] = r.cookies.get('_xsrf')
|
||||
s.post(login, data=login_params, stream=True, verify=False, timeout=(30, 60))
|
||||
r = s.get(url, auth=(username, password), params=fork_params, stream=True, verify=False, timeout=(30, 1800))
|
||||
elif section == "NzbDrone":
|
||||
|
|
|
@ -21,8 +21,10 @@ def autoFork(section, inputCategory):
|
|||
apikey = cfg.get("apikey")
|
||||
ssl = int(cfg.get("ssl", 0))
|
||||
web_root = cfg.get("web_root", "")
|
||||
replace = {'sickrage':'SickRage', 'sickragetv':'SickRageTV', 'sickgear':'SickGear', 'medusa':'Medusa'}
|
||||
f1 = replace[cfg.get("fork", "auto")] if cfg.get("fork", "auto") in replace else cfg.get("fork", "auto")
|
||||
try:
|
||||
fork = core.FORKS.items()[core.FORKS.keys().index(cfg.get("fork", "auto"))]
|
||||
fork = core.FORKS.items()[core.FORKS.keys().index(f1)]
|
||||
except:
|
||||
fork = "auto"
|
||||
protocol = "https://" if ssl else "http://"
|
||||
|
@ -67,6 +69,9 @@ def autoFork(section, inputCategory):
|
|||
login = "{protocol}{host}:{port}{root}/login".format(
|
||||
protocol=protocol, host=host, port=port, root=web_root)
|
||||
login_params = {'username': username, 'password': password}
|
||||
r = s.get(login, verify=False, timeout=(30,60))
|
||||
if r.status_code == 401 and r.cookies.get('_xsrf'):
|
||||
login_params['_xsrf'] = r.cookies.get('_xsrf')
|
||||
s.post(login, data=login_params, stream=True, verify=False)
|
||||
r = s.get(url, auth=(username, password), verify=False)
|
||||
except requests.ConnectionError:
|
||||
|
|
|
@ -263,6 +263,11 @@ class ConfigObj(configobj.ConfigObj, Section):
|
|||
logger.warning("{x} category is set for CouchPotato and Radarr. "
|
||||
"Please check your config in NZBGet".format
|
||||
(x=os.environ['NZBPO_RACATEGORY']))
|
||||
if 'NZBPO_LICATEGORY' in os.environ and 'NZBPO_HPCATEGORY' in os.environ:
|
||||
if os.environ['NZBPO_LICATEGORY'] == os.environ['NZBPO_HPCATEGORY']:
|
||||
logger.warning("{x} category is set for HeadPhones and Lidarr. "
|
||||
"Please check your config in NZBGet".format
|
||||
(x=os.environ['NZBPO_LICATEGORY']))
|
||||
section = "Nzb"
|
||||
key = 'NZBOP_DESTDIR'
|
||||
if key in os.environ:
|
||||
|
@ -413,6 +418,25 @@ class ConfigObj(configobj.ConfigObj, Section):
|
|||
if os.environ[envCatKey] in CFG_NEW['CouchPotato'].sections:
|
||||
CFG_NEW['CouchPotato'][envCatKey]['enabled'] = 0
|
||||
|
||||
section = "Lidarr"
|
||||
envCatKey = 'NZBPO_LICATEGORY'
|
||||
envKeys = ['ENABLED', 'HOST', 'APIKEY', 'PORT', 'SSL', 'WEB_ROOT', 'WATCH_DIR', 'FORK', 'DELETE_FAILED',
|
||||
'TORRENT_NOLINK', 'NZBEXTRACTIONBY', 'WAIT_FOR', 'DELETE_FAILED', 'REMOTE_PATH']
|
||||
cfgKeys = ['enabled', 'host', 'apikey', 'port', 'ssl', 'web_root', 'watch_dir', 'fork', 'delete_failed',
|
||||
'Torrent_NoLink', 'nzbExtractionBy', 'wait_for', 'delete_failed', 'remote_path']
|
||||
if envCatKey in os.environ:
|
||||
for index in range(len(envKeys)):
|
||||
key = 'NZBPO_RA{index}'.format(index=envKeys[index])
|
||||
if key in os.environ:
|
||||
option = cfgKeys[index]
|
||||
value = os.environ[key]
|
||||
if os.environ[envCatKey] not in CFG_NEW[section].sections:
|
||||
CFG_NEW[section][os.environ[envCatKey]] = {}
|
||||
CFG_NEW[section][os.environ[envCatKey]][option] = value
|
||||
CFG_NEW[section][os.environ[envCatKey]]['enabled'] = 1
|
||||
if os.environ[envCatKey] in CFG_NEW['CouchPotato'].sections:
|
||||
CFG_NEW['CouchPotato'][envCatKey]['enabled'] = 0
|
||||
|
||||
section = "Extensions"
|
||||
envKeys = ['COMPRESSEDEXTENSIONS', 'MEDIAEXTENSIONS', 'METAEXTENSIONS']
|
||||
cfgKeys = ['compressedExtensions', 'mediaExtensions', 'metaExtensions']
|
||||
|
|
|
@ -23,6 +23,7 @@ from core.linktastic import linktastic
|
|||
from core.synchronousdeluge.client import DelugeClient
|
||||
from core.utorrent.client import UTorrentClient
|
||||
from core.transmissionrpc.client import Client as TransmissionClient
|
||||
from core.qbittorrent.client import Client as qBittorrentClient
|
||||
from core import logger, nzbToMediaDB
|
||||
|
||||
requests.packages.urllib3.disable_warnings()
|
||||
|
@ -588,6 +589,34 @@ def parse_vuze(args):
|
|||
|
||||
return inputDirectory, inputName, inputCategory, inputHash, inputID
|
||||
|
||||
def parse_qbittorrent(args):
|
||||
# qbittorrent usage: C:\full\path\to\nzbToMedia\TorrentToMedia.py "%D|%N|%L|%I"
|
||||
try:
|
||||
input = args[1].split('|')
|
||||
except:
|
||||
input = []
|
||||
try:
|
||||
inputDirectory = os.path.normpath(input[0].replace('"',''))
|
||||
except:
|
||||
inputDirectory = ''
|
||||
try:
|
||||
inputName = input[1].replace('"','')
|
||||
except:
|
||||
inputName = ''
|
||||
try:
|
||||
inputCategory = input[2].replace('"','')
|
||||
except:
|
||||
inputCategory = ''
|
||||
try:
|
||||
inputHash = input[3].replace('"','')
|
||||
except:
|
||||
inputHash = ''
|
||||
try:
|
||||
inputID = input[3].replace('"','')
|
||||
except:
|
||||
inputID = ''
|
||||
|
||||
return inputDirectory, inputName, inputCategory, inputHash, inputID
|
||||
|
||||
def parse_args(clientAgent, args):
|
||||
clients = {
|
||||
|
@ -596,6 +625,7 @@ def parse_args(clientAgent, args):
|
|||
'utorrent': parse_utorrent,
|
||||
'deluge': parse_deluge,
|
||||
'transmission': parse_transmission,
|
||||
'qbittorrent': parse_qbittorrent,
|
||||
'vuze': parse_vuze,
|
||||
}
|
||||
|
||||
|
@ -796,6 +826,14 @@ def create_torrent_class(clientAgent):
|
|||
except:
|
||||
logger.error("Failed to connect to Deluge")
|
||||
|
||||
if clientAgent == 'qbittorrent':
|
||||
try:
|
||||
logger.debug("Connecting to {0}: http://{1}:{2}".format(clientAgent, core.QBITTORRENTHOST, core.QBITTORRENTPORT))
|
||||
tc = qBittorrentClient("http://{0}:{1}/".format(core.QBITTORRENTHOST, core.QBITTORRENTPORT))
|
||||
tc.login(core.QBITTORRENTUSR, core.QBITTORRENTPWD)
|
||||
except:
|
||||
logger.error("Failed to connect to qBittorrent")
|
||||
|
||||
return tc
|
||||
|
||||
|
||||
|
@ -808,6 +846,8 @@ def pause_torrent(clientAgent, inputHash, inputID, inputName):
|
|||
core.TORRENT_CLASS.stop_torrent(inputID)
|
||||
if clientAgent == 'deluge' and core.TORRENT_CLASS != "":
|
||||
core.TORRENT_CLASS.core.pause_torrent([inputID])
|
||||
if clientAgent == 'qbittorrent' and core.TORRENT_CLASS != "":
|
||||
core.TORRENT_CLASS.pause(inputHash)
|
||||
time.sleep(5)
|
||||
except:
|
||||
logger.warning("Failed to stop torrent {0} in {1}".format(inputName, clientAgent))
|
||||
|
@ -824,6 +864,8 @@ def resume_torrent(clientAgent, inputHash, inputID, inputName):
|
|||
core.TORRENT_CLASS.start_torrent(inputID)
|
||||
if clientAgent == 'deluge' and core.TORRENT_CLASS != "":
|
||||
core.TORRENT_CLASS.core.resume_torrent([inputID])
|
||||
if clientAgent == 'qbittorrent' and core.TORRENT_CLASS != "":
|
||||
core.TORRENT_CLASS.resume(inputHash)
|
||||
time.sleep(5)
|
||||
except:
|
||||
logger.warning("Failed to start torrent {0} in {1}".format(inputName, clientAgent))
|
||||
|
@ -840,6 +882,8 @@ def remove_torrent(clientAgent, inputHash, inputID, inputName):
|
|||
core.TORRENT_CLASS.remove_torrent(inputID, True)
|
||||
if clientAgent == 'deluge' and core.TORRENT_CLASS != "":
|
||||
core.TORRENT_CLASS.core.remove_torrent(inputID, True)
|
||||
if clientAgent == 'qbittorrent' and core.TORRENT_CLASS != "":
|
||||
core.TORRENT_CLASS.delete(inputHash)
|
||||
time.sleep(5)
|
||||
except:
|
||||
logger.warning("Failed to delete torrent {0} in {1}".format(inputName, clientAgent))
|
||||
|
@ -862,6 +906,11 @@ def find_download(clientAgent, download_id):
|
|||
return True
|
||||
if clientAgent == 'deluge':
|
||||
return False
|
||||
if clientAgent == 'qbittorrent':
|
||||
torrents = core.TORRENT_CLASS.torrents()
|
||||
for torrent in torrents:
|
||||
if torrent['hash'] == download_id:
|
||||
return True
|
||||
if clientAgent == 'sabnzbd':
|
||||
if "http" in core.SABNZBDHOST:
|
||||
baseURL = "{0}:{1}/api".format(core.SABNZBDHOST, core.SABNZBDPORT)
|
||||
|
|
1
core/qbittorrent/__init__.py
Normal file
1
core/qbittorrent/__init__.py
Normal file
|
@ -0,0 +1 @@
|
|||
# coding=utf-8
|
633
core/qbittorrent/client.py
Normal file
633
core/qbittorrent/client.py
Normal file
|
@ -0,0 +1,633 @@
|
|||
import requests
|
||||
import json
|
||||
|
||||
|
||||
class LoginRequired(Exception):
|
||||
def __str__(self):
|
||||
return 'Please login first.'
|
||||
|
||||
|
||||
class Client(object):
|
||||
"""class to interact with qBittorrent WEB API"""
|
||||
def __init__(self, url):
|
||||
if not url.endswith('/'):
|
||||
url += '/'
|
||||
self.url = url
|
||||
|
||||
session = requests.Session()
|
||||
check_prefs = session.get(url+'query/preferences')
|
||||
|
||||
if check_prefs.status_code == 200:
|
||||
self._is_authenticated = True
|
||||
self.session = session
|
||||
|
||||
elif check_prefs.status_code == 404:
|
||||
self._is_authenticated = False
|
||||
raise RuntimeError("""
|
||||
This wrapper only supports qBittorrent applications
|
||||
with version higher than 3.1.x.
|
||||
Please use the latest qBittorrent release.
|
||||
""")
|
||||
|
||||
else:
|
||||
self._is_authenticated = False
|
||||
|
||||
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()
|
||||
request.encoding = 'utf_8'
|
||||
|
||||
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='admin', password='admin'):
|
||||
"""
|
||||
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, **filters):
|
||||
"""
|
||||
Returns a list of torrents matching the supplied filters.
|
||||
|
||||
:param filter: Current status of the torrents.
|
||||
:param category: 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.
|
||||
"""
|
||||
params = {}
|
||||
for name, value in filters.items():
|
||||
# make sure that old 'status' argument still works
|
||||
name = 'filter' if name == 'status' else name
|
||||
params[name] = value
|
||||
|
||||
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, **kwargs):
|
||||
"""
|
||||
Download torrent using a link.
|
||||
|
||||
:param link: URL Link or list of.
|
||||
:param savepath: Path to download the torrent.
|
||||
:param category: Label or Category of the torrent(s).
|
||||
|
||||
:return: Empty JSON data.
|
||||
"""
|
||||
# old:new format
|
||||
old_arg_map = {'save_path': 'savepath'} # , 'label': 'category'}
|
||||
|
||||
# convert old option names to new option names
|
||||
options = kwargs.copy()
|
||||
for old_arg, new_arg in old_arg_map.items():
|
||||
if options.get(old_arg) and not options.get(new_arg):
|
||||
options[new_arg] = options[old_arg]
|
||||
|
||||
options['urls'] = link
|
||||
|
||||
# workaround to send multipart/formdata request
|
||||
# http://stackoverflow.com/a/23131823/4726598
|
||||
dummy_file = {'_dummy': (None, '_dummy')}
|
||||
|
||||
return self._post('command/download', data=options, files=dummy_file)
|
||||
|
||||
def download_from_file(self, file_buffer, **kwargs):
|
||||
"""
|
||||
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})
|
||||
else:
|
||||
torrent_files = {'torrents': file_buffer}
|
||||
|
||||
data = kwargs.copy()
|
||||
|
||||
if data.get('save_path'):
|
||||
data.update({'savepath': data['save_path']})
|
||||
return self._post('command/upload', data=data, files=torrent_files)
|
||||
|
||||
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 set_label(self, infohash_list, label):
|
||||
"""
|
||||
Set the label on multiple torrents.
|
||||
IMPORTANT: OLD API method, kept as it is to avoid breaking stuffs.
|
||||
|
||||
:param infohash_list: Single or list() of infohashes.
|
||||
"""
|
||||
data = self._process_infohash_list(infohash_list)
|
||||
data['label'] = label
|
||||
return self._post('command/setLabel', data=data)
|
||||
|
||||
def set_category(self, infohash_list, category):
|
||||
"""
|
||||
Set the category on multiple torrents.
|
||||
|
||||
:param infohash_list: Single or list() of infohashes.
|
||||
"""
|
||||
data = self._process_infohash_list(infohash_list)
|
||||
data['category'] = category
|
||||
return self._post('command/setCategory', 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.
|
||||
|
||||
: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)
|
||||
|
||||
def set_torrent_upload_limit(self, infohash_list, limit):
|
||||
"""
|
||||
Set upload 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/setTorrentsUpLimit', data=data)
|
||||
|
||||
# setting preferences
|
||||
def set_preferences(self, **kwargs):
|
||||
"""
|
||||
Set preferences of qBittorrent.
|
||||
Read all possible preferences @ http://git.io/vEgDQ
|
||||
|
||||
: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)
|
||||
|
||||
def get_alternative_speed_status(self):
|
||||
"""
|
||||
Get Alternative speed limits. (1/0)
|
||||
"""
|
||||
return self._get('command/alternativeSpeedLimitsEnabled')
|
||||
|
||||
alternative_speed_status = property(get_alternative_speed_status)
|
||||
|
||||
def toggle_alternative_speed(self):
|
||||
"""
|
||||
Toggle alternative speed limits.
|
||||
"""
|
||||
return self._get('command/toggleAlternativeSpeedLimits')
|
||||
|
||||
def toggle_sequential_download(self, infohash_list):
|
||||
"""
|
||||
Toggle sequential download in supplied torrents.
|
||||
|
||||
: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.
|
||||
|
||||
:param infohash_list: Single or list() of infohashes.
|
||||
"""
|
||||
data = self._process_infohash_list(infohash_list)
|
||||
return self._post('command/toggleFirstLastPiecePrio', data=data)
|
||||
|
||||
def force_start(self, infohash_list, value=True):
|
||||
"""
|
||||
Force start selected torrents.
|
||||
|
||||
: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)
|
245
nzbToLidarr.py
Executable file
245
nzbToLidarr.py
Executable file
|
@ -0,0 +1,245 @@
|
|||
#!/usr/bin/env python2
|
||||
# coding=utf-8
|
||||
#
|
||||
##############################################################################
|
||||
### NZBGET POST-PROCESSING SCRIPT ###
|
||||
|
||||
# Post-Process to Lidarr.
|
||||
#
|
||||
# This script sends the download to your automated media management servers.
|
||||
#
|
||||
# NOTE: This script requires Python to be installed on your system.
|
||||
|
||||
##############################################################################
|
||||
### OPTIONS ###
|
||||
|
||||
## General
|
||||
|
||||
# Auto Update nzbToMedia (0, 1).
|
||||
#
|
||||
# Set to 1 if you want nzbToMedia to automatically check for and update to the latest version
|
||||
#auto_update=0
|
||||
|
||||
# Check Media for corruption (0, 1).
|
||||
#
|
||||
# Enable/Disable media file checking using ffprobe.
|
||||
#check_media=1
|
||||
|
||||
# Safe Mode protection of DestDir (0, 1).
|
||||
#
|
||||
# Enable/Disable a safety check to ensure we don't process all downloads in the default_downloadDirectory by mistake.
|
||||
#safe_mode=1
|
||||
|
||||
# Disable additional extraction checks for failed (0, 1).
|
||||
#
|
||||
# Turn this on to disable additional extraction attempts for failed downloads. Default = 0 this will attempt to extract and verify if media is present.
|
||||
#no_extract_failed = 0
|
||||
|
||||
## Lidarr
|
||||
|
||||
# Lidarr script category.
|
||||
#
|
||||
# category that gets called for post-processing with NzbDrone.
|
||||
#liCategory=music2
|
||||
|
||||
# Lidarr host.
|
||||
#
|
||||
# The ipaddress for your Lidarr server. e.g For the Same system use localhost or 127.0.0.1
|
||||
#lihost=localhost
|
||||
|
||||
# Lidarr port.
|
||||
#liport=8686
|
||||
|
||||
# Lidarr API key.
|
||||
#liapikey=
|
||||
|
||||
# Lidarr uses ssl (0, 1).
|
||||
#
|
||||
# Set to 1 if using ssl, else set to 0.
|
||||
#lissl=0
|
||||
|
||||
# Lidarr web_root
|
||||
#
|
||||
# set this if using a reverse proxy.
|
||||
#liweb_root=
|
||||
|
||||
# Lidarr wait_for
|
||||
#
|
||||
# Set the number of minutes to wait after calling the renamer, to check the episode has changed status.
|
||||
#liwait_for=6
|
||||
|
||||
# Lidarr Delete Failed Downloads (0, 1).
|
||||
#
|
||||
# set to 1 to delete failed, or 0 to leave files in place.
|
||||
#lidelete_failed=0
|
||||
|
||||
# Lidarr and NZBGet are a different system (0, 1).
|
||||
#
|
||||
# Enable to replace local path with the path as per the mountPoints below.
|
||||
#liremote_path=0
|
||||
|
||||
## Network
|
||||
|
||||
# Network Mount Points (Needed for remote path above)
|
||||
#
|
||||
# Enter Mount points as LocalPath,RemotePath and separate each pair with '|'
|
||||
# e.g. mountPoints=/volume1/Public/,E:\|/volume2/share/,\\NAS\
|
||||
#mountPoints=
|
||||
|
||||
## Extensions
|
||||
|
||||
# Media Extensions
|
||||
#
|
||||
# This is a list of media extensions that are used to verify that the download does contain valid media.
|
||||
#mediaExtensions=.mkv,.avi,.divx,.xvid,.mov,.wmv,.mp4,.mpg,.mpeg,.vob,.iso,.ts
|
||||
|
||||
## Posix
|
||||
|
||||
# Niceness for external tasks Extractor and Transcoder.
|
||||
#
|
||||
# Set the Niceness value for the nice command. These range from -20 (most favorable to the process) to 19 (least favorable to the process).
|
||||
#niceness=10
|
||||
|
||||
# ionice scheduling class (0, 1, 2, 3).
|
||||
#
|
||||
# Set the ionice scheduling class. 0 for none, 1 for real time, 2 for best-effort, 3 for idle.
|
||||
#ionice_class=2
|
||||
|
||||
# ionice scheduling class data.
|
||||
#
|
||||
# Set the ionice scheduling class data. This defines the class data, if the class accepts an argument. For real time and best-effort, 0-7 is valid data.
|
||||
#ionice_classdata=4
|
||||
|
||||
## Transcoder
|
||||
|
||||
# getSubs (0, 1).
|
||||
#
|
||||
# set to 1 to download subtitles.
|
||||
#getSubs = 0
|
||||
|
||||
# subLanguages.
|
||||
#
|
||||
# subLanguages. create a list of languages in the order you want them in your subtitles.
|
||||
#subLanguages = eng,spa,fra
|
||||
|
||||
# Transcode (0, 1).
|
||||
#
|
||||
# set to 1 to transcode, otherwise set to 0.
|
||||
#transcode=0
|
||||
|
||||
# create a duplicate, or replace the original (0, 1).
|
||||
#
|
||||
# set to 1 to cretae a new file or 0 to replace the original
|
||||
#duplicate=1
|
||||
|
||||
# ignore extensions.
|
||||
#
|
||||
# list of extensions that won't be transcoded.
|
||||
#ignoreExtensions=.avi,.mkv
|
||||
|
||||
# outputFastStart (0,1).
|
||||
#
|
||||
# outputFastStart. 1 will use -movflags + faststart. 0 will disable this from being used.
|
||||
#outputFastStart = 0
|
||||
|
||||
# outputVideoPath.
|
||||
#
|
||||
# outputVideoPath. Set path you want transcoded videos moved to. Leave blank to disable.
|
||||
#outputVideoPath =
|
||||
|
||||
# processOutput (0,1).
|
||||
#
|
||||
# processOutput. 1 will send the outputVideoPath to SickBeard/CouchPotato. 0 will send original files.
|
||||
#processOutput = 0
|
||||
|
||||
# audioLanguage.
|
||||
#
|
||||
# audioLanguage. set the 3 letter language code you want as your primary audio track.
|
||||
#audioLanguage = eng
|
||||
|
||||
# allAudioLanguages (0,1).
|
||||
#
|
||||
# allAudioLanguages. 1 will keep all audio tracks (uses AudioCodec3) where available.
|
||||
#allAudioLanguages = 0
|
||||
|
||||
# allSubLanguages (0,1).
|
||||
#
|
||||
# allSubLanguages. 1 will keep all exisiting sub languages. 0 will discare those not in your list above.
|
||||
#allSubLanguages = 0
|
||||
|
||||
# embedSubs (0,1).
|
||||
#
|
||||
# embedSubs. 1 will embded external sub/srt subs into your video if this is supported.
|
||||
#embedSubs = 1
|
||||
|
||||
# burnInSubtitle (0,1).
|
||||
#
|
||||
# burnInSubtitle. burns the default sub language into your video (needed for players that don't support subs)
|
||||
#burnInSubtitle = 0
|
||||
|
||||
# extractSubs (0,1).
|
||||
#
|
||||
# extractSubs. 1 will extract subs from the video file and save these as external srt files.
|
||||
#extractSubs = 0
|
||||
|
||||
# externalSubDir.
|
||||
#
|
||||
# externalSubDir. set the directory where subs should be saved (if not the same directory as the video)
|
||||
#externalSubDir =
|
||||
|
||||
# outputDefault (None, iPad, iPad-1080p, iPad-720p, Apple-TV2, iPod, iPhone, PS3, xbox, Roku-1080p, Roku-720p, Roku-480p, mkv, mp4-scene-release, MKV-SD).
|
||||
#
|
||||
# outputDefault. Loads default configs for the selected device. The remaining options below are ignored.
|
||||
# If you want to use your own profile, set None and set the remaining options below.
|
||||
#outputDefault = None
|
||||
|
||||
# hwAccel (0,1).
|
||||
#
|
||||
# hwAccel. 1 will set ffmpeg to enable hardware acceleration (this requires a recent ffmpeg).
|
||||
#hwAccel=0
|
||||
|
||||
# ffmpeg output settings.
|
||||
#outputVideoExtension=.mp4
|
||||
#outputVideoCodec=libx264
|
||||
#VideoCodecAllow =
|
||||
#outputVideoResolution=720:-1
|
||||
#outputVideoPreset=medium
|
||||
#outputVideoFramerate=24
|
||||
#outputVideoBitrate=800k
|
||||
#outputAudioCodec=libmp3lame
|
||||
#AudioCodecAllow =
|
||||
#outputAudioBitrate=128k
|
||||
#outputQualityPercent = 0
|
||||
#outputAudioTrack2Codec = libfaac
|
||||
#AudioCodec2Allow =
|
||||
#outputAudioTrack2Bitrate = 128k
|
||||
#outputAudioOtherCodec = libmp3lame
|
||||
#AudioOtherCodecAllow =
|
||||
#outputAudioOtherBitrate = 128k
|
||||
#outputSubtitleCodec =
|
||||
|
||||
## WakeOnLan
|
||||
|
||||
# use WOL (0, 1).
|
||||
#
|
||||
# set to 1 to send WOL broadcast to the mac and test the server (e.g. xbmc) on the host and port specified.
|
||||
#wolwake=0
|
||||
|
||||
# WOL MAC
|
||||
#
|
||||
# enter the mac address of the system to be woken.
|
||||
#wolmac=00:01:2e:2D:64:e1
|
||||
|
||||
# Set the Host and Port of a server to verify system has woken.
|
||||
#wolhost=192.168.1.37
|
||||
#wolport=80
|
||||
|
||||
### NZBGET POST-PROCESSING SCRIPT ###
|
||||
##############################################################################
|
||||
|
||||
import sys
|
||||
import nzbToMedia
|
||||
|
||||
section = "Lidarr"
|
||||
result = nzbToMedia.main(sys.argv, section)
|
||||
sys.exit(result)
|
|
@ -273,6 +273,49 @@
|
|||
# Enable to replace local path with the path as per the mountPoints below.
|
||||
#hpremote_path=0
|
||||
|
||||
## Lidarr
|
||||
|
||||
# Lidarr script category.
|
||||
#
|
||||
# category that gets called for post-processing with NzbDrone.
|
||||
#liCategory=music2
|
||||
|
||||
# Lidarr host.
|
||||
#
|
||||
# The ipaddress for your Lidarr server. e.g For the Same system use localhost or 127.0.0.1
|
||||
#lihost=localhost
|
||||
|
||||
# Lidarr port.
|
||||
#liport=8686
|
||||
|
||||
# Lidarr API key.
|
||||
#liapikey=
|
||||
|
||||
# Lidarr uses ssl (0, 1).
|
||||
#
|
||||
# Set to 1 if using ssl, else set to 0.
|
||||
#lissl=0
|
||||
|
||||
# Lidarr web_root
|
||||
#
|
||||
# set this if using a reverse proxy.
|
||||
#liweb_root=
|
||||
|
||||
# Lidarr wait_for
|
||||
#
|
||||
# Set the number of minutes to wait after calling the renamer, to check the episode has changed status.
|
||||
#liwait_for=6
|
||||
|
||||
# Lidarr Delete Failed Downloads (0, 1).
|
||||
#
|
||||
# set to 1 to delete failed, or 0 to leave files in place.
|
||||
#lidelete_failed=0
|
||||
|
||||
# Lidarr and NZBGet are a different system (0, 1).
|
||||
#
|
||||
# Enable to replace local path with the path as per the mountPoints below.
|
||||
#liremote_path=0
|
||||
|
||||
## Mylar
|
||||
|
||||
# Mylar script category.
|
||||
|
@ -672,7 +715,7 @@ def process(inputDirectory, inputName=None, status=0, clientAgent='manual', down
|
|||
elif sectionName in ["SickBeard", "NzbDrone", "Sonarr"]:
|
||||
result = autoProcessTV().processEpisode(sectionName, inputDirectory, inputName, status, clientAgent,
|
||||
download_id, inputCategory, failureLink)
|
||||
elif sectionName == "HeadPhones":
|
||||
elif sectionName in ["HeadPhones", "Lidarr"]:
|
||||
result = autoProcessMusic().process(sectionName, inputDirectory, inputName, status, clientAgent, inputCategory)
|
||||
elif sectionName == "Mylar":
|
||||
result = autoProcessComics().processEpisode(sectionName, inputDirectory, inputName, status, clientAgent,
|
||||
|
@ -690,7 +733,7 @@ def process(inputDirectory, inputName=None, status=0, clientAgent='manual', down
|
|||
if clientAgent != 'manual':
|
||||
# update download status in our DB
|
||||
update_downloadInfoStatus(inputName, 1)
|
||||
if sectionName not in ['UserScript', 'NzbDrone', 'Sonarr', 'Radarr']:
|
||||
if sectionName not in ['UserScript', 'NzbDrone', 'Sonarr', 'Radarr', 'Lidarr']:
|
||||
# cleanup our processing folders of any misc unwanted files and empty directories
|
||||
cleanDir(inputDirectory, sectionName, inputCategory)
|
||||
|
||||
|
@ -763,6 +806,8 @@ def main(args, section=None):
|
|||
download_id = os.environ['NZBPR_SONARR']
|
||||
elif 'NZBPR_RADARR' in os.environ:
|
||||
download_id = os.environ['NZBPR_RADARR']
|
||||
elif 'NZBPR_LIDARR' in os.environ:
|
||||
download_id = os.environ['NZBPR_LIDARR']
|
||||
if 'NZBPR__DNZB_FAILURE' in os.environ:
|
||||
failureLink = os.environ['NZBPR__DNZB_FAILURE']
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue