Skip to content

Commit

Permalink
Skip items that have an -arr exclusion. Closes #10 (#12)
Browse files Browse the repository at this point in the history
  • Loading branch information
nylonee authored Oct 29, 2023
1 parent 3fe58bf commit 4f54d0f
Show file tree
Hide file tree
Showing 6 changed files with 81 additions and 20 deletions.
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,15 @@

Sync plex watchlists in realtime with Sonarr and Radarr. **Requires Plex Pass**

## Why?
### Sonarr/Radarr limitation
While Sonarr and Radarr have built-in functionality (in v4 and v3 respectively), they are set at 6 hour refresh intervals, and cannot be customized

### Ombi/Overseer limitation
While Ombi and Overseer have built-in functionality, there are two problems with this:
* They are customizable down to 5 minute intervals, so doesn't allow the "real-time" sync that Watchlistarr does
* They rely on Plex tokens, which expire and break the sync if you're not regularly logging into Ombi/Overseer

## Getting Started

The easiest way to try this code is using docker:
Expand All @@ -23,10 +32,12 @@ docker run \
| SONARR_BASE_URL | http://localhost:8989 | Yes | Base URL for Sonarr, including the 'http' and port |
| SONARR_QUALITY_PROFILE | 1080p | Yes | Quality profile for Sonarr, found in your Sonarr UI -> Profiles settings |
| SONARR_ROOT_FOLDER | /data/ | Yes | Root folder for Sonarr |
| SONARR_BYPASS_IGNORED | true | Yes | Boolean flag to bypass tv shows that are on the Sonarr Exclusion List |
| RADARR_API_KEY | 7a392fb4817a46e59f2e84e7d5f021bc | No | API key for Radarr, found in your Radarr UI -> General settings |
| RADARR_BASE_URL | http://127.0.0.1:7878 | Yes | Base URL for Radarr, including the 'http' and port |
| RADARR_QUALITY_PROFILE | 1080p | Yes | Quality profile for Radarr, found in your Radarr UI -> Profiles settings |
| RADARR_ROOT_FOLDER | /data/ | Yes | Root folder for Radarr |
| RADARR_BYPASS_IGNORED | true | Yes | Boolean flag to bypass movies that are on the Radarr Exclusion List |
| PLEX_WATCHLIST_URL_1 | https://rss.plex.tv/UUID | No | First Plex Watchlist URL |
| PLEX_WATCHLIST_URL_2 | https://rss.plex.tv/UUID | Yes | Second Plex Watchlist URL (if applicable) |

Expand Down
8 changes: 8 additions & 0 deletions docker/entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,12 @@ if [ -n "$REFRESH_INTERVAL_SECONDS" ]; then
CMD="$CMD -Dinterval.seconds=$REFRESH_INTERVAL_SECONDS"
fi

if [ -n "$SONARR_BYPASS_IGNORED" ]; then
CMD="$CMD -Dsonarr.bypassIgnored=$SONARR_BYPASS_IGNORED"
fi

if [ -n "$RADARR_BYPASS_IGNORED" ]; then
CMD="$CMD -Dradarr.bypassIgnored=$RADARR_BYPASS_IGNORED"
fi

exec $CMD
74 changes: 54 additions & 20 deletions src/main/scala/WatchlistSync.scala
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,16 @@ import utils.{ArrUtils, HttpClient}
object WatchlistSync {

private val logger = LoggerFactory.getLogger(getClass)

def run(config: Configuration): IO[Unit] = {

logger.debug("Starting watchlist sync")

for {
watchlistDatas <- config.plexWatchlistUrls.map(fetchWatchlist(config.client)).sequence
watchlistData = watchlistDatas.fold(Watchlist(Set.empty))(mergeWatchLists)
movies <- fetchMovies(config.client)(config.radarrApiKey, config.radarrBaseUrl)
series <- fetchSeries(config.client)(config.sonarrApiKey, config.sonarrBaseUrl)
movies <- fetchMovies(config.client)(config.radarrApiKey, config.radarrBaseUrl, config.radarrBypassIgnored)
series <- fetchSeries(config.client)(config.sonarrApiKey, config.sonarrBaseUrl, config.sonarrBypassIgnored)
allIds = merge(movies, series)
_ <- missingIds(config.client)(config)(allIds, watchlistData.items)
} yield ()
Expand All @@ -40,29 +41,62 @@ object WatchlistSync {
}
}

private def fetchMovies(client: HttpClient)(apiKey: String, baseUrl: Uri): IO[List[RadarrMovie]] =
ArrUtils.getToArr(client)(baseUrl, apiKey, "movie").map {
case Right(res) =>
res.as[List[RadarrMovie]].getOrElse {
logger.warn("Unable to fetch movies from Radarr - decoding failure. Returning empty list instead")
private def fetchMovies(client: HttpClient)(apiKey: String, baseUrl: Uri, bypass: Boolean): IO[List[RadarrMovie]] =
for {
movies <- ArrUtils.getToArr(client)(baseUrl, apiKey, "movie").map {
case Right(res) =>
res.as[List[RadarrMovie]].getOrElse {
logger.warn("Unable to fetch movies from Radarr - decoding failure. Returning empty list instead")
List.empty
}
case Left(err) =>
logger.warn(s"Received error while trying to fetch movies from Radarr: $err")
List.empty
}
exclusions <- if (bypass) {
IO.pure(List.empty)
} else {
ArrUtils.getToArr(client)(baseUrl, apiKey, "exclusions").map {
case Right(res) =>
res.as[List[RadarrMovieExclusion]].getOrElse {
logger.warn("Unable to fetch movie exclusions from Radarr - decoding failure. Returning empty list instead")
List.empty
}
case Left(err) =>
logger.warn(s"Received error while trying to fetch movie exclusions from Radarr: $err")
List.empty
}
case Left(err) =>
logger.warn(s"Received error while trying to fetch movies from Radarr: $err")
throw err
}
}
} yield movies ++ exclusions.map(_.toRadarrMovie)

private def fetchSeries(client: HttpClient)(apiKey: String, baseUrl: Uri): IO[List[SonarrSeries]] =
ArrUtils.getToArr(client)(baseUrl, apiKey, "series").map {
case Right(res) =>
res.as[List[SonarrSeries]].getOrElse {
logger.warn("Unable to fetch series from Sonarr - decoding failure. Returning empty list instead")
private def fetchSeries(client: HttpClient)(apiKey: String, baseUrl: Uri, bypass: Boolean): IO[List[SonarrSeries]] =
for {
shows <- ArrUtils.getToArr(client)(baseUrl, apiKey, "series").map {
case Right(res) =>
res.as[List[SonarrSeries]].getOrElse {
logger.warn("Unable to fetch series from Sonarr - decoding failure. Returning empty list instead")
List.empty
}
case Left(err) =>
logger.warn(s"Received error while trying to fetch movies from Radarr: $err")
List.empty
}
exclusions <- if (bypass) {
IO.pure(List.empty)
} else {
ArrUtils.getToArr(client)(baseUrl, apiKey, "importlistexclusion").map {
case Right(res) =>
res.as[List[SonarrSeries]].getOrElse {
logger.warn("Unable to fetch show exclusions from Sonarr - decoding failure. Returning empty list instead")
List.empty
}
case Left(err) =>
logger.warn(s"Received error while trying to fetch show exclusions from Sonarr: $err")
List.empty
}
case Left(err) =>
logger.warn(s"Received error while trying to fetch movies from Radarr: $err")
throw err
}
}
} yield shows ++ exclusions


private def merge(r: List[RadarrMovie], s: List[SonarrSeries]): Set[String] = {
val allIds = r.map(_.imdbId) ++ r.map(_.tmdbId) ++ s.map(_.imdbId) ++ s.map(_.tvdbId)
Expand Down
2 changes: 2 additions & 0 deletions src/main/scala/configuration/Configuration.scala
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,11 @@ class Configuration(configReader: ConfigurationReader, val client: HttpClient) {

val (sonarrBaseUrl, sonarrApiKey, sonarrQualityProfileId) = getAndTestSonarrUrlAndApiKey.unsafeRunSync()
val sonarrRootFolder: String = configReader.getConfigOption(Keys.sonarrRootFolder).getOrElse("/data/")
val sonarrBypassIgnored: Boolean = configReader.getConfigOption(Keys.sonarrBypassIgnored).exists(_.toBoolean)

val (radarrBaseUrl, radarrApiKey, radarrQualityProfileId) = getAndTestRadarrUrlAndApiKey.unsafeRunSync()
val radarrRootFolder: String = configReader.getConfigOption(Keys.radarrRootFolder).getOrElse("/data/")
val radarrBypassIgnored: Boolean = configReader.getConfigOption(Keys.radarrBypassIgnored).exists(_.toBoolean)

val plexWatchlistUrls: List[Uri] = getPlexWatchlistUrls

Expand Down
2 changes: 2 additions & 0 deletions src/main/scala/configuration/Keys.scala
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@ object Keys {
val sonarrApiKey = "sonarr.apikey"
val sonarrQualityProfile = "sonarr.qualityProfile"
val sonarrRootFolder = "sonarr.rootFolder"
val sonarrBypassIgnored = "sonarr.bypassIgnored"

val radarrBaseUrl = "radarr.baseUrl"
val radarrApiKey = "radarr.apikey"
val radarrQualityProfile = "radarr.qualityProfile"
val radarrRootFolder = "radarr.rootFolder"
val radarrBypassIgnored = "radarr.bypassIgnored"

val plexWatchlist1 = "plex.watchlist1"
val plexWatchlist2 = "plex.watchlist2"
Expand Down
4 changes: 4 additions & 0 deletions src/main/scala/model/RadarrMovie.scala
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
package model

case class RadarrMovie(title: String, imdbId: Option[String], tmdbId: Option[Long])

case class RadarrMovieExclusion(movieTitle: String, imdbId: Option[String], tmdbId: Option[Long]) {
def toRadarrMovie: RadarrMovie = RadarrMovie(this.movieTitle, this.imdbId, this.tmdbId)
}

0 comments on commit 4f54d0f

Please sign in to comment.