Added dedicated SiCKRAGE section with API version and SSO login support (#1805)

Added migration code to migrate SickBeard section with fork sickrage-api to new SiCKRAGE section
This commit is contained in:
echel0n 2021-01-12 16:16:41 -08:00 committed by GitHub
commit 0acf78f196
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
91 changed files with 13436 additions and 35 deletions

View file

@ -97,7 +97,6 @@ SABNZB_0717_NO_OF_ARGUMENTS = 9
FORK_DEFAULT = 'default'
FORK_FAILED = 'failed'
FORK_FAILED_TORRENT = 'failed-torrent'
FORK_SICKRAGE = 'SickRage'
FORK_SICKCHILL = 'SickChill'
FORK_SICKCHILL_API = 'SickChill-api'
FORK_SICKBEARD_API = 'SickBeard-api'
@ -111,7 +110,6 @@ FORKS = {
FORK_DEFAULT: {'dir': None},
FORK_FAILED: {'dirName': None, 'failed': None},
FORK_FAILED_TORRENT: {'dir': None, 'failed': None, 'process_method': None},
FORK_SICKRAGE: {'proc_dir': None, 'failed': None, 'process_method': None, 'force': None, 'delete_on': None},
FORK_SICKCHILL: {'proc_dir': None, 'failed': None, 'process_method': None, 'force': None, 'delete_on': None, 'force_next': None},
FORK_SICKCHILL_API: {'path': None, 'failed': None, 'process_method': None, 'force_replace': None, 'return_data': None, 'type': None, 'delete': None, 'force_next': None, 'is_priority': None, 'cmd': 'postprocess'},
FORK_SICKBEARD_API: {'path': None, 'failed': None, 'process_method': None, 'force_replace': None, 'return_data': None, 'type': None, 'delete': None, 'force_next': None, 'cmd': 'postprocess'},
@ -123,6 +121,10 @@ FORKS = {
}
ALL_FORKS = {k: None for k in set(list(itertools.chain.from_iterable([FORKS[x].keys() for x in FORKS.keys()])))}
# SiCKRAGE OAuth2
SICKRAGE_OAUTH_CLIENT_ID = 'nzbtomedia'
SICKRAGE_OAUTH_TOKEN_URL = 'https://auth.sickrage.ca/auth/realms/sickrage/protocol/openid-connect/token'
# NZBGet Exit Codes
NZBGET_POSTPROCESS_PAR_CHECK = 92
NZBGET_POSTPROCESS_SUCCESS = 93

View file

@ -14,6 +14,8 @@ import os
import time
import requests
from oauthlib.oauth2 import LegacyApplicationClient
from requests_oauthlib import OAuth2Session
import core
from core import logger, transcoder
@ -39,7 +41,6 @@ requests.packages.urllib3.disable_warnings()
def process(section, dir_name, input_name=None, failed=False, client_agent='manual', download_id=None, input_category=None, failure_link=None):
cfg = dict(core.CFG[section][input_category])
host = cfg['host']
@ -50,12 +51,15 @@ def process(section, dir_name, input_name=None, failed=False, client_agent='manu
username = cfg.get('username', '')
password = cfg.get('password', '')
apikey = cfg.get('apikey', '')
api_version = int(cfg.get('api_version', 2))
sso_username = cfg.get('sso_username', '')
sso_password = cfg.get('sso_password', '')
if server_responding('{0}{1}:{2}{3}'.format(protocol, host, port, web_root)):
# auto-detect correct fork
fork, fork_params = auto_fork(section, input_category)
elif not username and not apikey:
logger.info('No SickBeard username or Sonarr apikey entered. Performing transcoder functions only')
elif not username and not apikey and not sso_username:
logger.info('No SickBeard / SiCKRAGE username or Sonarr apikey entered. Performing transcoder functions only')
fork, fork_params = 'None', {}
else:
logger.error('Server did not respond. Exiting', section)
@ -202,7 +206,7 @@ def process(section, dir_name, input_name=None, failed=False, client_agent='manu
del fork_params['quiet']
if param == 'type':
if 'type' in fork_params: # only set if we haven't already deleted for 'failed' above.
if 'type' in fork_params: # only set if we haven't already deleted for 'failed' above.
fork_params[param] = 'manual'
if 'proc_type' in fork_params:
del fork_params['proc_type']
@ -285,6 +289,11 @@ def process(section, dir_name, input_name=None, failed=False, client_agent='manu
url = '{0}{1}:{2}{3}/home/postprocess/process_episode'.format(protocol, host, port, web_root)
else:
url = '{0}{1}:{2}{3}/home/postprocess/processEpisode'.format(protocol, host, port, web_root)
elif section == 'SiCKRAGE':
if api_version >= 2:
url = '{0}{1}:{2}{3}/api/v{4}/postprocess'.format(protocol, host, port, web_root, api_version)
else:
url = '{0}{1}:{2}{3}/api/v{4}/{5}/'.format(protocol, host, port, web_root, api_version, apikey)
elif section == 'NzbDrone':
url = '{0}{1}:{2}{3}/api/command'.format(protocol, host, port, web_root)
url2 = '{0}{1}:{2}{3}/api/config/downloadClient'.format(protocol, host, port, web_root)
@ -302,8 +311,9 @@ def process(section, dir_name, input_name=None, failed=False, client_agent='manu
try:
if section == 'SickBeard':
logger.debug('Opening URL: {0} with params: {1}'.format(url, fork_params), section)
s = requests.Session()
logger.debug('Opening URL: {0} with params: {1}'.format(url, fork_params), section)
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}
@ -312,6 +322,31 @@ def process(section, dir_name, input_name=None, failed=False, client_agent='manu
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 == 'SiCKRAGE':
s = requests.Session()
if api_version >= 2 and sso_username and sso_password:
oauth = OAuth2Session(client=LegacyApplicationClient(client_id=core.SICKRAGE_OAUTH_CLIENT_ID))
oauth_token = oauth.fetch_token(client_id=core.SICKRAGE_OAUTH_CLIENT_ID,
token_url=core.SICKRAGE_OAUTH_TOKEN_URL,
username=sso_username,
password=sso_password)
s.headers.update({'Authorization': 'Bearer ' + oauth_token['access_token']})
params = {
'path': fork_params['path'],
'failed': str(bool(fork_params['failed'])).lower(),
'processMethod': 'move',
'forceReplace': str(bool(fork_params['force_replace'])).lower(),
'returnData': str(bool(fork_params['return_data'])).lower(),
'delete': str(bool(fork_params['delete'])).lower(),
'forceNext': str(bool(fork_params['force_next'])).lower(),
'nzbName': fork_params['nzbName']
}
else:
params = fork_params
r = s.get(url, params=params, stream=True, verify=False, timeout=(30, 1800))
elif section == 'NzbDrone':
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))
@ -350,6 +385,12 @@ def process(section, dir_name, input_name=None, failed=False, client_agent='manu
if queued:
time.sleep(60)
elif section == 'SiCKRAGE':
if api_version >= 2:
success = True
else:
if r.json()['result'] == 'success':
success = True
elif section == 'NzbDrone':
try:
res = r.json()
@ -401,7 +442,8 @@ def process(section, dir_name, input_name=None, failed=False, client_agent='manu
# status_code=1,
# )
if completed_download_handling(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)
logger.debug('The Scan command did not return status completed, but complete Download Handling is enabled. Passing back to {0}.'.format(section),
section)
return ProcessResult(
message='{0}: Complete DownLoad Handling is enabled. Passing back to {0}'.format(section),
status_code=status,

View file

@ -146,11 +146,21 @@ class ConfigObj(configobj.ConfigObj, Section):
for newsection in CFG_NEW:
if CFG_NEW[newsection].sections:
subsections.update({newsection: CFG_NEW[newsection].sections})
for section in CFG_OLD:
if CFG_OLD[section].sections:
subsections.update({section: CFG_OLD[section].sections})
for option, value in CFG_OLD[section].items():
if option in ['category', 'cpsCategory', 'sbCategory', 'hpCategory', 'mlCategory', 'gzCategory', 'raCategory', 'ndCategory', 'W3Category']:
if option in ['category',
'cpsCategory',
'sbCategory',
'srCategory',
'hpCategory',
'mlCategory',
'gzCategory',
'raCategory',
'ndCategory',
'W3Category']:
if not isinstance(value, list):
value = [value]
@ -191,7 +201,7 @@ class ConfigObj(configobj.ConfigObj, Section):
if option == 'forceClean':
CFG_NEW['General']['force_clean'] = value
values.pop(option)
if option == 'qBittorrenHost': #We had a typo that is now fixed.
if option == 'qBittorrenHost': # We had a typo that is now fixed.
CFG_NEW['Torrent']['qBittorrentHost'] = value
values.pop(option)
if section in ['Transcoder']:
@ -204,6 +214,7 @@ class ConfigObj(configobj.ConfigObj, Section):
elif not value:
value = 0
values[option] = value
# remove any options that we no longer need so they don't migrate into our new config
if not list(ConfigObj.find_key(CFG_NEW, option)):
try:
@ -248,6 +259,20 @@ class ConfigObj(configobj.ConfigObj, Section):
elif section in CFG_OLD.keys():
process_section(section, subsection)
# migrate SiCRKAGE settings from SickBeard section to new dedicated SiCRKAGE section
if CFG_OLD['SickBeard']['tv']['enabled'] and CFG_OLD['SickBeard']['tv']['fork'] == 'sickrage-api':
for option, value in iteritems(CFG_OLD['SickBeard']['tv']):
if option in CFG_NEW['SiCKRAGE']['tv']:
CFG_NEW['SiCKRAGE']['tv'][option] = value
# set API version to 1 if API key detected and no SSO username is set
if CFG_NEW['SiCKRAGE']['tv']['apikey'] and not CFG_NEW['SiCKRAGE']['tv']['sso_username']:
CFG_NEW['SiCKRAGE']['tv']['api_version'] = 1
# disable SickBeard section
CFG_NEW['SickBeard']['tv']['enabled'] = 0
CFG_NEW['SickBeard']['tv']['fork'] = 'auto'
# create a backup of our old config
CFG_OLD.filename = '{config}.old'.format(config=core.CONFIG_FILE)
CFG_OLD.write()
@ -360,10 +385,10 @@ class ConfigObj(configobj.ConfigObj, Section):
section = 'SickBeard'
env_cat_key = 'NZBPO_SBCATEGORY'
env_keys = ['ENABLED', 'HOST', 'PORT', 'APIKEY', 'USERNAME', 'PASSWORD', 'SSL', 'WEB_ROOT', 'WATCH_DIR', 'FORK',
'DELETE_FAILED', 'TORRENT_NOLINK', 'NZBEXTRACTIONBY', 'REMOTE_PATH', 'PROCESS_METHOD']
cfg_keys = ['enabled', 'host', 'port', 'apikey', 'username', 'password', 'ssl', 'web_root', 'watch_dir', 'fork',
'delete_failed', 'Torrent_NoLink', 'nzbExtractionBy', 'remote_path', 'process_method']
env_keys = ['ENABLED', 'HOST', 'PORT', 'APIKEY', 'USERNAME', 'PASSWORD', 'SSL', 'WEB_ROOT', 'WATCH_DIR', 'FORK', 'DELETE_FAILED', 'TORRENT_NOLINK',
'NZBEXTRACTIONBY', 'REMOTE_PATH', 'PROCESS_METHOD']
cfg_keys = ['enabled', 'host', 'port', 'apikey', 'username', 'password', 'ssl', 'web_root', 'watch_dir', 'fork', 'delete_failed', 'Torrent_NoLink',
'nzbExtractionBy', 'remote_path', 'process_method']
if env_cat_key in os.environ:
for index in range(len(env_keys)):
key = 'NZBPO_SB{index}'.format(index=env_keys[index])
@ -374,6 +399,29 @@ class ConfigObj(configobj.ConfigObj, Section):
cfg_new[section][os.environ[env_cat_key]] = {}
cfg_new[section][os.environ[env_cat_key]][option] = value
cfg_new[section][os.environ[env_cat_key]]['enabled'] = 1
if os.environ[env_cat_key] in cfg_new['SiCKRAGE'].sections:
cfg_new['SiCKRAGE'][env_cat_key]['enabled'] = 0
if os.environ[env_cat_key] in cfg_new['NzbDrone'].sections:
cfg_new['NzbDrone'][env_cat_key]['enabled'] = 0
section = 'SiCKRAGE'
env_cat_key = 'NZBPO_SRCATEGORY'
env_keys = ['ENABLED', 'HOST', 'PORT', 'APIKEY', 'API_VERSION', 'SSO_USERNAME', 'SSO_PASSWORD', 'SSL', 'WEB_ROOT', 'WATCH_DIR', 'FORK',
'DELETE_FAILED', 'TORRENT_NOLINK', 'NZBEXTRACTIONBY', 'REMOTE_PATH', 'PROCESS_METHOD']
cfg_keys = ['enabled', 'host', 'port', 'apikey', 'api_version', 'sso_username', 'sso_password', 'ssl', 'web_root', 'watch_dir', 'fork',
'delete_failed', 'Torrent_NoLink', 'nzbExtractionBy', 'remote_path', 'process_method']
if env_cat_key in os.environ:
for index in range(len(env_keys)):
key = 'NZBPO_SR{index}'.format(index=env_keys[index])
if key in os.environ:
option = cfg_keys[index]
value = os.environ[key]
if os.environ[env_cat_key] not in cfg_new[section].sections:
cfg_new[section][os.environ[env_cat_key]] = {}
cfg_new[section][os.environ[env_cat_key]][option] = value
cfg_new[section][os.environ[env_cat_key]]['enabled'] = 1
if os.environ[env_cat_key] in cfg_new['SickBeard'].sections:
cfg_new['SickBeard'][env_cat_key]['enabled'] = 0
if os.environ[env_cat_key] in cfg_new['NzbDrone'].sections:
cfg_new['NzbDrone'][env_cat_key]['enabled'] = 0
@ -460,6 +508,8 @@ class ConfigObj(configobj.ConfigObj, Section):
cfg_new[section][os.environ[env_cat_key]]['enabled'] = 1
if os.environ[env_cat_key] in cfg_new['SickBeard'].sections:
cfg_new['SickBeard'][env_cat_key]['enabled'] = 0
if os.environ[env_cat_key] in cfg_new['SiCKRAGE'].sections:
cfg_new['SiCKRAGE'][env_cat_key]['enabled'] = 0
section = 'Radarr'
env_cat_key = 'NZBPO_RACATEGORY'

View file

@ -9,11 +9,14 @@ from __future__ import (
import requests
import six
from oauthlib.oauth2 import LegacyApplicationClient
from requests_oauthlib import OAuth2Session
from six import iteritems
import core
from core import logger
def api_check(r, params, rem_params):
try:
json_data = r.json()
@ -41,7 +44,7 @@ def api_check(r, params, rem_params):
optional_parameters = json_data['optionalParameters'].keys()
# Find excess parameters
excess_parameters = set(params).difference(optional_parameters)
excess_parameters.remove('cmd') # Don't remove cmd from api params
excess_parameters.remove('cmd') # Don't remove cmd from api params
logger.debug('Removing excess parameters: {}'.format(sorted(excess_parameters)))
rem_params.extend(excess_parameters)
return rem_params, True
@ -53,7 +56,7 @@ def api_check(r, params, rem_params):
def auto_fork(section, input_category):
# auto-detect correct section
# config settings
if core.FORK_SET: # keep using determined fork for multiple (manual) post-processing
if core.FORK_SET: # keep using determined fork for multiple (manual) post-processing
logger.info('{section}:{category} fork already set to {fork}'.format
(section=section, category=input_category, fork=core.FORK_SET[0]))
return core.FORK_SET[0], core.FORK_SET[1]
@ -62,9 +65,12 @@ def auto_fork(section, input_category):
host = cfg.get('host')
port = cfg.get('port')
username = cfg.get('username')
password = cfg.get('password')
apikey = cfg.get('apikey')
username = cfg.get('username', '')
password = cfg.get('password', '')
sso_username = cfg.get('sso_username', '')
sso_password = cfg.get('sso_password', '')
apikey = cfg.get('apikey', '')
api_version = int(cfg.get('api_version', 2))
ssl = int(cfg.get('ssl', 0))
web_root = cfg.get('web_root', '')
replace = {
@ -73,7 +79,6 @@ def auto_fork(section, input_category):
'sickbeard-api': 'SickBeard-api',
'sickgear': 'SickGear',
'sickchill': 'SickChill',
'sickrage': 'SickRage',
'stheno': 'Stheno',
}
_val = cfg.get('fork', 'auto')
@ -104,6 +109,53 @@ def auto_fork(section, input_category):
fork = ['default', {}]
elif section == 'SiCKRAGE':
logger.info('Attempting to verify {category} fork'.format
(category=input_category))
if api_version >= 2:
url = '{protocol}{host}:{port}{root}/api/v{api_version}/ping'.format(
protocol=protocol, host=host, port=port, root=web_root, api_version=api_version
)
api_params = {}
else:
url = '{protocol}{host}:{port}{root}/api/v{api_version}/{apikey}/'.format(
protocol=protocol, host=host, port=port, root=web_root, api_version=api_version, apikey=apikey,
)
api_params = {'cmd': 'postprocess', 'help': '1'}
try:
if api_version >= 2 and sso_username and sso_password:
oauth = OAuth2Session(client=LegacyApplicationClient(client_id=core.SICKRAGE_OAUTH_CLIENT_ID))
oauth_token = oauth.fetch_token(client_id=core.SICKRAGE_OAUTH_CLIENT_ID,
token_url=core.SICKRAGE_OAUTH_TOKEN_URL,
username=sso_username,
password=sso_password)
r = requests.get(url, headers={'Authorization': 'Bearer ' + oauth_token['access_token']}, stream=True, verify=False)
else:
r = requests.get(url, params=api_params, stream=True, verify=False)
if not r.ok:
logger.warning('Connection to {section}:{category} failed! '
'Check your configuration'.format
(section=section, category=input_category))
except requests.ConnectionError:
logger.warning('Could not connect to {0}:{1} to verify API version!'.format(section, input_category))
params = {
'path': None,
'failed': None,
'process_method': None,
'force_replace': None,
'return_data': None,
'type': None,
'delete': None,
'force_next': None,
'is_priority': None
}
fork = ['default', params]
elif fork == 'auto':
params = core.ALL_FORKS
rem_params = []
@ -125,6 +177,7 @@ def auto_fork(section, input_category):
# attempting to auto-detect fork
try:
s = requests.Session()
if not apikey and username and password:
login = '{protocol}{host}:{port}{root}/login'.format(
protocol=protocol, host=host, port=port, root=web_root)
@ -138,15 +191,19 @@ def auto_fork(section, input_category):
logger.info('Could not connect to {section}:{category} to perform auto-fork detection!'.format
(section=section, category=input_category))
r = []
if r and r.ok:
if apikey:
rem_params, found = api_check(r, params, rem_params)
if found:
params['cmd'] = 'sg.postprocess'
else: # try different api set for non-SickGear forks.
else: # try different api set for non-SickGear forks.
api_params = {'cmd': 'help', 'subject': 'postprocess'}
try:
r = s.get(url, auth=(username, password), params=api_params, verify=False)
if not apikey and username and password:
r = s.get(url, auth=(username, password), params=api_params, verify=False)
else:
r = s.get(url, params=api_params, verify=False)
except requests.ConnectionError:
logger.info('Could not connect to {section}:{category} to perform auto-fork detection!'.format
(section=section, category=input_category))
@ -181,7 +238,6 @@ def auto_fork(section, input_category):
(section=section, category=input_category))
fork = list(core.FORKS.items())[list(core.FORKS.keys()).index(core.FORK_DEFAULT)]
logger.info('{section}:{category} fork set to {fork}'.format
(section=section, category=input_category, fork=fork[0]))
core.FORK_SET = fork