From f5a7a3d16ec1eb96d544589b8bb48f20aafb49cc Mon Sep 17 00:00:00 2001 From: p0psicles Date: Mon, 15 Feb 2021 17:09:37 +0100 Subject: [PATCH] Implemented classes for PymedusaApiV1 and PymedusaApiv2. --- core/auto_process/managers/pymedusa.py | 152 +++++++++++++++++++++--- core/auto_process/managers/sickbeard.py | 88 +++++++++++++- 2 files changed, 219 insertions(+), 21 deletions(-) diff --git a/core/auto_process/managers/pymedusa.py b/core/auto_process/managers/pymedusa.py index e08b6173..b0300a93 100644 --- a/core/auto_process/managers/pymedusa.py +++ b/core/auto_process/managers/pymedusa.py @@ -1,38 +1,154 @@ import requests from core import logger +import time from .sickbeard import SickBeard - +from core.auto_process.common import ( + ProcessResult, + command_complete, + completed_download_handling, +) class PyMedusa(SickBeard): """PyMedusa class.""" def __init__(self, sb_init): super(PyMedusa, self).__init__(sb_init) - self.cfg = self.sb_init.config # in case we need something that's not already directly on self.sb_init. - - def _configure(): - """Configure pymedusa with config options.""" def _create_url(self): - if self.sb_init.apikey: - return '{0}{1}:{2}{3}/api/{4}/'.format(self.sb_init.protocol, self.sb_init.host, self.sb_init.port, self.sb_init.web_root, self.sb_init.apikey) return '{0}{1}:{2}{3}/home/postprocess/processEpisode'.format(self.sb_init.protocol, self.sb_init.host, self.sb_init.port, self.sb_init.web_root) - def api_call(self): - """Perform the api call with PyMedusa.""" - s = requests.Session() +class PyMedusaApiV1(SickBeard): + """PyMedusa apiv1 class.""" + + def __init__(self, sb_init): + super(PyMedusaApiV1, self).__init__(sb_init) + + def _create_url(self): + return '{0}{1}:{2}{3}/api/{4}/'.format(self.sb_init.protocol, self.sb_init.host, self.sb_init.port, self.sb_init.web_root, self.sb_init.apikey) + + def api_call(self): self._process_fork_prarams() url = self._create_url() logger.debug('Opening URL: {0} with params: {1}'.format(url, self.sb_init.fork_params), self.sb_init.section) - if not self.sb_init.apikey and self.sb_init.username and self.sb_init.password: - login = '{0}{1}:{2}{3}/login'.format(self.sb_init.protocol, self.sb_init.host, self.sb_init.port, self.sb_init.web_root) - login_params = {'username': self.sb_init.username, 'password': self.sb_init.password} - r = s.get(login, verify=False, timeout=(30, 60)) - if r.status_code in [401, 403] 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)) - return s.get(url, auth=(self.sb_init.username, self.sb_init.password), params=self.sb_init.fork_params, stream=True, verify=False, timeout=(30, 1800)) + try: + response = self.session.get(url, auth=(self.sb_init.username, self.sb_init.password), params=self.sb_init.fork_params, stream=True, verify=False, timeout=(30, 1800)) + except requests.ConnectionError: + logger.error('Unable to open URL: {0}'.format(url), self.sb_init.section) + return ProcessResult( + message='{0}: Failed to post-process - Unable to connect to {0}'.format(self.sb_init.section), + status_code=1, + ) + + if response.status_code not in [requests.codes.ok, requests.codes.created, requests.codes.accepted]: + logger.error('Server returned status {0}'.format(response.status_code), self.sb_init.section) + return ProcessResult( + message='{0}: Failed to post-process - Server returned status {1}'.format(self.sb_init.section, response.status_code), + status_code=1, + ) + + if response.json()['result'] == 'success': + return ProcessResult( + message='{0}: Successfully post-processed {1}'.format(self.sb_init.section, self.input_name), + status_code=0, + ) + return ProcessResult( + message='{0}: Failed to post-process - Returned log from {0} was not as expected.'.format(self.sb_init.section), + status_code=1, # We did not receive Success confirmation. + ) + + +class PyMedusaApiV2(SickBeard): + """PyMedusa apiv2 class.""" + + def __init__(self, sb_init): + super(PyMedusaApiV2, self).__init__(sb_init) + + def _create_url(self): + return '{0}{1}:{2}{3}/api/v2/postprocess'.format(self.sb_init.protocol, self.sb_init.host, self.sb_init.port, self.sb_init.web_root) + + def _get_identifier_status(self, url): + # Loop through requesting medusa for the status on the queueitem. + try: + response = self.session.get(url, verify=False, timeout=(30, 1800)) + except requests.ConnectionError: + logger.error('Unable to get postprocess identifier status', self.sb_init.section) + return False + + try: + jdata = response.json() + except ValueError: + return False + + return jdata + + def api_call(self): + self._process_fork_prarams() + url = self._create_url() + + logger.debug('Opening URL: {0}'.format(url), self.sb_init.section) + payload = self.sb_init.fork_params + payload['resource'] = self.sb_init.fork_params['nzbName'] + del payload['nzbName'] + + # Update the session with the x-api-key + self.session.headers.update({ + 'x-api-key': self.sb_init.apikey, + 'Content-type': 'application/json' + }) + + # Send postprocess request + try: + response = self.session.post(url, json=payload, verify=False, timeout=(30, 1800)) + except requests.ConnectionError: + logger.error('Unable to send postprocess request', self.sb_init.section) + return ProcessResult( + message='{0}: Unable to send postprocess request to PyMedusa', + status_code=1, + ) + + # Get UUID + if response: + try: + jdata = response.json() + except ValueError: + logger.debug('No data returned from provider') + return False + + if not jdata.get('status') or not jdata['status'] == 'success': + return False + + queueitem_identifier = jdata['queueItem']['identifier'] + + wait_for = self.sb_init.config.get('wait_for', 2) + n = 0 + response = {} + url = '{0}/{1}'.format(url, queueitem_identifier) + while n < 12: # set up wait_for minutes to see if command completes.. + time.sleep(5 * wait_for) + response = self._get_identifier_status(url) + if response and response.get('success'): + break + if 'error' in response: + break + n += 1 + + # Log Medusa's PP logs here. + if response.get('output'): + for line in response['output']: + logger.postprocess('{0}'.format(line), self.sb_init.section) + + # For now this will most likely always be True. But in the future we could return an exit state + # for when the PP in medusa didn't yield an expected result. + if response.get('success'): + return ProcessResult( + message='{0}: Successfully post-processed {1}'.format(self.sb_init.section, self.input_name), + status_code=0, + ) + return ProcessResult( + message='{0}: Failed to post-process - Returned log from {0} was not as expected.'.format(self.sb_init.section), + status_code=1, # We did not receive Success confirmation. + ) diff --git a/core/auto_process/managers/sickbeard.py b/core/auto_process/managers/sickbeard.py index 74b44ee8..d0eb784d 100644 --- a/core/auto_process/managers/sickbeard.py +++ b/core/auto_process/managers/sickbeard.py @@ -9,6 +9,9 @@ from __future__ import ( import copy +from core.auto_process.common import ( + ProcessResult, +) import core from core import logger @@ -88,13 +91,15 @@ class InitSickBeard(object): replace = { 'medusa': 'Medusa', 'medusa-api': 'Medusa-api', + 'medusa-apiv1': 'Medusa-api', + 'medusa-apiv2': 'Medusa-apiv2', 'sickbeard-api': 'SickBeard-api', 'sickgear': 'SickGear', 'sickchill': 'SickChill', 'stheno': 'Stheno', } _val = cfg.get('fork', 'auto') - f1 = replace.get(_val, _val) + f1 = replace.get(_val.lower(), _val) try: fork = f1, core.FORKS[f1] except KeyError: @@ -303,9 +308,13 @@ class InitSickBeard(object): return fork def _init_fork(self): - from .pymedusa import PyMedusa + # These need to be imported here, to prevent a circular import. + from .pymedusa import PyMedusa, PyMedusaApiV1, PyMedusaApiV2 + mapped_forks = { - 'Medusa': PyMedusa + 'Medusa': PyMedusa, + 'Medusa-api': PyMedusaApiV1, + 'Medusa-apiv2': PyMedusaApiV2 } logger.debug('Create object for fork {fork}'.format(fork=self.fork)) if self.fork and mapped_forks.get(self.fork): @@ -323,6 +332,8 @@ class SickBeard(object): def __init__(self, sb_init): """SB constructor.""" self.sb_init = sb_init + self.session = requests.Session() + self.failed = None self.status = None self.input_name = None @@ -336,10 +347,15 @@ class SickBeard(object): self.force = int(self.sb_init.config.get('force', 0)) self.delete_on = int(self.sb_init.config.get('delete_on', 0)) self.ignore_subs = int(self.sb_init.config.get('ignore_subs', 0)) + self.is_priority = int(self.sb_init.config.get('is_priority', 0)) # get importmode, default to 'Move' for consistency with legacy self.import_mode = self.sb_init.config.get('importMode', 'Move') + # Keep track of result state + self.success = False + self.error_message = '' + def initialize(self, dir_name, input_name=None, failed=False, client_agent='manual'): """We need to call this explicitely because we need some variables. @@ -419,8 +435,74 @@ class SickBeard(object): else: del fork_params[param] + if param == 'is_priority': + if self.is_priority: + fork_params[param] = self.is_priority + else: + del fork_params[param] + if param == 'force_next': fork_params[param] = 1 # delete any unused params so we don't pass them to SB by mistake [fork_params.pop(k) for k, v in list(fork_params.items()) if v is None] + + def api_call(self): + """Perform a base sickbeard api call.""" + self._process_fork_prarams() + url = self._create_url() + + logger.debug('Opening URL: {0} with params: {1}'.format(url, self.sb_init.fork_params), self.sb_init.section) + try: + if not self.sb_init.apikey and self.sb_init.username and self.sb_init.password: + # If not using the api, we need to login using user/pass first. + login = '{0}{1}:{2}{3}/login'.format(self.sb_init.protocol, self.sb_init.host, self.sb_init.port, self.sb_init.web_root) + login_params = {'username': self.sb_init.username, 'password': self.sb_init.password} + r = self.session.get(login, verify=False, timeout=(30, 60)) + if r.status_code in [401, 403] and r.cookies.get('_xsrf'): + login_params['_xsrf'] = r.cookies.get('_xsrf') + self.session.post(login, data=login_params, stream=True, verify=False, timeout=(30, 60)) + response = self.session.get(url, auth=(self.sb_init.username, self.sb_init.password), params=self.sb_init.fork_params, stream=True, verify=False, timeout=(30, 1800)) + except requests.ConnectionError: + logger.error('Unable to open URL: {0}'.format(url), self.sb_init.section) + return ProcessResult( + message='{0}: Failed to post-process - Unable to connect to {0}'.format(self.sb_init.section), + status_code=1, + ) + + if response.status_code not in [requests.codes.ok, requests.codes.created, requests.codes.accepted]: + logger.error('Server returned status {0}'.format(response.status_code), self.sb_init.section) + return ProcessResult( + message='{0}: Failed to post-process - Server returned status {1}'.format(self.sb_init.section, response.status_code), + status_code=1, + ) + + return self.process_response(response) + + def process_response(self, response): + """Iterate over the lines returned, and log. + + :param response: Streamed Requests response object. + This method will need to be overwritten in the forks, for alternative response handling. + """ + for line in response.iter_lines(): + if line: + line = line.decode('utf-8') + logger.postprocess('{0}'.format(line), self.sb_init.section) + # if 'Moving file from' in line: + # input_name = os.path.split(line)[1] + # if 'added to the queue' in line: + # queued = True + # For the refactoring i'm only considering vanilla sickbeard, as for the base class. + if 'Processing succeeded' in line or 'Successfully processed' in line: + self.success = True + + if self.success: + return ProcessResult( + message='{0}: Successfully post-processed {1}'.format(self.sb_init.section, self.input_name), + status_code=0, + ) + return ProcessResult( + message='{0}: Failed to post-process - Returned log from {0} was not as expected.'.format(self.sb_init.section), + status_code=1, # We did not receive Success confirmation. + )