From 4f54d0f0a30a4dc04bae28f1fb1c54e0ad0efb59 Mon Sep 17 00:00:00 2001 From: Nihal Mirpuri Date: Sun, 29 Oct 2023 11:20:42 +0000 Subject: [PATCH] Skip items that have an -arr exclusion. Closes #10 (#12) --- README.md | 11 +++ docker/entrypoint.sh | 8 ++ src/main/scala/WatchlistSync.scala | 74 ++++++++++++++----- .../scala/configuration/Configuration.scala | 2 + src/main/scala/configuration/Keys.scala | 2 + src/main/scala/model/RadarrMovie.scala | 4 + 6 files changed, 81 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index d5ed92c..ebfeb21 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,15 @@ Sync plex watchlists in realtime with Sonarr and Radarr. **Requires Plex Pass** +## Why? +### Sonarr/Radarr limitation +While Sonarr and Radarr have built-in functionality (in v4 and v3 respectively), they are set at 6 hour refresh intervals, and cannot be customized + +### Ombi/Overseer limitation +While Ombi and Overseer have built-in functionality, there are two problems with this: + * They are customizable down to 5 minute intervals, so doesn't allow the "real-time" sync that Watchlistarr does + * They rely on Plex tokens, which expire and break the sync if you're not regularly logging into Ombi/Overseer + ## Getting Started The easiest way to try this code is using docker: @@ -23,10 +32,12 @@ docker run \ | SONARR_BASE_URL | http://localhost:8989 | Yes | Base URL for Sonarr, including the 'http' and port | | SONARR_QUALITY_PROFILE | 1080p | Yes | Quality profile for Sonarr, found in your Sonarr UI -> Profiles settings | | SONARR_ROOT_FOLDER | /data/ | Yes | Root folder for Sonarr | +| SONARR_BYPASS_IGNORED | true | Yes | Boolean flag to bypass tv shows that are on the Sonarr Exclusion List | | RADARR_API_KEY | 7a392fb4817a46e59f2e84e7d5f021bc | No | API key for Radarr, found in your Radarr UI -> General settings | | RADARR_BASE_URL | http://127.0.0.1:7878 | Yes | Base URL for Radarr, including the 'http' and port | | RADARR_QUALITY_PROFILE | 1080p | Yes | Quality profile for Radarr, found in your Radarr UI -> Profiles settings | | RADARR_ROOT_FOLDER | /data/ | Yes | Root folder for Radarr | +| RADARR_BYPASS_IGNORED | true | Yes | Boolean flag to bypass movies that are on the Radarr Exclusion List | | PLEX_WATCHLIST_URL_1 | https://rss.plex.tv/UUID | No | First Plex Watchlist URL | | PLEX_WATCHLIST_URL_2 | https://rss.plex.tv/UUID | Yes | Second Plex Watchlist URL (if applicable) | diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh index 849bd49..b7d52ec 100644 --- a/docker/entrypoint.sh +++ b/docker/entrypoint.sh @@ -46,4 +46,12 @@ if [ -n "$REFRESH_INTERVAL_SECONDS" ]; then CMD="$CMD -Dinterval.seconds=$REFRESH_INTERVAL_SECONDS" fi +if [ -n "$SONARR_BYPASS_IGNORED" ]; then + CMD="$CMD -Dsonarr.bypassIgnored=$SONARR_BYPASS_IGNORED" +fi + +if [ -n "$RADARR_BYPASS_IGNORED" ]; then + CMD="$CMD -Dradarr.bypassIgnored=$RADARR_BYPASS_IGNORED" +fi + exec $CMD diff --git a/src/main/scala/WatchlistSync.scala b/src/main/scala/WatchlistSync.scala index 1acebe4..80cbc50 100644 --- a/src/main/scala/WatchlistSync.scala +++ b/src/main/scala/WatchlistSync.scala @@ -11,6 +11,7 @@ import utils.{ArrUtils, HttpClient} object WatchlistSync { private val logger = LoggerFactory.getLogger(getClass) + def run(config: Configuration): IO[Unit] = { logger.debug("Starting watchlist sync") @@ -18,8 +19,8 @@ object WatchlistSync { for { watchlistDatas <- config.plexWatchlistUrls.map(fetchWatchlist(config.client)).sequence watchlistData = watchlistDatas.fold(Watchlist(Set.empty))(mergeWatchLists) - movies <- fetchMovies(config.client)(config.radarrApiKey, config.radarrBaseUrl) - series <- fetchSeries(config.client)(config.sonarrApiKey, config.sonarrBaseUrl) + movies <- fetchMovies(config.client)(config.radarrApiKey, config.radarrBaseUrl, config.radarrBypassIgnored) + series <- fetchSeries(config.client)(config.sonarrApiKey, config.sonarrBaseUrl, config.sonarrBypassIgnored) allIds = merge(movies, series) _ <- missingIds(config.client)(config)(allIds, watchlistData.items) } yield () @@ -40,29 +41,62 @@ object WatchlistSync { } } - private def fetchMovies(client: HttpClient)(apiKey: String, baseUrl: Uri): IO[List[RadarrMovie]] = - ArrUtils.getToArr(client)(baseUrl, apiKey, "movie").map { - case Right(res) => - res.as[List[RadarrMovie]].getOrElse { - logger.warn("Unable to fetch movies from Radarr - decoding failure. Returning empty list instead") + private def fetchMovies(client: HttpClient)(apiKey: String, baseUrl: Uri, bypass: Boolean): IO[List[RadarrMovie]] = + for { + movies <- ArrUtils.getToArr(client)(baseUrl, apiKey, "movie").map { + case Right(res) => + res.as[List[RadarrMovie]].getOrElse { + logger.warn("Unable to fetch movies from Radarr - decoding failure. Returning empty list instead") + List.empty + } + case Left(err) => + logger.warn(s"Received error while trying to fetch movies from Radarr: $err") List.empty + } + exclusions <- if (bypass) { + IO.pure(List.empty) + } else { + ArrUtils.getToArr(client)(baseUrl, apiKey, "exclusions").map { + case Right(res) => + res.as[List[RadarrMovieExclusion]].getOrElse { + logger.warn("Unable to fetch movie exclusions from Radarr - decoding failure. Returning empty list instead") + List.empty + } + case Left(err) => + logger.warn(s"Received error while trying to fetch movie exclusions from Radarr: $err") + List.empty } - case Left(err) => - logger.warn(s"Received error while trying to fetch movies from Radarr: $err") - throw err - } + } + } yield movies ++ exclusions.map(_.toRadarrMovie) - private def fetchSeries(client: HttpClient)(apiKey: String, baseUrl: Uri): IO[List[SonarrSeries]] = - ArrUtils.getToArr(client)(baseUrl, apiKey, "series").map { - case Right(res) => - res.as[List[SonarrSeries]].getOrElse { - logger.warn("Unable to fetch series from Sonarr - decoding failure. Returning empty list instead") + private def fetchSeries(client: HttpClient)(apiKey: String, baseUrl: Uri, bypass: Boolean): IO[List[SonarrSeries]] = + for { + shows <- ArrUtils.getToArr(client)(baseUrl, apiKey, "series").map { + case Right(res) => + res.as[List[SonarrSeries]].getOrElse { + logger.warn("Unable to fetch series from Sonarr - decoding failure. Returning empty list instead") + List.empty + } + case Left(err) => + logger.warn(s"Received error while trying to fetch movies from Radarr: $err") List.empty + } + exclusions <- if (bypass) { + IO.pure(List.empty) + } else { + ArrUtils.getToArr(client)(baseUrl, apiKey, "importlistexclusion").map { + case Right(res) => + res.as[List[SonarrSeries]].getOrElse { + logger.warn("Unable to fetch show exclusions from Sonarr - decoding failure. Returning empty list instead") + List.empty + } + case Left(err) => + logger.warn(s"Received error while trying to fetch show exclusions from Sonarr: $err") + List.empty } - case Left(err) => - logger.warn(s"Received error while trying to fetch movies from Radarr: $err") - throw err - } + } + } yield shows ++ exclusions + private def merge(r: List[RadarrMovie], s: List[SonarrSeries]): Set[String] = { val allIds = r.map(_.imdbId) ++ r.map(_.tmdbId) ++ s.map(_.imdbId) ++ s.map(_.tvdbId) diff --git a/src/main/scala/configuration/Configuration.scala b/src/main/scala/configuration/Configuration.scala index 63641ee..1e03363 100644 --- a/src/main/scala/configuration/Configuration.scala +++ b/src/main/scala/configuration/Configuration.scala @@ -18,9 +18,11 @@ class Configuration(configReader: ConfigurationReader, val client: HttpClient) { val (sonarrBaseUrl, sonarrApiKey, sonarrQualityProfileId) = getAndTestSonarrUrlAndApiKey.unsafeRunSync() val sonarrRootFolder: String = configReader.getConfigOption(Keys.sonarrRootFolder).getOrElse("/data/") + val sonarrBypassIgnored: Boolean = configReader.getConfigOption(Keys.sonarrBypassIgnored).exists(_.toBoolean) val (radarrBaseUrl, radarrApiKey, radarrQualityProfileId) = getAndTestRadarrUrlAndApiKey.unsafeRunSync() val radarrRootFolder: String = configReader.getConfigOption(Keys.radarrRootFolder).getOrElse("/data/") + val radarrBypassIgnored: Boolean = configReader.getConfigOption(Keys.radarrBypassIgnored).exists(_.toBoolean) val plexWatchlistUrls: List[Uri] = getPlexWatchlistUrls diff --git a/src/main/scala/configuration/Keys.scala b/src/main/scala/configuration/Keys.scala index c6cf928..f67c923 100644 --- a/src/main/scala/configuration/Keys.scala +++ b/src/main/scala/configuration/Keys.scala @@ -7,11 +7,13 @@ object Keys { val sonarrApiKey = "sonarr.apikey" val sonarrQualityProfile = "sonarr.qualityProfile" val sonarrRootFolder = "sonarr.rootFolder" + val sonarrBypassIgnored = "sonarr.bypassIgnored" val radarrBaseUrl = "radarr.baseUrl" val radarrApiKey = "radarr.apikey" val radarrQualityProfile = "radarr.qualityProfile" val radarrRootFolder = "radarr.rootFolder" + val radarrBypassIgnored = "radarr.bypassIgnored" val plexWatchlist1 = "plex.watchlist1" val plexWatchlist2 = "plex.watchlist2" diff --git a/src/main/scala/model/RadarrMovie.scala b/src/main/scala/model/RadarrMovie.scala index 277f026..ceec55d 100644 --- a/src/main/scala/model/RadarrMovie.scala +++ b/src/main/scala/model/RadarrMovie.scala @@ -1,3 +1,7 @@ package model case class RadarrMovie(title: String, imdbId: Option[String], tmdbId: Option[Long]) + +case class RadarrMovieExclusion(movieTitle: String, imdbId: Option[String], tmdbId: Option[Long]) { + def toRadarrMovie: RadarrMovie = RadarrMovie(this.movieTitle, this.imdbId, this.tmdbId) +}