mirror of
https://github.com/fallenbagel/jellyseerr.git
synced 2026-01-01 04:08:45 -05:00
feat: add a button in ManageSlideOver to remove the movie and the file from Radarr/Sonarr
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user