diff --git a/src/main/resources/config-template.yaml b/src/main/resources/config-template.yaml index 7224b23..b631dfd 100644 --- a/src/main/resources/config-template.yaml +++ b/src/main/resources/config-template.yaml @@ -61,6 +61,9 @@ # tags: # - watchlistarr +## Tag to use in Radarr to bypass movie deletion +# skipTag: "keep" + ################################################################# ## Plex Configuration diff --git a/src/main/scala/PlexTokenDeleteSync.scala b/src/main/scala/PlexTokenDeleteSync.scala index 2754565..3dfcabf 100644 --- a/src/main/scala/PlexTokenDeleteSync.scala +++ b/src/main/scala/PlexTokenDeleteSync.scala @@ -69,7 +69,30 @@ object PlexTokenDeleteSync extends PlexUtils with SonarrUtils with RadarrUtils { private def deleteMovie(client: HttpClient, config: Configuration)(movie: Item): EitherT[IO, Throwable, Unit] = if (config.deleteConfiguration.movieDeleting) { - deleteFromRadarr(client, config.radarrConfiguration, config.deleteConfiguration.deleteFiles)(movie) + val tmdbId = movie.getTmdbId.getOrElse { + logger.warn(s"Unable to extract Tmdb ID from movie to delete: $movie") + 0L + } + + for { + eitherShouldSkip <- hasSkipMovieTag(client)( + config.radarrConfiguration.radarrApiKey, + config.radarrConfiguration.radarrBaseUrl, + config.radarrConfiguration.radarrSkipTag, + tmdbId + ).attempt + _ <- eitherShouldSkip match { + case Left(error) => + EitherT.liftF[IO, Throwable, Unit](IO(logger.error(s"Error in hasSkipMovieTag: ${error.getMessage}", error))) + case Right(shouldSkip) => + if (shouldSkip) { + logger.info(s"Skipping deletion of movie [${movie}] due to Skip Tag [${config.radarrConfiguration.radarrSkipTag}]") + EitherT.pure[IO, Throwable](None) + } else { + deleteFromRadarr(client, config.radarrConfiguration, config.deleteConfiguration.deleteFiles)(movie) + } + } + } yield eitherShouldSkip } else { logger.info(s"Found movie \"${movie.title}\" which is not watchlisted on Plex") EitherT.pure[IO, Throwable](()) diff --git a/src/main/scala/configuration/Configuration.scala b/src/main/scala/configuration/Configuration.scala index f176788..2a8faf2 100644 --- a/src/main/scala/configuration/Configuration.scala +++ b/src/main/scala/configuration/Configuration.scala @@ -29,7 +29,8 @@ case class RadarrConfiguration( radarrQualityProfileId: Int, radarrRootFolder: String, radarrBypassIgnored: Boolean, - radarrTagIds: Set[Int] + radarrTagIds: Set[Int], + radarrSkipTag: String ) case class PlexConfiguration( diff --git a/src/main/scala/configuration/ConfigurationRedactor.scala b/src/main/scala/configuration/ConfigurationRedactor.scala index c1ab3e1..e25ac8c 100644 --- a/src/main/scala/configuration/ConfigurationRedactor.scala +++ b/src/main/scala/configuration/ConfigurationRedactor.scala @@ -22,6 +22,7 @@ object ConfigurationRedactor { | radarrRootFolder: ${config.radarrConfiguration.radarrRootFolder} | radarrBypassIgnored: ${config.radarrConfiguration.radarrBypassIgnored} | radarrTagIds: ${config.radarrConfiguration.radarrTagIds.mkString(",")} + | radarrSkipTag: ${config.radarrConfiguration.radarrSkipTag} | | PlexConfiguration: | plexWatchlistUrls: ${config.plexConfiguration.plexWatchlistUrls.mkString(", ")} diff --git a/src/main/scala/configuration/ConfigurationUtils.scala b/src/main/scala/configuration/ConfigurationUtils.scala index dfff352..50ae4e8 100644 --- a/src/main/scala/configuration/ConfigurationUtils.scala +++ b/src/main/scala/configuration/ConfigurationUtils.scala @@ -34,7 +34,7 @@ object ConfigurationUtils { sonarrBypassIgnored = configReader.getConfigOption(Keys.sonarrBypassIgnored).exists(_.toBoolean) sonarrSeasonMonitoring = configReader.getConfigOption(Keys.sonarrSeasonMonitoring).getOrElse("all") radarrConfig <- getRadarrConfig(configReader, client) - (radarrBaseUrl, radarrApiKey, radarrQualityProfileId, radarrRootFolder, radarrTagIds) = radarrConfig + (radarrBaseUrl, radarrApiKey, radarrQualityProfileId, radarrRootFolder, radarrTagIds, radarrSkipTag) = radarrConfig radarrBypassIgnored = configReader.getConfigOption(Keys.radarrBypassIgnored).exists(_.toBoolean) plexTokens = getPlexTokens(configReader) skipFriendSync = configReader.getConfigOption(Keys.skipFriendSync).flatMap(_.toBooleanOption).getOrElse(false) @@ -66,7 +66,8 @@ object ConfigurationUtils { radarrQualityProfileId, radarrRootFolder, radarrBypassIgnored, - radarrTagIds + radarrTagIds, + radarrSkipTag ), PlexConfiguration( plexWatchlistUrls, @@ -159,8 +160,9 @@ object ConfigurationUtils { private def getRadarrConfig( configReader: ConfigurationReader, client: HttpClient - ): IO[(Uri, String, Int, String, Set[Int])] = { + ): IO[(Uri, String, Int, String, Set[Int], String)] = { val apiKey = configReader.getConfigOption(Keys.radarrApiKey).getOrElse(throwError("Unable to find radarr API key")) + val skipTag = configReader.getConfigOption(Keys.radarrSkipTag).orNull val configuredUrl = configReader.getConfigOption(Keys.radarrBaseUrl) val possibleUrls: Seq[String] = configuredUrl.map("http://" + _).toSeq ++ configuredUrl.toSeq ++ possibleLocalHosts.map(_ + ":7878") @@ -187,7 +189,7 @@ object ConfigurationUtils { .getConfigOption(Keys.radarrTags) .map(getTagIdsFromConfig(client, url, apiKey)) .getOrElse(IO.pure(Set.empty[Int])) - } yield (url, apiKey, qualityProfileId, rootFolder, tagIds) + } yield (url, apiKey, qualityProfileId, rootFolder, tagIds, skipTag) } private def getTagIdsFromConfig(client: HttpClient, url: Uri, apiKey: String)(tags: String): IO[Set[Int]] = { diff --git a/src/main/scala/configuration/Keys.scala b/src/main/scala/configuration/Keys.scala index 5e7ec4d..5bc175b 100644 --- a/src/main/scala/configuration/Keys.scala +++ b/src/main/scala/configuration/Keys.scala @@ -17,6 +17,7 @@ private[configuration] object Keys { val radarrRootFolder = "radarr.rootFolder" val radarrBypassIgnored = "radarr.bypassIgnored" val radarrTags = "radarr.tags" + val radarrSkipTag = "radarr.skipTag" val plexWatchlist1 = "plex.watchlist1" val plexWatchlist2 = "plex.watchlist2" diff --git a/src/main/scala/radarr/RadarrConversions.scala b/src/main/scala/radarr/RadarrConversions.scala index 3c838e2..4303b02 100644 --- a/src/main/scala/radarr/RadarrConversions.scala +++ b/src/main/scala/radarr/RadarrConversions.scala @@ -11,6 +11,6 @@ private[radarr] trait RadarrConversions { ) def toItem(movie: RadarrMovieExclusion): Item = toItem( - RadarrMovie(movie.movieTitle, movie.imdbId, movie.tmdbId, movie.id) + RadarrMovie(movie.movieTitle, movie.imdbId, movie.tmdbId, movie.id, List.empty) ) } diff --git a/src/main/scala/radarr/RadarrMovie.scala b/src/main/scala/radarr/RadarrMovie.scala index 4372fd2..c866b00 100644 --- a/src/main/scala/radarr/RadarrMovie.scala +++ b/src/main/scala/radarr/RadarrMovie.scala @@ -1,3 +1,3 @@ package radarr -private[radarr] case class RadarrMovie(title: String, imdbId: Option[String], tmdbId: Option[Long], id: Long) +private[radarr] case class RadarrMovie(title: String, imdbId: Option[String], tmdbId: Option[Long], id: Long, tags: List[Int]) diff --git a/src/main/scala/radarr/RadarrTag.scala b/src/main/scala/radarr/RadarrTag.scala new file mode 100644 index 0000000..84d9554 --- /dev/null +++ b/src/main/scala/radarr/RadarrTag.scala @@ -0,0 +1,3 @@ +package radarr + +private[radarr] case class RadarrTag(label: String, id: Int) \ No newline at end of file diff --git a/src/main/scala/radarr/RadarrUtils.scala b/src/main/scala/radarr/RadarrUtils.scala index 315f17b..0032c81 100644 --- a/src/main/scala/radarr/RadarrUtils.scala +++ b/src/main/scala/radarr/RadarrUtils.scala @@ -2,12 +2,11 @@ package radarr import cats.data.EitherT import cats.effect.IO -import cats.implicits._ import configuration.RadarrConfiguration import http.HttpClient -import io.circe.{Decoder, Json} import io.circe.generic.auto._ import io.circe.syntax.EncoderOps +import io.circe.{Decoder, Json} import model.Item import org.http4s.{MalformedMessageBodyFailure, Method, Uri} import org.slf4j.LoggerFactory @@ -16,6 +15,8 @@ trait RadarrUtils extends RadarrConversions { private val logger = LoggerFactory.getLogger(getClass) + private var skipTagId: Int = -1 + protected def fetchMovies( client: HttpClient )(apiKey: String, baseUrl: Uri, bypass: Boolean): EitherT[IO, Throwable, Set[Item]] = @@ -65,14 +66,47 @@ trait RadarrUtils extends RadarrConversions { } } + private def getSkipTagId(client: HttpClient)( + apiKey: String, + baseUrl: Uri, + skipTag: String + ): EitherT[IO, Throwable, Int] = { + if (this.skipTagId >= 0) return EitherT.pure[IO, Throwable](this.skipTagId) + for { + allRadarrTags <- getToArr[List[RadarrTag]](client)(baseUrl, apiKey, "tag") + skipTagId = allRadarrTags.find(t => t.label == skipTag).get.id + } yield skipTagId + } + + protected def hasSkipMovieTag(client: HttpClient)( + apiKey: String, + baseUrl: Uri, + skipTag: String, + tmdbId: Long + ): EitherT[IO, Throwable, Boolean] = { + for { + skipTagId <- this.getSkipTagId(client)(apiKey, baseUrl, skipTag) + movies <- getToArrWithParams[List[RadarrMovie]](client)(baseUrl, apiKey, "movie", Map("tmdbId" -> tmdbId.toString)) + } yield movies.head.tags.contains(skipTagId) + } + private def getToArr[T: Decoder]( client: HttpClient - )(baseUrl: Uri, apiKey: String, endpoint: String): EitherT[IO, Throwable, T] = + )(baseUrl: Uri, apiKey: String, endpoint: String): EitherT[IO, Throwable, T] = getToArrWithParams(client)(baseUrl, apiKey, endpoint, null) + + private def getToArrWithParams[T: Decoder]( + client: HttpClient + )(baseUrl: Uri, apiKey: String, endpoint: String, params: Map[String, String]): EitherT[IO, Throwable, T] = { + var uri = baseUrl / "api" / "v3" / endpoint + if (params != null) { + params.foreachEntry((k, v) => uri = uri.withQueryParam(k, v)) + } for { - response <- EitherT(client.httpRequest(Method.GET, baseUrl / "api" / "v3" / endpoint, Some(apiKey))) + response <- EitherT(client.httpRequest(Method.GET, uri, Some(apiKey))) maybeDecoded <- EitherT.pure[IO, Throwable](response.as[T]) decoded <- EitherT.fromOption[IO](maybeDecoded.toOption, new Throwable("Unable to decode response from Radarr")) } yield decoded + } private def postToArr[T: Decoder]( client: HttpClient diff --git a/src/test/scala/PlexTokenSyncSpec.scala b/src/test/scala/PlexTokenSyncSpec.scala index 29b8e93..73da2e2 100644 --- a/src/test/scala/PlexTokenSyncSpec.scala +++ b/src/test/scala/PlexTokenSyncSpec.scala @@ -48,7 +48,8 @@ class PlexTokenSyncSpec extends AnyFlatSpec with Matchers with MockFactory { radarrQualityProfileId = 1, radarrRootFolder = "/root/", radarrBypassIgnored = false, - radarrTagIds = Set(2) + radarrTagIds = Set(2), + "keep" ), PlexConfiguration( plexWatchlistUrls = Set(),