mirror of
https://github.com/fallenbagel/jellyseerr.git
synced 2025-12-24 02:39:18 -05:00
Compare commits
4 Commits
v1.4.1
...
preview-pr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bcd2bb7c96 | ||
|
|
5a72f5f86e | ||
|
|
7d4455ba6b | ||
|
|
2e7458457e |
@@ -5470,6 +5470,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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -302,6 +302,20 @@ class SonarrAPI extends ServarrBase<{ seriesId: number; episodeId: number }> {
|
||||
|
||||
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;
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import RadarrAPI from '@server/api/servarr/radarr';
|
||||
import SonarrAPI from '@server/api/servarr/sonarr';
|
||||
import TautulliAPI from '@server/api/tautulli';
|
||||
import TheMovieDb from '@server/api/themoviedb';
|
||||
import { MediaStatus, MediaType } from '@server/constants/media';
|
||||
import { getRepository } from '@server/datasource';
|
||||
import Media from '@server/entity/Media';
|
||||
@@ -168,6 +171,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: Number(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),
|
||||
|
||||
@@ -8,11 +8,20 @@ import useSettings from '@app/hooks/useSettings';
|
||||
import { Permission, useUser } from '@app/hooks/useUser';
|
||||
import globalMessages from '@app/i18n/globalMessages';
|
||||
import { ServerIcon, ViewListIcon } from '@heroicons/react/outline';
|
||||
import { CheckCircleIcon, DocumentRemoveIcon } from '@heroicons/react/solid';
|
||||
import {
|
||||
CheckCircleIcon,
|
||||
DocumentRemoveIcon,
|
||||
TrashIcon,
|
||||
} from '@heroicons/react/solid';
|
||||
import { IssueStatus } from '@server/constants/issue';
|
||||
import { MediaRequestStatus, MediaStatus } from '@server/constants/media';
|
||||
import {
|
||||
MediaRequestStatus,
|
||||
MediaStatus,
|
||||
MediaType,
|
||||
} from '@server/constants/media';
|
||||
import { MediaServerType } from '@server/constants/server';
|
||||
import type { MediaWatchDataResponse } from '@server/interfaces/api/mediaInterfaces';
|
||||
import type { RadarrSettings, SonarrSettings } from '@server/lib/settings';
|
||||
import type { MovieDetails } from '@server/models/Movie';
|
||||
import type { TvDetails } from '@server/models/Tv';
|
||||
import axios from 'axios';
|
||||
@@ -31,8 +40,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',
|
||||
@@ -86,6 +99,12 @@ const ManageSlideOver = ({
|
||||
? `/api/v1/media/${data.mediaInfo.id}/watch_data`
|
||||
: null
|
||||
);
|
||||
const { data: radarrData } = useSWR<RadarrSettings[]>(
|
||||
'/api/v1/settings/radarr'
|
||||
);
|
||||
const { data: sonarrData } = useSWR<SonarrSettings[]>(
|
||||
'/api/v1/settings/sonarr'
|
||||
);
|
||||
|
||||
const deleteMedia = async () => {
|
||||
if (data.mediaInfo) {
|
||||
@@ -94,6 +113,35 @@ const ManageSlideOver = ({
|
||||
}
|
||||
};
|
||||
|
||||
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 isDefaultService = () => {
|
||||
if (data.mediaInfo) {
|
||||
if (data.mediaInfo.mediaType === MediaType.MOVIE) {
|
||||
return (
|
||||
radarrData?.find(
|
||||
(radarr) =>
|
||||
radarr.isDefault && radarr.id === data.mediaInfo?.serviceId
|
||||
) !== undefined
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
sonarrData?.find(
|
||||
(sonarr) =>
|
||||
sonarr.isDefault && sonarr.id === data.mediaInfo?.serviceId
|
||||
) !== undefined
|
||||
);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const markAvailable = async (is4k = false) => {
|
||||
if (data.mediaInfo) {
|
||||
await axios.post(`/api/v1/media/${data.mediaInfo?.id}/available`, {
|
||||
@@ -326,6 +374,40 @@ const ManageSlideOver = ({
|
||||
</Button>
|
||||
</a>
|
||||
)}
|
||||
|
||||
{hasPermission(Permission.ADMIN) &&
|
||||
data?.mediaInfo?.serviceUrl &&
|
||||
isDefaultService() && (
|
||||
<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>
|
||||
)}
|
||||
@@ -431,21 +513,54 @@ const ManageSlideOver = ({
|
||||
</div>
|
||||
)}
|
||||
{data?.mediaInfo?.serviceUrl4k && (
|
||||
<a
|
||||
href={data?.mediaInfo?.serviceUrl4k}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="block"
|
||||
>
|
||||
<Button buttonType="ghost" className="w-full">
|
||||
<ServerIcon />
|
||||
<span>
|
||||
{intl.formatMessage(messages.openarr4k, {
|
||||
arr: mediaType === 'movie' ? 'Radarr' : 'Sonarr',
|
||||
})}
|
||||
</span>
|
||||
</Button>
|
||||
</a>
|
||||
<>
|
||||
<a
|
||||
href={data?.mediaInfo?.serviceUrl4k}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="block"
|
||||
>
|
||||
<Button buttonType="ghost" className="w-full">
|
||||
<ServerIcon />
|
||||
<span>
|
||||
{intl.formatMessage(messages.openarr4k, {
|
||||
arr: mediaType === 'movie' ? 'Radarr' : 'Sonarr',
|
||||
})}
|
||||
</span>
|
||||
</Button>
|
||||
</a>
|
||||
{isDefaultService() && (
|
||||
<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>
|
||||
|
||||
Reference in New Issue
Block a user