Skip to content

Commit

Permalink
Config files (#97)
Browse files Browse the repository at this point in the history
  • Loading branch information
nylonee authored Feb 17, 2024
1 parent 8394741 commit bd0236a
Show file tree
Hide file tree
Showing 8 changed files with 183 additions and 55 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ target/

### Environment file for local variables
.env
config/

*.txt

Expand Down
51 changes: 16 additions & 35 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,13 +52,10 @@ docker run \
-e SONARR_API_KEY=YOUR_API_KEY \
-e RADARR_API_KEY=YOUR_API_KEY \
-e PLEX_TOKEN=YOUR_PLEX_TOKEN \
-e REFRESH_INTERVAL_SECONDS=5 \
-v config:/app/config \
nylonee/watchlistarr
```

For a full list of possible environment variables to configure the app with, see the Environment Variables section of
this Readme

Docker tag options:

* `latest` - Stable version, follows the Releases
Expand Down Expand Up @@ -93,37 +90,21 @@ Save this file in the same directory as the .jar file, then create a shortcut to
Windows startup folder. In the properties of the shortcut, set it to start minimized (Thanks Redditor u/DanCBooper for
tip)

#### Java variables

For a full list of options to pass in when running the application on native java,
refer to the environment variables chart below, and cross-reference the key to the internal key
in [entrypoint.sh](https://github.com/nylonee/watchlistarr/blob/main/docker/entrypoint.sh)

### Environment Variables

| Key | Default | Description |
|--------------------------------|-----------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| SONARR_API_KEY* | | API key for Sonarr, found in your Sonarr UI -> General settings |
| RADARR_API_KEY* | | API key for Radarr, found in your Radarr UI -> General settings |
| PLEX_TOKEN* | | Token for Plex, retrieved via [this tutorial](https://support.plex.tv/articles/204059436-finding-an-authentication-token-x-plex-token/). Note that multiple tokens can be provided, comma separated |
| REFRESH_INTERVAL_SECONDS | 60 | Number of seconds to wait in between checking the watchlist |
| SONARR_BASE_URL | http://localhost:8989 | Base URL for Sonarr, including the 'http' and port and any configured urlbase |
| SONARR_QUALITY_PROFILE | | Quality profile for Sonarr, found in your Sonarr UI -> Profiles settings. If not set, will grab the first one it finds on Sonarr |
| SONARR_ROOT_FOLDER | | Root folder for Sonarr. If not set, will grab the first one it finds on Sonarr |
| SONARR_BYPASS_IGNORED | false | Boolean flag to bypass tv shows that are on the Sonarr Exclusion List |
| SONARR_SEASON_MONITORING | all | Default monitoring for new seasons added to Sonarr. Full list of options are found in the [Sonarr API Docs](https://sonarr.tv/docs/api/#/Series/post_api_v3_series) under **MonitorTypes** |
| SONARR_TAGS | | Tags to assign to tv shows that are added via Watchlistarr, comma separated |
| RADARR_BASE_URL | http://127.0.0.1:7878 | Base URL for Radarr, including the 'http' and port and any configured urlbase |
| RADARR_QUALITY_PROFILE | | Quality profile for Radarr, found in your Radarr UI -> Profiles settings. If not set, will grab the first one it finds on Radarr |
| RADARR_ROOT_FOLDER | | Root folder for Radarr. If not set, will grab the first one it finds on Radarr |
| RADARR_BYPASS_IGNORED | false | Boolean flag to bypass movies that are on the Radarr Exclusion List |
| RADARR_TAGS | | Tags to assign to movies that are added via Watchlistarr, comma separated |
| SKIP_FRIEND_SYNC | false | Boolean flag to toggle between only syncing your own content, vs syncing your own and all your friends content |
| ALLOW_MOVIE_DELETING | false | Boolean flag to enable/disable the full Watchlistarr sync for movies. If enabled, movies that are not watchlisted will be deleted from Radarr |
| ALLOW_ENDED_SHOW_DELETING | false | Boolean flag to enable/disable the full Watchlistarr sync for ended shows. If enabled, shows that have no more planned seasons and are not watchlisted will be deleted from Sonarr |
| ALLOW_CONTINUING_SHOW_DELETING | false | Boolean flag to enable/disable the full Watchlistarr sync for continuing shows. If enabled, shows that still have planned seasons and are not watchlisted will be deleted from Sonarr |
| DELETE_INTERVAL_DAYS | 7 | Number of days to wait before deleting content from the arrs (Deleting must be enabled) |
| LOG_LEVEL | INFO | Level of logging, set to DEBUG for more verbose logs, or WARN for less logs |
### Configuration

Running Watchlistarr successfully for the first time will generate a `config.yaml` file with additional configuration.
Modify this file to your heart's desire, then restart Watchlistarr

#### Enabling debug mode
Sometimes, you'll need more information from the app. To enable debug mode in Docker, add the following line to your command:
```
-e LOG_LEVEL=DEBUG
```

To enable debug mode in Java, add the following line:
```
"-Dlog.level=DEBUG"
```

## Plex Pass Alternative

Expand Down
2 changes: 2 additions & 0 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ val scalamockVersion = "5.2.0"
val scalatestVersion = "3.2.17"
val shapelessVersion = "2.3.10"
val slf4jVersion = "2.0.9"
val snakeYamlVersion = "2.0"
val vaultVersion = "3.5.0"

libraryDependencies ++= Seq(
Expand All @@ -40,6 +41,7 @@ libraryDependencies ++= Seq(
"org.typelevel" %% "vault" % vaultVersion,
"io.circe" %% "circe-generic" % circeVersion,
"io.circe" %% "circe-generic-extras" % circeGenericExtrasVersion,
"org.yaml" % "snakeyaml" % snakeYamlVersion,
"io.circe" %% "circe-parser" % circeVersion % Test,
"org.scalamock" %% "scalamock" % scalamockVersion % Test,
"org.scalatest" %% "scalatest" % scalatestVersion % Test
Expand Down
6 changes: 2 additions & 4 deletions docker/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,9 @@ FROM hseeberger/scala-sbt:eclipse-temurin-11.0.14.1_1.6.2_2.13.8 as build

WORKDIR /app

COPY / /
RUN sbt update
COPY . .

COPY .. .
RUN sbt compile stage
RUN sbt update compile stage

FROM openjdk:11-jre-slim

Expand Down
59 changes: 59 additions & 0 deletions src/main/resources/config-template.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
## Watchlistarr Configuration
## Uncomment the lines you would like to configure, then save this file and restart Watchlistarr

#################################################################
## How often do you want Watchlistarr to pull the latest from Plex? In general, 10 seconds is okay.
## If you're running this on a slower system (e.g. Raspberry Pi), you may want to increase this to 60 seconds.
#################################################################

#interval:
# seconds: 10


#################################################################
## Sonarr Configuration
#################################################################

#sonarr:
# baseUrl: "127.0.0.1:8989" # Base URL for Sonarr, including the 'http' and port and any configured urlbase
# apikey: "YOUR-API-KEY" # API key for Sonarr, found in your Sonarr UI -> General settings
# qualityProfile: "Your Desired Sonarr Quality Profile" # If not set, will grab the first one it finds on Sonarr
# rootFolder: "/root/folder/location" # Root folder for Sonarr. If not set, will grab the first one it finds on Sonarr
# bypassIgnored: false
# seasonMonitoring: all # Possible values under 'MonitorTypes' in sonarr.tv/docs/api
# tags:
# - watchlistarr


#################################################################
## Radarr Configuration
#################################################################

#radarr:
# baseUrl: "127.0.0.1:7878" # Base URL for Radarr, including the 'http' and port and any configured urlbase
# apikey: "YOUR-API-KEY"
# qualityProfile: "Your Desired Radarr Quality Profile" # If not set, will grab the first one it finds on Radarr
# rootFolder: "/root/folder/location" # If not set, will grab the first one it finds on Radarr
# bypassIgnored: false # Boolean flag to bypass tv shows that are on the Sonarr Exclusion List
# tags:
# - watchlistarr


#################################################################
## Plex Configuration
#################################################################

#plex:
# token: "YOUR-PLEX-TOKEN" # Multiple tokens can be provided
# skipfriendsync: false # Don't sync friends watchlists, only your own


#################################################################
## Delete Sync Configuration
#################################################################

#delete:
# movie: false # If enabled, movies that are not watchlisted will be deleted from Radarr
# endedShow: false # If enabled, shows that have no more planned seasons and are not watchlisted will be deleted from Sonarr
# continuingShow: false # If enabled, shows that still have planned seasons and are not watchlisted will be deleted from Sonarr
# interval.days: 7 # Number of days to wait before deleting content from the arrs (Deleting must be enabled)
34 changes: 19 additions & 15 deletions src/main/scala/Server.scala
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@

import cats.effect._
import cats.implicits.catsSyntaxTuple3Parallel
import configuration.{Configuration, ConfigurationUtils, SystemPropertyReader}
import configuration.{Configuration, ConfigurationUtils, FileAndSystemPropertyReader, SystemPropertyReader}
import http.HttpClient
import org.slf4j.LoggerFactory

Expand All @@ -18,40 +18,44 @@ object Server extends IOApp {
}

def run(args: List[String]): IO[ExitCode] = {
val configReader = SystemPropertyReader
val configReader = FileAndSystemPropertyReader
val httpClient = new HttpClient

for {
memoizedConfigIo <- ConfigurationUtils.create(configReader, httpClient).memoize
initialConfig <- ConfigurationUtils.create(configReader, httpClient)
configRef <- Ref.of[IO, Configuration](initialConfig)
result <- (
pingTokenSync(memoizedConfigIo, httpClient),
plexTokenSync(memoizedConfigIo, httpClient),
plexTokenDeleteSync(memoizedConfigIo, httpClient)
pingTokenSync(configRef, httpClient),
plexTokenSync(configRef, httpClient),
plexTokenDeleteSync(configRef, httpClient)
).parTupled.as(ExitCode.Success)
} yield result
}

private def pingTokenSync(configIO: IO[Configuration], httpClient: HttpClient): IO[Unit] =
private def fetchLatestConfig(configRef: Ref[IO, Configuration]): IO[Configuration] =
configRef.get

private def pingTokenSync(configRef: Ref[IO, Configuration], httpClient: HttpClient): IO[Unit] =
for {
config <- configIO
config <- fetchLatestConfig(configRef)
_ <- PingTokenSync.run(config, httpClient)
_ <- IO.sleep(24.hours)
_ <- pingTokenSync(configIO, httpClient)
_ <- pingTokenSync(configRef, httpClient)
} yield ()

private def plexTokenSync(configIO: 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 <- configIO
config <- fetchLatestConfig(configRef)
_ <- PlexTokenSync.run(config, httpClient, firstRun)
_ <- IO.sleep(config.refreshInterval)
_ <- plexTokenSync(configIO, httpClient, firstRun = false)
_ <- plexTokenSync(configRef, httpClient, firstRun = false)
} yield ()

private def plexTokenDeleteSync(configIO: IO[Configuration], httpClient: HttpClient): IO[Unit] =
private def plexTokenDeleteSync(configRef: Ref[IO, Configuration], httpClient: HttpClient): IO[Unit] =
for {
config <- configIO
config <- fetchLatestConfig(configRef)
_ <- PlexTokenDeleteSync.run(config, httpClient)
_ <- IO.sleep(config.deleteConfiguration.deleteInterval)
_ <- plexTokenDeleteSync(configIO, httpClient)
_ <- plexTokenDeleteSync(configRef, httpClient)
} yield ()
}
83 changes: 83 additions & 0 deletions src/main/scala/configuration/FileAndSystemPropertyReader.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package configuration

import org.slf4j.LoggerFactory
import org.yaml.snakeyaml.Yaml

import java.io.{File, FileInputStream}
import java.nio.file.{Files, Paths, StandardCopyOption}
import java.util
import scala.jdk.CollectionConverters.{ListHasAsScala, MapHasAsScala}

object FileAndSystemPropertyReader extends ConfigurationReader {

private val logger = LoggerFactory.getLogger(getClass)

private lazy val data: Map[String, String] = {
val yaml = new Yaml()
val configDirPath = "config"
val configFile = new File(s"$configDirPath/config.yaml")

try {
// Ensure parent config folder exists
val parentDir = configFile.getParentFile
if (!parentDir.exists()) parentDir.mkdirs()

if (!configFile.exists()) {
val resourceStream = getClass.getClassLoader.getResourceAsStream("config-template.yaml")
if (resourceStream != null) {
try {
Files.copy(resourceStream, Paths.get(configFile.toURI), StandardCopyOption.REPLACE_EXISTING)
logger.info(s"Created config file in ${configFile.getPath}")
} finally {
resourceStream.close()
}
} else {
logger.debug("config-template.yaml resource not found")
}
}

if (configFile.exists()) {
val inputStream = new FileInputStream(configFile)
val result = yaml.load[util.Map[String, Object]](inputStream).asScala
inputStream.close()
flattenYaml(Map.from(result))
} else {
Map.empty[String, String]
}
} catch {
case e: Exception =>
logger.debug(s"Failed to read from config.yaml: ${e.getMessage}")
Map.empty[String, String]
}
}

override def getConfigOption(key: String): Option[String] =
if (data.contains(key))
data.get(key)
else
SystemPropertyReader.getConfigOption(key)

private def flattenYaml(yaml: Map[String, _]): Map[String, String] = yaml.flatMap {
case (k, v: util.ArrayList[_]) =>
List((k, v.asScala.mkString(",")))

case (k, v: String) =>
List((k, v))

case (k, v: Integer) =>
List((k, v.toString))

case (k, v: java.lang.Boolean) =>
List((k, v.toString))

case (k, v: util.LinkedHashMap[String, _]) =>
val flattenedInner = flattenYaml(Map.from(v.asScala))
flattenedInner.map { case (innerK, innerV) =>
(s"$k.$innerK", innerV)
}.toList

case (k, v) =>
logger.warn(s"Unhandled config pair of type: ${k.getClass} -> ${v.getClass}")
List((k, v.toString))
}
}
2 changes: 1 addition & 1 deletion src/main/scala/configuration/SystemPropertyReader.scala
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
package configuration

object SystemPropertyReader extends ConfigurationReader {
def getConfigOption(key: String): Option[String] = Option(System.getProperty(key))
override def getConfigOption(key: String): Option[String] = Option(System.getProperty(key))
}

0 comments on commit bd0236a

Please sign in to comment.