diff --git a/README.md b/README.md index 6402259..a03ce64 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ upon startup of the app, where the logs will list the movies/tv shows that are o ### Requirements * Plex Pass Subscription -* Sonarr v4 or higher +* Sonarr v3 or higher * Radarr v3 or higher * Friends must change their privacy settings so that the main user can see their watchlists * Docker or Java @@ -65,9 +65,9 @@ Docker tag options: Running this using native java requires the fat jar, download the latest from the Releases tab, and run: ```bash -java -Dsonarr.apikey=YOUR_API_KEY\ - -Dradarr.apikey=YOUR_API_KEY\ - -Dplex.token=YOUR_PLEX_TOKEN\ +java "-Dsonarr.apikey=YOUR_API_KEY"\ + "-Dradarr.apikey=YOUR_API_KEY"\ + "-Dplex.token=YOUR_PLEX_TOKEN"\ -Xmx100m\ -jar watchlistarr.java ``` diff --git a/src/main/scala/configuration/Configuration.scala b/src/main/scala/configuration/Configuration.scala index ee5a443..4e19c3c 100644 --- a/src/main/scala/configuration/Configuration.scala +++ b/src/main/scala/configuration/Configuration.scala @@ -12,6 +12,7 @@ case class Configuration( sonarrRootFolder: String, sonarrBypassIgnored: Boolean, sonarrSeasonMonitoring: String, + sonarrLanguageProfileId: Int, radarrBaseUrl: Uri, radarrApiKey: String, radarrQualityProfileId: Int, diff --git a/src/main/scala/configuration/ConfigurationUtils.scala b/src/main/scala/configuration/ConfigurationUtils.scala index f6b6735..ac47c63 100644 --- a/src/main/scala/configuration/ConfigurationUtils.scala +++ b/src/main/scala/configuration/ConfigurationUtils.scala @@ -19,7 +19,7 @@ object ConfigurationUtils { for { sonarrConfig <- getSonarrConfig(configReader, client) refreshInterval = configReader.getConfigOption(Keys.intervalSeconds).flatMap(_.toIntOption).getOrElse(60).seconds - (sonarrBaseUrl, sonarrApiKey, sonarrQualityProfileId, sonarrRootFolder) = sonarrConfig + (sonarrBaseUrl, sonarrApiKey, sonarrQualityProfileId, sonarrRootFolder, sonarrLanguageProfileId) = sonarrConfig sonarrBypassIgnored = configReader.getConfigOption(Keys.sonarrBypassIgnored).exists(_.toBoolean) sonarrSeasonMonitoring = configReader.getConfigOption(Keys.sonarrSeasonMonitoring).getOrElse("all") radarrConfig <- getRadarrConfig(configReader, client) @@ -36,6 +36,7 @@ object ConfigurationUtils { sonarrRootFolder, sonarrBypassIgnored, sonarrSeasonMonitoring, + sonarrLanguageProfileId, radarrBaseUrl, radarrApiKey, radarrQualityProfileId, @@ -46,7 +47,7 @@ object ConfigurationUtils { skipFriendSync ) - private def getSonarrConfig(configReader: ConfigurationReader, client: HttpClient): IO[(Uri, String, Int, String)] = { + private def getSonarrConfig(configReader: ConfigurationReader, client: HttpClient): IO[(Uri, String, Int, String, Int)] = { val url = configReader.getConfigOption(Keys.sonarrBaseUrl).flatMap(Uri.fromString(_).toOption).getOrElse { val default = "http://localhost:8989" logger.warn(s"Unable to fetch sonarr baseUrl, using default $default") @@ -54,23 +55,34 @@ object ConfigurationUtils { } val apiKey = configReader.getConfigOption(Keys.sonarrApiKey).getOrElse(throwError("Unable to find sonarr API key")) - getToArr(client)(url, apiKey, "rootFolder").map { - case Right(res) => - logger.info("Successfully connected to Sonarr") - val allRootFolders = res.as[List[RootFolder]].getOrElse(List.empty) - selectRootFolder(allRootFolders, configReader.getConfigOption(Keys.sonarrRootFolder)) - case Left(err) => - throwError(s"Unable to connect to Sonarr at $url, with error $err") - }.flatMap(rootFolder => - getToArr(client)(url, apiKey, "qualityprofile").map { + for { + rootFolder <- getToArr(client)(url, apiKey, "rootFolder").map { + case Right(res) => + logger.info("Successfully connected to Sonarr") + val allRootFolders = res.as[List[RootFolder]].getOrElse(List.empty) + selectRootFolder(allRootFolders, configReader.getConfigOption(Keys.sonarrRootFolder)) + case Left(err) => + throwError(s"Unable to connect to Sonarr at $url, with error $err") + } + qualityProfileId <- getToArr(client)(url, apiKey, "qualityprofile").map { case Right(res) => val allQualityProfiles = res.as[List[QualityProfile]].getOrElse(List.empty) val chosenQualityProfile = configReader.getConfigOption(Keys.sonarrQualityProfile) - (url, apiKey, getQualityProfileId(allQualityProfiles, chosenQualityProfile), rootFolder) + getQualityProfileId(allQualityProfiles, chosenQualityProfile) case Left(err) => throwError(s"Unable to connect to Sonarr at $url, with error $err") } - ) + languageProfileId <- getToArr(client)(url, apiKey, "languageprofile").map { + case Right(res) => + val allLanguageProfiles = res.as[List[LanguageProfile]].getOrElse(List.empty) + allLanguageProfiles.headOption.map(_.id).getOrElse { + logger.warn("Unable to find a language profile, using 1 as default") + 1 + } + case Left(err) => + throwError(s"Unable to connect to Sonarr at $url, with error $err") + } + } yield (url, apiKey, qualityProfileId, rootFolder, languageProfileId) } private def getRadarrConfig(configReader: ConfigurationReader, client: HttpClient): IO[(Uri, String, Int, String)] = { diff --git a/src/main/scala/configuration/LanguageProfile.scala b/src/main/scala/configuration/LanguageProfile.scala new file mode 100644 index 0000000..5df453d --- /dev/null +++ b/src/main/scala/configuration/LanguageProfile.scala @@ -0,0 +1,3 @@ +package configuration + +private[configuration] case class LanguageProfile(name: String, id: Int) diff --git a/src/main/scala/sonarr/SonarrPost.scala b/src/main/scala/sonarr/SonarrPost.scala index c7f298e..a4ba7ac 100644 --- a/src/main/scala/sonarr/SonarrPost.scala +++ b/src/main/scala/sonarr/SonarrPost.scala @@ -1,3 +1,11 @@ package sonarr -private[sonarr] case class SonarrPost(title: String, tvdbId: Long, qualityProfileId: Int, rootFolderPath: String, addOptions: SonarrAddOptions, monitored: Boolean = true) +private[sonarr] case class SonarrPost( + title: String, + tvdbId: Long, + qualityProfileId: Int, + rootFolderPath: String, + addOptions: SonarrAddOptions, + languageProfileId: Int, + monitored: Boolean = true + ) diff --git a/src/main/scala/sonarr/SonarrUtils.scala b/src/main/scala/sonarr/SonarrUtils.scala index 851ab0f..17aa419 100644 --- a/src/main/scala/sonarr/SonarrUtils.scala +++ b/src/main/scala/sonarr/SonarrUtils.scala @@ -28,7 +28,14 @@ trait SonarrUtils extends SonarrConversions { protected def addToSonarr(client: HttpClient)(config: Configuration)(item: Item): IO[Unit] = { val addOptions = SonarrAddOptions(config.sonarrSeasonMonitoring) - val show = SonarrPost(item.title, item.getTvdbId.getOrElse(0L), config.sonarrQualityProfileId, config.sonarrRootFolder, addOptions) + val show = SonarrPost( + item.title, + item.getTvdbId.getOrElse(0L), + config.sonarrQualityProfileId, + config.sonarrRootFolder, + addOptions, + config.sonarrLanguageProfileId, + ) val result = postToArr[Unit](client)(config.sonarrBaseUrl, config.sonarrApiKey, "series")(show.asJson) .fold( diff --git a/src/test/resources/sonarr-language-profile.json b/src/test/resources/sonarr-language-profile.json new file mode 100644 index 0000000..e1b28ce --- /dev/null +++ b/src/test/resources/sonarr-language-profile.json @@ -0,0 +1,458 @@ +[ + { + "name": "English", + "upgradeAllowed": false, + "cutoff": { + "id": 1, + "name": "English" + }, + "languages": [ + { + "language": { + "id": 13, + "name": "Vietnamese" + }, + "allowed": false + }, + { + "language": { + "id": 0, + "name": "Unknown" + }, + "allowed": false + }, + { + "language": { + "id": 30, + "name": "Ukrainian" + }, + "allowed": false + }, + { + "language": { + "id": 17, + "name": "Turkish" + }, + "allowed": false + }, + { + "language": { + "id": 14, + "name": "Swedish" + }, + "allowed": false + }, + { + "language": { + "id": 3, + "name": "Spanish" + }, + "allowed": false + }, + { + "language": { + "id": 11, + "name": "Russian" + }, + "allowed": false + }, + { + "language": { + "id": 18, + "name": "Portuguese" + }, + "allowed": false + }, + { + "language": { + "id": 12, + "name": "Polish" + }, + "allowed": false + }, + { + "language": { + "id": 15, + "name": "Norwegian" + }, + "allowed": false + }, + { + "language": { + "id": 29, + "name": "Malayalam" + }, + "allowed": false + }, + { + "language": { + "id": 24, + "name": "Lithuanian" + }, + "allowed": false + }, + { + "language": { + "id": 21, + "name": "Korean" + }, + "allowed": false + }, + { + "language": { + "id": 8, + "name": "Japanese" + }, + "allowed": false + }, + { + "language": { + "id": 5, + "name": "Italian" + }, + "allowed": false + }, + { + "language": { + "id": 9, + "name": "Icelandic" + }, + "allowed": false + }, + { + "language": { + "id": 22, + "name": "Hungarian" + }, + "allowed": false + }, + { + "language": { + "id": 27, + "name": "Hindi" + }, + "allowed": false + }, + { + "language": { + "id": 23, + "name": "Hebrew" + }, + "allowed": false + }, + { + "language": { + "id": 20, + "name": "Greek" + }, + "allowed": false + }, + { + "language": { + "id": 4, + "name": "German" + }, + "allowed": false + }, + { + "language": { + "id": 2, + "name": "French" + }, + "allowed": false + }, + { + "language": { + "id": 19, + "name": "Flemish" + }, + "allowed": false + }, + { + "language": { + "id": 16, + "name": "Finnish" + }, + "allowed": false + }, + { + "language": { + "id": 1, + "name": "English" + }, + "allowed": true + }, + { + "language": { + "id": 7, + "name": "Dutch" + }, + "allowed": false + }, + { + "language": { + "id": 6, + "name": "Danish" + }, + "allowed": false + }, + { + "language": { + "id": 25, + "name": "Czech" + }, + "allowed": false + }, + { + "language": { + "id": 10, + "name": "Chinese" + }, + "allowed": false + }, + { + "language": { + "id": 28, + "name": "Bulgarian" + }, + "allowed": false + }, + { + "language": { + "id": 26, + "name": "Arabic" + }, + "allowed": false + } + ], + "id": 3 + }, + { + "name": "Arabic", + "upgradeAllowed": false, + "cutoff": { + "id": 26, + "name": "Arabic" + }, + "languages": [ + { + "language": { + "id": 0, + "name": "Unknown" + }, + "allowed": false + }, + { + "language": { + "id": 13, + "name": "Vietnamese" + }, + "allowed": false + }, + { + "language": { + "id": 30, + "name": "Ukrainian" + }, + "allowed": false + }, + { + "language": { + "id": 17, + "name": "Turkish" + }, + "allowed": false + }, + { + "language": { + "id": 14, + "name": "Swedish" + }, + "allowed": false + }, + { + "language": { + "id": 3, + "name": "Spanish" + }, + "allowed": false + }, + { + "language": { + "id": 11, + "name": "Russian" + }, + "allowed": false + }, + { + "language": { + "id": 18, + "name": "Portuguese" + }, + "allowed": false + }, + { + "language": { + "id": 12, + "name": "Polish" + }, + "allowed": false + }, + { + "language": { + "id": 15, + "name": "Norwegian" + }, + "allowed": false + }, + { + "language": { + "id": 29, + "name": "Malayalam" + }, + "allowed": false + }, + { + "language": { + "id": 24, + "name": "Lithuanian" + }, + "allowed": false + }, + { + "language": { + "id": 21, + "name": "Korean" + }, + "allowed": false + }, + { + "language": { + "id": 8, + "name": "Japanese" + }, + "allowed": false + }, + { + "language": { + "id": 5, + "name": "Italian" + }, + "allowed": false + }, + { + "language": { + "id": 9, + "name": "Icelandic" + }, + "allowed": false + }, + { + "language": { + "id": 22, + "name": "Hungarian" + }, + "allowed": false + }, + { + "language": { + "id": 27, + "name": "Hindi" + }, + "allowed": false + }, + { + "language": { + "id": 23, + "name": "Hebrew" + }, + "allowed": false + }, + { + "language": { + "id": 20, + "name": "Greek" + }, + "allowed": false + }, + { + "language": { + "id": 4, + "name": "German" + }, + "allowed": false + }, + { + "language": { + "id": 2, + "name": "French" + }, + "allowed": false + }, + { + "language": { + "id": 19, + "name": "Flemish" + }, + "allowed": false + }, + { + "language": { + "id": 16, + "name": "Finnish" + }, + "allowed": false + }, + { + "language": { + "id": 1, + "name": "English" + }, + "allowed": false + }, + { + "language": { + "id": 7, + "name": "Dutch" + }, + "allowed": false + }, + { + "language": { + "id": 6, + "name": "Danish" + }, + "allowed": false + }, + { + "language": { + "id": 25, + "name": "Czech" + }, + "allowed": false + }, + { + "language": { + "id": 10, + "name": "Chinese" + }, + "allowed": false + }, + { + "language": { + "id": 28, + "name": "Bulgarian" + }, + "allowed": false + }, + { + "language": { + "id": 26, + "name": "Arabic" + }, + "allowed": true + } + ], + "id": 2 + } +] diff --git a/src/test/scala/PlexTokenSyncSpec.scala b/src/test/scala/PlexTokenSyncSpec.scala index b7f8c2d..a17f841 100644 --- a/src/test/scala/PlexTokenSyncSpec.scala +++ b/src/test/scala/PlexTokenSyncSpec.scala @@ -40,6 +40,7 @@ class PlexTokenSyncSpec extends AnyFlatSpec with Matchers with MockFactory { sonarrRootFolder = "/root/", sonarrBypassIgnored = false, sonarrSeasonMonitoring = "all", + sonarrLanguageProfileId = 1, radarrBaseUrl = Uri.unsafeFromString("https://localhost:7878"), radarrApiKey = "radarr-api-key", radarrQualityProfileId = 1, diff --git a/src/test/scala/WatchlistSyncSpec.scala b/src/test/scala/WatchlistSyncSpec.scala index cd6e36c..76fd0f4 100644 --- a/src/test/scala/WatchlistSyncSpec.scala +++ b/src/test/scala/WatchlistSyncSpec.scala @@ -44,6 +44,7 @@ class WatchlistSyncSpec extends AnyFlatSpec with Matchers with MockFactory { | "searchForCutoffUnmetEpisodes" : true, | "searchForMissingEpisodes" : true | }, + | "languageProfileId" : 1, | "monitored" : true |}""".stripMargin defaultPlexMock(mockHttpClient) @@ -194,6 +195,7 @@ class WatchlistSyncSpec extends AnyFlatSpec with Matchers with MockFactory { sonarrRootFolder = "/root/", sonarrBypassIgnored = sonarrBypassIgnored, sonarrSeasonMonitoring = "all", + sonarrLanguageProfileId = 1, radarrBaseUrl = Uri.unsafeFromString("https://localhost:7878"), radarrApiKey = "radarr-api-key", radarrQualityProfileId = 1, diff --git a/src/test/scala/configuration/ConfigurationUtilsSpec.scala b/src/test/scala/configuration/ConfigurationUtilsSpec.scala index 362e607..3399d86 100644 --- a/src/test/scala/configuration/ConfigurationUtilsSpec.scala +++ b/src/test/scala/configuration/ConfigurationUtilsSpec.scala @@ -24,6 +24,7 @@ class ConfigurationUtilsSpec extends AnyFlatSpec with Matchers with MockFactory noException should be thrownBy config config.radarrApiKey shouldBe "radarr-api-key" config.sonarrApiKey shouldBe "sonarr-api-key" + config.sonarrLanguageProfileId shouldBe 3 } it should "fail if missing sonarr API key" in { @@ -180,6 +181,12 @@ class ConfigurationUtilsSpec extends AnyFlatSpec with Matchers with MockFactory Some("sonarr-api-key"), None ).returning(IO.pure(Right(defaultQualityProfileResponse.asJson))).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")), diff --git a/src/test/scala/plex/PlexUtilsSpec.scala b/src/test/scala/plex/PlexUtilsSpec.scala index 078a2fe..5418deb 100644 --- a/src/test/scala/plex/PlexUtilsSpec.scala +++ b/src/test/scala/plex/PlexUtilsSpec.scala @@ -171,6 +171,7 @@ class PlexUtilsSpec extends AnyFlatSpec with Matchers with PlexUtils with MockFa sonarrRootFolder = "/root/", sonarrBypassIgnored = false, sonarrSeasonMonitoring = "all", + sonarrLanguageProfileId = 1, radarrBaseUrl = Uri.unsafeFromString("https://localhost:7878"), radarrApiKey = "radarr-api-key", radarrQualityProfileId = 1,