diff --git a/media/pvr.py b/media/pvr.py new file mode 100644 index 0000000..8221ae7 --- /dev/null +++ b/media/pvr.py @@ -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 diff --git a/media/radarr.py b/media/radarr.py index 981af7e..5bb61fa 100644 --- a/media/radarr.py +++ b/media/radarr.py @@ -1,143 +1,28 @@ -import os.path - import backoff -import requests -from misc import helpers -from misc import str as misc_str +from media.pvr import PVR +from misc.helpers import backoff_handler, dict_merge from misc.log import logger 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 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 +class Radarr(PVR): + def get_objects(self): + return self._get_objects('api/movie') @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, search_missing=False): - try: - # generate payload - payload = { - 'tmdbId': movie_tmdbid, - 'title': movie_title, - 'year': movie_year, - 'qualityProfileId': profile_id, - 'images': [], - 'monitored': True, - 'rootFolderPath': root_folder, - 'minimumAvailability': 'released', - 'titleSlug': movie_title_slug, - 'addOptions': { - 'ignoreEpisodesWithFiles': False, - 'ignoreEpisodesWithoutFiles': False, - 'searchForMovie': search_missing - } + payload = self._prepare_add_object_payload(movie_title, movie_title_slug, profile_id, root_folder) + + payload = dict_merge(payload, { + 'tmdbId': movie_tmdbid, + 'year': movie_year, + 'minimumAvailability': 'released', + 'addOptions': { + 'searchForMovie': search_missing } + }) - # make request - 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 + return self._add_object('api/movie', payload, identifier_field='tmdbId', identifier=movie_tmdbid) diff --git a/media/sonarr.py b/media/sonarr.py index 6caa082..6f287f9 100644 --- a/media/sonarr.py +++ b/media/sonarr.py @@ -3,114 +3,17 @@ import os.path 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 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 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 +class Sonarr(PVR): + def get_objects(self): + return self._get_objects('api/series') @backoff.on_predicate(backoff.expo, lambda x: x is None, max_tries=4, on_backoff=backoff_handler) 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) def add_series(self, series_tvdbid, series_title, series_title_slug, profile_id, root_folder, tag_ids=None, search_missing=False): - try: - # generate payload - payload = { - 'tvdbId': series_tvdbid, - 'title': series_title, - 'titleSlug': series_title_slug, - 'qualityProfileId': profile_id, - 'tags': [] if not tag_ids or not isinstance(tag_ids, list) else tag_ids, - 'images': [], - 'seasons': [], - 'seasonFolder': True, - 'monitored': True, - 'rootFolderPath': root_folder, - 'addOptions': { - 'ignoreEpisodesWithFiles': False, - 'ignoreEpisodesWithoutFiles': False, - 'searchForMissingEpisodes': search_missing - } + payload = self._prepare_add_object_payload(series_title, series_title_slug, profile_id, root_folder) + + payload = dict_merge(payload, { + 'tvdbId': series_tvdbid, + 'tags': [] if not tag_ids or not isinstance(tag_ids, list) else tag_ids, + 'seasons': [], + 'seasonFolder': True, + 'addOptions': { + 'searchForMissingEpisodes': search_missing } + }) - # make request - 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 + return self._add_object('api/series', payload, identifier_field='tvdbId', identifier=series_tvdbid) diff --git a/media/trakt.py b/media/trakt.py index cb46a0e..a2c67c5 100644 --- a/media/trakt.py +++ b/media/trakt.py @@ -3,17 +3,12 @@ import time import backoff import requests +from misc.helpers import backoff_handler from misc.log import logger 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: non_user_lists = ['anticipated', 'trending', 'popular', 'boxoffice'] diff --git a/misc/helpers.py b/misc/helpers.py index 60ad23e..e4f654a 100644 --- a/misc/helpers.py +++ b/misc/helpers.py @@ -410,3 +410,21 @@ def get_response_dict(response, key_field=None, key_value=None): except Exception: log.exception("Exception determining response for %s: ", 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 diff --git a/traktarr.py b/traktarr.py index 2c63dda..21af418 100755 --- a/traktarr.py +++ b/traktarr.py @@ -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)) # get sonarr series list - sonarr_series_list = sonarr.get_series() + sonarr_series_list = sonarr.get_objects() if not sonarr_series_list: log.error("Aborting due to failure to retrieve Sonarr shows list") 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) # get radarr movies list - radarr_movie_list = radarr.get_movies() + radarr_movie_list = radarr.get_objects() if not radarr_movie_list: log.error("Aborting due to failure to retrieve Radarr movies list") if notifications: