From 5ed068322f6a276c96dedea6a12fda08242cad79 Mon Sep 17 00:00:00 2001 From: Nihal Mirpuri Date: Tue, 9 Apr 2024 08:46:52 +0100 Subject: [PATCH] scalafmt --- src/main/scala/PlexTokenDeleteSync.scala | 37 ++- src/main/scala/PlexTokenSync.scala | 59 ++-- src/main/scala/Server.scala | 31 +- .../scala/configuration/Configuration.scala | 64 ++-- .../configuration/ConfigurationRedactor.scala | 3 +- .../configuration/ConfigurationUtils.scala | 154 ++++++---- .../FileAndSystemPropertyReader.scala | 7 +- src/main/scala/configuration/Keys.scala | 30 +- src/main/scala/http/HttpClient.scala | 23 +- src/main/scala/model/Item.scala | 4 +- src/main/scala/plex/PlexUtils.scala | 158 +++++----- src/main/scala/plex/TokenWatchlistItem.scala | 10 +- src/main/scala/radarr/RadarrConversions.scala | 4 +- src/main/scala/radarr/RadarrDelete.scala | 8 +- .../scala/radarr/RadarrMovieExclusion.scala | 7 +- src/main/scala/radarr/RadarrPost.scala | 14 +- src/main/scala/radarr/RadarrUtils.scala | 47 ++- src/main/scala/sonarr/SonarrAddOptions.scala | 6 +- src/main/scala/sonarr/SonarrPost.scala | 18 +- src/main/scala/sonarr/SonarrSeries.scala | 12 +- src/main/scala/sonarr/SonarrUtils.scala | 33 ++- src/test/scala/PlexTokenSyncSpec.scala | 248 +++++++++------- .../ConfigurationUtilsSpec.scala | 236 +++++++++------ src/test/scala/plex/PlexUtilsSpec.scala | 274 +++++++++++------- src/test/scala/radarr/RadarrUtilsSpec.scala | 84 ++++-- src/test/scala/sonarr/SonarrUtilsSpec.scala | 93 ++++-- 26 files changed, 1028 insertions(+), 636 deletions(-) diff --git a/src/main/scala/PlexTokenDeleteSync.scala b/src/main/scala/PlexTokenDeleteSync.scala index 1c73d7b..e4d6523 100644 --- a/src/main/scala/PlexTokenDeleteSync.scala +++ b/src/main/scala/PlexTokenDeleteSync.scala @@ -16,23 +16,39 @@ object PlexTokenDeleteSync extends PlexUtils with SonarrUtils with RadarrUtils { def run(config: Configuration, client: HttpClient): IO[Unit] = { val result = for { selfWatchlist <- getSelfWatchlist(config.plexConfiguration, client) - othersWatchlist <- if (config.plexConfiguration.skipFriendSync) EitherT.pure[IO, Throwable](Set.empty[Item]) else getOthersWatchlist(config.plexConfiguration, client) - watchlistDatas <- EitherT[IO, Throwable, List[Set[Item]]](config.plexConfiguration.plexWatchlistUrls.map(fetchWatchlistFromRss(client)).toList.sequence.map(Right(_))) + othersWatchlist <- + if (config.plexConfiguration.skipFriendSync) EitherT.pure[IO, Throwable](Set.empty[Item]) + else getOthersWatchlist(config.plexConfiguration, client) + watchlistDatas <- EitherT[IO, Throwable, List[Set[Item]]]( + config.plexConfiguration.plexWatchlistUrls.map(fetchWatchlistFromRss(client)).toList.sequence.map(Right(_)) + ) watchlistData = watchlistDatas.flatten.toSet - moviesWithoutExclusions <- fetchMovies(client)(config.radarrConfiguration.radarrApiKey, config.radarrConfiguration.radarrBaseUrl, bypass = true) - seriesWithoutExclusions <- fetchSeries(client)(config.sonarrConfiguration.sonarrApiKey, config.sonarrConfiguration.sonarrBaseUrl, bypass = true) + moviesWithoutExclusions <- fetchMovies(client)( + config.radarrConfiguration.radarrApiKey, + config.radarrConfiguration.radarrBaseUrl, + bypass = true + ) + seriesWithoutExclusions <- fetchSeries(client)( + config.sonarrConfiguration.sonarrApiKey, + config.sonarrConfiguration.sonarrBaseUrl, + bypass = true + ) allIdsWithoutExclusions = moviesWithoutExclusions ++ seriesWithoutExclusions _ <- missingIdsOnPlex(client)(config)(allIdsWithoutExclusions, selfWatchlist ++ othersWatchlist ++ watchlistData) } yield () - result.leftMap { - err => + result + .leftMap { err => logger.warn(s"An error occurred: $err") err - }.value.map(_.getOrElse(())) + } + .value + .map(_.getOrElse(())) } - private def missingIdsOnPlex(client: HttpClient)(config: Configuration)(existingItems: Set[Item], watchlist: Set[Item]): EitherT[IO, Throwable, Set[Unit]] = { + private def missingIdsOnPlex( + client: HttpClient + )(config: Configuration)(existingItems: Set[Item], watchlist: Set[Item]): EitherT[IO, Throwable, Set[Unit]] = { for { item <- existingItems maybeExistingItem = watchlist.exists(_.matches(item)) @@ -53,13 +69,13 @@ 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)(movie) + deleteFromRadarr(client, config.radarrConfiguration)(movie) } else { logger.info(s"Found movie \"${movie.title}\" which is not watchlisted on Plex") EitherT.pure[IO, Throwable](()) } - private def deleteSeries(client: HttpClient, config: Configuration)(show: Item): EitherT[IO, Throwable, Unit] = { + private def deleteSeries(client: HttpClient, config: Configuration)(show: Item): EitherT[IO, Throwable, Unit] = if (show.ended.contains(true) && config.deleteConfiguration.endedShowDeleting) { deleteFromSonarr(client, config.sonarrConfiguration)(show) } else if (show.ended.contains(false) && config.deleteConfiguration.continuingShowDeleting) { @@ -68,6 +84,5 @@ object PlexTokenDeleteSync extends PlexUtils with SonarrUtils with RadarrUtils { logger.info(s"Found show \"${show.title}\" which is not watchlisted on Plex") EitherT[IO, Throwable, Unit](IO.pure(Right(()))) } - } } diff --git a/src/main/scala/PlexTokenSync.scala b/src/main/scala/PlexTokenSync.scala index c2b6ea7..6efb58f 100644 --- a/src/main/scala/PlexTokenSync.scala +++ b/src/main/scala/PlexTokenSync.scala @@ -17,37 +17,54 @@ object PlexTokenSync extends PlexUtils with SonarrUtils with RadarrUtils { val runTokenSync = firstRun || !config.plexConfiguration.hasPlexPass val result = for { - selfWatchlist <- if (runTokenSync) - getSelfWatchlist(config.plexConfiguration, client) - else - EitherT.pure[IO, Throwable](Set.empty[Item]) + selfWatchlist <- + if (runTokenSync) + getSelfWatchlist(config.plexConfiguration, client) + else + EitherT.pure[IO, Throwable](Set.empty[Item]) _ = if (runTokenSync) logger.info(s"Found ${selfWatchlist.size} items on user's watchlist using the plex token") - othersWatchlist <- if (config.plexConfiguration.skipFriendSync || !runTokenSync) - EitherT.pure[IO, Throwable](Set.empty[Item]) - else - getOthersWatchlist(config.plexConfiguration, client) - watchlistDatas <- EitherT[IO, Throwable, List[Set[Item]]](config.plexConfiguration.plexWatchlistUrls.map(fetchWatchlistFromRss(client)).toList.sequence.map(Right(_))) + othersWatchlist <- + if (config.plexConfiguration.skipFriendSync || !runTokenSync) + EitherT.pure[IO, Throwable](Set.empty[Item]) + else + getOthersWatchlist(config.plexConfiguration, client) + watchlistDatas <- EitherT[IO, Throwable, List[Set[Item]]]( + config.plexConfiguration.plexWatchlistUrls.map(fetchWatchlistFromRss(client)).toList.sequence.map(Right(_)) + ) watchlistData = watchlistDatas.flatten.toSet - _ = if (runTokenSync) logger.info(s"Found ${othersWatchlist.size} items on other available watchlists using the plex token") - movies <- fetchMovies(client)(config.radarrConfiguration.radarrApiKey, config.radarrConfiguration.radarrBaseUrl, config.radarrConfiguration.radarrBypassIgnored) - series <- fetchSeries(client)(config.sonarrConfiguration.sonarrApiKey, config.sonarrConfiguration.sonarrBaseUrl, config.sonarrConfiguration.sonarrBypassIgnored) + _ = if (runTokenSync) + logger.info(s"Found ${othersWatchlist.size} items on other available watchlists using the plex token") + movies <- fetchMovies(client)( + config.radarrConfiguration.radarrApiKey, + config.radarrConfiguration.radarrBaseUrl, + config.radarrConfiguration.radarrBypassIgnored + ) + series <- fetchSeries(client)( + config.sonarrConfiguration.sonarrApiKey, + config.sonarrConfiguration.sonarrBaseUrl, + config.sonarrConfiguration.sonarrBypassIgnored + ) allIds = movies ++ series _ <- missingIds(client)(config)(allIds, selfWatchlist ++ othersWatchlist ++ watchlistData) } yield () - result.leftMap { - err => + result + .leftMap { err => logger.warn(s"An error occurred: $err") err - }.value.map(_.getOrElse(())) + } + .value + .map(_.getOrElse(())) } - private def missingIds(client: HttpClient)(config: Configuration)(existingItems: Set[Item], watchlist: Set[Item]): EitherT[IO, Throwable, Set[Unit]] = { + private def missingIds( + client: HttpClient + )(config: Configuration)(existingItems: Set[Item], watchlist: Set[Item]): EitherT[IO, Throwable, Set[Unit]] = { for { watchlistedItem <- watchlist maybeExistingItem = existingItems.exists(_.matches(watchlistedItem)) - category = watchlistedItem.category + category = watchlistedItem.category task = EitherT.fromEither[IO]((maybeExistingItem, category) match { case (true, c) => logger.debug(s"$c \"${watchlistedItem.title}\" already exists in Sonarr/Radarr") @@ -58,7 +75,9 @@ object PlexTokenSync extends PlexUtils with SonarrUtils with RadarrUtils { logger.debug(s"Found show \"${watchlistedItem.title}\" which does not exist yet in Sonarr") Right(addToSonarr(client)(config.sonarrConfiguration)(watchlistedItem)) } else { - logger.debug(s"Found show \"${watchlistedItem.title}\" which does not exist yet in Sonarr, but we do not have the tvdb ID so will skip adding") + logger.debug( + s"Found show \"${watchlistedItem.title}\" which does not exist yet in Sonarr, but we do not have the tvdb ID so will skip adding" + ) Right(IO.unit) } case (false, "movie") => @@ -66,7 +85,9 @@ object PlexTokenSync extends PlexUtils with SonarrUtils with RadarrUtils { logger.debug(s"Found movie \"${watchlistedItem.title}\" which does not exist yet in Radarr") Right(addToRadarr(client)(config.radarrConfiguration)(watchlistedItem)) } else { - logger.debug(s"Found movie \"${watchlistedItem.title}\" which does not exist yet in Radarr, but we do not have the tmdb ID so will skip adding") + logger.debug( + s"Found movie \"${watchlistedItem.title}\" which does not exist yet in Radarr, but we do not have the tmdb ID so will skip adding" + ) Right(IO.unit) } diff --git a/src/main/scala/Server.scala b/src/main/scala/Server.scala index c517628..5191445 100644 --- a/src/main/scala/Server.scala +++ b/src/main/scala/Server.scala @@ -1,4 +1,3 @@ - import cats.effect._ import cats.implicits.catsSyntaxTuple3Parallel import configuration.{Configuration, ConfigurationUtils, FileAndSystemPropertyReader, SystemPropertyReader} @@ -14,16 +13,16 @@ object Server extends IOApp { override protected def reportFailure(err: Throwable): IO[Unit] = err match { case _: ClosedChannelException => IO.pure(logger.debug("Suppressing ClosedChannelException error", err)) - case _ => IO.pure(logger.error("Failure caught and handled by IOApp", err)) + case _ => IO.pure(logger.error("Failure caught and handled by IOApp", err)) } def run(args: List[String]): IO[ExitCode] = { val configReader = FileAndSystemPropertyReader - val httpClient = new HttpClient + val httpClient = new HttpClient for { initialConfig <- ConfigurationUtils.create(configReader, httpClient) - configRef <- Ref.of[IO, Configuration](initialConfig) + configRef <- Ref.of[IO, Configuration](initialConfig) result <- ( pingTokenSync(configRef, httpClient), plexTokenSync(configRef, httpClient), @@ -38,24 +37,28 @@ object Server extends IOApp { private def pingTokenSync(configRef: Ref[IO, Configuration], httpClient: HttpClient): IO[Unit] = for { config <- fetchLatestConfig(configRef) - _ <- PingTokenSync.run(config, httpClient) - _ <- IO.sleep(24.hours) - _ <- pingTokenSync(configRef, httpClient) + _ <- PingTokenSync.run(config, httpClient) + _ <- IO.sleep(24.hours) + _ <- pingTokenSync(configRef, httpClient) } yield () - private def plexTokenSync(configRef: Ref[IO, Configuration], httpClient: HttpClient, firstRun: Boolean = true): IO[Unit] = + private def plexTokenSync( + configRef: Ref[IO, Configuration], + httpClient: HttpClient, + firstRun: Boolean = true + ): IO[Unit] = for { config <- fetchLatestConfig(configRef) - _ <- PlexTokenSync.run(config, httpClient, firstRun) - _ <- IO.sleep(config.refreshInterval) - _ <- plexTokenSync(configRef, httpClient, firstRun = false) + _ <- PlexTokenSync.run(config, httpClient, firstRun) + _ <- IO.sleep(config.refreshInterval) + _ <- plexTokenSync(configRef, httpClient, firstRun = false) } yield () private def plexTokenDeleteSync(configRef: Ref[IO, Configuration], httpClient: HttpClient): IO[Unit] = for { config <- fetchLatestConfig(configRef) - _ <- PlexTokenDeleteSync.run(config, httpClient) - _ <- IO.sleep(config.deleteConfiguration.deleteInterval) - _ <- plexTokenDeleteSync(configRef, httpClient) + _ <- PlexTokenDeleteSync.run(config, httpClient) + _ <- IO.sleep(config.deleteConfiguration.deleteInterval) + _ <- plexTokenDeleteSync(configRef, httpClient) } yield () } diff --git a/src/main/scala/configuration/Configuration.scala b/src/main/scala/configuration/Configuration.scala index dd9b8c7..632ae42 100644 --- a/src/main/scala/configuration/Configuration.scala +++ b/src/main/scala/configuration/Configuration.scala @@ -5,43 +5,43 @@ import org.http4s.Uri import scala.concurrent.duration.FiniteDuration case class Configuration( - refreshInterval: FiniteDuration, - sonarrConfiguration: SonarrConfiguration, - radarrConfiguration: RadarrConfiguration, - plexConfiguration: PlexConfiguration, - deleteConfiguration: DeleteConfiguration - ) + refreshInterval: FiniteDuration, + sonarrConfiguration: SonarrConfiguration, + radarrConfiguration: RadarrConfiguration, + plexConfiguration: PlexConfiguration, + deleteConfiguration: DeleteConfiguration +) case class SonarrConfiguration( - sonarrBaseUrl: Uri, - sonarrApiKey: String, - sonarrQualityProfileId: Int, - sonarrRootFolder: String, - sonarrBypassIgnored: Boolean, - sonarrSeasonMonitoring: String, - sonarrLanguageProfileId: Int, - sonarrTagIds: Set[Int] - ) + sonarrBaseUrl: Uri, + sonarrApiKey: String, + sonarrQualityProfileId: Int, + sonarrRootFolder: String, + sonarrBypassIgnored: Boolean, + sonarrSeasonMonitoring: String, + sonarrLanguageProfileId: Int, + sonarrTagIds: Set[Int] +) case class RadarrConfiguration( - radarrBaseUrl: Uri, - radarrApiKey: String, - radarrQualityProfileId: Int, - radarrRootFolder: String, - radarrBypassIgnored: Boolean, - radarrTagIds: Set[Int] - ) + radarrBaseUrl: Uri, + radarrApiKey: String, + radarrQualityProfileId: Int, + radarrRootFolder: String, + radarrBypassIgnored: Boolean, + radarrTagIds: Set[Int] +) case class PlexConfiguration( - plexWatchlistUrls: Set[Uri], - plexTokens: Set[String], - skipFriendSync: Boolean, - hasPlexPass: Boolean - ) + plexWatchlistUrls: Set[Uri], + plexTokens: Set[String], + skipFriendSync: Boolean, + hasPlexPass: Boolean +) case class DeleteConfiguration( - movieDeleting: Boolean, - endedShowDeleting: Boolean, - continuingShowDeleting: Boolean, - deleteInterval: FiniteDuration - ) + movieDeleting: Boolean, + endedShowDeleting: Boolean, + continuingShowDeleting: Boolean, + deleteInterval: FiniteDuration +) diff --git a/src/main/scala/configuration/ConfigurationRedactor.scala b/src/main/scala/configuration/ConfigurationRedactor.scala index 2215b19..e2efdc4 100644 --- a/src/main/scala/configuration/ConfigurationRedactor.scala +++ b/src/main/scala/configuration/ConfigurationRedactor.scala @@ -1,7 +1,7 @@ package configuration object ConfigurationRedactor { - def redactToString(config: Configuration): String = { + def redactToString(config: Configuration): String = s""" |Configuration: | refreshInterval: ${config.refreshInterval.toSeconds} seconds @@ -36,5 +36,4 @@ object ConfigurationRedactor { | deleteInterval: ${config.deleteConfiguration.deleteInterval.toDays} days | |""".stripMargin - } } diff --git a/src/main/scala/configuration/ConfigurationUtils.scala b/src/main/scala/configuration/ConfigurationUtils.scala index 554ebaa..5eaf4ac 100644 --- a/src/main/scala/configuration/ConfigurationUtils.scala +++ b/src/main/scala/configuration/ConfigurationUtils.scala @@ -29,20 +29,24 @@ object ConfigurationUtils { val config = for { sonarrConfig <- getSonarrConfig(configReader, client) refreshInterval = configReader.getConfigOption(Keys.intervalSeconds).flatMap(_.toIntOption).getOrElse(60).seconds - (sonarrBaseUrl, sonarrApiKey, sonarrQualityProfileId, sonarrRootFolder, sonarrLanguageProfileId, sonarrTagIds) = sonarrConfig - sonarrBypassIgnored = configReader.getConfigOption(Keys.sonarrBypassIgnored).exists(_.toBoolean) + (sonarrBaseUrl, sonarrApiKey, sonarrQualityProfileId, sonarrRootFolder, sonarrLanguageProfileId, sonarrTagIds) = + sonarrConfig + sonarrBypassIgnored = configReader.getConfigOption(Keys.sonarrBypassIgnored).exists(_.toBoolean) sonarrSeasonMonitoring = configReader.getConfigOption(Keys.sonarrSeasonMonitoring).getOrElse("all") radarrConfig <- getRadarrConfig(configReader, client) (radarrBaseUrl, radarrApiKey, radarrQualityProfileId, radarrRootFolder, radarrTagIds) = radarrConfig radarrBypassIgnored = configReader.getConfigOption(Keys.radarrBypassIgnored).exists(_.toBoolean) - plexTokens = getPlexTokens(configReader) + plexTokens = getPlexTokens(configReader) skipFriendSync = configReader.getConfigOption(Keys.skipFriendSync).flatMap(_.toBooleanOption).getOrElse(false) plexWatchlistUrls <- getPlexWatchlistUrls(client)(configReader, plexTokens, skipFriendSync) - deleteMovies = configReader.getConfigOption(Keys.deleteMovies).flatMap(_.toBooleanOption).getOrElse(false) + deleteMovies = configReader.getConfigOption(Keys.deleteMovies).flatMap(_.toBooleanOption).getOrElse(false) deleteEndedShows = configReader.getConfigOption(Keys.deleteEndedShow).flatMap(_.toBooleanOption).getOrElse(false) - deleteContinuingShows = configReader.getConfigOption(Keys.deleteContinuingShow).flatMap(_.toBooleanOption).getOrElse(false) + deleteContinuingShows = configReader + .getConfigOption(Keys.deleteContinuingShow) + .flatMap(_.toBooleanOption) + .getOrElse(false) deleteInterval = configReader.getConfigOption(Keys.deleteIntervalDays).flatMap(_.toIntOption).getOrElse(7).days - hasPlexPass = plexWatchlistUrls.nonEmpty + hasPlexPass = plexWatchlistUrls.nonEmpty } yield Configuration( if (hasPlexPass) refreshInterval else 19.minutes, SonarrConfiguration( @@ -106,10 +110,14 @@ object ConfigurationUtils { } } - private def getSonarrConfig(configReader: ConfigurationReader, client: HttpClient): IO[(Uri, String, Int, String, Int, Set[Int])] = { + private def getSonarrConfig( + configReader: ConfigurationReader, + client: HttpClient + ): IO[(Uri, String, Int, String, Int, Set[Int])] = { val apiKey = configReader.getConfigOption(Keys.sonarrApiKey).getOrElse(throwError("Unable to find sonarr API key")) val configuredUrl = configReader.getConfigOption(Keys.sonarrBaseUrl) - val possibleUrls: Seq[String] = configuredUrl.map("http://" + _).toSeq ++ configuredUrl.toSeq ++ possibleLocalHosts.map(_ + ":8989") + val possibleUrls: Seq[String] = + configuredUrl.map("http://" + _).toSeq ++ configuredUrl.toSeq ++ possibleLocalHosts.map(_ + ":8989") for { url <- findCorrectUrl(client)(possibleUrls, apiKey) @@ -123,7 +131,7 @@ object ConfigurationUtils { } qualityProfileId <- toArr(client)(url, apiKey, "qualityprofile").map { case Right(res) => - val allQualityProfiles = res.as[List[QualityProfile]].getOrElse(List.empty) + val allQualityProfiles = res.as[List[QualityProfile]].getOrElse(List.empty) val chosenQualityProfile = configReader.getConfigOption(Keys.sonarrQualityProfile) getQualityProfileId(allQualityProfiles, chosenQualityProfile) case Left(err) => @@ -139,14 +147,21 @@ object ConfigurationUtils { case Left(err) => throwError(s"Unable to connect to Sonarr at $url, with error $err") } - tagIds <- configReader.getConfigOption(Keys.sonarrTags).map(getTagIdsFromConfig(client, url, apiKey)).getOrElse(IO.pure(Set.empty[Int])) + tagIds <- configReader + .getConfigOption(Keys.sonarrTags) + .map(getTagIdsFromConfig(client, url, apiKey)) + .getOrElse(IO.pure(Set.empty[Int])) } yield (url, apiKey, qualityProfileId, rootFolder, languageProfileId, tagIds) } - private def getRadarrConfig(configReader: ConfigurationReader, client: HttpClient): IO[(Uri, String, Int, String, Set[Int])] = { + private def getRadarrConfig( + configReader: ConfigurationReader, + client: HttpClient + ): IO[(Uri, String, Int, String, Set[Int])] = { val apiKey = configReader.getConfigOption(Keys.radarrApiKey).getOrElse(throwError("Unable to find radarr API key")) val configuredUrl = configReader.getConfigOption(Keys.radarrBaseUrl) - val possibleUrls: Seq[String] = configuredUrl.map("http://" + _).toSeq ++ configuredUrl.toSeq ++ possibleLocalHosts.map(_ + ":7878") + val possibleUrls: Seq[String] = + configuredUrl.map("http://" + _).toSeq ++ configuredUrl.toSeq ++ possibleLocalHosts.map(_ + ":7878") for { url <- findCorrectUrl(client)(possibleUrls, apiKey) @@ -160,32 +175,40 @@ object ConfigurationUtils { } qualityProfileId <- toArr(client)(url, apiKey, "qualityprofile").map { case Right(res) => - val allQualityProfiles = res.as[List[QualityProfile]].getOrElse(List.empty) + val allQualityProfiles = res.as[List[QualityProfile]].getOrElse(List.empty) val chosenQualityProfile = configReader.getConfigOption(Keys.radarrQualityProfile) getQualityProfileId(allQualityProfiles, chosenQualityProfile) case Left(err) => throwError(s"Unable to connect to Radarr at $url, with error $err") } - tagIds <- configReader.getConfigOption(Keys.radarrTags).map(getTagIdsFromConfig(client, url, apiKey)).getOrElse(IO.pure(Set.empty[Int])) + tagIds <- configReader + .getConfigOption(Keys.radarrTags) + .map(getTagIdsFromConfig(client, url, apiKey)) + .getOrElse(IO.pure(Set.empty[Int])) } yield (url, apiKey, qualityProfileId, rootFolder, tagIds) } private def getTagIdsFromConfig(client: HttpClient, url: Uri, apiKey: String)(tags: String): IO[Set[Int]] = { val tagsSplit = tags.split(',').map(_.trim).toSet - tagsSplit.map { tagName => - val json = Json.obj(("label", Json.fromString(tagName.toLowerCase))) - logger.info(s"Fetching information for tag: $tagName") - toArr(client)(url, apiKey, "tag", Some(json)).map { - case Left(err) => - logger.warn(s"Attempted to set a tag in an Arr app but got the error: $err") - None - case Right(result) => - result.hcursor.get[Int]("id").toOption + tagsSplit + .map { tagName => + val json = Json.obj(("label", Json.fromString(tagName.toLowerCase))) + logger.info(s"Fetching information for tag: $tagName") + toArr(client)(url, apiKey, "tag", Some(json)).map { + case Left(err) => + logger.warn(s"Attempted to set a tag in an Arr app but got the error: $err") + None + case Right(result) => + result.hcursor.get[Int]("id").toOption + } } - }.toList.sequence.map(_.collect { - case Some(r) => r - }).map(_.toSet) + .toList + .sequence + .map(_.collect { case Some(r) => + r + }) + .map(_.toSet) } private def getQualityProfileId(allProfiles: List[QualityProfile], maybeEnvVariable: Option[String]): Int = @@ -199,9 +222,12 @@ object ConfigurationUtils { logger.debug("Multiple quality profiles found, selecting the first one in the list.") allProfiles.head.id case (_, Some(profileName)) => - allProfiles.find(_.name.toLowerCase == profileName.toLowerCase).map(_.id).getOrElse( - throwError(s"Unable to find quality profile $profileName. Possible values are $allProfiles") - ) + allProfiles + .find(_.name.toLowerCase == profileName.toLowerCase) + .map(_.id) + .getOrElse( + throwError(s"Unable to find quality profile $profileName. Possible values are $allProfiles") + ) } private def selectRootFolder(allRootFolders: List[RootFolder], maybeEnvVariable: Option[String]): String = @@ -209,41 +235,57 @@ object ConfigurationUtils { case (Nil, _) => throwError("Could not find any root folders, check your Sonarr/Radarr settings") case (_, Some(path)) => - allRootFolders.filter(_.accessible).find(r => normalizePath(r.path) == normalizePath(path)).map(_.path).getOrElse( - throwError(s"Unable to find root folder $path. Possible values are ${allRootFolders.filter(_.accessible).map(_.path)}") - ) + allRootFolders + .filter(_.accessible) + .find(r => normalizePath(r.path) == normalizePath(path)) + .map(_.path) + .getOrElse( + throwError( + s"Unable to find root folder $path. Possible values are ${allRootFolders.filter(_.accessible).map(_.path)}" + ) + ) case (_, None) => - allRootFolders.find(_.accessible).map(_.path).getOrElse( - throwError("Found root folders, but they are not accessible by Sonarr/Radarr") - ) + allRootFolders + .find(_.accessible) + .map(_.path) + .getOrElse( + throwError("Found root folders, but they are not accessible by Sonarr/Radarr") + ) } private def normalizePath(path: String): String = (if (path.endsWith("/") && path.length > 1) path.dropRight(1) else path) .replace("//", "/") - private def getPlexWatchlistUrls(client: HttpClient)(configReader: ConfigurationReader, tokens: Set[String], skipFriendSync: Boolean): IO[Set[Uri]] = { + private def getPlexWatchlistUrls( + client: HttpClient + )(configReader: ConfigurationReader, tokens: Set[String], skipFriendSync: Boolean): IO[Set[Uri]] = { val watchlistsFromConfigDeprecated = Set( configReader.getConfigOption(Keys.plexWatchlist1), configReader.getConfigOption(Keys.plexWatchlist2) - ).collect { - case Some(url) => url + ).collect { case Some(url) => + url } - val watchlistsFromTokenIo = tokens.map { token => - for { - selfWatchlist <- getRssFromPlexToken(client)(token, "watchlist") - _ = logger.info(s"Generated watchlist RSS feed for self: $selfWatchlist") - otherWatchlist <- if (skipFriendSync) - IO.pure(None) - else { - getRssFromPlexToken(client)(token, "friendsWatchlist") + val watchlistsFromTokenIo = tokens + .map { token => + for { + selfWatchlist <- getRssFromPlexToken(client)(token, "watchlist") + _ = logger.info(s"Generated watchlist RSS feed for self: $selfWatchlist") + otherWatchlist <- + if (skipFriendSync) + IO.pure(None) + else { + getRssFromPlexToken(client)(token, "friendsWatchlist") + } + _ = logger.info(s"Generated watchlist RSS feed for friends: $otherWatchlist") + } yield Set(selfWatchlist, otherWatchlist).collect { case Some(url) => + url } - _ = logger.info(s"Generated watchlist RSS feed for friends: $otherWatchlist") - } yield Set(selfWatchlist, otherWatchlist).collect { - case Some(url) => url } - }.toList.sequence.map(_.flatten) + .toList + .sequence + .map(_.flatten) watchlistsFromTokenIo.map { watchlistsFromToken => (watchlistsFromConfigDeprecated ++ watchlistsFromToken).toList match { @@ -271,9 +313,11 @@ object ConfigurationUtils { "rss.plex.tv" ).map(Uri.Host.unsafeFromString) - val rawUri = Uri.fromString(url).getOrElse( - throwError(s"Plex watchlist $url is not a valid uri") - ) + val rawUri = Uri + .fromString(url) + .getOrElse( + throwError(s"Plex watchlist $url is not a valid uri") + ) val host = rawUri.host.getOrElse( throwError(s"Plex watchlist host not found in $rawUri") @@ -290,7 +334,9 @@ object ConfigurationUtils { throw new IllegalArgumentException(message) } - private def toArr(client: HttpClient)(baseUrl: Uri, apiKey: String, endpoint: String, payload: Option[Json] = None): IO[Either[Throwable, Json]] = + private def toArr( + client: HttpClient + )(baseUrl: Uri, apiKey: String, endpoint: String, payload: Option[Json] = None): IO[Either[Throwable, Json]] = payload match { case None => client.httpRequest(Method.GET, baseUrl / "api" / "v3" / endpoint, Some(apiKey)) diff --git a/src/main/scala/configuration/FileAndSystemPropertyReader.scala b/src/main/scala/configuration/FileAndSystemPropertyReader.scala index be4b970..e911266 100644 --- a/src/main/scala/configuration/FileAndSystemPropertyReader.scala +++ b/src/main/scala/configuration/FileAndSystemPropertyReader.scala @@ -13,7 +13,7 @@ object FileAndSystemPropertyReader extends ConfigurationReader { private val logger = LoggerFactory.getLogger(getClass) private lazy val data: Map[String, String] = { - val yaml = new Yaml() + val yaml = new Yaml() val configFile = new File(SystemPropertyReader.getConfigOption("configPath").getOrElse(s"config/config.yaml")) try { @@ -27,9 +27,8 @@ object FileAndSystemPropertyReader extends ConfigurationReader { try { Files.copy(resourceStream, Paths.get(configFile.toURI), StandardCopyOption.REPLACE_EXISTING) logger.info(s"Created config file in ${configFile.getPath}") - } finally { + } finally resourceStream.close() - } } else { logger.debug("config-template.yaml resource not found") } @@ -37,7 +36,7 @@ object FileAndSystemPropertyReader extends ConfigurationReader { if (configFile.exists()) { val inputStream = new FileInputStream(configFile) - val result = yaml.load[util.Map[String, Object]](inputStream).asScala + val result = yaml.load[util.Map[String, Object]](inputStream).asScala inputStream.close() flattenYaml(Map.from(result)) } else { diff --git a/src/main/scala/configuration/Keys.scala b/src/main/scala/configuration/Keys.scala index a6bf11a..af609cc 100644 --- a/src/main/scala/configuration/Keys.scala +++ b/src/main/scala/configuration/Keys.scala @@ -3,28 +3,28 @@ package configuration private[configuration] object Keys { val intervalSeconds = "interval.seconds" - val sonarrBaseUrl = "sonarr.baseUrl" - val sonarrApiKey = "sonarr.apikey" - val sonarrQualityProfile = "sonarr.qualityProfile" - val sonarrRootFolder = "sonarr.rootFolder" - val sonarrBypassIgnored = "sonarr.bypassIgnored" + val sonarrBaseUrl = "sonarr.baseUrl" + val sonarrApiKey = "sonarr.apikey" + val sonarrQualityProfile = "sonarr.qualityProfile" + val sonarrRootFolder = "sonarr.rootFolder" + val sonarrBypassIgnored = "sonarr.bypassIgnored" val sonarrSeasonMonitoring = "sonarr.seasonMonitoring" - val sonarrTags = "sonarr.tags" + val sonarrTags = "sonarr.tags" - val radarrBaseUrl = "radarr.baseUrl" - val radarrApiKey = "radarr.apikey" + val radarrBaseUrl = "radarr.baseUrl" + val radarrApiKey = "radarr.apikey" val radarrQualityProfile = "radarr.qualityProfile" - val radarrRootFolder = "radarr.rootFolder" - val radarrBypassIgnored = "radarr.bypassIgnored" - val radarrTags = "radarr.tags" + val radarrRootFolder = "radarr.rootFolder" + val radarrBypassIgnored = "radarr.bypassIgnored" + val radarrTags = "radarr.tags" val plexWatchlist1 = "plex.watchlist1" val plexWatchlist2 = "plex.watchlist2" - val plexToken = "plex.token" + val plexToken = "plex.token" val skipFriendSync = "plex.skipfriendsync" - val deleteIntervalDays = "delete.interval.days" - val deleteMovies = "delete.movie" - val deleteEndedShow = "delete.endedShow" + val deleteIntervalDays = "delete.interval.days" + val deleteMovies = "delete.movie" + val deleteEndedShow = "delete.endedShow" val deleteContinuingShow = "delete.continuingShow" } diff --git a/src/main/scala/http/HttpClient.scala b/src/main/scala/http/HttpClient.scala index b8a371a..9e425d9 100644 --- a/src/main/scala/http/HttpClient.scala +++ b/src/main/scala/http/HttpClient.scala @@ -10,10 +10,17 @@ import org.typelevel.ci.CIString class HttpClient { - val client = EmberClientBuilder.default[IO].build + val client = EmberClientBuilder + .default[IO] + .build .map(FollowRedirect(5)) - def httpRequest(method: Method, url: Uri, apiKey: Option[String] = None, payload: Option[Json] = None): IO[Either[Throwable, Json]] = { + def httpRequest( + method: Method, + url: Uri, + apiKey: Option[String] = None, + payload: Option[Json] = None + ): IO[Either[Throwable, Json]] = { val host = s"${url.host.getOrElse(Uri.Host.unsafeFromString("127.0.0.1")).value}" val baseRequest = Request[IO](method = method, uri = url) @@ -22,11 +29,13 @@ class HttpClient { Header.Raw(CIString("Content-Type"), "application/json"), Header.Raw(CIString("Host"), host) ) - val requestWithApiKey = apiKey.fold(baseRequest)(key => baseRequest.withHeaders( - Header.Raw(CIString("X-Api-Key"), key), - Header.Raw(CIString("X-Plex-Token"), key), - baseRequest.headers - )) + val requestWithApiKey = apiKey.fold(baseRequest)(key => + baseRequest.withHeaders( + Header.Raw(CIString("X-Api-Key"), key), + Header.Raw(CIString("X-Plex-Token"), key), + baseRequest.headers + ) + ) val requestWithPayload = payload.fold(requestWithApiKey)(p => requestWithApiKey.withEntity(p)) client.use(_.expect[Json](requestWithPayload).attempt) diff --git a/src/main/scala/model/Item.scala b/src/main/scala/model/Item.scala index ac88bac..9cc67f8 100644 --- a/src/main/scala/model/Item.scala +++ b/src/main/scala/model/Item.scala @@ -15,8 +15,8 @@ case class Item(title: String, guids: List[String], category: String, ended: Opt def matches(that: Any): Boolean = that match { case Item(_, theirGuids, c, _) if c == this.category => - theirGuids.foldLeft(false) { - case (acc, guid) => acc || guids.contains(guid) + theirGuids.foldLeft(false) { case (acc, guid) => + acc || guids.contains(guid) } case _ => false } diff --git a/src/main/scala/plex/PlexUtils.scala b/src/main/scala/plex/PlexUtils.scala index ee54c8a..304def9 100644 --- a/src/main/scala/plex/PlexUtils.scala +++ b/src/main/scala/plex/PlexUtils.scala @@ -17,8 +17,7 @@ trait PlexUtils { private val logger = LoggerFactory.getLogger(getClass) implicit val customConfig: extras.Configuration = - extras.Configuration.default - .withDefaults + extras.Configuration.default.withDefaults protected def fetchWatchlistFromRss(client: HttpClient)(url: Uri): IO[Set[Item]] = client.httpRequest(Method.GET, url).map { @@ -33,7 +32,7 @@ trait PlexUtils { } } - protected def ping(client: HttpClient)(config: PlexConfiguration): IO[Unit] = { + protected def ping(client: HttpClient)(config: PlexConfiguration): IO[Unit] = config.plexTokens.map { token => val url = Uri .unsafeFromString("https://plex.tv/api/v2/ping") @@ -46,41 +45,54 @@ trait PlexUtils { case Left(err) => logger.warn(s"Unable to ping plex.tv to update access token expiry: $err") } - } - }.toList.sequence.map(_ => ()) - - protected def getSelfWatchlist(config: PlexConfiguration, client: HttpClient, containerStart: Int = 0): EitherT[IO, Throwable, Set[Item]] = config.plexTokens.map { token => - val containerSize = 300 - val url = Uri - .unsafeFromString("https://metadata.provider.plex.tv/library/sections/watchlist/all") - .withQueryParam("X-Plex-Token", token) - .withQueryParam("X-Plex-Container-Start", containerStart) - .withQueryParam("X-Plex-Container-Size", containerSize) + }.toList.sequence.map(_ => ()) + + protected def getSelfWatchlist( + config: PlexConfiguration, + client: HttpClient, + containerStart: Int = 0 + ): EitherT[IO, Throwable, Set[Item]] = config.plexTokens + .map { token => + val containerSize = 300 + val url = Uri + .unsafeFromString("https://metadata.provider.plex.tv/library/sections/watchlist/all") + .withQueryParam("X-Plex-Token", token) + .withQueryParam("X-Plex-Container-Start", containerStart) + .withQueryParam("X-Plex-Container-Size", containerSize) - for { - response <- EitherT(client.httpRequest(Method.GET, url)) - tokenWatchlist <- EitherT(IO.pure(response.as[TokenWatchlist])).leftMap(err => new Throwable(err)) - result <- EitherT.liftF(toItems(config, client)(tokenWatchlist)) - nextPage <- if (tokenWatchlist.MediaContainer.totalSize > containerStart + containerSize) - getSelfWatchlist(config, client, containerStart + containerSize) - else - EitherT.pure[IO, Throwable](Set.empty[Item]) - } yield result ++ nextPage - }.toList.sequence.map(_.toSet.flatten) + for { + response <- EitherT(client.httpRequest(Method.GET, url)) + tokenWatchlist <- EitherT(IO.pure(response.as[TokenWatchlist])).leftMap(err => new Throwable(err)) + result <- EitherT.liftF(toItems(config, client)(tokenWatchlist)) + nextPage <- + if (tokenWatchlist.MediaContainer.totalSize > containerStart + containerSize) + getSelfWatchlist(config, client, containerStart + containerSize) + else + EitherT.pure[IO, Throwable](Set.empty[Item]) + } yield result ++ nextPage + } + .toList + .sequence + .map(_.toSet.flatten) protected def getOthersWatchlist(config: PlexConfiguration, client: HttpClient): EitherT[IO, Throwable, Set[Item]] = for { friends <- getFriends(config, client) - watchlistItems <- friends.map { case (friend, token) => getWatchlistIdsForUser(config, client, token)(friend) }.toList.sequence.map(_.flatten) + watchlistItems <- friends + .map { case (friend, token) => getWatchlistIdsForUser(config, client, token)(friend) } + .toList + .sequence + .map(_.flatten) items <- watchlistItems.map(i => toItems(config, client, i)).sequence.map(_.toSet) } yield items - protected def getFriends(config: PlexConfiguration, client: HttpClient): EitherT[IO, Throwable, Set[(User, String)]] = config.plexTokens.map { token => - val url = Uri - .unsafeFromString("https://community.plex.tv/api") + protected def getFriends(config: PlexConfiguration, client: HttpClient): EitherT[IO, Throwable, Set[(User, String)]] = + config.plexTokens + .map { token => + val url = Uri + .unsafeFromString("https://community.plex.tv/api") - val query = GraphQLQuery( - """query GetAllFriends { + val query = GraphQLQuery("""query GetAllFriends { | allFriendsV2 { | user { | id @@ -89,19 +101,25 @@ trait PlexUtils { | } | }""".stripMargin) - EitherT(client.httpRequest(Method.POST, url, Some(token), Some(query.asJson)).map { - case Left(err) => - logger.warn(s"Unable to fetch friends from Plex: $err") - Left(err) - case Right(json) => - json.as[Users] match { - case Right(v) => Right(v.data.allFriendsV2.map(_.user).toSet).map(_.map(u => (u, token))) - case Left(v) => Left(new Throwable(v)) - } - }) - }.toList.sequence.map(_.toSet.flatten) - - protected def getWatchlistIdsForUser(config: PlexConfiguration, client: HttpClient, token: String)(user: User, page: Option[String] = None): EitherT[IO, Throwable, Set[TokenWatchlistItem]] = { + EitherT(client.httpRequest(Method.POST, url, Some(token), Some(query.asJson)).map { + case Left(err) => + logger.warn(s"Unable to fetch friends from Plex: $err") + Left(err) + case Right(json) => + json.as[Users] match { + case Right(v) => Right(v.data.allFriendsV2.map(_.user).toSet).map(_.map(u => (u, token))) + case Left(v) => Left(new Throwable(v)) + } + }) + } + .toList + .sequence + .map(_.toSet.flatten) + + protected def getWatchlistIdsForUser(config: PlexConfiguration, client: HttpClient, token: String)( + user: User, + page: Option[String] = None + ): EitherT[IO, Throwable, Set[TokenWatchlistItem]] = { val url = Uri.unsafeFromString("https://community.plex.tv/api") val query = GraphQLQuery( @@ -124,47 +142,55 @@ trait PlexUtils { type }""".stripMargin, if (page.isEmpty) { - Some( - s"""{ + Some(s"""{ | "first": 100, | "uuid": "${user.id}" |}""".stripMargin.asJson) } else { - Some( - s"""{ + Some(s"""{ | "first": 100, | "after": "${page.getOrElse("")}", | "uuid": "${user.id}" |}""".stripMargin.asJson) - }) + } + ) for { responseJson <- EitherT(client.httpRequest(Method.POST, url, Some(token), Some(query.asJson))) - watchlist <- EitherT.fromEither[IO](responseJson.as[TokenWatchlistFriend]).leftMap(new Throwable(_)) - extraContent <- if (watchlist.data.user.watchlist.pageInfo.hasNextPage && watchlist.data.user.watchlist.pageInfo.endCursor.nonEmpty) - getWatchlistIdsForUser(config, client, token)(user, watchlist.data.user.watchlist.pageInfo.endCursor) - else - EitherT.pure[IO, Throwable](Set.empty[TokenWatchlistItem]) + watchlist <- EitherT.fromEither[IO](responseJson.as[TokenWatchlistFriend]).leftMap(new Throwable(_)) + extraContent <- + if ( + watchlist.data.user.watchlist.pageInfo.hasNextPage && watchlist.data.user.watchlist.pageInfo.endCursor.nonEmpty + ) + getWatchlistIdsForUser(config, client, token)(user, watchlist.data.user.watchlist.pageInfo.endCursor) + else + EitherT.pure[IO, Throwable](Set.empty[TokenWatchlistItem]) } yield watchlist.data.user.watchlist.nodes.map(_.toTokenWatchlistItem).toSet ++ extraContent } // We don't have all the information available in TokenWatchlist // so we need to make additional calls to Plex to get more information private def toItems(config: PlexConfiguration, client: HttpClient)(plex: TokenWatchlist): IO[Set[Item]] = - plex.MediaContainer.Metadata.map(i => toItems(config, client, i).leftMap { - err => - logger.warn(s"Found item ${i.title} on the watchlist, but we cannot find this in Plex's database.") - err - } - ).foldLeft(IO.pure(Set.empty[Item])) { case (acc, eitherT) => - for { - eitherItem <- eitherT.value - itemsToAdd = eitherItem.map(Set(_)).getOrElse(Set.empty) - accumulatedItems <- acc - } yield accumulatedItems ++ itemsToAdd - } + plex.MediaContainer.Metadata + .map(i => + toItems(config, client, i).leftMap { err => + logger.warn(s"Found item ${i.title} on the watchlist, but we cannot find this in Plex's database.") + err + } + ) + .foldLeft(IO.pure(Set.empty[Item])) { case (acc, eitherT) => + for { + eitherItem <- eitherT.value + itemsToAdd = eitherItem.map(Set(_)).getOrElse(Set.empty) + accumulatedItems <- acc + } yield accumulatedItems ++ itemsToAdd + } - private def toItems(config: PlexConfiguration, client: HttpClient, i: TokenWatchlistItem): EitherT[IO, Throwable, Item] = { + private def toItems( + config: PlexConfiguration, + client: HttpClient, + i: TokenWatchlistItem + ): EitherT[IO, Throwable, Item] = { val key = cleanKey(i.key) val url = Uri @@ -173,7 +199,7 @@ trait PlexUtils { val guids: EitherT[IO, Throwable, List[String]] = for { response <- EitherT(client.httpRequest(Method.GET, url)) - result <- EitherT(IO.pure(response.as[TokenWatchlist])).leftMap(err => new Throwable(err)) + result <- EitherT(IO.pure(response.as[TokenWatchlist])).leftMap(err => new Throwable(err)) guids = result.MediaContainer.Metadata.flatMap(_.Guid.map(_.id)) } yield guids diff --git a/src/main/scala/plex/TokenWatchlistItem.scala b/src/main/scala/plex/TokenWatchlistItem.scala index 41351b8..5471e94 100644 --- a/src/main/scala/plex/TokenWatchlistItem.scala +++ b/src/main/scala/plex/TokenWatchlistItem.scala @@ -1,5 +1,11 @@ package plex -private[plex] case class TokenWatchlistItem(title: String, guid: String, `type`: String, key: String, Guid: List[Guid] = List.empty) +private[plex] case class TokenWatchlistItem( + title: String, + guid: String, + `type`: String, + key: String, + Guid: List[Guid] = List.empty +) -private[plex] case class Guid(id: String) \ No newline at end of file +private[plex] case class Guid(id: String) diff --git a/src/main/scala/radarr/RadarrConversions.scala b/src/main/scala/radarr/RadarrConversions.scala index bfc1478..3c838e2 100644 --- a/src/main/scala/radarr/RadarrConversions.scala +++ b/src/main/scala/radarr/RadarrConversions.scala @@ -10,5 +10,7 @@ private[radarr] trait RadarrConversions { None ) - def toItem(movie: RadarrMovieExclusion): Item = toItem(RadarrMovie(movie.movieTitle, movie.imdbId, movie.tmdbId, movie.id)) + def toItem(movie: RadarrMovieExclusion): Item = toItem( + RadarrMovie(movie.movieTitle, movie.imdbId, movie.tmdbId, movie.id) + ) } diff --git a/src/main/scala/radarr/RadarrDelete.scala b/src/main/scala/radarr/RadarrDelete.scala index 78b7c25..5584096 100644 --- a/src/main/scala/radarr/RadarrDelete.scala +++ b/src/main/scala/radarr/RadarrDelete.scala @@ -1,3 +1,9 @@ package radarr -private case class RadarrDelete(title: String, tmdbId: Long, qualityProfileId: Int = 6, rootFolderPath: String, addOptions: AddOptions = AddOptions()) +private case class RadarrDelete( + title: String, + tmdbId: Long, + qualityProfileId: Int = 6, + rootFolderPath: String, + addOptions: AddOptions = AddOptions() +) diff --git a/src/main/scala/radarr/RadarrMovieExclusion.scala b/src/main/scala/radarr/RadarrMovieExclusion.scala index da53f93..915b994 100644 --- a/src/main/scala/radarr/RadarrMovieExclusion.scala +++ b/src/main/scala/radarr/RadarrMovieExclusion.scala @@ -1,3 +1,8 @@ package radarr -private[radarr] case class RadarrMovieExclusion(movieTitle: String, imdbId: Option[String], tmdbId: Option[Long], id: Long) +private[radarr] case class RadarrMovieExclusion( + movieTitle: String, + imdbId: Option[String], + tmdbId: Option[Long], + id: Long +) diff --git a/src/main/scala/radarr/RadarrPost.scala b/src/main/scala/radarr/RadarrPost.scala index b55055d..7a38406 100644 --- a/src/main/scala/radarr/RadarrPost.scala +++ b/src/main/scala/radarr/RadarrPost.scala @@ -1,10 +1,10 @@ package radarr private case class RadarrPost( - title: String, - tmdbId: Long, - qualityProfileId: Int = 6, - rootFolderPath: String, - addOptions: AddOptions = AddOptions(), - tags: List[Int] = List.empty[Int] - ) + title: String, + tmdbId: Long, + qualityProfileId: Int = 6, + rootFolderPath: String, + addOptions: AddOptions = AddOptions(), + tags: List[Int] = List.empty[Int] +) diff --git a/src/main/scala/radarr/RadarrUtils.scala b/src/main/scala/radarr/RadarrUtils.scala index 824ec53..86f81cc 100644 --- a/src/main/scala/radarr/RadarrUtils.scala +++ b/src/main/scala/radarr/RadarrUtils.scala @@ -16,18 +16,27 @@ trait RadarrUtils extends RadarrConversions { private val logger = LoggerFactory.getLogger(getClass) - protected def fetchMovies(client: HttpClient)(apiKey: String, baseUrl: Uri, bypass: Boolean): EitherT[IO, Throwable, Set[Item]] = + protected def fetchMovies( + client: HttpClient + )(apiKey: String, baseUrl: Uri, bypass: Boolean): EitherT[IO, Throwable, Set[Item]] = for { movies <- getToArr[List[RadarrMovie]](client)(baseUrl, apiKey, "movie") - exclusions <- if (bypass) { - EitherT.pure[IO, Throwable](List.empty[RadarrMovieExclusion]) - } else { - getToArr[List[RadarrMovieExclusion]](client)(baseUrl, apiKey, "exclusions") - } + exclusions <- + if (bypass) { + EitherT.pure[IO, Throwable](List.empty[RadarrMovieExclusion]) + } else { + getToArr[List[RadarrMovieExclusion]](client)(baseUrl, apiKey, "exclusions") + } } yield (movies.map(toItem) ++ exclusions.map(toItem)).toSet protected def addToRadarr(client: HttpClient)(config: RadarrConfiguration)(item: Item): IO[Unit] = { - val movie = RadarrPost(item.title, item.getTmdbId.getOrElse(0L), config.radarrQualityProfileId, config.radarrRootFolder, tags = config.radarrTagIds.toList) + val movie = RadarrPost( + item.title, + item.getTmdbId.getOrElse(0L), + config.radarrQualityProfileId, + config.radarrRootFolder, + tags = config.radarrTagIds.toList + ) val result = postToArr[Unit](client)(config.radarrBaseUrl, config.radarrApiKey, "movie")(movie.asJson) .fold( @@ -41,7 +50,9 @@ trait RadarrUtils extends RadarrConversions { } } - protected def deleteFromRadarr(client: HttpClient, config: RadarrConfiguration)(item: Item): EitherT[IO, Throwable, Unit] = { + protected def deleteFromRadarr(client: HttpClient, config: RadarrConfiguration)( + item: Item + ): EitherT[IO, Throwable, Unit] = { val movieId = item.getRadarrId.getOrElse { logger.warn(s"Unable to extract Radarr ID from movie to delete: $item") 0L @@ -49,21 +60,27 @@ trait RadarrUtils extends RadarrConversions { deleteToArr(client)(config.radarrBaseUrl, config.radarrApiKey, movieId) .map { r => - logger.info(s"Deleted ${item.title} from Radarr") - r - } + logger.info(s"Deleted ${item.title} from Radarr") + r + } } - private def getToArr[T: Decoder](client: HttpClient)(baseUrl: Uri, apiKey: String, endpoint: String): EitherT[IO, Throwable, T] = + private def getToArr[T: Decoder]( + client: HttpClient + )(baseUrl: Uri, apiKey: String, endpoint: String): EitherT[IO, Throwable, T] = for { - response <- EitherT(client.httpRequest(Method.GET, baseUrl / "api" / "v3" / endpoint, Some(apiKey))) + response <- EitherT(client.httpRequest(Method.GET, baseUrl / "api" / "v3" / endpoint, 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)(baseUrl: Uri, apiKey: String, endpoint: String)(payload: Json): EitherT[IO, Throwable, T] = + private def postToArr[T: Decoder]( + client: HttpClient + )(baseUrl: Uri, apiKey: String, endpoint: String)(payload: Json): EitherT[IO, Throwable, T] = for { - response <- EitherT(client.httpRequest(Method.POST, baseUrl / "api" / "v3" / endpoint, Some(apiKey), Some(payload))) + response <- EitherT( + client.httpRequest(Method.POST, baseUrl / "api" / "v3" / endpoint, Some(apiKey), Some(payload)) + ) maybeDecoded <- EitherT.pure[IO, Throwable](response.as[T]) decoded <- EitherT.fromOption[IO](maybeDecoded.toOption, new Throwable("Unable to decode response from Radarr")) } yield decoded diff --git a/src/main/scala/sonarr/SonarrAddOptions.scala b/src/main/scala/sonarr/SonarrAddOptions.scala index 1a79927..9dd04e7 100644 --- a/src/main/scala/sonarr/SonarrAddOptions.scala +++ b/src/main/scala/sonarr/SonarrAddOptions.scala @@ -1,3 +1,7 @@ package sonarr -private[sonarr] case class SonarrAddOptions(monitor: String, searchForCutoffUnmetEpisodes: Boolean = true, searchForMissingEpisodes: Boolean = true) +private[sonarr] case class SonarrAddOptions( + monitor: String, + searchForCutoffUnmetEpisodes: Boolean = true, + searchForMissingEpisodes: Boolean = true +) diff --git a/src/main/scala/sonarr/SonarrPost.scala b/src/main/scala/sonarr/SonarrPost.scala index 5630309..8c5c8ef 100644 --- a/src/main/scala/sonarr/SonarrPost.scala +++ b/src/main/scala/sonarr/SonarrPost.scala @@ -1,12 +1,12 @@ package sonarr private[sonarr] case class SonarrPost( - title: String, - tvdbId: Long, - qualityProfileId: Int, - rootFolderPath: String, - addOptions: SonarrAddOptions, - languageProfileId: Int, - monitored: Boolean = true, - tags: List[Int] = List.empty[Int] - ) + title: String, + tvdbId: Long, + qualityProfileId: Int, + rootFolderPath: String, + addOptions: SonarrAddOptions, + languageProfileId: Int, + monitored: Boolean = true, + tags: List[Int] = List.empty[Int] +) diff --git a/src/main/scala/sonarr/SonarrSeries.scala b/src/main/scala/sonarr/SonarrSeries.scala index 34b63c5..daab69d 100644 --- a/src/main/scala/sonarr/SonarrSeries.scala +++ b/src/main/scala/sonarr/SonarrSeries.scala @@ -1,9 +1,9 @@ package sonarr private[sonarr] case class SonarrSeries( - title: String, - imdbId: Option[String], - tvdbId: Option[Long], - id: Long, - ended: Option[Boolean] - ) + title: String, + imdbId: Option[String], + tvdbId: Option[Long], + id: Long, + ended: Option[Boolean] +) diff --git a/src/main/scala/sonarr/SonarrUtils.scala b/src/main/scala/sonarr/SonarrUtils.scala index 23a0ab6..f3eb183 100644 --- a/src/main/scala/sonarr/SonarrUtils.scala +++ b/src/main/scala/sonarr/SonarrUtils.scala @@ -15,14 +15,17 @@ trait SonarrUtils extends SonarrConversions { private val logger = LoggerFactory.getLogger(getClass) - protected def fetchSeries(client: HttpClient)(apiKey: String, baseUrl: Uri, bypass: Boolean): EitherT[IO, Throwable, Set[Item]] = + protected def fetchSeries( + client: HttpClient + )(apiKey: String, baseUrl: Uri, bypass: Boolean): EitherT[IO, Throwable, Set[Item]] = for { shows <- getToArr[List[SonarrSeries]](client)(baseUrl, apiKey, "series") - exclusions <- if (bypass) { - EitherT.pure[IO, Throwable](List.empty[SonarrSeries]) - } else { - getToArr[List[SonarrSeries]](client)(baseUrl, apiKey, "importlistexclusion") - } + exclusions <- + if (bypass) { + EitherT.pure[IO, Throwable](List.empty[SonarrSeries]) + } else { + getToArr[List[SonarrSeries]](client)(baseUrl, apiKey, "importlistexclusion") + } } yield (shows.map(toItem) ++ exclusions.map(toItem)).toSet protected def addToSonarr(client: HttpClient)(config: SonarrConfiguration)(item: Item): IO[Unit] = { @@ -50,7 +53,9 @@ trait SonarrUtils extends SonarrConversions { } } - protected def deleteFromSonarr(client: HttpClient, config: SonarrConfiguration)(item: Item): EitherT[IO, Throwable, Unit] = { + protected def deleteFromSonarr(client: HttpClient, config: SonarrConfiguration)( + item: Item + ): EitherT[IO, Throwable, Unit] = { val showId = item.getSonarrId.getOrElse { logger.warn(s"Unable to extract Sonarr ID from show to delete: $item") 0L @@ -73,16 +78,22 @@ trait SonarrUtils extends SonarrConversions { .map(_ => ()) } - private def getToArr[T: Decoder](client: HttpClient)(baseUrl: Uri, apiKey: String, endpoint: String): EitherT[IO, Throwable, T] = + private def getToArr[T: Decoder]( + client: HttpClient + )(baseUrl: Uri, apiKey: String, endpoint: String): EitherT[IO, Throwable, T] = for { - response <- EitherT(client.httpRequest(Method.GET, baseUrl / "api" / "v3" / endpoint, Some(apiKey))) + response <- EitherT(client.httpRequest(Method.GET, baseUrl / "api" / "v3" / endpoint, Some(apiKey))) maybeDecoded <- EitherT.pure[IO, Throwable](response.as[T]) decoded <- EitherT.fromOption[IO](maybeDecoded.toOption, new Throwable("Unable to decode response from Sonarr")) } yield decoded - private def postToArr[T: Decoder](client: HttpClient)(baseUrl: Uri, apiKey: String, endpoint: String)(payload: Json): EitherT[IO, Throwable, T] = + private def postToArr[T: Decoder]( + client: HttpClient + )(baseUrl: Uri, apiKey: String, endpoint: String)(payload: Json): EitherT[IO, Throwable, T] = for { - response <- EitherT(client.httpRequest(Method.POST, baseUrl / "api" / "v3" / endpoint, Some(apiKey), Some(payload))) + response <- EitherT( + client.httpRequest(Method.POST, baseUrl / "api" / "v3" / endpoint, Some(apiKey), Some(payload)) + ) maybeDecoded <- EitherT.pure[IO, Throwable](response.as[T]) decoded <- EitherT.fromOption[IO](maybeDecoded.toOption, new Throwable("Unable to decode response from Sonarr")) } yield decoded diff --git a/src/test/scala/PlexTokenSyncSpec.scala b/src/test/scala/PlexTokenSyncSpec.scala index 3780b29..22a5c54 100644 --- a/src/test/scala/PlexTokenSyncSpec.scala +++ b/src/test/scala/PlexTokenSyncSpec.scala @@ -1,5 +1,3 @@ - - import cats.effect.IO import cats.effect.unsafe.implicits.global import configuration.{Configuration, DeleteConfiguration, PlexConfiguration, RadarrConfiguration, SonarrConfiguration} @@ -20,7 +18,7 @@ class PlexTokenSyncSpec extends AnyFlatSpec with Matchers with MockFactory { "PlexTokenSync.run" should "do a single sync with all required fields provided" in { val mockHttpClient = mock[HttpClient] - val config = createConfiguration(plexTokens = Set("plex-token")) + val config = createConfiguration(plexTokens = Set("plex-token")) defaultPlexMock(mockHttpClient) defaultRadarrMock(mockHttpClient) defaultSonarrMock(mockHttpClient) @@ -31,8 +29,8 @@ class PlexTokenSyncSpec extends AnyFlatSpec with Matchers with MockFactory { } private def createConfiguration( - plexTokens: Set[String] - ): Configuration = Configuration( + plexTokens: Set[String] + ): Configuration = Configuration( refreshInterval = 10.seconds, SonarrConfiguration( sonarrBaseUrl = Uri.unsafeFromString("https://localhost:8989"), @@ -67,26 +65,40 @@ class PlexTokenSyncSpec extends AnyFlatSpec with Matchers with MockFactory { ) private def defaultPlexMock(httpClient: HttpClient): HttpClient = { - (httpClient.httpRequest _).expects( - Method.GET, - Uri.unsafeFromString("https://metadata.provider.plex.tv/library/sections/watchlist/all?X-Plex-Token=plex-token&X-Plex-Container-Start=0&X-Plex-Container-Size=300"), - None, - None - ).returning(IO.pure(parse(Source.fromResource("self-watchlist-from-token.json").getLines().mkString("\n")))).once() - (httpClient.httpRequest _).expects( - Method.GET, - Uri.unsafeFromString("https://discover.provider.plex.tv/library/metadata/5df46a38237002001dce338d?X-Plex-Token=plex-token"), - None, - None - ).returning(IO.pure(parse(Source.fromResource("single-item-plex-metadata.json").getLines().mkString("\n")))).once() - (httpClient.httpRequest _).expects( - Method.GET, - Uri.unsafeFromString("https://discover.provider.plex.tv/library/metadata/617d3ab142705b2183b1b20b?X-Plex-Token=plex-token"), - None, - None - ).returning(IO.pure(parse(Source.fromResource("single-item-plex-metadata2.json").getLines().mkString("\n")))).once() - val query = GraphQLQuery( - """query GetAllFriends { + (httpClient.httpRequest _) + .expects( + Method.GET, + Uri.unsafeFromString( + "https://metadata.provider.plex.tv/library/sections/watchlist/all?X-Plex-Token=plex-token&X-Plex-Container-Start=0&X-Plex-Container-Size=300" + ), + None, + None + ) + .returning(IO.pure(parse(Source.fromResource("self-watchlist-from-token.json").getLines().mkString("\n")))) + .once() + (httpClient.httpRequest _) + .expects( + Method.GET, + Uri.unsafeFromString( + "https://discover.provider.plex.tv/library/metadata/5df46a38237002001dce338d?X-Plex-Token=plex-token" + ), + None, + None + ) + .returning(IO.pure(parse(Source.fromResource("single-item-plex-metadata.json").getLines().mkString("\n")))) + .once() + (httpClient.httpRequest _) + .expects( + Method.GET, + Uri.unsafeFromString( + "https://discover.provider.plex.tv/library/metadata/617d3ab142705b2183b1b20b?X-Plex-Token=plex-token" + ), + None, + None + ) + .returning(IO.pure(parse(Source.fromResource("single-item-plex-metadata2.json").getLines().mkString("\n")))) + .once() + val query = GraphQLQuery("""query GetAllFriends { | allFriendsV2 { | user { | id @@ -94,36 +106,57 @@ class PlexTokenSyncSpec extends AnyFlatSpec with Matchers with MockFactory { | } | } | }""".stripMargin) - (httpClient.httpRequest _).expects( - Method.POST, - Uri.unsafeFromString("https://community.plex.tv/api"), - Some("plex-token"), - Some(query.asJson) - ).returning(IO.pure(parse(Source.fromResource("plex-get-all-friends.json").getLines().mkString("\n")))).once() - (httpClient.httpRequest _).expects( - Method.POST, - Uri.unsafeFromString("https://community.plex.tv/api"), - Some("plex-token"), - * - ).returning(IO.pure(parse(Source.fromResource("plex-get-watchlist-from-friend.json").getLines().mkString("\n")))).twice() - (httpClient.httpRequest _).expects( - Method.GET, - Uri.unsafeFromString("https://discover.provider.plex.tv/library/metadata/5d77688b9ab54400214e789b?X-Plex-Token=plex-token"), - None, - None - ).returning(IO.pure(parse(Source.fromResource("single-item-plex-metadata2.json").getLines().mkString("\n")))).once() - (httpClient.httpRequest _).expects( - Method.GET, - Uri.unsafeFromString("https://discover.provider.plex.tv/library/metadata/5d77688b594b2b001e68f2f0?X-Plex-Token=plex-token"), - None, - None - ).returning(IO.pure(parse(Source.fromResource("single-item-plex-metadata2.json").getLines().mkString("\n")))).anyNumberOfTimes() - (httpClient.httpRequest _).expects( - Method.GET, - Uri.unsafeFromString("https://discover.provider.plex.tv/library/metadata/5d77688b9ab54400214e789b?X-Plex-Token=plex-token"), - None, - None - ).returning(IO.pure(parse(Source.fromResource("single-item-plex-metadata2.json").getLines().mkString("\n")))).once() + (httpClient.httpRequest _) + .expects( + Method.POST, + Uri.unsafeFromString("https://community.plex.tv/api"), + Some("plex-token"), + Some(query.asJson) + ) + .returning(IO.pure(parse(Source.fromResource("plex-get-all-friends.json").getLines().mkString("\n")))) + .once() + (httpClient.httpRequest _) + .expects( + Method.POST, + Uri.unsafeFromString("https://community.plex.tv/api"), + Some("plex-token"), + * + ) + .returning(IO.pure(parse(Source.fromResource("plex-get-watchlist-from-friend.json").getLines().mkString("\n")))) + .twice() + (httpClient.httpRequest _) + .expects( + Method.GET, + Uri.unsafeFromString( + "https://discover.provider.plex.tv/library/metadata/5d77688b9ab54400214e789b?X-Plex-Token=plex-token" + ), + None, + None + ) + .returning(IO.pure(parse(Source.fromResource("single-item-plex-metadata2.json").getLines().mkString("\n")))) + .once() + (httpClient.httpRequest _) + .expects( + Method.GET, + Uri.unsafeFromString( + "https://discover.provider.plex.tv/library/metadata/5d77688b594b2b001e68f2f0?X-Plex-Token=plex-token" + ), + None, + None + ) + .returning(IO.pure(parse(Source.fromResource("single-item-plex-metadata2.json").getLines().mkString("\n")))) + .anyNumberOfTimes() + (httpClient.httpRequest _) + .expects( + Method.GET, + Uri.unsafeFromString( + "https://discover.provider.plex.tv/library/metadata/5d77688b9ab54400214e789b?X-Plex-Token=plex-token" + ), + None, + None + ) + .returning(IO.pure(parse(Source.fromResource("single-item-plex-metadata2.json").getLines().mkString("\n")))) + .once() httpClient } @@ -166,52 +199,73 @@ class PlexTokenSyncSpec extends AnyFlatSpec with Matchers with MockFactory { | 2 | ] |}""".stripMargin - (httpClient.httpRequest _).expects( - Method.GET, - Uri.unsafeFromString("https://localhost:7878/api/v3/movie"), - Some("radarr-api-key"), - None - ).returning(IO.pure(parse(Source.fromResource("radarr.json").getLines().mkString("\n")))).once() - (httpClient.httpRequest _).expects( - Method.GET, - Uri.unsafeFromString("https://localhost:7878/api/v3/exclusions"), - Some("radarr-api-key"), - None - ).returning(IO.pure(parse(Source.fromResource("exclusions.json").getLines().mkString("\n")))).once() - (httpClient.httpRequest _).expects( - Method.POST, - Uri.unsafeFromString("https://localhost:7878/api/v3/movie"), - Some("radarr-api-key"), - parse(movieToAdd).toOption - ).returning(IO.pure(parse("{}"))).once() - (httpClient.httpRequest _).expects( - Method.POST, - Uri.unsafeFromString("https://localhost:7878/api/v3/movie"), - Some("radarr-api-key"), - parse(movieToAdd2).toOption - ).returning(IO.pure(parse("{}"))).once() - (httpClient.httpRequest _).expects( - Method.POST, - Uri.unsafeFromString("https://localhost:7878/api/v3/movie"), - Some("radarr-api-key"), - parse(movieToAdd3).toOption - ).returning(IO.pure(parse("{}"))).once() + (httpClient.httpRequest _) + .expects( + Method.GET, + Uri.unsafeFromString("https://localhost:7878/api/v3/movie"), + Some("radarr-api-key"), + None + ) + .returning(IO.pure(parse(Source.fromResource("radarr.json").getLines().mkString("\n")))) + .once() + (httpClient.httpRequest _) + .expects( + Method.GET, + Uri.unsafeFromString("https://localhost:7878/api/v3/exclusions"), + Some("radarr-api-key"), + None + ) + .returning(IO.pure(parse(Source.fromResource("exclusions.json").getLines().mkString("\n")))) + .once() + (httpClient.httpRequest _) + .expects( + Method.POST, + Uri.unsafeFromString("https://localhost:7878/api/v3/movie"), + Some("radarr-api-key"), + parse(movieToAdd).toOption + ) + .returning(IO.pure(parse("{}"))) + .once() + (httpClient.httpRequest _) + .expects( + Method.POST, + Uri.unsafeFromString("https://localhost:7878/api/v3/movie"), + Some("radarr-api-key"), + parse(movieToAdd2).toOption + ) + .returning(IO.pure(parse("{}"))) + .once() + (httpClient.httpRequest _) + .expects( + Method.POST, + Uri.unsafeFromString("https://localhost:7878/api/v3/movie"), + Some("radarr-api-key"), + parse(movieToAdd3).toOption + ) + .returning(IO.pure(parse("{}"))) + .once() httpClient } private def defaultSonarrMock(httpClient: HttpClient): HttpClient = { - (httpClient.httpRequest _).expects( - Method.GET, - Uri.unsafeFromString("https://localhost:8989/api/v3/series"), - Some("sonarr-api-key"), - None - ).returning(IO.pure(parse(Source.fromResource("sonarr.json").getLines().mkString("\n")))).once() - (httpClient.httpRequest _).expects( - Method.GET, - Uri.unsafeFromString("https://localhost:8989/api/v3/importlistexclusion"), - Some("sonarr-api-key"), - None - ).returning(IO.pure(parse(Source.fromResource("importlistexclusion.json").getLines().mkString("\n")))).once() + (httpClient.httpRequest _) + .expects( + Method.GET, + Uri.unsafeFromString("https://localhost:8989/api/v3/series"), + Some("sonarr-api-key"), + None + ) + .returning(IO.pure(parse(Source.fromResource("sonarr.json").getLines().mkString("\n")))) + .once() + (httpClient.httpRequest _) + .expects( + Method.GET, + Uri.unsafeFromString("https://localhost:8989/api/v3/importlistexclusion"), + Some("sonarr-api-key"), + None + ) + .returning(IO.pure(parse(Source.fromResource("importlistexclusion.json").getLines().mkString("\n")))) + .once() httpClient } diff --git a/src/test/scala/configuration/ConfigurationUtilsSpec.scala b/src/test/scala/configuration/ConfigurationUtilsSpec.scala index 05a9587..cb7e9ef 100644 --- a/src/test/scala/configuration/ConfigurationUtilsSpec.scala +++ b/src/test/scala/configuration/ConfigurationUtilsSpec.scala @@ -16,7 +16,7 @@ class ConfigurationUtilsSpec extends AnyFlatSpec with Matchers with MockFactory "ConfigurationUtils.create" should "start with all required values provided" in { val mockConfigReader = createMockConfigReader() - val mockHttpClient = createMockHttpClient() + val mockHttpClient = createMockHttpClient() val config = ConfigurationUtils.create(mockConfigReader, mockHttpClient).unsafeRunSync() noException should be thrownBy config @@ -28,23 +28,27 @@ class ConfigurationUtilsSpec extends AnyFlatSpec with Matchers with MockFactory it should "fail if missing sonarr API key" in { val mockConfigReader = createMockConfigReader(sonarrApiKey = None) - val mockHttpClient = createMockHttpClient() + val mockHttpClient = createMockHttpClient() - an[IllegalArgumentException] should be thrownBy ConfigurationUtils.create(mockConfigReader, mockHttpClient).unsafeRunSync() + an[IllegalArgumentException] should be thrownBy ConfigurationUtils + .create(mockConfigReader, mockHttpClient) + .unsafeRunSync() } it should "fail if missing radarr API key" in { val mockConfigReader = createMockConfigReader(radarrApiKey = None) - val mockHttpClient = createMockHttpClient() + val mockHttpClient = createMockHttpClient() - an[IllegalArgumentException] should be thrownBy ConfigurationUtils.create(mockConfigReader, mockHttpClient).unsafeRunSync() + an[IllegalArgumentException] should be thrownBy ConfigurationUtils + .create(mockConfigReader, mockHttpClient) + .unsafeRunSync() } it should "fetch the first accessible root folder of sonarr if none is provided" in { val mockConfigReader = createMockConfigReader() - val mockHttpClient = createMockHttpClient() + val mockHttpClient = createMockHttpClient() val config = ConfigurationUtils.create(mockConfigReader, mockHttpClient).unsafeRunSync() noException should be thrownBy config @@ -54,7 +58,7 @@ class ConfigurationUtilsSpec extends AnyFlatSpec with Matchers with MockFactory it should "find the root folder provided in sonarr config" in { val mockConfigReader = createMockConfigReader(sonarrRootFolder = Some("/data3")) - val mockHttpClient = createMockHttpClient() + val mockHttpClient = createMockHttpClient() val config = ConfigurationUtils.create(mockConfigReader, mockHttpClient).unsafeRunSync() noException should be thrownBy config @@ -64,7 +68,7 @@ class ConfigurationUtilsSpec extends AnyFlatSpec with Matchers with MockFactory it should "find the root folder with a trailing slash provided in sonarr config" in { val mockConfigReader = createMockConfigReader(sonarrRootFolder = Some("/data3/")) - val mockHttpClient = createMockHttpClient() + val mockHttpClient = createMockHttpClient() val config = ConfigurationUtils.create(mockConfigReader, mockHttpClient).unsafeRunSync() noException should be thrownBy config @@ -74,7 +78,7 @@ class ConfigurationUtilsSpec extends AnyFlatSpec with Matchers with MockFactory it should "find the root folder with an escaped slash provided in sonarr config" in { val mockConfigReader = createMockConfigReader(sonarrRootFolder = Some("//data3")) - val mockHttpClient = createMockHttpClient() + val mockHttpClient = createMockHttpClient() val config = ConfigurationUtils.create(mockConfigReader, mockHttpClient).unsafeRunSync() noException should be thrownBy config @@ -84,26 +88,27 @@ class ConfigurationUtilsSpec extends AnyFlatSpec with Matchers with MockFactory it should "throw an error if the sonarr root folder provided can't be found" in { val mockConfigReader = createMockConfigReader(sonarrRootFolder = Some("/unknown")) - val mockHttpClient = createMockHttpClient() + val mockHttpClient = createMockHttpClient() - an[IllegalArgumentException] should be thrownBy ConfigurationUtils.create(mockConfigReader, mockHttpClient).unsafeRunSync() + an[IllegalArgumentException] should be thrownBy ConfigurationUtils + .create(mockConfigReader, mockHttpClient) + .unsafeRunSync() } it should "fetch the first accessible root folder of radarr if none is provided" in { val mockConfigReader = createMockConfigReader() - val mockHttpClient = createMockHttpClient() + val mockHttpClient = createMockHttpClient() val config = ConfigurationUtils.create(mockConfigReader, mockHttpClient).unsafeRunSync() noException should be thrownBy config config.radarrConfiguration.radarrRootFolder shouldBe "/data2" } - it should "find the root folder provided in radarr config" in { val mockConfigReader = createMockConfigReader(radarrRootFolder = Some("/data3")) - val mockHttpClient = createMockHttpClient() + val mockHttpClient = createMockHttpClient() val config = ConfigurationUtils.create(mockConfigReader, mockHttpClient).unsafeRunSync() noException should be thrownBy config @@ -113,7 +118,7 @@ class ConfigurationUtilsSpec extends AnyFlatSpec with Matchers with MockFactory it should "find the root folder with a trailing slash provided in radarr config" in { val mockConfigReader = createMockConfigReader(radarrRootFolder = Some("/data3/")) - val mockHttpClient = createMockHttpClient() + val mockHttpClient = createMockHttpClient() val config = ConfigurationUtils.create(mockConfigReader, mockHttpClient).unsafeRunSync() noException should be thrownBy config @@ -123,7 +128,7 @@ class ConfigurationUtilsSpec extends AnyFlatSpec with Matchers with MockFactory it should "find the root folder with an escaped slash provided in radarr config" in { val mockConfigReader = createMockConfigReader(radarrRootFolder = Some("//data3")) - val mockHttpClient = createMockHttpClient() + val mockHttpClient = createMockHttpClient() val config = ConfigurationUtils.create(mockConfigReader, mockHttpClient).unsafeRunSync() noException should be thrownBy config @@ -133,15 +138,17 @@ class ConfigurationUtilsSpec extends AnyFlatSpec with Matchers with MockFactory it should "throw an error if the radarr root folder provided can't be found" in { val mockConfigReader = createMockConfigReader(radarrRootFolder = Some("/unknown")) - val mockHttpClient = createMockHttpClient() + val mockHttpClient = createMockHttpClient() - an[IllegalArgumentException] should be thrownBy ConfigurationUtils.create(mockConfigReader, mockHttpClient).unsafeRunSync() + an[IllegalArgumentException] should be thrownBy ConfigurationUtils + .create(mockConfigReader, mockHttpClient) + .unsafeRunSync() } it should "work even if quality profiles are multiple words" in { val mockConfigReader = createMockConfigReader(qualityProfile = Some("HD - 720p/1080p")) - val mockHttpClient = createMockHttpClient() + val mockHttpClient = createMockHttpClient() val config = ConfigurationUtils.create(mockConfigReader, mockHttpClient).unsafeRunSync() noException should be thrownBy config @@ -152,7 +159,7 @@ class ConfigurationUtilsSpec extends AnyFlatSpec with Matchers with MockFactory it should "fetch a tag from Sonarr/Radarr" in { val mockConfigReader = createMockConfigReader(tags = Some("test-tag")) - val mockHttpClient = createMockHttpClient() + val mockHttpClient = createMockHttpClient() val config = ConfigurationUtils.create(mockConfigReader, mockHttpClient).unsafeRunSync() noException should be thrownBy config @@ -161,16 +168,16 @@ class ConfigurationUtilsSpec extends AnyFlatSpec with Matchers with MockFactory } private def createMockConfigReader( - sonarrApiKey: Option[String] = Some("sonarr-api-key"), - sonarrRootFolder: Option[String] = None, - radarrRootFolder: Option[String] = None, - radarrApiKey: Option[String] = Some("radarr-api-key"), - plexWatchlist1: Option[String] = None, - plexWatchlist2: Option[String] = None, - plexToken: Option[String] = Some("test-token"), - qualityProfile: Option[String] = None, - tags: Option[String] = None - ): ConfigurationReader = { + sonarrApiKey: Option[String] = Some("sonarr-api-key"), + sonarrRootFolder: Option[String] = None, + radarrRootFolder: Option[String] = None, + radarrApiKey: Option[String] = Some("radarr-api-key"), + plexWatchlist1: Option[String] = None, + plexWatchlist2: Option[String] = None, + plexToken: Option[String] = Some("test-token"), + qualityProfile: Option[String] = None, + tags: Option[String] = None + ): ConfigurationReader = { val unset = None val mockConfigReader = mock[ConfigurationReader] @@ -201,72 +208,109 @@ class ConfigurationUtilsSpec extends AnyFlatSpec with Matchers with MockFactory private def createMockHttpClient(): HttpClient = { val mockHttpClient = mock[HttpClient] - (mockHttpClient.httpRequest _).expects( - Method.GET, - Uri.unsafeFromString("http://localhost:8989").withPath(Uri.Path.unsafeFromString("/api/v3/health")), - Some("sonarr-api-key"), - None - ).returning(IO.pure(Right(Json.Null))).anyNumberOfTimes() - (mockHttpClient.httpRequest _).expects( - Method.GET, - Uri.unsafeFromString("http://localhost:7878").withPath(Uri.Path.unsafeFromString("/api/v3/health")), - Some("radarr-api-key"), - None - ).returning(IO.pure(Right(Json.Null))).anyNumberOfTimes() - (mockHttpClient.httpRequest _).expects( - Method.GET, - Uri.unsafeFromString("http://localhost:8989").withPath(Uri.Path.unsafeFromString("/api/v3/qualityprofile")), - Some("sonarr-api-key"), - None - ).returning(IO.pure(parse(Source.fromResource("quality-profile.json").getLines().mkString("\n")))).anyNumberOfTimes() - (mockHttpClient.httpRequest _).expects( - Method.GET, - Uri.unsafeFromString("http://localhost:8989").withPath(Uri.Path.unsafeFromString("/api/v3/languageprofile")), - Some("sonarr-api-key"), - None - ).returning(IO.pure(parse(Source.fromResource("sonarr-language-profile.json").getLines().mkString("\n")))).anyNumberOfTimes() - (mockHttpClient.httpRequest _).expects( - Method.GET, - Uri.unsafeFromString("http://localhost:7878").withPath(Uri.Path.unsafeFromString("/api/v3/qualityprofile")), - Some("radarr-api-key"), - None - ).returning(IO.pure(parse(Source.fromResource("quality-profile.json").getLines().mkString("\n")))).anyNumberOfTimes() - (mockHttpClient.httpRequest _).expects( - Method.GET, - Uri.unsafeFromString("http://localhost:8989").withPath(Uri.Path.unsafeFromString("/api/v3/rootFolder")), - Some("sonarr-api-key"), - None - ).returning(IO.pure(parse(Source.fromResource("rootFolder.json").getLines().mkString("\n")))).anyNumberOfTimes() - (mockHttpClient.httpRequest _).expects( - Method.GET, - Uri.unsafeFromString("http://localhost:7878").withPath(Uri.Path.unsafeFromString("/api/v3/rootFolder")), - Some("radarr-api-key"), - None - ).returning(IO.pure(parse(Source.fromResource("rootFolder.json").getLines().mkString("\n")))).anyNumberOfTimes() - (mockHttpClient.httpRequest _).expects( - Method.POST, - Uri.unsafeFromString("https://discover.provider.plex.tv/rss?X-Plex-Token=test-token&X-Plex-Client-Identifier=watchlistarr"), - None, - Some(parse("""{"feedType": "watchlist"}""").getOrElse(Json.Null)) - ).returning(IO.pure(parse(Source.fromResource("rss-feed-generated.json").getLines().mkString("\n")))).anyNumberOfTimes() - (mockHttpClient.httpRequest _).expects( - Method.POST, - Uri.unsafeFromString("https://discover.provider.plex.tv/rss?X-Plex-Token=test-token&X-Plex-Client-Identifier=watchlistarr"), - None, - Some(parse("""{"feedType": "friendsWatchlist"}""").getOrElse(Json.Null)) - ).returning(IO.pure(parse(Source.fromResource("rss-feed-generated.json").getLines().mkString("\n")))).anyNumberOfTimes() - (mockHttpClient.httpRequest _).expects( - Method.POST, - Uri.unsafeFromString("http://localhost:8989").withPath(Uri.Path.unsafeFromString("/api/v3/tag")), - Some("sonarr-api-key"), - * - ).returning(IO.pure(parse(Source.fromResource("tag-response.json").getLines().mkString("\n")))).anyNumberOfTimes() - (mockHttpClient.httpRequest _).expects( - Method.POST, - Uri.unsafeFromString("http://localhost:7878").withPath(Uri.Path.unsafeFromString("/api/v3/tag")), - Some("radarr-api-key"), - * - ).returning(IO.pure(parse(Source.fromResource("tag-response.json").getLines().mkString("\n")))).anyNumberOfTimes() + (mockHttpClient.httpRequest _) + .expects( + Method.GET, + Uri.unsafeFromString("http://localhost:8989").withPath(Uri.Path.unsafeFromString("/api/v3/health")), + Some("sonarr-api-key"), + None + ) + .returning(IO.pure(Right(Json.Null))) + .anyNumberOfTimes() + (mockHttpClient.httpRequest _) + .expects( + Method.GET, + Uri.unsafeFromString("http://localhost:7878").withPath(Uri.Path.unsafeFromString("/api/v3/health")), + Some("radarr-api-key"), + None + ) + .returning(IO.pure(Right(Json.Null))) + .anyNumberOfTimes() + (mockHttpClient.httpRequest _) + .expects( + Method.GET, + Uri.unsafeFromString("http://localhost:8989").withPath(Uri.Path.unsafeFromString("/api/v3/qualityprofile")), + Some("sonarr-api-key"), + None + ) + .returning(IO.pure(parse(Source.fromResource("quality-profile.json").getLines().mkString("\n")))) + .anyNumberOfTimes() + (mockHttpClient.httpRequest _) + .expects( + Method.GET, + Uri.unsafeFromString("http://localhost:8989").withPath(Uri.Path.unsafeFromString("/api/v3/languageprofile")), + Some("sonarr-api-key"), + None + ) + .returning(IO.pure(parse(Source.fromResource("sonarr-language-profile.json").getLines().mkString("\n")))) + .anyNumberOfTimes() + (mockHttpClient.httpRequest _) + .expects( + Method.GET, + Uri.unsafeFromString("http://localhost:7878").withPath(Uri.Path.unsafeFromString("/api/v3/qualityprofile")), + Some("radarr-api-key"), + None + ) + .returning(IO.pure(parse(Source.fromResource("quality-profile.json").getLines().mkString("\n")))) + .anyNumberOfTimes() + (mockHttpClient.httpRequest _) + .expects( + Method.GET, + Uri.unsafeFromString("http://localhost:8989").withPath(Uri.Path.unsafeFromString("/api/v3/rootFolder")), + Some("sonarr-api-key"), + None + ) + .returning(IO.pure(parse(Source.fromResource("rootFolder.json").getLines().mkString("\n")))) + .anyNumberOfTimes() + (mockHttpClient.httpRequest _) + .expects( + Method.GET, + Uri.unsafeFromString("http://localhost:7878").withPath(Uri.Path.unsafeFromString("/api/v3/rootFolder")), + Some("radarr-api-key"), + None + ) + .returning(IO.pure(parse(Source.fromResource("rootFolder.json").getLines().mkString("\n")))) + .anyNumberOfTimes() + (mockHttpClient.httpRequest _) + .expects( + Method.POST, + Uri.unsafeFromString( + "https://discover.provider.plex.tv/rss?X-Plex-Token=test-token&X-Plex-Client-Identifier=watchlistarr" + ), + None, + Some(parse("""{"feedType": "watchlist"}""").getOrElse(Json.Null)) + ) + .returning(IO.pure(parse(Source.fromResource("rss-feed-generated.json").getLines().mkString("\n")))) + .anyNumberOfTimes() + (mockHttpClient.httpRequest _) + .expects( + Method.POST, + Uri.unsafeFromString( + "https://discover.provider.plex.tv/rss?X-Plex-Token=test-token&X-Plex-Client-Identifier=watchlistarr" + ), + None, + Some(parse("""{"feedType": "friendsWatchlist"}""").getOrElse(Json.Null)) + ) + .returning(IO.pure(parse(Source.fromResource("rss-feed-generated.json").getLines().mkString("\n")))) + .anyNumberOfTimes() + (mockHttpClient.httpRequest _) + .expects( + Method.POST, + Uri.unsafeFromString("http://localhost:8989").withPath(Uri.Path.unsafeFromString("/api/v3/tag")), + Some("sonarr-api-key"), + * + ) + .returning(IO.pure(parse(Source.fromResource("tag-response.json").getLines().mkString("\n")))) + .anyNumberOfTimes() + (mockHttpClient.httpRequest _) + .expects( + Method.POST, + Uri.unsafeFromString("http://localhost:7878").withPath(Uri.Path.unsafeFromString("/api/v3/tag")), + Some("radarr-api-key"), + * + ) + .returning(IO.pure(parse(Source.fromResource("tag-response.json").getLines().mkString("\n")))) + .anyNumberOfTimes() mockHttpClient } } diff --git a/src/test/scala/plex/PlexUtilsSpec.scala b/src/test/scala/plex/PlexUtilsSpec.scala index 84df2bd..76715c0 100644 --- a/src/test/scala/plex/PlexUtilsSpec.scala +++ b/src/test/scala/plex/PlexUtilsSpec.scala @@ -20,12 +20,15 @@ class PlexUtilsSpec extends AnyFlatSpec with Matchers with PlexUtils with MockFa "PlexUtils" should "successfully fetch a watchlist from RSS feeds" in { val mockClient = mock[HttpClient] - (mockClient.httpRequest _).expects( - Method.GET, - Uri.unsafeFromString("http://localhost:9090"), - None, - None - ).returning(IO.pure(parse(Source.fromResource("watchlist.json").getLines().mkString("\n")))).once() + (mockClient.httpRequest _) + .expects( + Method.GET, + Uri.unsafeFromString("http://localhost:9090"), + None, + None + ) + .returning(IO.pure(parse(Source.fromResource("watchlist.json").getLines().mkString("\n")))) + .once() val result = fetchWatchlistFromRss(mockClient)(Uri.unsafeFromString("http://localhost:9090")).unsafeRunSync() @@ -34,12 +37,15 @@ class PlexUtilsSpec extends AnyFlatSpec with Matchers with PlexUtils with MockFa it should "not fail when the list returned is empty" in { val mockClient = mock[HttpClient] - (mockClient.httpRequest _).expects( - Method.GET, - Uri.unsafeFromString("http://localhost:9090"), - None, - None - ).returning(IO.pure(parse("{}"))).once() + (mockClient.httpRequest _) + .expects( + Method.GET, + Uri.unsafeFromString("http://localhost:9090"), + None, + None + ) + .returning(IO.pure(parse("{}"))) + .once() val result = fetchWatchlistFromRss(mockClient)(Uri.unsafeFromString("http://localhost:9090")).unsafeRunSync() @@ -48,40 +54,60 @@ class PlexUtilsSpec extends AnyFlatSpec with Matchers with PlexUtils with MockFa it should "successfully ping the Plex server" in { val mockClient = mock[HttpClient] - val config = createConfiguration(Set("test-token")) - (mockClient.httpRequest _).expects( - Method.GET, - Uri.unsafeFromString("https://plex.tv/api/v2/ping?X-Plex-Token=test-token&X-Plex-Client-Identifier=watchlistarr"), - None, - None - ).returning(IO.pure(parse("{}"))).once() + val config = createConfiguration(Set("test-token")) + (mockClient.httpRequest _) + .expects( + Method.GET, + Uri.unsafeFromString( + "https://plex.tv/api/v2/ping?X-Plex-Token=test-token&X-Plex-Client-Identifier=watchlistarr" + ), + None, + None + ) + .returning(IO.pure(parse("{}"))) + .once() val result: Unit = ping(mockClient)(config).unsafeRunSync() - result shouldBe() + result shouldBe () } it should "successfully fetch the watchlist using the plex token" in { val mockClient = mock[HttpClient] - val config = createConfiguration(Set("test-token")) - (mockClient.httpRequest _).expects( - Method.GET, - Uri.unsafeFromString("https://metadata.provider.plex.tv/library/sections/watchlist/all?X-Plex-Token=test-token&X-Plex-Container-Start=0&X-Plex-Container-Size=300"), - None, - None - ).returning(IO.pure(parse(Source.fromResource("self-watchlist-from-token.json").getLines().mkString("\n")))).once() - (mockClient.httpRequest _).expects( - Method.GET, - Uri.unsafeFromString("https://discover.provider.plex.tv/library/metadata/5df46a38237002001dce338d?X-Plex-Token=test-token"), - None, - None - ).returning(IO.pure(parse(Source.fromResource("single-item-plex-metadata.json").getLines().mkString("\n")))).once() - (mockClient.httpRequest _).expects( - Method.GET, - Uri.unsafeFromString("https://discover.provider.plex.tv/library/metadata/617d3ab142705b2183b1b20b?X-Plex-Token=test-token"), - None, - None - ).returning(IO.pure(parse(Source.fromResource("single-item-plex-metadata.json").getLines().mkString("\n")))).once() + val config = createConfiguration(Set("test-token")) + (mockClient.httpRequest _) + .expects( + Method.GET, + Uri.unsafeFromString( + "https://metadata.provider.plex.tv/library/sections/watchlist/all?X-Plex-Token=test-token&X-Plex-Container-Start=0&X-Plex-Container-Size=300" + ), + None, + None + ) + .returning(IO.pure(parse(Source.fromResource("self-watchlist-from-token.json").getLines().mkString("\n")))) + .once() + (mockClient.httpRequest _) + .expects( + Method.GET, + Uri.unsafeFromString( + "https://discover.provider.plex.tv/library/metadata/5df46a38237002001dce338d?X-Plex-Token=test-token" + ), + None, + None + ) + .returning(IO.pure(parse(Source.fromResource("single-item-plex-metadata.json").getLines().mkString("\n")))) + .once() + (mockClient.httpRequest _) + .expects( + Method.GET, + Uri.unsafeFromString( + "https://discover.provider.plex.tv/library/metadata/617d3ab142705b2183b1b20b?X-Plex-Token=test-token" + ), + None, + None + ) + .returning(IO.pure(parse(Source.fromResource("single-item-plex-metadata.json").getLines().mkString("\n")))) + .once() val eitherResult = getSelfWatchlist(config, mockClient).value.unsafeRunSync() @@ -91,16 +117,20 @@ class PlexUtilsSpec extends AnyFlatSpec with Matchers with PlexUtils with MockFa result.head shouldBe Item("The Test", List("imdb://tt11347692", "tmdb://95837", "tvdb://372848"), "show") } - it should "successfully fetch an empty watchlist using the plex token" in { val mockClient = mock[HttpClient] - val config = createConfiguration(Set("test-token")) - (mockClient.httpRequest _).expects( - Method.GET, - Uri.unsafeFromString("https://metadata.provider.plex.tv/library/sections/watchlist/all?X-Plex-Token=test-token&X-Plex-Container-Start=0&X-Plex-Container-Size=300"), - None, - None - ).returning(IO.pure(parse(Source.fromResource("empty-watchlist-from-token.json").getLines().mkString("\n")))).once() + val config = createConfiguration(Set("test-token")) + (mockClient.httpRequest _) + .expects( + Method.GET, + Uri.unsafeFromString( + "https://metadata.provider.plex.tv/library/sections/watchlist/all?X-Plex-Token=test-token&X-Plex-Container-Start=0&X-Plex-Container-Size=300" + ), + None, + None + ) + .returning(IO.pure(parse(Source.fromResource("empty-watchlist-from-token.json").getLines().mkString("\n")))) + .once() val eitherResult = getSelfWatchlist(config, mockClient).value.unsafeRunSync() @@ -111,25 +141,40 @@ class PlexUtilsSpec extends AnyFlatSpec with Matchers with PlexUtils with MockFa it should "fetch the healthy part of a watchlist using the plex token" in { val mockClient = mock[HttpClient] - val config = createConfiguration(Set("test-token")) - (mockClient.httpRequest _).expects( - Method.GET, - Uri.unsafeFromString("https://metadata.provider.plex.tv/library/sections/watchlist/all?X-Plex-Token=test-token&X-Plex-Container-Start=0&X-Plex-Container-Size=300"), - None, - None - ).returning(IO.pure(parse(Source.fromResource("self-watchlist-from-token.json").getLines().mkString("\n")))).once() - (mockClient.httpRequest _).expects( - Method.GET, - Uri.unsafeFromString("https://discover.provider.plex.tv/library/metadata/5df46a38237002001dce338d?X-Plex-Token=test-token"), - None, - None - ).returning(IO.pure(parse(Source.fromResource("single-item-plex-metadata.json").getLines().mkString("\n")))).once() - (mockClient.httpRequest _).expects( - Method.GET, - Uri.unsafeFromString("https://discover.provider.plex.tv/library/metadata/617d3ab142705b2183b1b20b?X-Plex-Token=test-token"), - None, - None - ).returning(IO.pure(Left(new Exception("404")))).once() + val config = createConfiguration(Set("test-token")) + (mockClient.httpRequest _) + .expects( + Method.GET, + Uri.unsafeFromString( + "https://metadata.provider.plex.tv/library/sections/watchlist/all?X-Plex-Token=test-token&X-Plex-Container-Start=0&X-Plex-Container-Size=300" + ), + None, + None + ) + .returning(IO.pure(parse(Source.fromResource("self-watchlist-from-token.json").getLines().mkString("\n")))) + .once() + (mockClient.httpRequest _) + .expects( + Method.GET, + Uri.unsafeFromString( + "https://discover.provider.plex.tv/library/metadata/5df46a38237002001dce338d?X-Plex-Token=test-token" + ), + None, + None + ) + .returning(IO.pure(parse(Source.fromResource("single-item-plex-metadata.json").getLines().mkString("\n")))) + .once() + (mockClient.httpRequest _) + .expects( + Method.GET, + Uri.unsafeFromString( + "https://discover.provider.plex.tv/library/metadata/617d3ab142705b2183b1b20b?X-Plex-Token=test-token" + ), + None, + None + ) + .returning(IO.pure(Left(new Exception("404")))) + .once() val eitherResult = getSelfWatchlist(config, mockClient).value.unsafeRunSync() @@ -141,9 +186,8 @@ class PlexUtilsSpec extends AnyFlatSpec with Matchers with PlexUtils with MockFa it should "successfully fetch friends from Plex" in { val mockClient = mock[HttpClient] - val config = createConfiguration(Set("test-token")) - val query = GraphQLQuery( - """query GetAllFriends { + val config = createConfiguration(Set("test-token")) + val query = GraphQLQuery("""query GetAllFriends { | allFriendsV2 { | user { | id @@ -151,12 +195,15 @@ class PlexUtilsSpec extends AnyFlatSpec with Matchers with PlexUtils with MockFa | } | } | }""".stripMargin) - (mockClient.httpRequest _).expects( - Method.POST, - Uri.unsafeFromString("https://community.plex.tv/api"), - Some("test-token"), - Some(query.asJson) - ).returning(IO.pure(parse(Source.fromResource("plex-get-all-friends.json").getLines().mkString("\n")))).once() + (mockClient.httpRequest _) + .expects( + Method.POST, + Uri.unsafeFromString("https://community.plex.tv/api"), + Some("test-token"), + Some(query.asJson) + ) + .returning(IO.pure(parse(Source.fromResource("plex-get-all-friends.json").getLines().mkString("\n")))) + .once() val eitherResult = getFriends(config, mockClient).value.unsafeRunSync() @@ -169,44 +216,69 @@ class PlexUtilsSpec extends AnyFlatSpec with Matchers with PlexUtils with MockFa it should "successfully fetch a watchlist from a friend on Plex" in { val mockClient = mock[HttpClient] - val config = createConfiguration(Set("test-token")) - (mockClient.httpRequest _).expects( - Method.POST, - Uri.unsafeFromString("https://community.plex.tv/api"), - Some("test-token"), - * - ).returning(IO.pure(parse(Source.fromResource("plex-get-watchlist-from-friend.json").getLines().mkString("\n")))).once() - - val eitherResult = getWatchlistIdsForUser(config, mockClient, "test-token")(User("ecdb6as0230e2115", "friend-1")).value.unsafeRunSync() + val config = createConfiguration(Set("test-token")) + (mockClient.httpRequest _) + .expects( + Method.POST, + Uri.unsafeFromString("https://community.plex.tv/api"), + Some("test-token"), + * + ) + .returning(IO.pure(parse(Source.fromResource("plex-get-watchlist-from-friend.json").getLines().mkString("\n")))) + .once() + + val eitherResult = getWatchlistIdsForUser(config, mockClient, "test-token")( + User("ecdb6as0230e2115", "friend-1") + ).value.unsafeRunSync() eitherResult shouldBe a[Right[_, _]] val result = eitherResult.getOrElse(Set.empty[TokenWatchlistItem]) result.size shouldBe 2 - result.head shouldBe TokenWatchlistItem("The Twilight Saga: Breaking Dawn - Part 2", "5d77688b9ab54400214e789b", "movie", "/library/metadata/5d77688b9ab54400214e789b") + result.head shouldBe TokenWatchlistItem( + "The Twilight Saga: Breaking Dawn - Part 2", + "5d77688b9ab54400214e789b", + "movie", + "/library/metadata/5d77688b9ab54400214e789b" + ) } it should "successfully fetch multiple watchlist pages from a friend on Plex" in { val mockClient = mock[HttpClient] - val config = createConfiguration(Set("test-token")) - (mockClient.httpRequest _).expects( - Method.POST, - Uri.unsafeFromString("https://community.plex.tv/api"), - Some("test-token"), - * - ).returning(IO.pure(parse(Source.fromResource("plex-get-watchlist-from-friend-page-1.json").getLines().mkString("\n")))).repeat(13) - (mockClient.httpRequest _).expects( - Method.POST, - Uri.unsafeFromString("https://community.plex.tv/api"), - Some("test-token"), - * - ).returning(IO.pure(parse(Source.fromResource("plex-get-watchlist-from-friend.json").getLines().mkString("\n")))).once() - - val eitherResult = getWatchlistIdsForUser(config, mockClient, "test-token")(User("ecdb6as0230e2115", "friend-1")).value.unsafeRunSync() + val config = createConfiguration(Set("test-token")) + (mockClient.httpRequest _) + .expects( + Method.POST, + Uri.unsafeFromString("https://community.plex.tv/api"), + Some("test-token"), + * + ) + .returning( + IO.pure(parse(Source.fromResource("plex-get-watchlist-from-friend-page-1.json").getLines().mkString("\n"))) + ) + .repeat(13) + (mockClient.httpRequest _) + .expects( + Method.POST, + Uri.unsafeFromString("https://community.plex.tv/api"), + Some("test-token"), + * + ) + .returning(IO.pure(parse(Source.fromResource("plex-get-watchlist-from-friend.json").getLines().mkString("\n")))) + .once() + + val eitherResult = getWatchlistIdsForUser(config, mockClient, "test-token")( + User("ecdb6as0230e2115", "friend-1") + ).value.unsafeRunSync() eitherResult shouldBe a[Right[_, _]] val result = eitherResult.getOrElse(Set.empty[TokenWatchlistItem]) result.size shouldBe 2 - result.head shouldBe TokenWatchlistItem("The Twilight Saga: Breaking Dawn - Part 2", "5d77688b9ab54400214e789b", "movie", "/library/metadata/5d77688b9ab54400214e789b") + result.head shouldBe TokenWatchlistItem( + "The Twilight Saga: Breaking Dawn - Part 2", + "5d77688b9ab54400214e789b", + "movie", + "/library/metadata/5d77688b9ab54400214e789b" + ) } private def createConfiguration(plexTokens: Set[String]): PlexConfiguration = PlexConfiguration( diff --git a/src/test/scala/radarr/RadarrUtilsSpec.scala b/src/test/scala/radarr/RadarrUtilsSpec.scala index da6ee72..9a844d2 100644 --- a/src/test/scala/radarr/RadarrUtilsSpec.scala +++ b/src/test/scala/radarr/RadarrUtilsSpec.scala @@ -15,49 +15,71 @@ import scala.io.Source class RadarrUtilsSpec extends AnyFlatSpec with Matchers with RadarrUtils with MockFactory { "RadarrUtils" should "successfully fetch a list of movies and exclusions from Radarr" in { - val movieJsonStr = Source.fromResource("radarr.json").getLines().mkString("\n") + val movieJsonStr = Source.fromResource("radarr.json").getLines().mkString("\n") val exclusionsJsonStr = Source.fromResource("exclusions.json").getLines().mkString("\n") - val mockClient = mock[HttpClient] - (mockClient.httpRequest _).expects( - Method.GET, - Uri.unsafeFromString("http://localhost:7878").withPath(Uri.Path.unsafeFromString("/api/v3/movie")), - Some("radarr-api-key"), - None - ).returning(IO.pure(parse(movieJsonStr))).once() - (mockClient.httpRequest _).expects( - Method.GET, - Uri.unsafeFromString("http://localhost:7878").withPath(Uri.Path.unsafeFromString("/api/v3/exclusions")), - Some("radarr-api-key"), - None - ).returning(IO.pure(parse(exclusionsJsonStr))).once() + val mockClient = mock[HttpClient] + (mockClient.httpRequest _) + .expects( + Method.GET, + Uri.unsafeFromString("http://localhost:7878").withPath(Uri.Path.unsafeFromString("/api/v3/movie")), + Some("radarr-api-key"), + None + ) + .returning(IO.pure(parse(movieJsonStr))) + .once() + (mockClient.httpRequest _) + .expects( + Method.GET, + Uri.unsafeFromString("http://localhost:7878").withPath(Uri.Path.unsafeFromString("/api/v3/exclusions")), + Some("radarr-api-key"), + None + ) + .returning(IO.pure(parse(exclusionsJsonStr))) + .once() - val eitherResult = fetchMovies(mockClient)("radarr-api-key", Uri.unsafeFromString("http://localhost:7878"), false).value.unsafeRunSync() + val eitherResult = + fetchMovies(mockClient)("radarr-api-key", Uri.unsafeFromString("http://localhost:7878"), false).value + .unsafeRunSync() eitherResult shouldBe a[Right[_, _]] val result = eitherResult.getOrElse(Set.empty) result.size shouldBe 157 - result.find(_.title == "Moonlight") shouldBe Some(Item("Moonlight", List("tt4975722", "tmdb://376867", "radarr://32"), "movie")) - result.find(_.title == "Oculus") shouldBe Some(Item("Oculus", List("tt2388715", "tmdb://157547", "radarr://21"), "movie")) + result.find(_.title == "Moonlight") shouldBe Some( + Item("Moonlight", List("tt4975722", "tmdb://376867", "radarr://32"), "movie") + ) + result.find(_.title == "Oculus") shouldBe Some( + Item("Oculus", List("tt2388715", "tmdb://157547", "radarr://21"), "movie") + ) // Check that exclusions are added - result.find(_.title == "Monty Python and the Holy Grail") shouldBe Some(Item("Monty Python and the Holy Grail", List("tmdb://762", "radarr://2"), "movie")) + result.find(_.title == "Monty Python and the Holy Grail") shouldBe Some( + Item("Monty Python and the Holy Grail", List("tmdb://762", "radarr://2"), "movie") + ) } it should "not fail when the list returned is empty" in { val mockClient = mock[HttpClient] - (mockClient.httpRequest _).expects( - Method.GET, - Uri.unsafeFromString("http://localhost:7878").withPath(Uri.Path.unsafeFromString("/api/v3/movie")), - Some("radarr-api-key"), - None - ).returning(IO.pure(parse("[]"))).once() - (mockClient.httpRequest _).expects( - Method.GET, - Uri.unsafeFromString("http://localhost:7878").withPath(Uri.Path.unsafeFromString("/api/v3/exclusions")), - Some("radarr-api-key"), - None - ).returning(IO.pure(parse("[]"))).once() + (mockClient.httpRequest _) + .expects( + Method.GET, + Uri.unsafeFromString("http://localhost:7878").withPath(Uri.Path.unsafeFromString("/api/v3/movie")), + Some("radarr-api-key"), + None + ) + .returning(IO.pure(parse("[]"))) + .once() + (mockClient.httpRequest _) + .expects( + Method.GET, + Uri.unsafeFromString("http://localhost:7878").withPath(Uri.Path.unsafeFromString("/api/v3/exclusions")), + Some("radarr-api-key"), + None + ) + .returning(IO.pure(parse("[]"))) + .once() - val eitherResult = fetchMovies(mockClient)("radarr-api-key", Uri.unsafeFromString("http://localhost:7878"), false).value.unsafeRunSync() + val eitherResult = + fetchMovies(mockClient)("radarr-api-key", Uri.unsafeFromString("http://localhost:7878"), false).value + .unsafeRunSync() eitherResult shouldBe Right(Set.empty) } diff --git a/src/test/scala/sonarr/SonarrUtilsSpec.scala b/src/test/scala/sonarr/SonarrUtilsSpec.scala index 4fd4f66..aa06530 100644 --- a/src/test/scala/sonarr/SonarrUtilsSpec.scala +++ b/src/test/scala/sonarr/SonarrUtilsSpec.scala @@ -15,49 +15,80 @@ import scala.io.Source class SonarrUtilsSpec extends AnyFlatSpec with Matchers with SonarrUtils with MockFactory { "SonarrUtils" should "successfully fetch a list of series and exclusions from Sonarr" in { - val seriesJsonStr = Source.fromResource("sonarr.json").getLines().mkString("\n") + val seriesJsonStr = Source.fromResource("sonarr.json").getLines().mkString("\n") val exclusionsJsonStr = Source.fromResource("importlistexclusion.json").getLines().mkString("\n") - val mockClient = mock[HttpClient] - (mockClient.httpRequest _).expects( - Method.GET, - Uri.unsafeFromString("http://localhost:8989").withPath(Uri.Path.unsafeFromString("/api/v3/series")), - Some("sonarr-api-key"), - None - ).returning(IO.pure(parse(seriesJsonStr))).once() - (mockClient.httpRequest _).expects( - Method.GET, - Uri.unsafeFromString("http://localhost:8989").withPath(Uri.Path.unsafeFromString("/api/v3/importlistexclusion")), - Some("sonarr-api-key"), - None - ).returning(IO.pure(parse(exclusionsJsonStr))).once() + val mockClient = mock[HttpClient] + (mockClient.httpRequest _) + .expects( + Method.GET, + Uri.unsafeFromString("http://localhost:8989").withPath(Uri.Path.unsafeFromString("/api/v3/series")), + Some("sonarr-api-key"), + None + ) + .returning(IO.pure(parse(seriesJsonStr))) + .once() + (mockClient.httpRequest _) + .expects( + Method.GET, + Uri + .unsafeFromString("http://localhost:8989") + .withPath(Uri.Path.unsafeFromString("/api/v3/importlistexclusion")), + Some("sonarr-api-key"), + None + ) + .returning(IO.pure(parse(exclusionsJsonStr))) + .once() - val eitherResult = fetchSeries(mockClient)("sonarr-api-key", Uri.unsafeFromString("http://localhost:8989"), false).value.unsafeRunSync() + val eitherResult = + fetchSeries(mockClient)("sonarr-api-key", Uri.unsafeFromString("http://localhost:8989"), false).value + .unsafeRunSync() eitherResult shouldBe a[Right[_, _]] val result = eitherResult.getOrElse(Set.empty) result.size shouldBe 76 - result.find(_.title == "The Secret Life of 4, 5 and 6 Year Olds") shouldBe Some(Item("The Secret Life of 4, 5 and 6 Year Olds", List("tt6620876", "tvdb://304746", "sonarr://76"), "show", Some(true))) - result.find(_.title == "Maternal") shouldBe Some(Item("Maternal", List("tt21636214", "tvdb://424724", "sonarr://70"), "show", Some(true))) + result.find(_.title == "The Secret Life of 4, 5 and 6 Year Olds") shouldBe Some( + Item( + "The Secret Life of 4, 5 and 6 Year Olds", + List("tt6620876", "tvdb://304746", "sonarr://76"), + "show", + Some(true) + ) + ) + result.find(_.title == "Maternal") shouldBe Some( + Item("Maternal", List("tt21636214", "tvdb://424724", "sonarr://70"), "show", Some(true)) + ) // Check that exclusions are added - result.find(_.title == "The Test") shouldBe Some(Item("The Test", List("tvdb://372848", "sonarr://1"), "show", None)) + result.find(_.title == "The Test") shouldBe Some( + Item("The Test", List("tvdb://372848", "sonarr://1"), "show", None) + ) } it should "not fail when the list returned is empty" in { val mockClient = mock[HttpClient] - (mockClient.httpRequest _).expects( - Method.GET, - Uri.unsafeFromString("http://localhost:8989").withPath(Uri.Path.unsafeFromString("/api/v3/series")), - Some("sonarr-api-key"), - None - ).returning(IO.pure(parse("[]"))).once() - (mockClient.httpRequest _).expects( - Method.GET, - Uri.unsafeFromString("http://localhost:8989").withPath(Uri.Path.unsafeFromString("/api/v3/importlistexclusion")), - Some("sonarr-api-key"), - None - ).returning(IO.pure(parse("[]"))).once() + (mockClient.httpRequest _) + .expects( + Method.GET, + Uri.unsafeFromString("http://localhost:8989").withPath(Uri.Path.unsafeFromString("/api/v3/series")), + Some("sonarr-api-key"), + None + ) + .returning(IO.pure(parse("[]"))) + .once() + (mockClient.httpRequest _) + .expects( + Method.GET, + Uri + .unsafeFromString("http://localhost:8989") + .withPath(Uri.Path.unsafeFromString("/api/v3/importlistexclusion")), + Some("sonarr-api-key"), + None + ) + .returning(IO.pure(parse("[]"))) + .once() - val eitherResult = fetchSeries(mockClient)("sonarr-api-key", Uri.unsafeFromString("http://localhost:8989"), false).value.unsafeRunSync() + val eitherResult = + fetchSeries(mockClient)("sonarr-api-key", Uri.unsafeFromString("http://localhost:8989"), false).value + .unsafeRunSync() eitherResult shouldBe Right(Set.empty) }