diff --git a/modules/config.py b/modules/config.py index 81540f40..86fa05a2 100644 --- a/modules/config.py +++ b/modules/config.py @@ -10,8 +10,8 @@ from modules.letterboxd import LetterboxdAPI from modules.mal import MyAnimeListAPI from modules.omdb import OMDbAPI from modules.plex import PlexAPI -from modules.radarr import RadarrAPI -from modules.sonarr import SonarrAPI +from modules.radarr import Radarr +from modules.sonarr import Sonarr from modules.tautulli import TautulliAPI from modules.tmdb import TMDbAPI from modules.trakttv import TraktAPI @@ -292,7 +292,6 @@ class Config: self.general["radarr"] = {} self.general["radarr"]["url"] = check_for_attribute(self.data, "url", parent="radarr", var_type="url", default_is_none=True) self.general["radarr"]["token"] = check_for_attribute(self.data, "token", parent="radarr", default_is_none=True) - self.general["radarr"]["version"] = check_for_attribute(self.data, "version", parent="radarr", test_list=radarr_versions, default="v3") self.general["radarr"]["add"] = check_for_attribute(self.data, "add", parent="radarr", var_type="bool", default=False) self.general["radarr"]["root_folder_path"] = check_for_attribute(self.data, "root_folder_path", parent="radarr", default_is_none=True) self.general["radarr"]["monitor"] = check_for_attribute(self.data, "monitor", parent="radarr", var_type="bool", default=True) @@ -304,7 +303,6 @@ class Config: self.general["sonarr"] = {} self.general["sonarr"]["url"] = check_for_attribute(self.data, "url", parent="sonarr", var_type="url", default_is_none=True) self.general["sonarr"]["token"] = check_for_attribute(self.data, "token", parent="sonarr", default_is_none=True) - self.general["sonarr"]["version"] = check_for_attribute(self.data, "version", parent="sonarr", test_list=sonarr_versions, default="v3") self.general["sonarr"]["add"] = check_for_attribute(self.data, "add", parent="sonarr", var_type="bool", default=False) self.general["sonarr"]["root_folder_path"] = check_for_attribute(self.data, "root_folder_path", parent="sonarr", default_is_none=True) self.general["sonarr"]["monitor"] = check_for_attribute(self.data, "monitor", parent="sonarr", test_list=sonarr_monitors, default="all") @@ -467,7 +465,6 @@ class Config: try: radarr_params["url"] = check_for_attribute(lib, "url", parent="radarr", var_type="url", default=self.general["radarr"]["url"], req_default=True, save=False) radarr_params["token"] = check_for_attribute(lib, "token", parent="radarr", default=self.general["radarr"]["token"], req_default=True, save=False) - radarr_params["version"] = check_for_attribute(lib, "version", parent="radarr", test_list=radarr_versions, default=self.general["radarr"]["version"], save=False) radarr_params["add"] = check_for_attribute(lib, "add", parent="radarr", var_type="bool", default=self.general["radarr"]["add"], save=False) radarr_params["root_folder_path"] = check_for_attribute(lib, "root_folder_path", parent="radarr", default=self.general["radarr"]["root_folder_path"], req_default=True, save=False) radarr_params["monitor"] = check_for_attribute(lib, "monitor", parent="radarr", var_type="bool", default=self.general["radarr"]["monitor"], save=False) @@ -475,7 +472,7 @@ class Config: radarr_params["quality_profile"] = check_for_attribute(lib, "quality_profile", parent="radarr", default=self.general["radarr"]["quality_profile"], req_default=True, save=False) radarr_params["tag"] = check_for_attribute(lib, "search", parent="radarr", var_type="lower_list", default=self.general["radarr"]["tag"], default_is_none=True, save=False) radarr_params["search"] = check_for_attribute(lib, "search", parent="radarr", var_type="bool", default=self.general["radarr"]["search"], save=False) - library.Radarr = RadarrAPI(radarr_params) + library.Radarr = Radarr(radarr_params) except Failed as e: util.print_multiline(e, error=True) logger.info("") @@ -491,7 +488,6 @@ class Config: try: sonarr_params["url"] = check_for_attribute(lib, "url", parent="sonarr", var_type="url", default=self.general["sonarr"]["url"], req_default=True, save=False) sonarr_params["token"] = check_for_attribute(lib, "token", parent="sonarr", default=self.general["sonarr"]["token"], req_default=True, save=False) - sonarr_params["version"] = check_for_attribute(lib, "version", parent="sonarr", test_list=sonarr_versions, default=self.general["sonarr"]["version"], save=False) sonarr_params["add"] = check_for_attribute(lib, "add", parent="sonarr", var_type="bool", default=self.general["sonarr"]["add"], save=False) sonarr_params["root_folder_path"] = check_for_attribute(lib, "root_folder_path", parent="sonarr", default=self.general["sonarr"]["root_folder_path"], req_default=True, save=False) sonarr_params["monitor"] = check_for_attribute(lib, "monitor", parent="sonarr", test_list=sonarr_monitors, default=self.general["sonarr"]["monitor"], save=False) @@ -505,7 +501,7 @@ class Config: sonarr_params["tag"] = check_for_attribute(lib, "search", parent="sonarr", var_type="lower_list", default=self.general["sonarr"]["tag"], default_is_none=True, save=False) sonarr_params["search"] = check_for_attribute(lib, "search", parent="sonarr", var_type="bool", default=self.general["sonarr"]["search"], save=False) sonarr_params["cutoff_search"] = check_for_attribute(lib, "cutoff_search", parent="sonarr", var_type="bool", default=self.general["sonarr"]["cutoff_search"], save=False) - library.Sonarr = SonarrAPI(sonarr_params, library.Plex.language) + library.Sonarr = Sonarr(sonarr_params) except Failed as e: util.print_multiline(e, error=True) logger.info("") diff --git a/modules/radarr.py b/modules/radarr.py index 1f7e3203..77585bab 100644 --- a/modules/radarr.py +++ b/modules/radarr.py @@ -1,7 +1,8 @@ -import logging, requests +import logging from modules import util from modules.util import Failed -from retrying import retry +from arrapi import RadarrAPI +from arrapi.exceptions import ArrException, Invalid logger = logging.getLogger("Plex Meta Manager") @@ -12,121 +13,48 @@ availability_translation = { "db": "preDB" } -class RadarrAPI: +class Radarr: def __init__(self, params): self.url = params["url"] self.token = params["token"] - self.version = params["version"] - self.base_url = f"{self.url}/api{'/v3' if self.version == 'v3' else ''}/" try: - result = requests.get(f"{self.base_url}system/status", params={"apikey": f"{self.token}"}).json() - except Exception: - util.print_stacktrace() - raise Failed(f"Radarr Error: Could not connect to Radarr at {self.url}") - if "error" in result and result["error"] == "Unauthorized": - raise Failed("Radarr Error: Invalid API Key") - if "version" not in result: - raise Failed("Radarr Error: Unexpected Response Check URL") + self.api = RadarrAPI(self.url, self.token) + except ArrException as e: + raise Failed(e) self.add = params["add"] self.root_folder_path = params["root_folder_path"] self.monitor = params["monitor"] self.availability = params["availability"] - self.quality_profile_id = self.get_profile_id(params["quality_profile"]) + self.quality_profile = params["quality_profile"] self.tag = params["tag"] - self.tags = self.get_tags() self.search = params["search"] - def get_profile_id(self, profile_name): - profiles = "" - for profile in self._get("qualityProfile" if self.version == "v3" else "profile"): - if len(profiles) > 0: - profiles += ", " - profiles += profile["name"] - if profile["name"] == profile_name: - return profile["id"] - raise Failed(f"Radarr Error: quality_profile: {profile_name} does not exist in radarr. Profiles available: {profiles}") - - def get_tags(self): - return {tag["label"]: tag["id"] for tag in self._get("tag")} - - def add_tags(self, tags): - added = False - for label in tags: - if str(label).lower() not in self.tags: - added = True - self._post("tag", {"label": str(label).lower()}) - if added: - self.tags = self.get_tags() - - def lookup(self, tmdb_id): - results = self._get("movie/lookup", params={"term": f"tmdb:{tmdb_id}"}) - if results: - return results[0] - else: - raise Failed(f"Sonarr Error: TMDb ID: {tmdb_id} not found") - def add_tmdb(self, tmdb_ids, **options): logger.info("") util.separator(f"Adding to Radarr", space=False, border=False) logger.info("") logger.debug(f"TMDb IDs: {tmdb_ids}") - tag_nums = [] - add_count = 0 + logger.debug("") folder = options["folder"] if "folder" in options else self.root_folder_path monitor = options["monitor"] if "monitor" in options else self.monitor - availability = options["availability"] if "availability" in options else self.availability - quality_profile_id = self.get_profile_id(options["quality"]) if "quality" in options else self.quality_profile_id + availability = availability_translation[options["availability"] if "availability" in options else self.availability] + quality_profile = options["quality"] if "quality" in options else self.quality_profile tags = options["tag"] if "tag" in options else self.tag search = options["search"] if "search" in options else self.search - if tags: - self.add_tags(tags) - tag_nums = [self.tags[label.lower()] for label in tags if label.lower() in self.tags] - for tmdb_id in tmdb_ids: - try: - movie_info = self.lookup(tmdb_id) - except Failed as e: - logger.error(e) - continue - - poster_url = None - for image in movie_info["images"]: - if "coverType" in image and image["coverType"] == "poster" and "remoteUrl" in image: - poster_url = image["remoteUrl"] - - url_json = { - "title": movie_info["title"], - f"{'qualityProfileId' if self.version == 'v3' else 'profileId'}": quality_profile_id, - "year": int(movie_info["year"]), - "tmdbid": int(tmdb_id), - "titleslug": movie_info["titleSlug"], - "minimumAvailability": availability_translation[availability], - "monitored": monitor, - "rootFolderPath": folder, - "images": [{"covertype": "poster", "url": poster_url}], - "addOptions": {"searchForMovie": search} - } - if tag_nums: - url_json["tags"] = tag_nums - response = self._post("movie", url_json) - if response.status_code < 400: - logger.info(f"Added to Radarr | {tmdb_id:<6} | {movie_info['title']}") - add_count += 1 - else: - try: - logger.error(f"Radarr Error: ({tmdb_id}) {movie_info['title']}: ({response.status_code}) {response.json()[0]['errorMessage']}") - except KeyError: - logger.debug(url_json) - logger.error(f"Radarr Error: {response.json()}") - logger.info(f"{add_count} Movie{'s' if add_count > 1 else ''} added to Radarr") - - @retry(stop_max_attempt_number=6, wait_fixed=10000) - def _get(self, url, params=None): - url_params = {"apikey": f"{self.token}"} - if params: - for param in params: - url_params[param] = params[param] - return requests.get(f"{self.base_url}{url}", params=url_params).json() - - @retry(stop_max_attempt_number=6, wait_fixed=10000) - def _post(self, url, url_json): - return requests.post(f"{self.base_url}{url}", json=url_json, params={"apikey": f"{self.token}"}) + try: + added, exists, invalid = self.api.add_multiple_movies(tmdb_ids, folder, quality_profile, monitor, search, availability, tags) + except Invalid as e: + raise Failed(f"Radarr Error: {e}") + + if len(added) > 0: + for movie in added: + logger.info(f"Added to Radarr | {movie.tmdbId:<6} | {movie.title}") + logger.info(f"{len(added)} Movie{'s' if len(added) > 1 else ''} added to Radarr") + + if len(exists) > 0: + for movie in exists: + logger.info(f"Already in Radarr | {movie.tmdbId:<6} | {movie.title}") + logger.info(f"{len(exists)} Movie{'s' if len(exists) > 1 else ''} already existing in Radarr") + + for movie in invalid: + logger.info(f"Invalid TMDb ID | {movie}") diff --git a/modules/sonarr.py b/modules/sonarr.py index 4a631216..23efc6a1 100644 --- a/modules/sonarr.py +++ b/modules/sonarr.py @@ -1,8 +1,8 @@ -import logging, requests -from json.decoder import JSONDecodeError +import logging from modules import util from modules.util import Failed -from retrying import retry +from arrapi import SonarrAPI +from arrapi.exceptions import ArrException, Invalid logger = logging.getLogger("Plex Meta Manager") @@ -18,149 +18,56 @@ monitor_translation = { "none": "none" } -class SonarrAPI: - def __init__(self, params, language): +class Sonarr: + def __init__(self, params): self.url = params["url"] self.token = params["token"] - self.version = params["version"] - self.base_url = f"{self.url}/api{'/v3/' if self.version == 'v3' else '/'}" try: - result = requests.get(f"{self.base_url}system/status", params={"apikey": f"{self.token}"}).json() - except Exception: - util.print_stacktrace() - raise Failed(f"Sonarr Error: Could not connect to Sonarr at {self.url}") - if "error" in result and result["error"] == "Unauthorized": - raise Failed("Sonarr Error: Invalid API Key") - if "version" not in result: - raise Failed("Sonarr Error: Unexpected Response Check URL") + self.api = SonarrAPI(self.url, self.token) + except ArrException as e: + raise Failed(e) self.add = params["add"] self.root_folder_path = params["root_folder_path"] self.monitor = params["monitor"] - self.quality_profile_id = self.get_profile_id(params["quality_profile"], "quality_profile") + self.quality_profile = params["quality_profile"] self.language_profile_id = None - if self.version == "v3" and params["language_profile"] is not None: - self.language_profile_id = self.get_profile_id(params["language_profile"], "language_profile") - if self.language_profile_id is None: - self.language_profile_id = 1 + self.language_profile = params["language_profile"] self.series_type = params["series_type"] self.season_folder = params["season_folder"] self.tag = params["tag"] - self.tags = self.get_tags() self.search = params["search"] self.cutoff_search = params["cutoff_search"] - self.language = language - - def get_profile_id(self, profile_name, profile_type): - profiles = "" - if profile_type == "quality_profile" and self.version == "v3": - endpoint = "qualityProfile" - elif profile_type == "language_profile": - endpoint = "languageProfile" - else: - endpoint = "profile" - for profile in self._get(endpoint): - if len(profiles) > 0: - profiles += ", " - profiles += profile["name"] - if profile["name"] == profile_name: - return profile["id"] - raise Failed(f"Sonarr Error: {profile_type}: {profile_name} does not exist in sonarr. Profiles available: {profiles}") - - def get_tags(self): - return {tag["label"]: tag["id"] for tag in self._get("tag")} - - def add_tags(self, tags): - added = False - for label in tags: - if str(label).lower() not in self.tags: - added = True - self._post("tag", {"label": str(label).lower()}) - if added: - self.tags = self.get_tags() - - def lookup(self, tvdb_id): - results = self._get("series/lookup", params={"term": f"tvdb:{tvdb_id}"}) - if results: - return results[0] - else: - raise Failed(f"Sonarr Error: TVDb ID: {tvdb_id} not found") def add_tvdb(self, tvdb_ids, **options): logger.info("") util.separator(f"Adding to Sonarr", space=False, border=False) logger.info("") logger.debug(f"TVDb IDs: {tvdb_ids}") - tag_nums = [] - add_count = 0 + logger.debug("") folder = options["folder"] if "folder" in options else self.root_folder_path - monitor = options["monitor"] if "monitor" in options else self.monitor - quality_profile_id = self.get_profile_id(options["quality"], "quality_profile") if "quality" in options else self.quality_profile_id - language_profile_id = self.get_profile_id(options["language"], "language_profile") if "quality" in options else self.language_profile_id + monitor = monitor_translation[options["monitor"] if "monitor" in options else self.monitor] + quality_profile = options["quality"] if "quality" in options else self.quality_profile + language_profile = options["language"] if "language" in options else self.language_profile + language_profile = language_profile if self.api.v3 else 1 series = options["series"] if "series" in options else self.series_type season = options["season"] if "season" in options else self.season_folder tags = options["tag"] if "tag" in options else self.tag search = options["search"] if "search" in options else self.search cutoff_search = options["cutoff_search"] if "cutoff_search" in options else self.cutoff_search - if tags: - self.add_tags(tags) - tag_nums = [self.tags[label.lower()] for label in tags if label.lower() in self.tags] - for tvdb_id in tvdb_ids: - try: - show_info = self.lookup(tvdb_id) - except Failed as e: - logger.error(e) - continue - - poster_url = None - for image in show_info["images"]: - if "coverType" in image and image["coverType"] == "poster" and "remoteUrl" in image: - poster_url = image["remoteUrl"] - - url_json = { - "title": show_info["title"], - f"{'qualityProfileId' if self.version == 'v3' else 'profileId'}": quality_profile_id, - "languageProfileId": language_profile_id, - "tvdbId": int(tvdb_id), - "titleslug": show_info["titleSlug"], - "language": self.language, - "monitored": monitor != "none", - "seasonFolder": season, - "seriesType": series, - "rootFolderPath": folder, - "seasons": [], - "images": [{"covertype": "poster", "url": poster_url}], - "addOptions": { - "searchForMissingEpisodes": search, - "searchForCutoffUnmetEpisodes": cutoff_search, - "monitor": monitor_translation[monitor] - } - } - if tag_nums: - url_json["tags"] = tag_nums - response = self._post("series", url_json) - if response.status_code < 400: - logger.info(f"Added to Sonarr | {tvdb_id:<6} | {show_info['title']}") - add_count += 1 - else: - try: - logger.error(f"Sonarr Error: ({tvdb_id}) {show_info['title']}: ({response.status_code}) {response.json()[0]['errorMessage']}") - except KeyError: - logger.debug(url_json) - logger.error(f"Sonarr Error: {response.json()}") - except JSONDecodeError: - logger.debug(url_json) - logger.error(f"Sonarr Error: {response}") + try: + added, exists, invalid = self.api.add_multiple_series(tvdb_ids, folder, quality_profile, language_profile, monitor, season, search, cutoff_search, series, tags) + except Invalid as e: + raise Failed(f"Sonarr Error: {e}") - logger.info(f"{add_count} Show{'s' if add_count > 1 else ''} added to Sonarr") + if len(added) > 0: + for series in added: + logger.info(f"Added to Sonarr | {series.tvdbId:<6} | {series.title}") + logger.info(f"{len(added)} Series added to Sonarr") - @retry(stop_max_attempt_number=6, wait_fixed=10000) - def _get(self, url, params=None): - url_params = {"apikey": f"{self.token}"} - if params: - for param in params: - url_params[param] = params[param] - return requests.get(f"{self.base_url}{url}", params=url_params).json() + if len(exists) > 0: + for series in exists: + logger.info(f"Already in Sonarr | {series.tvdbId:<6} | {series.title}") + logger.info(f"{len(exists)} Series already existing in Sonarr") - @retry(stop_max_attempt_number=6, wait_fixed=10000) - def _post(self, url, url_json): - return requests.post(f"{self.base_url}{url}", json=url_json, params={"apikey": f"{self.token}"}) + for series in invalid: + logger.info(f"Invalid TVDb ID | {series}") diff --git a/requirements.txt b/requirements.txt index 0fcb4c69..f91cf8b6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,6 +3,7 @@ PlexAPI==4.5.2 tmdbv3api==1.7.5 trakt.py==4.3.0 +arrapi==1.0.0 # More common, flexible lxml requests>=2.4.2