Refactor Sonarr and Radarr classes to share common functionality

pull/54/head
Mitchell Klijs 7 years ago
parent ff77578828
commit 1aa46b6f24

@ -0,0 +1,141 @@
import os.path
from abc import ABC, abstractmethod
import backoff
import requests
from misc import helpers
from misc import str as misc_str
from misc.helpers import backoff_handler
from misc.log import logger
log = logger.get_logger(__name__)
class PVR(ABC):
def __init__(self, server_url, api_key):
self.server_url = server_url
self.api_key = api_key
self.headers = {
'Content-Type': 'application/json',
'X-Api-Key': self.api_key,
}
def validate_api_key(self):
try:
# request system status to validate api_key
req = requests.get(
os.path.join(misc_str.ensure_endswith(self.server_url, "/"), 'api/system/status'),
headers=self.headers,
timeout=60
)
log.debug("Request Response: %d", req.status_code)
if req.status_code == 200 and 'version' in req.json():
return True
return False
except Exception:
log.exception("Exception validating api_key: ")
return False
@abstractmethod
def get_objects(self):
pass
@backoff.on_predicate(backoff.expo, lambda x: x is None, max_tries=4, on_backoff=backoff_handler)
def _get_objects(self, endpoint):
try:
# make request
req = requests.get(
os.path.join(misc_str.ensure_endswith(self.server_url, "/"), endpoint),
headers=self.headers,
timeout=60
)
log.debug("Request URL: %s", req.url)
log.debug("Request Response: %d", req.status_code)
if req.status_code == 200:
resp_json = req.json()
log.debug("Found %d objects", len(resp_json))
return resp_json
else:
log.error("Failed to retrieve all objects, request response: %d", req.status_code)
except Exception:
log.exception("Exception retrieving objects: ")
return None
@backoff.on_predicate(backoff.expo, lambda x: x is None, max_tries=4, on_backoff=backoff_handler)
def get_profile_id(self, profile_name):
try:
# make request
req = requests.get(
os.path.join(misc_str.ensure_endswith(self.server_url, "/"), 'api/profile'),
headers=self.headers,
timeout=60
)
log.debug("Request URL: %s", req.url)
log.debug("Request Response: %d", req.status_code)
if req.status_code == 200:
resp_json = req.json()
for profile in resp_json:
if profile['name'].lower() == profile_name.lower():
log.debug("Found id of %s profile: %d", profile_name, profile['id'])
return profile['id']
log.debug("Profile %s with id %d did not match %s", profile['name'], profile['id'], profile_name)
else:
log.error("Failed to retrieve all quality profiles, request response: %d", req.status_code)
except Exception:
log.exception("Exception retrieving id of profile %s: ", profile_name)
return None
def _prepare_add_object_payload(self, title, title_slug, profile_id, root_folder):
return {
'title': title,
'titleSlug': title_slug,
'qualityProfileId': profile_id,
'images': [],
'monitored': True,
'rootFolderPath': root_folder,
'addOptions': {
'ignoreEpisodesWithFiles': False,
'ignoreEpisodesWithoutFiles': False,
}
}
@backoff.on_predicate(backoff.expo, lambda x: x is None, max_tries=4, on_backoff=backoff_handler)
def _add_object(self, endpoint, payload, identifier_field, identifier):
try:
# make request
req = requests.post(
os.path.join(misc_str.ensure_endswith(self.server_url, "/"), endpoint),
headers=self.headers,
json=payload,
timeout=60
)
log.debug("Request URL: %s", req.url)
log.debug("Request Payload: %s", payload)
log.debug("Request Response Code: %d", req.status_code)
log.debug("Request Response Text:\n%s", req.text)
response_json = None
if 'json' in req.headers['Content-Type'].lower():
response_json = helpers.get_response_dict(req.json(), identifier_field, identifier)
if (req.status_code == 201 or req.status_code == 200) \
and (response_json and identifier_field in response_json) \
and response_json[identifier_field] == identifier:
log.debug("Successfully added %s (%d)", payload['title'], identifier)
return True
elif response_json and ('errorMessage' in response_json or 'message' in response_json):
message = response_json['errorMessage'] if 'errorMessage' in response_json else response_json['message']
log.error("Failed to add %s (%d) - status_code: %d, reason: %s", payload['title'], identifier,
req.status_code, message)
return False
else:
log.error("Failed to add %s (%d), unexpected response:\n%s", payload['title'], identifier, req.text)
return False
except Exception:
log.exception("Exception adding %s (%d): ", payload['title'], identifier)
return None

@ -1,143 +1,28 @@
import os.path
import backoff import backoff
import requests
from misc import helpers from media.pvr import PVR
from misc import str as misc_str from misc.helpers import backoff_handler, dict_merge
from misc.log import logger from misc.log import logger
log = logger.get_logger(__name__) log = logger.get_logger(__name__)
def backoff_handler(details): class Radarr(PVR):
log.warning("Backing off {wait:0.1f} seconds afters {tries} tries " def get_objects(self):
"calling function {target} with args {args} and kwargs " return self._get_objects('api/movie')
"{kwargs}".format(**details))
class Radarr:
def __init__(self, server_url, api_key):
self.server_url = server_url
self.api_key = api_key
self.headers = {
'Content-Type': 'application/json',
'X-Api-Key': self.api_key,
}
def validate_api_key(self):
try:
# request system status to validate api_key
req = requests.get(
os.path.join(misc_str.ensure_endswith(self.server_url, "/"), 'api/system/status'),
headers=self.headers,
timeout=60
)
log.debug("Request Response: %d", req.status_code)
if req.status_code == 200 and 'version' in req.json():
return True
return False
except Exception:
log.exception("Exception validating api_key: ")
return False
@backoff.on_predicate(backoff.expo, lambda x: x is None, max_tries=4, on_backoff=backoff_handler)
def get_movies(self):
try:
# make request
req = requests.get(
os.path.join(misc_str.ensure_endswith(self.server_url, "/"), 'api/movie'),
headers=self.headers,
timeout=60
)
log.debug("Request URL: %s", req.url)
log.debug("Request Response: %d", req.status_code)
if req.status_code == 200:
resp_json = req.json()
log.debug("Found %d movies", len(resp_json))
return resp_json
else:
log.error("Failed to retrieve all movies, request response: %d", req.status_code)
except Exception:
log.exception("Exception retrieving movies: ")
return None
@backoff.on_predicate(backoff.expo, lambda x: x is None, max_tries=4, on_backoff=backoff_handler)
def get_profile_id(self, profile_name):
try:
# make request
req = requests.get(
os.path.join(misc_str.ensure_endswith(self.server_url, "/"), 'api/profile'),
headers=self.headers,
timeout=60
)
log.debug("Request URL: %s", req.url)
log.debug("Request Response: %d", req.status_code)
if req.status_code == 200:
resp_json = req.json()
for profile in resp_json:
if profile['name'].lower() == profile_name.lower():
log.debug("Found id of %s profile: %d", profile_name, profile['id'])
return profile['id']
log.debug("Profile %s with id %d did not match %s", profile['name'], profile['id'], profile_name)
else:
log.error("Failed to retrieve all quality profiles, request response: %d", req.status_code)
except Exception:
log.exception("Exception retrieving id of profile %s: ", profile_name)
return None
@backoff.on_predicate(backoff.expo, lambda x: x is None, max_tries=4, on_backoff=backoff_handler) @backoff.on_predicate(backoff.expo, lambda x: x is None, max_tries=4, on_backoff=backoff_handler)
def add_movie(self, movie_tmdbid, movie_title, movie_year, movie_title_slug, profile_id, root_folder, def add_movie(self, movie_tmdbid, movie_title, movie_year, movie_title_slug, profile_id, root_folder,
search_missing=False): search_missing=False):
try: payload = self._prepare_add_object_payload(movie_title, movie_title_slug, profile_id, root_folder)
# generate payload
payload = { payload = dict_merge(payload, {
'tmdbId': movie_tmdbid, 'tmdbId': movie_tmdbid,
'title': movie_title, 'year': movie_year,
'year': movie_year, 'minimumAvailability': 'released',
'qualityProfileId': profile_id, 'addOptions': {
'images': [], 'searchForMovie': search_missing
'monitored': True,
'rootFolderPath': root_folder,
'minimumAvailability': 'released',
'titleSlug': movie_title_slug,
'addOptions': {
'ignoreEpisodesWithFiles': False,
'ignoreEpisodesWithoutFiles': False,
'searchForMovie': search_missing
}
} }
})
# make request return self._add_object('api/movie', payload, identifier_field='tmdbId', identifier=movie_tmdbid)
req = requests.post(
os.path.join(misc_str.ensure_endswith(self.server_url, "/"), 'api/movie'),
headers=self.headers,
json=payload,
timeout=60
)
log.debug("Request URL: %s", req.url)
log.debug("Request Payload: %s", payload)
log.debug("Request Response Code: %d", req.status_code)
log.debug("Request Response Text:\n%s", req.text)
response_json = None
if 'json' in req.headers['Content-Type'].lower():
response_json = helpers.get_response_dict(req.json(), 'tmdbId', movie_tmdbid)
if (req.status_code == 201 or req.status_code == 200) and (response_json and 'tmdbId' in response_json) \
and response_json['tmdbId'] == movie_tmdbid:
log.debug("Successfully added %s (%d)", movie_title, movie_tmdbid)
return True
elif response_json and 'message' in response_json:
log.error("Failed to add %s (%d) - status_code: %d, reason: %s", movie_title, movie_tmdbid,
req.status_code, response_json['message'])
return False
else:
log.error("Failed to add %s (%d), unexpected response:\n%s", movie_title, movie_tmdbid, req.text)
return False
except Exception:
log.exception("Exception adding movie %s (%d): ", movie_title, movie_tmdbid)
return None

@ -3,114 +3,17 @@ import os.path
import backoff import backoff
import requests import requests
from misc import helpers from media.pvr import PVR
from misc import str as misc_str from misc import str as misc_str
from misc.helpers import backoff_handler, dict_merge
from misc.log import logger from misc.log import logger
log = logger.get_logger(__name__) log = logger.get_logger(__name__)
def backoff_handler(details): class Sonarr(PVR):
log.warning("Backing off {wait:0.1f} seconds afters {tries} tries " def get_objects(self):
"calling function {target} with args {args} and kwargs " return self._get_objects('api/series')
"{kwargs}".format(**details))
class Sonarr:
def __init__(self, server_url, api_key):
self.server_url = server_url
self.api_key = api_key
self.headers = {
'X-Api-Key': self.api_key,
}
def validate_api_key(self):
try:
# request system status to validate api_key
req = requests.get(os.path.join(misc_str.ensure_endswith(self.server_url, "/"), 'api/system/status'),
headers=self.headers, timeout=60)
log.debug("Request Response: %d", req.status_code)
if req.status_code == 200 and 'version' in req.json():
return True
return False
except Exception:
log.exception("Exception validating api_key: ")
return False
@backoff.on_predicate(backoff.expo, lambda x: x is None, max_tries=4, on_backoff=backoff_handler)
def get_series(self):
try:
# make request
req = requests.get(
os.path.join(misc_str.ensure_endswith(self.server_url, "/"), 'api/series'),
headers=self.headers,
timeout=60
)
log.debug("Request URL: %s", req.url)
log.debug("Request Response: %d", req.status_code)
if req.status_code == 200:
resp_json = req.json()
log.debug("Found %d shows", len(resp_json))
return resp_json
else:
log.error("Failed to retrieve all shows, request response: %d", req.status_code)
except Exception:
log.exception("Exception retrieving show: ")
return None
@backoff.on_predicate(backoff.expo, lambda x: x is None, max_tries=4, on_backoff=backoff_handler)
def get_profile_id(self, profile_name):
try:
# make request
req = requests.get(
os.path.join(misc_str.ensure_endswith(self.server_url, "/"), 'api/profile'),
headers=self.headers,
timeout=60
)
log.debug("Request URL: %s", req.url)
log.debug("Request Response: %d", req.status_code)
if req.status_code == 200:
resp_json = req.json()
log.debug("Found %d quality profiles", len(resp_json))
for profile in resp_json:
if profile['name'].lower() == profile_name.lower():
log.debug("Found id of %s profile: %d", profile_name, profile['id'])
return profile['id']
log.debug("Profile %s with id %d did not match %s", profile['name'], profile['id'], profile_name)
else:
log.error("Failed to retrieve all quality profiles, request response: %d", req.status_code)
except Exception:
log.exception("Exception retrieving id of profile %s: ", profile_name)
return None
@backoff.on_predicate(backoff.expo, lambda x: x is None, max_tries=4, on_backoff=backoff_handler)
def get_tag_id(self, tag_name):
try:
# make request
req = requests.get(
os.path.join(misc_str.ensure_endswith(self.server_url, "/"), 'api/tag'),
headers=self.headers,
timeout=60
)
log.debug("Request URL: %s", req.url)
log.debug("Request Response: %d", req.status_code)
if req.status_code == 200:
resp_json = req.json()
log.debug("Found %d tags", len(resp_json))
for tag in resp_json:
if tag['label'].lower() == tag_name.lower():
log.debug("Found id of %s tag: %d", tag_name, tag['id'])
return tag['id']
log.debug("Tag %s with id %d did not match %s", tag['label'], tag['id'], tag_name)
else:
log.error("Failed to retrieve all tags, request response: %d", req.status_code)
except Exception:
log.exception("Exception retrieving id of tag %s: ", tag_name)
return None
@backoff.on_predicate(backoff.expo, lambda x: x is None, max_tries=4, on_backoff=backoff_handler) @backoff.on_predicate(backoff.expo, lambda x: x is None, max_tries=4, on_backoff=backoff_handler)
def get_tags(self): def get_tags(self):
@ -140,53 +43,16 @@ class Sonarr:
@backoff.on_predicate(backoff.expo, lambda x: x is None, max_tries=4, on_backoff=backoff_handler) @backoff.on_predicate(backoff.expo, lambda x: x is None, max_tries=4, on_backoff=backoff_handler)
def add_series(self, series_tvdbid, series_title, series_title_slug, profile_id, root_folder, tag_ids=None, def add_series(self, series_tvdbid, series_title, series_title_slug, profile_id, root_folder, tag_ids=None,
search_missing=False): search_missing=False):
try: payload = self._prepare_add_object_payload(series_title, series_title_slug, profile_id, root_folder)
# generate payload
payload = { payload = dict_merge(payload, {
'tvdbId': series_tvdbid, 'tvdbId': series_tvdbid,
'title': series_title, 'tags': [] if not tag_ids or not isinstance(tag_ids, list) else tag_ids,
'titleSlug': series_title_slug, 'seasons': [],
'qualityProfileId': profile_id, 'seasonFolder': True,
'tags': [] if not tag_ids or not isinstance(tag_ids, list) else tag_ids, 'addOptions': {
'images': [], 'searchForMissingEpisodes': search_missing
'seasons': [],
'seasonFolder': True,
'monitored': True,
'rootFolderPath': root_folder,
'addOptions': {
'ignoreEpisodesWithFiles': False,
'ignoreEpisodesWithoutFiles': False,
'searchForMissingEpisodes': search_missing
}
} }
})
# make request return self._add_object('api/series', payload, identifier_field='tvdbId', identifier=series_tvdbid)
req = requests.post(
os.path.join(misc_str.ensure_endswith(self.server_url, "/"), 'api/series'),
headers=self.headers,
json=payload,
timeout=60
)
log.debug("Request URL: %s", req.url)
log.debug("Request Payload: %s", payload)
log.debug("Request Response Code: %d", req.status_code)
log.debug("Request Response Text:\n%s", req.text)
response_json = None
if 'json' in req.headers['Content-Type'].lower():
response_json = helpers.get_response_dict(req.json(), 'tvdbId', series_tvdbid)
if (req.status_code == 201 or req.status_code == 200) and (response_json and 'tvdbId' in response_json) \
and response_json['tvdbId'] == series_tvdbid:
log.debug("Successfully added %s (%d)", series_title, series_tvdbid)
return True
elif response_json and 'errorMessage' in response_json:
log.error("Failed to add %s (%d) - status_code: %d, reason: %s", series_title, series_tvdbid,
req.status_code, response_json['errorMessage'])
return False
else:
log.error("Failed to add %s (%d), unexpected response:\n%s", series_title, series_tvdbid, req.text)
return False
except Exception:
log.exception("Exception adding show %s (%d): ", series_title, series_tvdbid)
return None

@ -3,17 +3,12 @@ import time
import backoff import backoff
import requests import requests
from misc.helpers import backoff_handler
from misc.log import logger from misc.log import logger
log = logger.get_logger(__name__) log = logger.get_logger(__name__)
def backoff_handler(details):
log.warning("Backing off {wait:0.1f} seconds afters {tries} tries "
"calling function {target} with args {args} and kwargs "
"{kwargs}".format(**details))
class Trakt: class Trakt:
non_user_lists = ['anticipated', 'trending', 'popular', 'boxoffice'] non_user_lists = ['anticipated', 'trending', 'popular', 'boxoffice']

@ -410,3 +410,21 @@ def get_response_dict(response, key_field=None, key_value=None):
except Exception: except Exception:
log.exception("Exception determining response for %s: ", response) log.exception("Exception determining response for %s: ", response)
return found_response return found_response
def backoff_handler(details):
log.warning("Backing off {wait:0.1f} seconds afters {tries} tries "
"calling function {target} with args {args} and kwargs "
"{kwargs}".format(**details))
def dict_merge(dct, merge_dct):
for k, v in merge_dct.items():
import collections
if k in dct and isinstance(dct[k], dict) and isinstance(merge_dct[k], collections.Mapping):
dict_merge(dct[k], merge_dct[k])
else:
dct[k] = merge_dct[k]
return dct

@ -216,7 +216,7 @@ def shows(list_type, add_limit=0, add_delay=2.5, genre=None, folder=None, no_sea
log.info("Retrieved %d Tag ID's", len(profile_tags)) log.info("Retrieved %d Tag ID's", len(profile_tags))
# get sonarr series list # get sonarr series list
sonarr_series_list = sonarr.get_series() sonarr_series_list = sonarr.get_objects()
if not sonarr_series_list: if not sonarr_series_list:
log.error("Aborting due to failure to retrieve Sonarr shows list") log.error("Aborting due to failure to retrieve Sonarr shows list")
if notifications: if notifications:
@ -439,7 +439,7 @@ def movies(list_type, add_limit=0, add_delay=2.5, genre=None, folder=None, no_se
log.info("Retrieved Profile ID for %s: %d", cfg.radarr.profile, profile_id) log.info("Retrieved Profile ID for %s: %d", cfg.radarr.profile, profile_id)
# get radarr movies list # get radarr movies list
radarr_movie_list = radarr.get_movies() radarr_movie_list = radarr.get_objects()
if not radarr_movie_list: if not radarr_movie_list:
log.error("Aborting due to failure to retrieve Radarr movies list") log.error("Aborting due to failure to retrieve Radarr movies list")
if notifications: if notifications:

Loading…
Cancel
Save