Compare commits

...

1 Commits

Author SHA1 Message Date
Gauthier
1a23f62a02 fix: correct "Remove from *arr" button
This PR fixes the "Delete from *arr" button in the request list. It checks from the API whether the
*arr server corresponding to the request still exists before displaying the remove button, and fixes
a cache removal issue that could cause problems when deleting recently added media. This PR also
reverts #1476, which introduced problems during removal.

fix #1494
2025-03-31 00:41:44 +02:00
8 changed files with 75 additions and 90 deletions

View File

@@ -289,7 +289,7 @@ class ExternalAPI {
return data; return data;
} }
protected removeCache(endpoint: string, options?: Record<string, string>) { protected removeCache(endpoint: string, options?: Record<string, unknown>) {
const cacheKey = this.serializeCacheKey(endpoint, { const cacheKey = this.serializeCacheKey(endpoint, {
...this.params, ...this.params,
...options, ...options,

View File

@@ -242,10 +242,13 @@ class RadarrAPI extends ServarrBase<{ movieId: number }> {
if (tmdbId) { if (tmdbId) {
this.removeCache('/movie/lookup', { this.removeCache('/movie/lookup', {
term: `tmdb:${tmdbId}`, term: `tmdb:${tmdbId}`,
headers: this.defaultHeaders,
}); });
} }
if (externalId) { if (externalId) {
this.removeCache(`/movie/${externalId}`); this.removeCache(`/movie/${externalId}`, {
headers: this.defaultHeaders,
});
} }
}; };
} }

View File

@@ -368,14 +368,18 @@ class SonarrAPI extends ServarrBase<{
if (tvdbId) { if (tvdbId) {
this.removeCache('/series/lookup', { this.removeCache('/series/lookup', {
term: `tvdb:${tvdbId}`, term: `tvdb:${tvdbId}`,
headers: this.defaultHeaders,
}); });
} }
if (externalId) { if (externalId) {
this.removeCache(`/series/${externalId}`); this.removeCache(`/series/${externalId}`, {
headers: this.defaultHeaders,
});
} }
if (title) { if (title) {
this.removeCache('/series/lookup', { this.removeCache('/series/lookup', {
term: title, term: title,
headers: this.defaultHeaders,
}); });
} }
}; };

View File

@@ -3,7 +3,10 @@ import type { MediaRequest } from '@server/entity/MediaRequest';
import type { NonFunctionProperties, PaginatedResponse } from './common'; import type { NonFunctionProperties, PaginatedResponse } from './common';
export interface RequestResultsResponse extends PaginatedResponse { export interface RequestResultsResponse extends PaginatedResponse {
results: NonFunctionProperties<MediaRequest>[]; results: (NonFunctionProperties<MediaRequest> & {
profileName?: string;
canRemove?: boolean;
})[];
} }
export type MediaRequestBody = { export type MediaRequestBody = {

View File

@@ -237,19 +237,6 @@ mediaRoutes.delete(
} }
if (isMovie) { if (isMovie) {
// check if the movie exists
try {
await (service as RadarrAPI).getMovie({
id: parseInt(
is4k
? (media.externalServiceSlug4k as string)
: (media.externalServiceSlug as string)
),
});
} catch {
return res.status(204).send();
}
// remove the movie
await (service as RadarrAPI).removeMovie( await (service as RadarrAPI).removeMovie(
parseInt( parseInt(
is4k is4k
@@ -264,13 +251,6 @@ mediaRoutes.delete(
if (!tvdbId) { if (!tvdbId) {
throw new Error('TVDB ID not found'); throw new Error('TVDB ID not found');
} }
// check if the series exists
try {
await (service as SonarrAPI).getSeriesByTvdbId(tvdbId);
} catch {
return res.status(204).send();
}
// remove the series
await (service as SonarrAPI).removeSerie(tvdbId); await (service as SonarrAPI).removeSerie(tvdbId);
} }

View File

@@ -189,7 +189,7 @@ requestRoutes.get<Record<string, unknown>, RequestResultsResponse>(
); );
// add profile names to the media requests, with undefined if not found // add profile names to the media requests, with undefined if not found
const requestsWithProfileNames = requests.map((r) => { let mappedRequests = requests.map((r) => {
switch (r.type) { switch (r.type) {
case MediaType.MOVIE: { case MediaType.MOVIE: {
const profileName = radarrServers const profileName = radarrServers
@@ -212,6 +212,36 @@ requestRoutes.get<Record<string, unknown>, RequestResultsResponse>(
} }
}); });
// add canRemove prop if user has permission
if (req.user?.hasPermission(Permission.MANAGE_REQUESTS)) {
mappedRequests = mappedRequests.map((r) => {
switch (r.type) {
case MediaType.MOVIE: {
return {
...r,
// check if the radarr server for this request is configured
canRemove: radarrServers.some(
(server) =>
server.id ===
(r.is4k ? r.media.serviceId4k : r.media.serviceId)
),
};
}
case MediaType.TV: {
return {
...r,
// check if the sonarr server for this request is configured
canRemove: sonarrServers.some(
(server) =>
server.id ===
(r.is4k ? r.media.serviceId4k : r.media.serviceId)
),
};
}
}
});
}
return res.status(200).json({ return res.status(200).json({
pageInfo: { pageInfo: {
pages: Math.ceil(requestCount / pageSize), pages: Math.ceil(requestCount / pageSize),
@@ -219,7 +249,7 @@ requestRoutes.get<Record<string, unknown>, RequestResultsResponse>(
results: requestCount, results: requestCount,
page: Math.ceil(skip / pageSize) + 1, page: Math.ceil(skip / pageSize) + 1,
}, },
results: requestsWithProfileNames, results: mappedRequests,
}); });
} catch (e) { } catch (e) {
next({ status: 500, message: e.message }); next({ status: 500, message: e.message });

View File

@@ -17,10 +17,10 @@ import {
TrashIcon, TrashIcon,
XMarkIcon, XMarkIcon,
} from '@heroicons/react/24/solid'; } from '@heroicons/react/24/solid';
import { MediaRequestStatus, MediaType } from '@server/constants/media'; import { MediaRequestStatus } from '@server/constants/media';
import type { MediaRequest } from '@server/entity/MediaRequest'; import type { MediaRequest } from '@server/entity/MediaRequest';
import type { NonFunctionProperties } from '@server/interfaces/api/common'; import type { NonFunctionProperties } from '@server/interfaces/api/common';
import type { RadarrSettings, SonarrSettings } from '@server/lib/settings'; import type { RequestResultsResponse } from '@server/interfaces/api/requestInterfaces';
import type { MovieDetails } from '@server/models/Movie'; import type { MovieDetails } from '@server/models/Movie';
import type { TvDetails } from '@server/models/Tv'; import type { TvDetails } from '@server/models/Tv';
import Link from 'next/link'; import Link from 'next/link';
@@ -292,18 +292,11 @@ const RequestItemError = ({
}; };
interface RequestItemProps { interface RequestItemProps {
request: NonFunctionProperties<MediaRequest> & { profileName?: string }; request: RequestResultsResponse['results'][number];
revalidateList: () => void; revalidateList: () => void;
radarrData?: RadarrSettings[];
sonarrData?: SonarrSettings[];
} }
const RequestItem = ({ const RequestItem = ({ request, revalidateList }: RequestItemProps) => {
request,
revalidateList,
radarrData,
sonarrData,
}: RequestItemProps) => {
const settings = useSettings(); const settings = useSettings();
const { ref, inView } = useInView({ const { ref, inView } = useInView({
triggerOnce: true, triggerOnce: true,
@@ -398,23 +391,6 @@ const RequestItem = ({
iOSPlexUrl4k: requestData?.media?.iOSPlexUrl4k, iOSPlexUrl4k: requestData?.media?.iOSPlexUrl4k,
}); });
const serviceExists = () => {
if (title?.mediaInfo) {
if (title?.mediaInfo.mediaType === MediaType.MOVIE) {
return (
radarrData?.find((radarr) => radarr.id === request.serverId) !==
undefined
);
} else {
return (
sonarrData?.find((sonarr) => sonarr.id === request.serverId) !==
undefined
);
}
}
return false;
};
if (!title && !error) { if (!title && !error) {
return ( return (
<div <div
@@ -722,30 +698,30 @@ const RequestItem = ({
)} )}
{requestData.status !== MediaRequestStatus.PENDING && {requestData.status !== MediaRequestStatus.PENDING &&
hasPermission(Permission.MANAGE_REQUESTS) && ( hasPermission(Permission.MANAGE_REQUESTS) && (
<ConfirmButton <>
onClick={() => deleteRequest()} <ConfirmButton
confirmText={intl.formatMessage(globalMessages.areyousure)} onClick={() => deleteRequest()}
className="w-full" confirmText={intl.formatMessage(globalMessages.areyousure)}
> className="w-full"
<TrashIcon /> >
<span>{intl.formatMessage(messages.deleterequest)}</span> <TrashIcon />
</ConfirmButton> <span>{intl.formatMessage(messages.deleterequest)}</span>
)} </ConfirmButton>
{hasPermission(Permission.MANAGE_REQUESTS) && {request.canRemove && (
title?.mediaInfo?.serviceId && <ConfirmButton
serviceExists() && ( onClick={() => deleteMediaFile()}
<ConfirmButton confirmText={intl.formatMessage(globalMessages.areyousure)}
onClick={() => deleteMediaFile()} className="w-full"
confirmText={intl.formatMessage(globalMessages.areyousure)} >
className="w-full" <TrashIcon />
> <span>
<TrashIcon /> {intl.formatMessage(messages.removearr, {
<span> arr: request.type === 'movie' ? 'Radarr' : 'Sonarr',
{intl.formatMessage(messages.removearr, { })}
arr: request.type === 'movie' ? 'Radarr' : 'Sonarr', </span>
})} </ConfirmButton>
</span> )}
</ConfirmButton> </>
)} )}
{requestData.status === MediaRequestStatus.PENDING && {requestData.status === MediaRequestStatus.PENDING &&
hasPermission(Permission.MANAGE_REQUESTS) && ( hasPermission(Permission.MANAGE_REQUESTS) && (

View File

@@ -17,8 +17,6 @@ import {
FunnelIcon, FunnelIcon,
} from '@heroicons/react/24/solid'; } from '@heroicons/react/24/solid';
import type { RequestResultsResponse } from '@server/interfaces/api/requestInterfaces'; import type { RequestResultsResponse } from '@server/interfaces/api/requestInterfaces';
import { Permission } from '@server/lib/permissions';
import type { RadarrSettings, SonarrSettings } from '@server/lib/settings';
import Link from 'next/link'; import Link from 'next/link';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
@@ -53,7 +51,7 @@ const RequestList = () => {
const { user } = useUser({ const { user } = useUser({
id: Number(router.query.userId), id: Number(router.query.userId),
}); });
const { user: currentUser, hasPermission } = useUser(); const { user: currentUser } = useUser();
const [currentFilter, setCurrentFilter] = useState<Filter>(Filter.PENDING); const [currentFilter, setCurrentFilter] = useState<Filter>(Filter.PENDING);
const [currentSort, setCurrentSort] = useState<Sort>('added'); const [currentSort, setCurrentSort] = useState<Sort>('added');
const [currentSortDirection, setCurrentSortDirection] = const [currentSortDirection, setCurrentSortDirection] =
@@ -64,13 +62,6 @@ const RequestList = () => {
const pageIndex = page - 1; const pageIndex = page - 1;
const updateQueryParams = useUpdateQueryParams({ page: page.toString() }); const updateQueryParams = useUpdateQueryParams({ page: page.toString() });
const { data: radarrData } = useSWR<RadarrSettings[]>(
hasPermission(Permission.ADMIN) ? '/api/v1/settings/radarr' : null
);
const { data: sonarrData } = useSWR<SonarrSettings[]>(
hasPermission(Permission.ADMIN) ? '/api/v1/settings/sonarr' : null
);
const { const {
data, data,
error, error,
@@ -254,8 +245,6 @@ const RequestList = () => {
<RequestItem <RequestItem
request={request} request={request}
revalidateList={() => revalidate()} revalidateList={() => revalidate()}
radarrData={radarrData}
sonarrData={sonarrData}
/> />
</div> </div>
); );