pull/3690/merge
dd060606 2 weeks ago committed by GitHub
commit 7e6acf7224
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -5738,6 +5738,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 file
/media/{mediaId}/{status}:
post:
summary: Update media status

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

@ -316,6 +316,21 @@ class SonarrAPI extends ServarrBase<{
}
}
public async removeSerie(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(`[Sonarr] Removed serie ${title}`);
} catch (e) {
throw new Error(`[Sonarr] Failed to remove serie: ${e.message}`);
}
}
private buildSeasonList(
seasons: number[],
existingSeasons?: SonarrSeason[]

@ -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),

@ -9,10 +9,19 @@ import useSettings from '@app/hooks/useSettings';
import { Permission, useUser } from '@app/hooks/useUser';
import globalMessages from '@app/i18n/globalMessages';
import { Bars4Icon, ServerIcon } from '@heroicons/react/24/outline';
import { CheckCircleIcon, DocumentMinusIcon } from '@heroicons/react/24/solid';
import {
CheckCircleIcon,
DocumentMinusIcon,
TrashIcon,
} from '@heroicons/react/24/solid';
import { IssueStatus } from '@server/constants/issue';
import { MediaRequestStatus, MediaStatus } from '@server/constants/media';
import {
MediaRequestStatus,
MediaStatus,
MediaType,
} from '@server/constants/media';
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.',
manageModalDeleteMediaWarning:
'* This will delete this {mediaType} from {arr}, including the media file.',
openarr: 'Open in {arr}',
deletearr: 'Remove from {arr}',
openarr4k: 'Open in 4K {arr}',
deletearr4k: 'Remove from 4K {arr}',
downloadstatus: 'Downloads',
markavailable: 'Mark as Available',
mark4kavailable: 'Mark as Available in 4K',
@ -85,6 +98,13 @@ const ManageSlideOver = ({
: 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) {
await axios.delete(`/api/v1/media/${data.mediaInfo.id}`);
@ -92,6 +112,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`, {
@ -123,7 +172,6 @@ const ManageSlideOver = ({
</>
);
};
return (
<SlideOver
show={show}
@ -317,21 +365,54 @@ const ManageSlideOver = ({
</div>
)}
{data.mediaInfo?.serviceUrl && (
<a
href={data?.mediaInfo?.serviceUrl}
target="_blank"
rel="noreferrer"
className="block"
>
<Button buttonType="ghost" className="w-full">
<ServerIcon />
<span>
{intl.formatMessage(messages.openarr, {
arr: mediaType === 'movie' ? 'Radarr' : 'Sonarr',
})}
</span>
</Button>
</a>
<>
<a
href={data?.mediaInfo?.serviceUrl}
target="_blank"
rel="noreferrer"
className="block"
>
<Button buttonType="ghost" className="w-full">
<ServerIcon />
<span>
{intl.formatMessage(messages.openarr, {
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.deletearr, {
arr: mediaType === 'movie' ? 'Radarr' : 'Sonarr',
})}
</span>
</ConfirmButton>
<div className="mt-1 text-xs text-gray-400">
{intl.formatMessage(
messages.manageModalDeleteMediaWarning,
{
mediaType: intl.formatMessage(
mediaType === 'movie'
? messages.movie
: messages.tvshow
),
arr: mediaType === 'movie' ? 'Radarr' : 'Sonarr',
}
)}
</div>
</div>
)}
</>
)}
</div>
</div>
@ -443,21 +524,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.deletearr4k, {
arr: mediaType === 'movie' ? 'Radarr' : 'Sonarr',
})}
</span>
</ConfirmButton>
<div className="mt-1 text-xs text-gray-400">
{intl.formatMessage(
messages.manageModalDeleteMediaWarning,
{
mediaType: intl.formatMessage(
mediaType === 'movie'
? messages.movie
: messages.tvshow
),
arr: mediaType === 'movie' ? 'Radarr' : 'Sonarr',
}
)}
</div>
</div>
)}
</>
)}
</div>
</div>

@ -228,10 +228,13 @@
"components.Login.validationemailrequired": "You must provide a valid email address",
"components.Login.validationpasswordrequired": "You must provide a password",
"components.ManageSlideOver.alltime": "All Time",
"components.ManageSlideOver.deletearr": "Remove from {arr}",
"components.ManageSlideOver.deletearr4k": "Remove from 4K {arr}",
"components.ManageSlideOver.downloadstatus": "Downloads",
"components.ManageSlideOver.manageModalAdvanced": "Advanced",
"components.ManageSlideOver.manageModalClearMedia": "Clear Data",
"components.ManageSlideOver.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.",
"components.ManageSlideOver.manageModalDeleteMediaWarning": "* This will delete this {mediaType} from {arr}, including the media file.",
"components.ManageSlideOver.manageModalIssues": "Open Issues",
"components.ManageSlideOver.manageModalMedia": "Media",
"components.ManageSlideOver.manageModalMedia4k": "4K Media",

@ -992,6 +992,9 @@
"components.ManageSlideOver.plays": "<strong>{playCount, number}</strong> {playCount, plural, one {lecture} other {lectures}}",
"components.Settings.Notifications.NotificationsPushbullet.channelTag": "Étiquette de canal",
"components.ManageSlideOver.alltime": "Tout le temps",
"components.ManageSlideOver.deletearr": "Supprimer de {arr}",
"components.ManageSlideOver.deletearr4k": "Supprimer de {arr} 4K",
"components.ManageSlideOver.manageModalDeleteMediaWarning": "* Cela va supprimer ce média de {arr}, ainsi que le fichier associé.",
"components.ManageSlideOver.manageModalMedia": "Média(s)",
"components.ManageSlideOver.markallseasonsavailable": "Marquer toutes les saisons comme disponibles",
"components.Settings.validationUrlBaseTrailingSlash": "L'URL de base ne doit pas se terminer par un slash",

Loading…
Cancel
Save