diff --git a/server/api/servarr/radarr.ts b/server/api/servarr/radarr.ts index 1d35778c0f..325f1fdc17 100644 --- a/server/api/servarr/radarr.ts +++ b/server/api/servarr/radarr.ts @@ -270,6 +270,17 @@ class RadarrAPI extends ServarrBase<{ movieId: number }> { public removeMovie = async (movieId: number): Promise => { try { const { id, title } = await this.getMovieByTmdbId(movieId); + if (!id) { + // Movie is not in the Radarr library (e.g. already removed). Treat the + // desired end-state as reached so retries remain idempotent. + logger.info( + '[Radarr] Movie not present in library; nothing to remove', + { + tmdbId: movieId, + } + ); + return; + } await this.axios.delete(`/movie/${id}`, { params: { deleteFiles: true, @@ -284,6 +295,26 @@ class RadarrAPI extends ServarrBase<{ movieId: number }> { } }; + public removeTagFromMovie = async ( + tmdbId: number, + tagId: number + ): Promise => { + try { + const movie = await this.getMovieByTmdbId(tmdbId); + const updatedTags = movie.tags.filter((t) => t !== tagId); + await this.axios.put(`/movie`, { + ...movie, + tags: updatedTags, + }); + logger.info(`[Radarr] Removed tag ${tagId} from movie ${movie.title}`); + } catch (e) { + throw new Error( + `[Radarr] Failed to remove tag from movie: ${e.message}`, + { cause: e } + ); + } + }; + public clearCache = ({ tmdbId, externalId, diff --git a/server/api/servarr/sonarr.ts b/server/api/servarr/sonarr.ts index 33354e1cda..476c8ebe74 100644 --- a/server/api/servarr/sonarr.ts +++ b/server/api/servarr/sonarr.ts @@ -413,6 +413,15 @@ class SonarrAPI extends ServarrBase<{ public removeSeries = async (serieId: number): Promise => { try { const { id, title } = await this.getSeriesByTvdbId(serieId); + if (!id) { + // Series is not in the Sonarr library (e.g. already removed). Treat the + // desired end-state as reached so retries remain idempotent. + logger.info( + '[Sonarr] Series not present in library; nothing to remove', + { tvdbId: serieId } + ); + return; + } await this.axios.delete(`/series/${id}`, { params: { deleteFiles: true, @@ -427,6 +436,84 @@ class SonarrAPI extends ServarrBase<{ } }; + public removeTagFromSeries = async ( + tvdbId: number, + tagId: number + ): Promise => { + try { + const series = await this.getSeriesByTvdbId(tvdbId); + if (!series.id) { + throw new Error('Series not found in Sonarr'); + } + const updatedTags = series.tags.filter((t) => t !== tagId); + await this.axios.put(`/series/${series.id}`, { + ...series, + tags: updatedTags, + }); + logger.info(`[Sonarr] Removed tag ${tagId} from series ${series.title}`); + } catch (e) { + throw new Error( + `[Sonarr] Failed to remove tag from series: ${e.message}`, + { cause: e } + ); + } + }; + + public removeSeasonFiles = async ( + tvdbId: number, + seasonNumbers: number[] + ): Promise => { + try { + const series = await this.getSeriesByTvdbId(tvdbId); + if (!series.id) { + // Series is not in the Sonarr library (e.g. already removed). Treat the + // desired end-state as reached so retries remain idempotent. + logger.info( + '[Sonarr] Series not present in library; nothing to remove', + { tvdbId } + ); + return; + } + + const episodes = await this.getEpisodes(series.id); + const targetEpisodes = episodes.filter((ep) => + seasonNumbers.includes(ep.seasonNumber) + ); + const episodeFileIds = targetEpisodes + .filter((ep) => ep.hasFile && ep.episodeFileId > 0) + .map((ep) => ep.episodeFileId); + + // Unmonitor the affected episodes before deleting files + const episodeIds = targetEpisodes.map((ep) => ep.id); + if (episodeIds.length > 0) { + await this.axios.put('/episode/monitor', { + episodeIds, + monitored: false, + }); + } + + // Delete episode files + for (const fileId of [...new Set(episodeFileIds)]) { + await this.axios.delete(`/episodefile/${fileId}`); + } + + // Unmonitor the seasons + series.seasons = series.seasons.map((s) => ({ + ...s, + monitored: seasonNumbers.includes(s.seasonNumber) ? false : s.monitored, + })); + await this.axios.put(`/series/${series.id}`, series); + + logger.info( + `[Sonarr] Removed files for seasons ${seasonNumbers.join(', ')} of ${series.title}` + ); + } catch (e) { + throw new Error(`[Sonarr] Failed to remove season files: ${e.message}`, { + cause: e, + }); + } + }; + public clearCache = ({ tvdbId, externalId,