diff --git a/README.md b/README.md index 4aa48c4..33a16eb 100644 --- a/README.md +++ b/README.md @@ -324,13 +324,14 @@ You can repeat this process for as many users as you like. "radarr": { "api_key": "", "minimum_availability": "released", - "profile": "HD-1080p", + "quality": "HD-1080p", "root_folder": "/movies/", "url": "http://localhost:7878/" }, "sonarr": { "api_key": "", - "profile": "HD-1080p", + "language": "English", + "quality": "HD-1080p", "root_folder": "/tv/", "tags": {}, "url": "http://localhost:8989/" @@ -898,14 +899,14 @@ Radarr configuration. "radarr": { "api_key": "", "minimum_availability": "released", - "profile": "HD-1080p", + "quality": "HD-1080p", "root_folder": "/movies/", "url": "http://localhost:7878" }, ``` `api_key` - Radarr's API Key. -`profile` - Profile that movies are assigned to. +`quality` - Quality Profile that movies are assigned to. `minimum_availability` - The minimum availability the movies are set to. @@ -926,7 +927,8 @@ Sonarr configuration. ```json "sonarr": { "api_key": "", - "profile": "HD-1080p", + "language": "English", + "quality": "HD-1080p", "root_folder": "/tv/", "tags": {}, "url": "http://localhost:8989" @@ -935,7 +937,9 @@ Sonarr configuration. `api_key` - Sonarr's API Key. -`profile` - Profile that TV shows are assigned to. +`language` - Language Profile that TV shows are assigned to. Only applies to Sonarr v3. + +`quality` - Quality Profile that TV shows are assigned to. `root_folder` - Root folder for TV shows. diff --git a/media/pvr.py b/media/pvr.py index 8925f06..ccb6174 100644 --- a/media/pvr.py +++ b/media/pvr.py @@ -1,5 +1,6 @@ import os.path from abc import ABC, abstractmethod +from distutils.version import LooseVersion as Version import backoff import requests @@ -67,7 +68,7 @@ class PVR(ABC): 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): + def get_quality_profile_id(self, profile_name): try: # make request req = requests.get( @@ -83,21 +84,70 @@ class PVR(ABC): resp_json = req.json() for profile in resp_json: if profile['name'].lower() == profile_name.lower(): - log.debug("Found Profile ID for \'%s\': %d", profile_name, profile['id']) + log.debug("Found Quality Profile ID for \'%s\': %d", profile_name, profile['id']) return profile['id'] - log.debug("Profile \'%s\' with ID \'%d\' did not match Profile \'%s\'", profile['name'], + log.debug("Profile \'%s\' with ID \'%d\' did not match Quality Profile \'%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) + log.exception("Exception retrieving ID of quality profile %s: ", profile_name) return None - def _prepare_add_object_payload(self, title, title_slug, profile_id, root_folder): + @backoff.on_exception(backoff.expo, requests.exceptions.RequestException, max_tries=4, on_backoff=backoff_handler) + def get_language_profile_id(self, language_name): + try: + # check if sonarr is v3 + + # make request + ver_req = requests.get( + os.path.join(misc_str.ensure_endswith(self.server_url, "/"), 'api/system/status'), + headers=self.headers, + timeout=60, + allow_redirects=False + ) + + if ver_req.status_code == 200: + ver_resp_json = ver_req.json() + if not Version(ver_resp_json['version']) > Version('3'): + log.debug("Skipping Language Profile lookup because Sonarr version is \'%s\'.", + ver_resp_json['version']) + return None + + except Exception: + log.exception("Exception verifying Sonarr version.") + return None + + try: + # make request + req = requests.get( + os.path.join(misc_str.ensure_endswith(self.server_url, "/"), 'api/v3/languageprofile'), + headers=self.headers, + timeout=60, + allow_redirects=False + ) + 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() == language_name.lower(): + log.debug("Found Language Profile ID for \'%s\': %d", language_name, profile['id']) + return profile['id'] + log.debug("Profile \'%s\' with ID \'%d\' did not match Language Profile \'%s\'", profile['name'], + profile['id'], language_name) + else: + log.error("Failed to retrieve all language profiles, request response: %d", req.status_code) + except Exception: + log.exception("Exception retrieving ID of language profile %s: ", language_name) + return None + + def _prepare_add_object_payload(self, title, title_slug, quality_profile_id, root_folder): return { 'title': title, 'titleSlug': title_slug, - 'qualityProfileId': profile_id, + 'qualityProfileId': quality_profile_id, 'images': [], 'monitored': True, 'rootFolderPath': root_folder, diff --git a/media/radarr.py b/media/radarr.py index a1c6ac5..7b544d5 100644 --- a/media/radarr.py +++ b/media/radarr.py @@ -12,9 +12,9 @@ class Radarr(PVR): 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_tmdb_id, movie_title, movie_year, movie_title_slug, profile_id, root_folder, + def add_movie(self, movie_tmdb_id, movie_title, movie_year, movie_title_slug, quality_profile_id, root_folder, min_availability_temp, search_missing=False): - payload = self._prepare_add_object_payload(movie_title, movie_title_slug, profile_id, root_folder) + payload = self._prepare_add_object_payload(movie_title, movie_title_slug, quality_profile_id, root_folder) # replace radarr minimum_availability if supplied if min_availability_temp == 'announced': diff --git a/media/sonarr.py b/media/sonarr.py index 4406a8e..ad21481 100644 --- a/media/sonarr.py +++ b/media/sonarr.py @@ -42,9 +42,9 @@ class Sonarr(PVR): return None @backoff.on_predicate(backoff.expo, lambda x: x is None, max_tries=4, on_backoff=backoff_handler) - def add_series(self, series_tvdb_id, series_title, series_title_slug, profile_id, root_folder, tag_ids=None, - search_missing=False, series_type='standard'): - payload = self._prepare_add_object_payload(series_title, series_title_slug, profile_id, root_folder) + def add_series(self, series_tvdb_id, series_title, series_title_slug, quality_profile_id, language_profile_id, + root_folder, tag_ids=None, search_missing=False, series_type='standard'): + payload = self._prepare_add_object_payload(series_title, series_title_slug, quality_profile_id, root_folder) payload = dict_merge(payload, { 'tvdbId': series_tvdb_id, @@ -57,4 +57,10 @@ class Sonarr(PVR): } }) - return self._add_object('api/series', payload, identifier_field='tvdbId', identifier=series_tvdb_id) + if language_profile_id: + payload['languageProfileId'] = language_profile_id + endpoint = 'api/v3/series' + else: + endpoint = 'api/series' + + return self._add_object(endpoint, payload, identifier_field='tvdbId', identifier=series_tvdb_id) diff --git a/misc/config.py b/misc/config.py index e3bf3c0..e53d73f 100644 --- a/misc/config.py +++ b/misc/config.py @@ -37,27 +37,23 @@ class Config(object, metaclass=Singleton): 'core': { 'debug': False }, - 'trakt': { - 'client_id': '', - 'client_secret': '' + 'notifications': { + 'verbose': True }, - 'sonarr': { - 'api_key': '', - 'profile': 'HD-1080p', - 'root_folder': '/tv/', - 'tags': { + 'automatic': { + 'movies': { + 'interval': 20, + 'anticipated': 3, + 'trending': 3, + 'popular': 3, + 'boxoffice': 10 }, - 'url': 'http://localhost:8989/' - }, - 'radarr': { - 'api_key': '', - 'minimum_availability': 'released', - 'profile': 'HD-1080p', - 'root_folder': '/movies/', - 'url': 'http://localhost:7878/' - }, - 'omdb': { - 'api_key': '' + 'shows': { + 'interval': 48, + 'anticipated': 10, + 'trending': 1, + 'popular': 1 + } }, 'filters': { 'shows': { @@ -85,23 +81,28 @@ class Config(object, metaclass=Singleton): 'rotten_tomatoes': "" } }, - 'automatic': { - 'movies': { - 'interval': 20, - 'anticipated': 3, - 'trending': 3, - 'popular': 3, - 'boxoffice': 10 + 'radarr': { + 'api_key': '', + 'minimum_availability': 'released', + 'quality': 'HD-1080p', + 'root_folder': '/movies/', + 'url': 'http://localhost:7878/' + }, + 'sonarr': { + 'api_key': '', + 'language': 'English', + 'quality': 'HD-1080p', + 'root_folder': '/tv/', + 'tags': { }, - 'shows': { - 'interval': 48, - 'anticipated': 10, - 'trending': 1, - 'popular': 1 - } + 'url': 'http://localhost:8989/' }, - 'notifications': { - 'verbose': True + 'omdb': { + 'api_key': '' + }, + 'trakt': { + 'client_id': '', + 'client_secret': '' } } diff --git a/sample/config.json b/sample/config.json index 4d503dd..3b0416b 100644 --- a/sample/config.json +++ b/sample/config.json @@ -2,6 +2,9 @@ "core": { "debug": false }, + "notifications": { + "verbose": false + }, "automatic": { "movies": { "anticipated": 3, @@ -17,9 +20,6 @@ "trending": 2 } }, - "notifications": { - "verbose": false - }, "filters": { "movies": { "disabled_for": [], @@ -93,18 +93,22 @@ }, "radarr": { "api_key": "", - "profile": "HD-1080p", + "quality": "HD-1080p", "minimum_availability": "released", "url": "http://localhost:7878/", "root_folder": "/movies/" }, "sonarr": { "api_key": "", - "profile": "HD-1080p", + "language": "English", + "quality": "HD-1080p", "url": "http://localhost:8989/", "root_folder": "/tv/", "tags": {} }, + "omdb": { + "api_key": "" + }, "trakt": { "client_id": "", "client_secret": "" diff --git a/traktarr.py b/traktarr.py index 2bd4411..80a92db 100755 --- a/traktarr.py +++ b/traktarr.py @@ -56,6 +56,10 @@ def app(config, cachefile, logfile): cfg['filters']['movies']['blacklisted_title_keywords'] = cfg['filters']['movies']['blacklist_title_keywords'] if cfg.filters.movies.rating_limit: cfg['filters']['movies']['rotten_tomatoes'] = cfg['filters']['movies']['rating_limit'] + if cfg.radarr.profile: + cfg['radarr']['quality'] = cfg['radarr']['profile'] + if cfg.sonarr.profile: + cfg['sonarr']['quality'] = cfg['sonarr']['profile'] # Load logger from misc.log import logger @@ -104,14 +108,24 @@ def validate_pvr(pvr, pvr_type, notifications): log.info("Validated %s URL & API Key.", pvr_type) -def get_profile_id(pvr, profile): - # retrieve profile id for requested profile - profile_id = pvr.get_profile_id(profile) - if not profile_id or not profile_id > 0: - log.error("Aborting due to failure to retrieve Profile ID for: %s", profile) +def get_quality_profile_id(pvr, quality_profile): + # retrieve profile id for requested quality profile + quality_profile_id = pvr.get_quality_profile_id(quality_profile) + if not quality_profile_id or not quality_profile_id > 0: + log.error("Aborting due to failure to retrieve Quality Profile ID for: %s", quality_profile) exit() - log.info("Retrieved Profile ID for \'%s\': %d", profile, profile_id) - return profile_id + log.info("Retrieved Quality Profile ID for \'%s\': %d", quality_profile, quality_profile_id) + return quality_profile_id + + +def get_language_profile_id(pvr, language_profile): + # retrieve profile id for requested language profile + language_profile_id = pvr.get_language_profile_id(language_profile) + if not language_profile_id or not language_profile_id > 0: + log.error("No Language Profile ID for: %s", language_profile) + else: + log.info("Retrieved Language Profile ID for \'%s\': %d", language_profile, language_profile_id) + return language_profile_id def get_profile_tags(pvr): @@ -188,8 +202,11 @@ def show(show_id, folder=None, no_search=False): log.info("Retrieved Trakt show information for \'%s\': \'%s (%s)\'", show_id, series_title, series_year) - # profile id - profile_id = get_profile_id(sonarr, cfg.sonarr.profile) + # quality profile id + quality_profile_id = get_quality_profile_id(sonarr, cfg.sonarr.quality) + + # language profile id + language_profile_id = get_language_profile_id(sonarr, cfg.sonarr.language) # profile tags profile_tags = None @@ -214,7 +231,8 @@ def show(show_id, folder=None, no_search=False): if sonarr.add_series(trakt_show['ids']['tvdb'], series_title, trakt_show['ids']['slug'], - profile_id, + quality_profile_id, + language_profile_id, cfg.sonarr.root_folder, use_tags, not no_search, @@ -331,7 +349,12 @@ def shows(list_type, add_limit=0, add_delay=2.5, sort='votes', genre=None, folde validate_trakt(trakt, notifications) validate_pvr(sonarr, 'Sonarr', notifications) - profile_id = get_profile_id(sonarr, cfg.sonarr.profile) + # quality profile id + quality_profile_id = get_quality_profile_id(sonarr, cfg.sonarr.quality) + + # language profile id + language_profile_id = get_language_profile_id(sonarr, cfg.sonarr.language) + profile_tags = get_profile_tags(sonarr) if cfg.sonarr.tags else None pvr_objects_list = get_objects(sonarr, 'Sonarr', notifications) @@ -480,7 +503,8 @@ def shows(list_type, add_limit=0, add_delay=2.5, sort='votes', genre=None, folde if sonarr.add_series(series['show']['ids']['tvdb'], series_title, series['show']['ids']['slug'], - profile_id, + quality_profile_id, + language_profile_id, cfg.sonarr.root_folder, use_tags, not no_search, @@ -571,7 +595,8 @@ def movie(movie_id, folder=None, minimum_availability=None, no_search=False): validate_trakt(trakt, False) validate_pvr(radarr, 'Radarr', False) - profile_id = get_profile_id(radarr, cfg.radarr.profile) + # quality profile id + quality_profile_id = get_quality_profile_id(radarr, cfg.radarr.quality) # get trakt movie trakt_movie = trakt.get_movie(movie_id) @@ -590,7 +615,7 @@ def movie(movie_id, folder=None, minimum_availability=None, no_search=False): trakt_movie['title'], trakt_movie['year'], trakt_movie['ids']['slug'], - profile_id, + quality_profile_id, cfg.radarr.root_folder, cfg.radarr.minimum_availability, not no_search): @@ -717,7 +742,8 @@ def movies(list_type, add_limit=0, add_delay=2.5, sort='votes', rotten_tomatoes= validate_trakt(trakt, notifications) validate_pvr(radarr, 'Radarr', notifications) - profile_id = get_profile_id(radarr, cfg.radarr.profile) + # quality profile id + quality_profile_id = get_quality_profile_id(radarr, cfg.radarr.quality) pvr_objects_list = get_objects(radarr, 'Radarr', notifications) @@ -867,7 +893,7 @@ def movies(list_type, add_limit=0, add_delay=2.5, sort='votes', rotten_tomatoes= movie_title, movie_year, sorted_movie['movie']['ids']['slug'], - profile_id, + quality_profile_id, cfg.radarr.root_folder, cfg.radarr.minimum_availability, not no_search):