From e34ba9af89b07e668824b6a7593eaddf201ae73e Mon Sep 17 00:00:00 2001 From: clinton-hall Date: Sun, 21 Jan 2018 22:37:10 +1300 Subject: [PATCH 01/13] add qBittorrent parsing. Fixes #1300 --- autoProcessMedia.cfg.spec | 2 +- core/__init__.py | 4 ++-- core/nzbToMediaUtil.py | 29 +++++++++++++++++++++++++++++ 3 files changed, 32 insertions(+), 3 deletions(-) diff --git a/autoProcessMedia.cfg.spec b/autoProcessMedia.cfg.spec index f99be837..52484e00 100644 --- a/autoProcessMedia.cfg.spec +++ b/autoProcessMedia.cfg.spec @@ -259,7 +259,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 diff --git a/core/__init__.py b/core/__init__.py index eb23a558..0afb1644 100644 --- a/core/__init__.py +++ b/core/__init__.py @@ -50,7 +50,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 @@ -357,7 +357,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"] diff --git a/core/nzbToMediaUtil.py b/core/nzbToMediaUtil.py index a996525b..6d581c4b 100644 --- a/core/nzbToMediaUtil.py +++ b/core/nzbToMediaUtil.py @@ -588,6 +588,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 +624,7 @@ def parse_args(clientAgent, args): 'utorrent': parse_utorrent, 'deluge': parse_deluge, 'transmission': parse_transmission, + 'qbittorrent': parse_qbittorrent, 'vuze': parse_vuze, } From 12a7e3c5ceddbfb16d3461dd79aa5da4084894a0 Mon Sep 17 00:00:00 2001 From: clinton-hall Date: Mon, 22 Jan 2018 08:41:44 +1300 Subject: [PATCH 02/13] add qbittorrent client. Fixes #1300 --- autoProcessMedia.cfg.spec | 5 + core/__init__.py | 12 +- core/nzbToMediaUtil.py | 20 ++ core/qbittorrent/__init__.py | 1 + core/qbittorrent/client.py | 633 +++++++++++++++++++++++++++++++++++ 5 files changed, 670 insertions(+), 1 deletion(-) create mode 100644 core/qbittorrent/__init__.py create mode 100644 core/qbittorrent/client.py diff --git a/autoProcessMedia.cfg.spec b/autoProcessMedia.cfg.spec index 52484e00..bf7e3dbd 100644 --- a/autoProcessMedia.cfg.spec +++ b/autoProcessMedia.cfg.spec @@ -285,6 +285,11 @@ DelugePort = 58846 DelugeUSR = your username DelugePWD = your password + ###### qBittorrent (You must edit this if your 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 diff --git a/core/__init__.py b/core/__init__.py index 0afb1644..813bdd88 100644 --- a/core/__init__.py +++ b/core/__init__.py @@ -135,6 +135,11 @@ DELUGEPORT = None DELUGEUSR = None DELUGEPWD = None +QBITTORRENTHOST = None +QBITTORRENTPORT = None +QBITTORRENTUSR = None +QBITTORRENTPWD = None + PLEXSSL = None PLEXHOST = None PLEXPORT = None @@ -234,7 +239,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 @@ -385,6 +390,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): diff --git a/core/nzbToMediaUtil.py b/core/nzbToMediaUtil.py index 6d581c4b..77bf6f8e 100644 --- a/core/nzbToMediaUtil.py +++ b/core/nzbToMediaUtil.py @@ -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() @@ -825,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://{1}:{2}/".format(core.QBITTORRENTHOST, core.QBITTORRENTPORT)) + tc.login(core.QBITTORRENTUSR, core.QBITTORRENTPWD) + except: + logger.error("Failed to connect to qBittorrent") + return tc @@ -837,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)) @@ -853,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)) @@ -869,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)) @@ -891,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['infohash'] == download_id: + return True if clientAgent == 'sabnzbd': if "http" in core.SABNZBDHOST: baseURL = "{0}:{1}/api".format(core.SABNZBDHOST, core.SABNZBDPORT) diff --git a/core/qbittorrent/__init__.py b/core/qbittorrent/__init__.py new file mode 100644 index 00000000..bf893c06 --- /dev/null +++ b/core/qbittorrent/__init__.py @@ -0,0 +1 @@ +# coding=utf-8 \ No newline at end of file diff --git a/core/qbittorrent/client.py b/core/qbittorrent/client.py new file mode 100644 index 00000000..73d8d753 --- /dev/null +++ b/core/qbittorrent/client.py @@ -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) From d67ed51104a56c3c46d060842e7fa0201118b1fb Mon Sep 17 00:00:00 2001 From: clinton-hall Date: Mon, 22 Jan 2018 23:07:48 +1300 Subject: [PATCH 03/13] fix hash lookup in qbittorrent. Fixes #1300 --- core/nzbToMediaUtil.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/nzbToMediaUtil.py b/core/nzbToMediaUtil.py index 77bf6f8e..0f06dfbe 100644 --- a/core/nzbToMediaUtil.py +++ b/core/nzbToMediaUtil.py @@ -909,7 +909,7 @@ def find_download(clientAgent, download_id): if clientAgent == 'qbittorrent': torrents = core.TORRENT_CLASS.torrents() for torrent in torrents: - if torrent['infohash'] == download_id: + if torrent['hash'] == download_id: return True if clientAgent == 'sabnzbd': if "http" in core.SABNZBDHOST: From 4fa0dc4572576c351ff3faa46cc223a38e0579ba Mon Sep 17 00:00:00 2001 From: clinton-hall Date: Tue, 23 Jan 2018 06:59:45 +1300 Subject: [PATCH 04/13] fix tuple index error preventing qbittorrent login. Fixes #1300 --- core/nzbToMediaUtil.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/nzbToMediaUtil.py b/core/nzbToMediaUtil.py index 0f06dfbe..b007e651 100644 --- a/core/nzbToMediaUtil.py +++ b/core/nzbToMediaUtil.py @@ -829,7 +829,7 @@ def create_torrent_class(clientAgent): if clientAgent == 'qbittorrent': try: logger.debug("Connecting to {0}: http://{1}:{2}".format(clientAgent, core.QBITTORRENTHOST, core.QBITTORRENTPORT)) - tc = qBittorrentClient("http://{1}:{2}/".format(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") From 45b9115634daa8f3dc7f1a284ce5a765cbd4481d Mon Sep 17 00:00:00 2001 From: clinton-hall Date: Wed, 24 Jan 2018 08:49:35 +1300 Subject: [PATCH 05/13] always return release matching even if no imdb id. Fixes #1300 --- core/autoProcess/autoProcessMovie.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/autoProcess/autoProcessMovie.py b/core/autoProcess/autoProcessMovie.py index 6285162e..ebadc71b 100644 --- a/core/autoProcess/autoProcessMovie.py +++ b/core/autoProcess/autoProcessMovie.py @@ -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'] From 2063e302ca6721ac6f283010cccde50274abc16c Mon Sep 17 00:00:00 2001 From: clinton-hall Date: Wed, 24 Jan 2018 22:36:32 +1300 Subject: [PATCH 06/13] match release even if all UPPER in CP database. Fixes #1300 --- core/autoProcess/autoProcessMovie.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/autoProcess/autoProcessMovie.py b/core/autoProcess/autoProcessMovie.py index ebadc71b..12483269 100644 --- a/core/autoProcess/autoProcessMovie.py +++ b/core/autoProcess/autoProcessMovie.py @@ -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'] From 23fa4242385d66c5ff5139262320c09c7dc79f85 Mon Sep 17 00:00:00 2001 From: clinton-hall Date: Wed, 28 Feb 2018 21:02:13 +1300 Subject: [PATCH 07/13] disable valid media check for failed HP downloads. Fixes #1228 fix grammar! How did I let these go so long? --- autoProcessMedia.cfg.spec | 10 +++++----- core/autoProcess/autoProcessMusic.py | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/autoProcessMedia.cfg.spec b/autoProcessMedia.cfg.spec index bf7e3dbd..90a736a0 100644 --- a/autoProcessMedia.cfg.spec +++ b/autoProcessMedia.cfg.spec @@ -251,7 +251,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 = @@ -271,21 +271,21 @@ 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 your using TorrentToMedia.py with qBittorrent) + ###### qBittorrent (You must edit this if you're using TorrentToMedia.py with qBittorrent) qBittorrenHost = localhost qBittorrentPort = 8080 qBittorrentUSR = your username diff --git a/core/autoProcess/autoProcessMusic.py b/core/autoProcess/autoProcessMusic.py index 38985633..125b3569 100644 --- a/core/autoProcess/autoProcessMusic.py +++ b/core/autoProcess/autoProcessMusic.py @@ -119,9 +119,9 @@ 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: From b5ee0c241ca536d44d63f31869553da73e6b8beb Mon Sep 17 00:00:00 2001 From: clinton-hall Date: Thu, 12 Apr 2018 21:31:59 +1200 Subject: [PATCH 08/13] added check for single file Torrent. Fixes #924 --- TorrentToMedia.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/TorrentToMedia.py b/TorrentToMedia.py index 3dd7580a..9fdbd587 100755 --- a/TorrentToMedia.py +++ b/TorrentToMedia.py @@ -142,7 +142,11 @@ 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) - logger.debug("Found {0} files in {1}".format(len(inputFiles), inputDirectory)) + 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) fileName, fileExt = os.path.splitext(os.path.basename(inputFile)) From 9baef39f999c0dbe6cd69f5c62df74de4ff9e29f Mon Sep 17 00:00:00 2001 From: clinton-hall Date: Sun, 20 May 2018 09:33:56 +1200 Subject: [PATCH 09/13] dded flow and config for Lidarr handling. #1350 --- TorrentToMedia.py | 4 +- autoProcessMedia.cfg.spec | 27 +++ core/autoProcess/autoProcessMusic.py | 113 +++++++++++- core/nzbToMediaConfig.py | 24 +++ nzbToLidarr.py | 245 +++++++++++++++++++++++++++ nzbToMedia.py | 49 +++++- 6 files changed, 454 insertions(+), 8 deletions(-) create mode 100755 nzbToLidarr.py diff --git a/TorrentToMedia.py b/TorrentToMedia.py index 9fdbd587..1bd92b28 100755 --- a/TorrentToMedia.py +++ b/TorrentToMedia.py @@ -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. @@ -239,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': diff --git a/autoProcessMedia.cfg.spec b/autoProcessMedia.cfg.spec index 90a736a0..62aef675 100644 --- a/autoProcessMedia.cfg.spec +++ b/autoProcessMedia.cfg.spec @@ -192,6 +192,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 diff --git a/core/autoProcess/autoProcessMusic.py b/core/autoProcess/autoProcessMusic.py index 125b3569..f9ff1057 100644 --- a/core/autoProcess/autoProcessMusic.py +++ b/core/autoProcess/autoProcessMusic.py @@ -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,7 +130,10 @@ class autoProcessMusic(object): else: extract = int(cfg.get("extract", 0)) - url = "{0}{1}:{2}{3}/api".format(protocol, host, port, web_root) + 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) return [1, "{0}: Failed to post-process - {1} did not respond.".format(section, section)] @@ -123,7 +160,7 @@ class autoProcessMusic(object): # 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: - logger.warning("FAILED DOWNLOAD DETECTED", section) - return [1, "{0}: Failed to post-process. {1} does not support failed downloads".format(section, section)] \ No newline at end of file + 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) + 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. \ No newline at end of file diff --git a/core/nzbToMediaConfig.py b/core/nzbToMediaConfig.py index 0b2222d3..369bc45c 100644 --- a/core/nzbToMediaConfig.py +++ b/core/nzbToMediaConfig.py @@ -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'] diff --git a/nzbToLidarr.py b/nzbToLidarr.py new file mode 100755 index 00000000..a11a5eca --- /dev/null +++ b/nzbToLidarr.py @@ -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) diff --git a/nzbToMedia.py b/nzbToMedia.py index eadec7ee..b7717475 100755 --- a/nzbToMedia.py +++ b/nzbToMedia.py @@ -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'] From bd962d0e38fd12decb11e856bf1f888dfb604f78 Mon Sep 17 00:00:00 2001 From: clinton-hall Date: Wed, 23 May 2018 11:10:31 +1200 Subject: [PATCH 10/13] added _xsrf login params for SickGear support. Fixes #1346 --- README.md | 2 +- core/autoProcess/autoProcessTV.py | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 8de70f0f..ec29ba83 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/core/autoProcess/autoProcessTV.py b/core/autoProcess/autoProcessTV.py index 75ff4805..9bec4eb1 100644 --- a/core/autoProcess/autoProcessTV.py +++ b/core/autoProcess/autoProcessTV.py @@ -270,6 +270,9 @@ class autoProcessTV(object): s = requests.Session() login = "{0}{1}:{2}{3}/login".format(protocol, host, port, web_root) login_params = {'username': username, 'password': password} + r = s.get(login, verify=False, timeour=(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": From edd31b3eee309389faa2bd8536b46be028f648fb Mon Sep 17 00:00:00 2001 From: clinton-hall Date: Wed, 23 May 2018 11:14:38 +1200 Subject: [PATCH 11/13] fixed tab v space formatting. Fixes #1346 --- core/autoProcess/autoProcessTV.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/autoProcess/autoProcessTV.py b/core/autoProcess/autoProcessTV.py index 9bec4eb1..ec833624 100644 --- a/core/autoProcess/autoProcessTV.py +++ b/core/autoProcess/autoProcessTV.py @@ -270,9 +270,9 @@ class autoProcessTV(object): s = requests.Session() login = "{0}{1}:{2}{3}/login".format(protocol, host, port, web_root) login_params = {'username': username, 'password': password} - r = s.get(login, verify=False, timeour=(30,60)) - if r.status_code == 401 and r.cookies.get('_xsrf'): - login_params['_xsrf'] = r.cookies.get('_xsrf') + r = s.get(login, verify=False, timeour=(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": From 189dde26c8551e1c71fc18b9124c7d81de535ddd Mon Sep 17 00:00:00 2001 From: clinton-hall Date: Wed, 23 May 2018 20:13:13 +1200 Subject: [PATCH 12/13] fix typo. --- core/autoProcess/autoProcessTV.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/autoProcess/autoProcessTV.py b/core/autoProcess/autoProcessTV.py index ec833624..9ce67044 100644 --- a/core/autoProcess/autoProcessTV.py +++ b/core/autoProcess/autoProcessTV.py @@ -270,7 +270,7 @@ class autoProcessTV(object): s = requests.Session() login = "{0}{1}:{2}{3}/login".format(protocol, host, port, web_root) login_params = {'username': username, 'password': password} - r = s.get(login, verify=False, timeour=(30,60)) + 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)) From 0259b83afadc365a6fb821a01d06516ecbe9a52d Mon Sep 17 00:00:00 2001 From: clinton-hall Date: Wed, 23 May 2018 22:24:50 +1200 Subject: [PATCH 13/13] Change Case for SickBeard Fork names. https://github.com/SickGear/SickGear/issues/1099 Fix login to SickGear during auto-frok detection. Fixes #1346 --- core/__init__.py | 8 ++++---- core/nzbToMediaAutoFork.py | 7 ++++++- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/core/__init__.py b/core/__init__.py index 813bdd88..8b44c1db 100644 --- a/core/__init__.py +++ b/core/__init__.py @@ -61,10 +61,10 @@ FORKS = {} FORK_DEFAULT = "default" FORK_FAILED = "failed" FORK_FAILED_TORRENT = "failed-torrent" -FORK_SICKRAGETV = "sickragetv" -FORK_SICKRAGE = "sickrage" -FORK_MEDUSA = "medusa" -FORK_SICKGEAR = "sickgear" +FORK_SICKRAGETV = "SickRageTV" +FORK_SICKRAGE = "SickRage" +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} diff --git a/core/nzbToMediaAutoFork.py b/core/nzbToMediaAutoFork.py index 1f5b418f..e38f735c 100644 --- a/core/nzbToMediaAutoFork.py +++ b/core/nzbToMediaAutoFork.py @@ -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://" @@ -61,6 +63,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) else: