feat: add a button in ManageSlideOver to remove the movie and the file from Radarr/Sonarr

This commit is contained in:
dd060606
2022-07-08 10:51:19 +02:00
parent b67844a0ee
commit 2e7458457e
5 changed files with 237 additions and 16 deletions

View File

@@ -5362,6 +5362,23 @@ paths:
responses: responses:
'204': '204':
description: Succesfully removed media item 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}: /media/{mediaId}/{status}:
post: post:
summary: Update media status summary: Update media status

View File

@@ -213,6 +213,20 @@ class RadarrAPI extends ServarrBase<{ movieId: number }> {
); );
} }
} }
public removeMovie = async (movieId: number): Promise<void> => {
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; export default RadarrAPI;

View File

@@ -302,6 +302,20 @@ class SonarrAPI extends ServarrBase<{ seriesId: number; episodeId: number }> {
return newSeasons; return newSeasons;
} }
public removeSerie = async (serieId: number): Promise<void> => {
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; export default SonarrAPI;

View File

@@ -1,6 +1,9 @@
import { Router } from 'express'; import { Router } from 'express';
import { FindOneOptions, FindOperator, getRepository, In } from 'typeorm'; 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 TautulliAPI from '../api/tautulli';
import TheMovieDb from '../api/themoviedb';
import { MediaStatus, MediaType } from '../constants/media'; import { MediaStatus, MediaType } from '../constants/media';
import Media from '../entity/Media'; import Media from '../entity/Media';
import { User } from '../entity/User'; 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>( mediaRoutes.get<{ id: string }, MediaWatchDataResponse>(
'/:id/watch_data', '/:id/watch_data',
isAuthenticated(Permission.ADMIN), isAuthenticated(Permission.ADMIN),

View File

@@ -1,5 +1,9 @@
import { ServerIcon, ViewListIcon } from '@heroicons/react/outline'; 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 axios from 'axios';
import Link from 'next/link'; import Link from 'next/link';
import React from 'react'; import React from 'react';
@@ -34,8 +38,12 @@ const messages = defineMessages({
manageModalClearMedia: 'Clear Data', manageModalClearMedia: 'Clear Data',
manageModalClearMediaWarning: 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.', '* 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}', openarr: 'Open in {arr}',
removearr: 'Remove from {arr}',
openarr4k: 'Open in 4K {arr}', openarr4k: 'Open in 4K {arr}',
removearr4k: 'Remove from 4K {arr}',
downloadstatus: 'Downloads', downloadstatus: 'Downloads',
markavailable: 'Mark as Available', markavailable: 'Mark as Available',
mark4kavailable: 'Mark as Available in 4K', mark4kavailable: 'Mark as Available in 4K',
@@ -90,6 +98,13 @@ const ManageSlideOver: React.FC<
revalidate(); 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) => { const markAvailable = async (is4k = false) => {
if (data.mediaInfo) { if (data.mediaInfo) {
@@ -319,6 +334,39 @@ const ManageSlideOver: React.FC<
</Button> </Button>
</a> </a>
)} )}
{hasPermission(Permission.ADMIN) &&
data?.mediaInfo?.serviceUrl && (
<div>
<ConfirmButton
onClick={() => deleteMediaFile()}
confirmText={intl.formatMessage(
globalMessages.areyousure
)}
className="w-full"
>
<TrashIcon />
<span>
{intl.formatMessage(messages.removearr, {
arr: mediaType === 'movie' ? 'Radarr' : 'Sonarr',
})}
</span>
</ConfirmButton>
<div className="mt-1 text-xs text-gray-400">
{intl.formatMessage(
messages.manageModalRemoveMediaWarning,
{
mediaType: intl.formatMessage(
mediaType === 'movie'
? messages.movie
: messages.tvshow
),
arr: mediaType === 'movie' ? 'Radarr' : 'Sonarr',
}
)}
</div>
</div>
)}
</div> </div>
</div> </div>
)} )}
@@ -418,21 +466,52 @@ const ManageSlideOver: React.FC<
</div> </div>
)} )}
{data?.mediaInfo?.serviceUrl4k && ( {data?.mediaInfo?.serviceUrl4k && (
<a <>
href={data?.mediaInfo?.serviceUrl4k} <a
target="_blank" href={data?.mediaInfo?.serviceUrl4k}
rel="noreferrer" target="_blank"
className="block" rel="noreferrer"
> className="block"
<Button buttonType="ghost" className="w-full"> >
<ServerIcon /> <Button buttonType="ghost" className="w-full">
<span> <ServerIcon />
{intl.formatMessage(messages.openarr4k, { <span>
arr: mediaType === 'movie' ? 'Radarr' : 'Sonarr', {intl.formatMessage(messages.openarr4k, {
})} arr: mediaType === 'movie' ? 'Radarr' : 'Sonarr',
</span> })}
</Button> </span>
</a> </Button>
</a>
<div>
<ConfirmButton
onClick={() => deleteMediaFile()}
confirmText={intl.formatMessage(
globalMessages.areyousure
)}
className="w-full"
>
<TrashIcon />
<span>
{intl.formatMessage(messages.removearr4k, {
arr: mediaType === 'movie' ? 'Radarr' : 'Sonarr',
})}
</span>
</ConfirmButton>
<div className="mt-1 text-xs text-gray-400">
{intl.formatMessage(
messages.manageModalRemoveMediaWarning,
{
mediaType: intl.formatMessage(
mediaType === 'movie'
? messages.movie
: messages.tvshow
),
arr: mediaType === 'movie' ? 'Radarr' : 'Sonarr',
}
)}
</div>
</div>
</>
)} )}
</div> </div>
</div> </div>