From 6c0e41ab8a219851c6bb3b62fd4779f0c7f1acd8 Mon Sep 17 00:00:00 2001 From: Nihal Mirpuri Date: Thu, 16 Nov 2023 22:17:39 +0000 Subject: [PATCH] Ping plex token every 24 hours (#35) --- docker/entrypoint.sh | 4 ++++ src/main/scala/PlexTokenSync.scala | 9 +++++++++ src/main/scala/Server.scala | 16 +++++++++++++--- src/main/scala/configuration/Configuration.scala | 3 ++- .../scala/configuration/ConfigurationUtils.scala | 4 +++- src/main/scala/configuration/Keys.scala | 1 + src/main/scala/plex/PlexUtils.scala | 16 ++++++++++++++++ .../configuration/ConfigurationUtilsSpec.scala | 14 +++++++++++++- 8 files changed, 61 insertions(+), 6 deletions(-) create mode 100644 src/main/scala/PlexTokenSync.scala diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh index 4820e1c..43aa446 100644 --- a/docker/entrypoint.sh +++ b/docker/entrypoint.sh @@ -58,4 +58,8 @@ if [ -n "$SONARR_SEASON_MONITORING" ]; then CMD="$CMD -Dsonarr.seasonMonitoring=$SONARR_SEASON_MONITORING" fi +if [ -n "$PLEX_TOKEN" ]; then + CMD="$CMD -Dplex.token=$PLEX_TOKEN" +fi + exec $CMD diff --git a/src/main/scala/PlexTokenSync.scala b/src/main/scala/PlexTokenSync.scala new file mode 100644 index 0000000..4625eff --- /dev/null +++ b/src/main/scala/PlexTokenSync.scala @@ -0,0 +1,9 @@ +import cats.effect.IO +import configuration.Configuration +import http.HttpClient +import plex.PlexUtils + +object PlexTokenSync extends PlexUtils { + + def run(config: Configuration, client: HttpClient): IO[Unit] = ping(client)(config) +} diff --git a/src/main/scala/Server.scala b/src/main/scala/Server.scala index 09f887a..4997313 100644 --- a/src/main/scala/Server.scala +++ b/src/main/scala/Server.scala @@ -1,10 +1,12 @@ import cats.effect._ +import cats.implicits.catsSyntaxTuple2Parallel import configuration.{Configuration, ConfigurationUtils, SystemPropertyReader} import http.HttpClient import org.slf4j.LoggerFactory import java.nio.channels.ClosedChannelException +import scala.concurrent.duration.DurationInt object Server extends IOApp { @@ -21,15 +23,23 @@ object Server extends IOApp { for { memoizedConfigIo <- ConfigurationUtils.create(configReader, httpClient).memoize - result <- periodicTask(memoizedConfigIo, httpClient).foreverM.as(ExitCode.Success) + result <- (watchlistSync(memoizedConfigIo, httpClient), plexTokenSync(memoizedConfigIo, httpClient)).parTupled.as(ExitCode.Success) } yield result } - private def periodicTask(configIO: IO[Configuration], httpClient: HttpClient): IO[Unit] = + private def watchlistSync(configIO: IO[Configuration], httpClient: HttpClient): IO[Unit] = for { config <- configIO _ <- WatchlistSync.run(config, httpClient) _ <- IO.sleep(config.refreshInterval) - _ <- periodicTask(configIO, httpClient) + _ <- watchlistSync(configIO, httpClient) + } yield () + + private def plexTokenSync(configIO: IO[Configuration], httpClient: HttpClient): IO[Unit] = + for { + config <- configIO + _ <- PlexTokenSync.run(config, httpClient) + _ <- IO.sleep(24.hours) + _ <- plexTokenSync(configIO, httpClient) } yield () } diff --git a/src/main/scala/configuration/Configuration.scala b/src/main/scala/configuration/Configuration.scala index c36a684..72a4fe5 100644 --- a/src/main/scala/configuration/Configuration.scala +++ b/src/main/scala/configuration/Configuration.scala @@ -17,5 +17,6 @@ case class Configuration( radarrQualityProfileId: Int, radarrRootFolder: String, radarrBypassIgnored: Boolean, - plexWatchlistUrls: List[Uri] + plexWatchlistUrls: List[Uri], + plexToken: Option[String] ) diff --git a/src/main/scala/configuration/ConfigurationUtils.scala b/src/main/scala/configuration/ConfigurationUtils.scala index acc521c..9884726 100644 --- a/src/main/scala/configuration/ConfigurationUtils.scala +++ b/src/main/scala/configuration/ConfigurationUtils.scala @@ -24,6 +24,7 @@ object ConfigurationUtils { (radarrBaseUrl, radarrApiKey, radarrQualityProfileId, radarrRootFolder) = radarrConfig radarrBypassIgnored = configReader.getConfigOption(Keys.radarrBypassIgnored).exists(_.toBoolean) plexWatchlistUrls = getPlexWatchlistUrls(configReader) + plexToken = configReader.getConfigOption(Keys.plexToken) } yield Configuration( refreshInterval, sonarrBaseUrl, @@ -37,7 +38,8 @@ object ConfigurationUtils { radarrQualityProfileId, radarrRootFolder, radarrBypassIgnored, - plexWatchlistUrls + plexWatchlistUrls, + plexToken ) private def getSonarrConfig(configReader: ConfigurationReader, client: HttpClient): IO[(Uri, String, Int, String)] = { diff --git a/src/main/scala/configuration/Keys.scala b/src/main/scala/configuration/Keys.scala index 432184d..86e2ac7 100644 --- a/src/main/scala/configuration/Keys.scala +++ b/src/main/scala/configuration/Keys.scala @@ -18,4 +18,5 @@ private[configuration] object Keys { val plexWatchlist1 = "plex.watchlist1" val plexWatchlist2 = "plex.watchlist2" + val plexToken = "plex.token" } diff --git a/src/main/scala/plex/PlexUtils.scala b/src/main/scala/plex/PlexUtils.scala index a378e1c..d200252 100644 --- a/src/main/scala/plex/PlexUtils.scala +++ b/src/main/scala/plex/PlexUtils.scala @@ -2,6 +2,7 @@ package plex import io.circe.generic.auto._ import cats.effect.IO +import configuration.Configuration import http.HttpClient import model.Item import org.http4s.{Method, Uri} @@ -23,4 +24,19 @@ trait PlexUtils { Set.empty } } + + protected def ping(client: HttpClient)(config: Configuration): IO[Unit] = + config.plexToken.map { token => + val url = Uri + .unsafeFromString("https://plex.tv/api/v2/ping") + .withQueryParam("X-Plex-Token", token) + .withQueryParam("X-Plex-Client-Identifier", "watchlistarr") + + client.httpRequest(Method.GET, url).map { + case Right(_) => + logger.info(s"Refreshed the access token expiry") + case Left(err) => + logger.warn(s"Unable to ping plex.tv: $err") + } + }.getOrElse(IO.unit) } diff --git a/src/test/scala/configuration/ConfigurationUtilsSpec.scala b/src/test/scala/configuration/ConfigurationUtilsSpec.scala index 237e47d..0d4d988 100644 --- a/src/test/scala/configuration/ConfigurationUtilsSpec.scala +++ b/src/test/scala/configuration/ConfigurationUtilsSpec.scala @@ -150,13 +150,24 @@ class ConfigurationUtilsSpec extends AnyFlatSpec with Matchers with MockFactory an[IllegalArgumentException] should be thrownBy ConfigurationUtils.create(mockConfigReader, mockHttpClient).unsafeRunSync() } + it should "allow an optional plex token to be passed in" in { + + val mockConfigReader = createMockConfigReader(plexToken = Some("test-token")) + val mockHttpClient = createMockHttpClient() + + val config = ConfigurationUtils.create(mockConfigReader, mockHttpClient).unsafeRunSync() + noException should be thrownBy config + config.plexToken shouldBe Some("test-token") + } + 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] = Some(s"https://rss.plex.tv/1"), - plexWatchlist2: Option[String] = None + plexWatchlist2: Option[String] = None, + plexToken: Option[String] = None ): ConfigurationReader = { val unset = None @@ -175,6 +186,7 @@ class ConfigurationUtilsSpec extends AnyFlatSpec with Matchers with MockFactory (mockConfigReader.getConfigOption _).expects(Keys.plexWatchlist1).returning(plexWatchlist1).anyNumberOfTimes() (mockConfigReader.getConfigOption _).expects(Keys.plexWatchlist2).returning(plexWatchlist2).anyNumberOfTimes() (mockConfigReader.getConfigOption _).expects(Keys.sonarrSeasonMonitoring).returning(unset).anyNumberOfTimes() + (mockConfigReader.getConfigOption _).expects(Keys.plexToken).returning(plexToken).anyNumberOfTimes() mockConfigReader }