From 91e6446b9b5edf1f5b16c9fa3b5937a36ce056e0 Mon Sep 17 00:00:00 2001 From: Schaka <2223171+Schaka@users.noreply.github.com> Date: Sat, 28 Sep 2024 12:44:15 +0200 Subject: [PATCH] [Sonarr/Radarr] Allow deleting files without deleting database entry --- .../janitorr/servarr/radarr/RadarrClient.kt | 7 +++++ .../servarr/radarr/RadarrProperties.kt | 1 + .../servarr/radarr/RadarrRestService.kt | 16 ++++++++-- .../servarr/sonarr/SonarrRestService.kt | 29 ++++++++++++++++++- src/main/resources/application-template.yml | 3 +- 5 files changed, 52 insertions(+), 4 deletions(-) diff --git a/src/main/kotlin/com/github/schaka/janitorr/servarr/radarr/RadarrClient.kt b/src/main/kotlin/com/github/schaka/janitorr/servarr/radarr/RadarrClient.kt index b3dc729..6eb949f 100644 --- a/src/main/kotlin/com/github/schaka/janitorr/servarr/radarr/RadarrClient.kt +++ b/src/main/kotlin/com/github/schaka/janitorr/servarr/radarr/RadarrClient.kt @@ -3,6 +3,7 @@ package com.github.schaka.janitorr.servarr.radarr import com.github.schaka.janitorr.servarr.data_structures.Tag import com.github.schaka.janitorr.servarr.history.HistoryResponse import com.github.schaka.janitorr.servarr.quality_profile.QualityProfile +import com.github.schaka.janitorr.servarr.radarr.movie.MovieFile import com.github.schaka.janitorr.servarr.radarr.movie.MoviePayload import feign.Param import feign.RequestLine @@ -15,6 +16,9 @@ interface RadarrClient { @RequestLine("GET /movie") fun getAllMovies(): List + @RequestLine("GET /moviefile?movieId={id}") + fun getMovieFiles(@Param("id") id: Int): List + @RequestLine("GET /tag") fun getAllTags(): List @@ -27,6 +31,9 @@ interface RadarrClient { @RequestLine("DELETE /movie/{id}?deleteFiles={deleteFiles}") fun deleteMovie(@Param("id") id: Int, @Param("deleteFiles") deleteFiles: Boolean = true) + @RequestLine("DELETE /moviefile/{id}") + fun deleteMovieFile(@Param("id") id: Int) + @RequestLine("GET /qualityprofile") fun getAllQualityProfiles(): List } \ No newline at end of file diff --git a/src/main/kotlin/com/github/schaka/janitorr/servarr/radarr/RadarrProperties.kt b/src/main/kotlin/com/github/schaka/janitorr/servarr/radarr/RadarrProperties.kt index bf0e11c..7c32683 100644 --- a/src/main/kotlin/com/github/schaka/janitorr/servarr/radarr/RadarrProperties.kt +++ b/src/main/kotlin/com/github/schaka/janitorr/servarr/radarr/RadarrProperties.kt @@ -10,4 +10,5 @@ data class RadarrProperties( override val url: String, override val apiKey: String, override val determineAgeBy: HistorySort? = null, + val onlyDeleteFiles: Boolean = false, ) : ServarrProperties \ No newline at end of file diff --git a/src/main/kotlin/com/github/schaka/janitorr/servarr/radarr/RadarrRestService.kt b/src/main/kotlin/com/github/schaka/janitorr/servarr/radarr/RadarrRestService.kt index cb11ce3..249c4c2 100644 --- a/src/main/kotlin/com/github/schaka/janitorr/servarr/radarr/RadarrRestService.kt +++ b/src/main/kotlin/com/github/schaka/janitorr/servarr/radarr/RadarrRestService.kt @@ -10,6 +10,7 @@ import com.github.schaka.janitorr.servarr.ServarrService import com.github.schaka.janitorr.servarr.data_structures.Tag import com.github.schaka.janitorr.servarr.history.HistoryResponse import com.github.schaka.janitorr.servarr.quality_profile.QualityProfile +import com.github.schaka.janitorr.servarr.radarr.movie.MovieFile import com.github.schaka.janitorr.servarr.radarr.movie.MoviePayload import jakarta.annotation.PostConstruct import org.slf4j.LoggerFactory @@ -21,7 +22,7 @@ import java.time.LocalDateTime import kotlin.io.path.exists @Service -@RegisterReflectionForBinding(classes = [QualityProfile::class, Tag::class, MoviePayload::class, HistoryResponse::class]) +@RegisterReflectionForBinding(classes = [QualityProfile::class, Tag::class, MoviePayload::class, MovieFile::class, HistoryResponse::class]) class RadarrRestService( val radarrClient: RadarrClient, @@ -92,7 +93,7 @@ class RadarrRestService( if (!applicationProperties.dryRun) { unmonitorMovie(movie.id) - radarrClient.deleteMovie(movie.id) + deleteMovie(movie.id) log.info("Deleting movie ({}), id: {}, imdb: {}", movie.parentPath, movie.id, movie.imdbId) } else { log.info("Deleting movie ({}), id: {}, imdb: {}", movie.parentPath, movie.id, movie.imdbId) @@ -100,6 +101,17 @@ class RadarrRestService( } } + private fun deleteMovie(movieId: Int) { + if (!applicationProperties.onlyDeleteFiles) { + radarrClient.deleteMovie(movieId) + return + } + + radarrClient.getMovieFiles(movieId) + .map(MovieFile::id) + .forEach(radarrClient::deleteMovieFile) + } + private fun unmonitorMovie(movieId: Int) { val movie = radarrClient.getMovie(movieId) val isMonitored = movie.monitored diff --git a/src/main/kotlin/com/github/schaka/janitorr/servarr/sonarr/SonarrRestService.kt b/src/main/kotlin/com/github/schaka/janitorr/servarr/sonarr/SonarrRestService.kt index 24c2415..e5cbeb1 100644 --- a/src/main/kotlin/com/github/schaka/janitorr/servarr/sonarr/SonarrRestService.kt +++ b/src/main/kotlin/com/github/schaka/janitorr/servarr/sonarr/SonarrRestService.kt @@ -159,7 +159,7 @@ class SonarrRestService( for (show in affectedShows) { // no seeding check, we just delete everything - checking every single file for seeding isn't feasible if (!applicationProperties.dryRun) { - sonarrClient.deleteSeries(show.id, true) + deleteShow(show) } log.info("Deleting ${show.title} [${show.id}}]") } @@ -169,6 +169,33 @@ class SonarrRestService( } } + /** + * Deletes entire TV show (or its associated files) + */ + private fun deleteShow(show: SeriesPayload) { + if (sonarrProperties.deleteEmptyShows) { + sonarrClient.deleteSeries(show.id, true) + return + } + + // Unmonitor everything + unmonitorSeasons(show.id, *show.seasons.map(Season::seasonNumber).toIntArray()) + + // then delete each season's episode files + for (season in show.seasons) { + val episodes = sonarrClient.getAllEpisodes(show.id, season.seasonNumber) + for (episode in episodes) { + if (episode.episodeFileId != null && episode.episodeFileId != 0) { + sonarrClient.deleteEpisodeFile(episode.episodeFileId) + log.info("Deleting {} - episode {} ({}) of season {}", show.path, episode.episodeNumber, episode.episodeFileId, episode.seasonNumber) + } + } + } + } + + /** + * Removes a season's files if that season is in the relevant items. + */ private fun removeBySeason(items: List) { // we are always treating seasons as a whole, even if technically episodes could be handled individually for (item in items) { diff --git a/src/main/resources/application-template.yml b/src/main/resources/application-template.yml index b8c892d..0562c05 100644 --- a/src/main/resources/application-template.yml +++ b/src/main/resources/application-template.yml @@ -55,12 +55,13 @@ clients: enabled: true url: "http://localhost:8989" api-key: "4ed7f4d0e8584d65aa2d47d944077ff6" - delete-empty-shows: true # If a show that was "touched" by Janitorr contains no files and has no monitored seasons at all, it will get deleted as part of orphan cleanup + delete-empty-shows: true # Delete empty shows if deleting by season. Otherwise leaves Sonarr entries behind. determine-age-by: most_recent # Optional property, use 'most_recent' or 'oldest' - remove this line if Janitorr should determine by upgrades enabled for your profile radarr: enabled: true url: "http://localhost:7878" api-key: "cd0912f129d348c9b69bb20d49fcbe44" + only-delete-files: true # NOT RECOMMENDED - When set to true, Janitorr will only delete your media files but keep the entries in Radarr determine-age-by: most_recent # Optional property, use 'most_recent' or 'oldest' - remove this line if Janitorr should determine by upgrades enabled for your profile ## You can only choose one out of Jellyfin or Emby.