From 2e7458457e995dd3ec6dd96035fe997646cdd446 Mon Sep 17 00:00:00 2001 From: dd060606 Date: Fri, 8 Jul 2022 10:51:19 +0200 Subject: [PATCH] feat: add a button in ManageSlideOver to remove the movie and the file from Radarr/Sonarr --- overseerr-api.yml | 17 ++++ server/api/servarr/radarr.ts | 14 +++ server/api/servarr/sonarr.ts | 14 +++ server/routes/media.ts | 97 ++++++++++++++++++++ src/components/ManageSlideOver/index.tsx | 111 +++++++++++++++++++---- 5 files changed, 237 insertions(+), 16 deletions(-) diff --git a/overseerr-api.yml b/overseerr-api.yml index 551f7dd91..24a0ed10e 100644 --- a/overseerr-api.yml +++ b/overseerr-api.yml @@ -5362,6 +5362,23 @@ paths: responses: '204': description: Succesfully removed media item + /media/{mediaId}/file: + delete: + summary: Delete media file + description: Removes a media file from radarr/sonarr. The `ADMIN` permission is required to perform this action. + tags: + - media + parameters: + - in: path + name: mediaId + description: Media ID + required: true + example: '1' + schema: + type: string + responses: + '204': + description: Succesfully removed media item /media/{mediaId}/{status}: post: summary: Update media status diff --git a/server/api/servarr/radarr.ts b/server/api/servarr/radarr.ts index 7305baf09..6064fa30b 100644 --- a/server/api/servarr/radarr.ts +++ b/server/api/servarr/radarr.ts @@ -213,6 +213,20 @@ class RadarrAPI extends ServarrBase<{ movieId: number }> { ); } } + public removeMovie = async (movieId: number): Promise => { + try { + const { id, title } = await this.getMovieByTmdbId(movieId); + await this.axios.delete(`/movie/${id}`, { + params: { + deleteFiles: true, + addImportExclusion: false, + }, + }); + logger.info(`[Radarr] Removed movie ${title}`); + } catch (e) { + throw new Error(`[Radarr] Failed to remove movie: ${e.message}`); + } + }; } export default RadarrAPI; diff --git a/server/api/servarr/sonarr.ts b/server/api/servarr/sonarr.ts index 7440d2786..fdc00aadb 100644 --- a/server/api/servarr/sonarr.ts +++ b/server/api/servarr/sonarr.ts @@ -302,6 +302,20 @@ class SonarrAPI extends ServarrBase<{ seriesId: number; episodeId: number }> { return newSeasons; } + public removeSerie = async (serieId: number): Promise => { + try { + const { id, title } = await this.getSeriesByTvdbId(serieId); + await this.axios.delete(`/series/${id}`, { + params: { + deleteFiles: true, + addImportExclusion: false, + }, + }); + logger.info(`[Radarr] Removed serie ${title}`); + } catch (e) { + throw new Error(`[Radarr] Failed to remove serie: ${e.message}`); + } + }; } export default SonarrAPI; diff --git a/server/routes/media.ts b/server/routes/media.ts index 429b2010f..73f08cabe 100644 --- a/server/routes/media.ts +++ b/server/routes/media.ts @@ -1,6 +1,9 @@ import { Router } from 'express'; import { FindOneOptions, FindOperator, getRepository, In } from 'typeorm'; +import RadarrAPI from '../api/servarr/radarr'; +import SonarrAPI from '../api/servarr/sonarr'; import TautulliAPI from '../api/tautulli'; +import TheMovieDb from '../api/themoviedb'; import { MediaStatus, MediaType } from '../constants/media'; import Media from '../entity/Media'; import { User } from '../entity/User'; @@ -167,6 +170,100 @@ mediaRoutes.delete( } ); +mediaRoutes.delete( + '/:id/file', + isAuthenticated(Permission.MANAGE_REQUESTS), + async (req, res, next) => { + try { + const settings = getSettings(); + const mediaRepository = getRepository(Media); + const media = await mediaRepository.findOneOrFail({ + where: { id: req.params.id }, + }); + const is4k = media.serviceUrl4k !== undefined; + const isMovie = media.mediaType === MediaType.MOVIE; + let serviceSettings; + if (isMovie) { + serviceSettings = settings.radarr.find( + (radarr) => radarr.isDefault && radarr.is4k === is4k + ); + } else { + serviceSettings = settings.sonarr.find( + (sonarr) => sonarr.isDefault && sonarr.is4k === is4k + ); + } + + if ( + media.serviceId && + media.serviceId >= 0 && + serviceSettings?.id !== media.serviceId + ) { + if (isMovie) { + serviceSettings = settings.radarr.find( + (radarr) => radarr.id === media.serviceId + ); + } else { + serviceSettings = settings.sonarr.find( + (sonarr) => sonarr.id === media.serviceId + ); + } + } + if (!serviceSettings) { + logger.warn( + `There is no default ${ + is4k ? '4K ' : '' + isMovie ? 'Radarr' : 'Sonarr' + }/ server configured. Did you set any of your ${ + is4k ? '4K ' : '' + isMovie ? 'Radarr' : 'Sonarr' + } servers as default?`, + { + label: 'Media Request', + mediaId: media.id, + } + ); + return; + } + let service; + if (isMovie) { + service = new RadarrAPI({ + apiKey: serviceSettings?.apiKey, + url: RadarrAPI.buildUrl(serviceSettings, '/api/v3'), + }); + } else { + service = new SonarrAPI({ + apiKey: serviceSettings?.apiKey, + url: SonarrAPI.buildUrl(serviceSettings, '/api/v3'), + }); + } + + if (isMovie) { + await (service as RadarrAPI).removeMovie( + parseInt( + is4k + ? (media.externalServiceSlug4k as string) + : (media.externalServiceSlug as string) + ) + ); + } else { + const tmdb = new TheMovieDb(); + const series = await tmdb.getTvShow({ tvId: media.tmdbId }); + const tvdbId = series.external_ids.tvdb_id ?? media.tvdbId; + if (!tvdbId) { + throw new Error('TVDB ID not found'); + } + await (service as SonarrAPI).removeSerie(tvdbId); + } + + return res.status(204).send(); + } catch (e) { + logger.error('Something went wrong fetching media in delete request', { + label: 'Media', + message: e.message, + }); + next({ status: 404, message: 'Media not found' }); + } + } +); + mediaRoutes.get<{ id: string }, MediaWatchDataResponse>( '/:id/watch_data', isAuthenticated(Permission.ADMIN), diff --git a/src/components/ManageSlideOver/index.tsx b/src/components/ManageSlideOver/index.tsx index a1e9bab17..3d2c590da 100644 --- a/src/components/ManageSlideOver/index.tsx +++ b/src/components/ManageSlideOver/index.tsx @@ -1,5 +1,9 @@ import { ServerIcon, ViewListIcon } from '@heroicons/react/outline'; -import { CheckCircleIcon, DocumentRemoveIcon } from '@heroicons/react/solid'; +import { + CheckCircleIcon, + DocumentRemoveIcon, + TrashIcon, +} from '@heroicons/react/solid'; import axios from 'axios'; import Link from 'next/link'; import React from 'react'; @@ -34,8 +38,12 @@ const messages = defineMessages({ manageModalClearMedia: 'Clear Data', manageModalClearMediaWarning: '* This will irreversibly remove all data for this {mediaType}, including any requests. If this item exists in your Plex library, the media information will be recreated during the next scan.', + manageModalRemoveMediaWarning: + '* This will irreversibly remove this {mediaType} from {arr}, including all files.', openarr: 'Open in {arr}', + removearr: 'Remove from {arr}', openarr4k: 'Open in 4K {arr}', + removearr4k: 'Remove from 4K {arr}', downloadstatus: 'Downloads', markavailable: 'Mark as Available', mark4kavailable: 'Mark as Available in 4K', @@ -90,6 +98,13 @@ const ManageSlideOver: React.FC< revalidate(); } }; + const deleteMediaFile = async () => { + if (data.mediaInfo) { + await axios.delete(`/api/v1/media/${data.mediaInfo.id}/file`); + await axios.delete(`/api/v1/media/${data.mediaInfo.id}`); + revalidate(); + } + }; const markAvailable = async (is4k = false) => { if (data.mediaInfo) { @@ -319,6 +334,39 @@ const ManageSlideOver: React.FC< )} + + {hasPermission(Permission.ADMIN) && + data?.mediaInfo?.serviceUrl && ( +
+ deleteMediaFile()} + confirmText={intl.formatMessage( + globalMessages.areyousure + )} + className="w-full" + > + + + {intl.formatMessage(messages.removearr, { + arr: mediaType === 'movie' ? 'Radarr' : 'Sonarr', + })} + + +
+ {intl.formatMessage( + messages.manageModalRemoveMediaWarning, + { + mediaType: intl.formatMessage( + mediaType === 'movie' + ? messages.movie + : messages.tvshow + ), + arr: mediaType === 'movie' ? 'Radarr' : 'Sonarr', + } + )} +
+
+ )} )} @@ -418,21 +466,52 @@ const ManageSlideOver: React.FC< )} {data?.mediaInfo?.serviceUrl4k && ( - - - + <> + + + +
+ deleteMediaFile()} + confirmText={intl.formatMessage( + globalMessages.areyousure + )} + className="w-full" + > + + + {intl.formatMessage(messages.removearr4k, { + arr: mediaType === 'movie' ? 'Radarr' : 'Sonarr', + })} + + +
+ {intl.formatMessage( + messages.manageModalRemoveMediaWarning, + { + mediaType: intl.formatMessage( + mediaType === 'movie' + ? messages.movie + : messages.tvshow + ), + arr: mediaType === 'movie' ? 'Radarr' : 'Sonarr', + } + )} +
+
+ )}