From a9ec3e3ebcf73496695b2f3f00c3f55bb046fb6d Mon Sep 17 00:00:00 2001 From: strNophix Date: Fri, 22 Jul 2022 13:49:21 +0200 Subject: [PATCH] Added Radarr support --- .env.example | 11 +++++++ al_arr_sync/__main__.py | 32 +++++++++++++-------- al_arr_sync/radarr.py | 64 +++++++++++++++++++++++++++++++++++++++++ al_arr_sync/sonarr.py | 11 +++++-- al_arr_sync/types.py | 8 ++++++ 5 files changed, 111 insertions(+), 15 deletions(-) create mode 100644 al_arr_sync/radarr.py diff --git a/.env.example b/.env.example index 33e87c2..cb11d87 100644 --- a/.env.example +++ b/.env.example @@ -4,9 +4,20 @@ ANILIST_USERNAME= # The Sonarr instance to sync to SONARR_API_URL= SONARR_API_KEY= + # Root folder to save the shows to SONARR_FOLDER_PATH= # IDs of the quality and language profiles to use SONARR_QUALITY_PROFILE=4 SONARR_LANGUAGE_PROFILE=1 + +# The Radarr instance to sync to +RADARR_API_URL= +RADARR_API_KEY= + +# Root folder to save the movies to +RADARR_FOLDER_PATH= + +# ID of the quality profile to use +RADARR_QUALITY_PROFILE=4 diff --git a/al_arr_sync/__main__.py b/al_arr_sync/__main__.py index 7a73880..652dace 100644 --- a/al_arr_sync/__main__.py +++ b/al_arr_sync/__main__.py @@ -1,35 +1,43 @@ import os -import typing from dotenv import load_dotenv from al_arr_sync.anilist import AniListClient from al_arr_sync.sonarr import SonarrClient -from al_arr_sync.types import AnyDict +from al_arr_sync.radarr import RadarrClient +from al_arr_sync.types import DlAutomator load_dotenv() def main() -> int: al = AniListClient() - sonarr = SonarrClient.from_env() + sonarr: DlAutomator = SonarrClient.from_env() + radarr: DlAutomator = RadarrClient.from_env() username = os.environ["ANILIST_USERNAME"] media = al.currently_watching(username) - series: typing.List[AnyDict] = [] for entry in media: media_format = entry["media"]["format"] - if media_format == "TV": - series.append(entry) + show_name = entry["media"]["title"]["english"] + + client: DlAutomator + if media_format == "TV": + client = sonarr + elif media_format == "MOVIE": + client = radarr + else: + continue + + results = client.lookup_series(show_name) + if len(results) == 0: + print(f"No results found for: {show_name}") + continue - for show in series: - show_name = show["media"]["title"]["english"] - results = sonarr.lookup_series(show_name) try: - sonarr.add_series(results[0]) - print(f"Successfully added series {show_name}") + client.add_series(results[0]) + print(f"Successfully added: {show_name}") except Exception as e: print(e) - continue return 0 diff --git a/al_arr_sync/radarr.py b/al_arr_sync/radarr.py new file mode 100644 index 0000000..a56b6e8 --- /dev/null +++ b/al_arr_sync/radarr.py @@ -0,0 +1,64 @@ +import typing +from requests import Session +from requests import PreparedRequest +import os +from urllib.parse import urljoin +from al_arr_sync.types import AnyDict + + +class RadarrClient: + def __init__(self, radarr_url: str, api_key: str) -> None: + self.radarr_url = radarr_url + self.api_key = api_key + self.http_session = Session() + + @staticmethod + def from_env() -> "RadarrClient": + return RadarrClient( + radarr_url=os.environ["RADARR_API_URL"], + api_key=os.environ["RADARR_API_KEY"], + ) + + def _prepare_request( + self, + endpoint: str, + method: str = "GET", + params: AnyDict = {}, + json: typing.Optional[AnyDict] = None, + ) -> PreparedRequest: + url = urljoin(self.radarr_url, endpoint) + headers = {"X-Api-Key": self.api_key} + + req = PreparedRequest() + req.prepare(method=method, url=url, headers=headers, params=params, json=json) + return req + + def lookup_series(self, query: str) -> typing.List[AnyDict]: + req = self._prepare_request("/api/v3/movie/lookup", params={"term": query}) + resp = self.http_session.send(req) + return resp.json() + + def add_series(self, *series: AnyDict): + for show in series: + payload: AnyDict = show.copy() + payload.update( + { + "addOptions": { + "searchForMovie": True, + "ignoreEpisodesWithFiles": False, + "ignoreEpisodesWithoutFiles": False, + }, + "rootFolderPath": os.environ["RADARR_FOLDER_PATH"], + "qualityProfileId": int(os.environ["RADARR_QUALITY_PROFILE"]), + } + ) + req = self._prepare_request("/api/v3/movie", method="POST", json=payload) + resp = self.http_session.send(req) + + if resp.status_code != 201: + resp_data = resp.json() + error_codes = map(lambda x: x["errorCode"], resp_data) + if "SeriesExistsValidator" in error_codes: + raise Exception(f"Movie already exists: {show['title']}") + + raise Exception(f"Failed to add movie: {show['title']}:\n{resp_data}") diff --git a/al_arr_sync/sonarr.py b/al_arr_sync/sonarr.py index 7dc44d9..2b34c58 100644 --- a/al_arr_sync/sonarr.py +++ b/al_arr_sync/sonarr.py @@ -33,8 +33,8 @@ class SonarrClient: req.prepare(method=method, url=url, headers=headers, params=params, json=json) return req - def lookup_series(self, title: str) -> typing.List[AnyDict]: - req = self._prepare_request("/api/v3/series/lookup", params={"term": title}) + def lookup_series(self, query: str) -> typing.List[AnyDict]: + req = self._prepare_request("/api/v3/series/lookup", params={"term": query}) resp = self.http_session.send(req) return resp.json() @@ -57,4 +57,9 @@ class SonarrClient: resp = self.http_session.send(req) if resp.status_code != 201: - raise Exception(f"Failed to add series {show['title']}:\n{resp.json()}") + resp_data = resp.json() + error_codes = map(lambda x: x["errorCode"], resp_data) + if "SeriesExistsValidator" in error_codes: + raise Exception(f"Series already exists: {show['title']}") + + raise Exception(f"Failed to add series: {show['title']}:\n{resp_data}") diff --git a/al_arr_sync/types.py b/al_arr_sync/types.py index 62b2b20..5a3ada5 100644 --- a/al_arr_sync/types.py +++ b/al_arr_sync/types.py @@ -1,3 +1,11 @@ import typing AnyDict = typing.Dict[typing.Any, typing.Any] + + +class DlAutomator(typing.Protocol): + def lookup_series(self, query: str) -> typing.List[AnyDict]: + ... + + def add_series(self, *series: AnyDict): + ...