fix(requests): scope download status to requested seasons on request cards (#3412)

This commit is contained in:
0xsysr3ll
2026-09-05 09:33:21 +02:00
committed by GitHub
parent a4f5eaa21e
commit c604bccc00
5 changed files with 193 additions and 56 deletions
+3
View File
@@ -59,6 +59,9 @@ if (positionals.length > 0) {
for await (const entry of glob(join(BASE_DIR, 'server/**/*.test.ts'))) {
files.push(resolve(entry));
}
for await (const entry of glob(join(BASE_DIR, 'src/**/*.test.ts'))) {
files.push(resolve(entry));
}
files.sort();
}
+24 -29
View File
@@ -10,7 +10,10 @@ import useToasts from '@app/hooks/useToasts';
import { Permission, useUser } from '@app/hooks/useUser';
import globalMessages from '@app/i18n/globalMessages';
import defineMessages from '@app/utils/defineMessages';
import { refreshIntervalHelper } from '@app/utils/refreshIntervalHelper';
import {
getRequestDownloadStatus,
refreshIntervalHelper,
} from '@app/utils/refreshIntervalHelper';
import { withProperties } from '@app/utils/typeHelpers';
import {
ArrowPathIcon,
@@ -75,6 +78,15 @@ const RequestCardError = ({ requestData }: RequestCardErrorProps) => {
iOSPlexUrl4k: requestData?.media?.iOSPlexUrl4k,
});
const requestDownloadStatus = getRequestDownloadStatus(
requestData?.media?.[
requestData?.is4k ? 'downloadStatus4k' : 'downloadStatus'
],
requestData?.type === 'tv'
? (requestData?.seasons ?? []).map((season) => season.seasonNumber)
: []
);
const deleteRequest = async () => {
await axios.delete(`/api/v1/media/${requestData?.media.id}`);
mutate('/api/v1/media?filter=allavailable&take=20&sort=mediaAdded');
@@ -149,23 +161,9 @@ const RequestCardError = ({ requestData }: RequestCardErrorProps) => {
requestData.is4k ? 'status4k' : 'status'
]
}
downloadItem={
requestData.media[
requestData.is4k
? 'downloadStatus4k'
: 'downloadStatus'
]
}
downloadItem={requestDownloadStatus}
title={intl.formatMessage(messages.unknowntitle)}
inProgress={
(
requestData.media[
requestData.is4k
? 'downloadStatus4k'
: 'downloadStatus'
] ?? []
).length > 0
}
inProgress={requestDownloadStatus.length > 0}
is4k={requestData.is4k}
mediaType={requestData.type}
plexUrl={requestData.is4k ? plexUrl4k : plexUrl}
@@ -327,6 +325,13 @@ const RequestCard = ({ request, onTitleData }: RequestCardProps) => {
return <RequestCardError requestData={requestData} />;
}
const requestDownloadStatus = getRequestDownloadStatus(
requestData.media[requestData.is4k ? 'downloadStatus4k' : 'downloadStatus'],
requestData.type === 'tv'
? requestData.seasons.map((season) => season.seasonNumber)
: []
);
return (
<>
<RequestModal
@@ -457,19 +462,9 @@ const RequestCard = ({ request, onTitleData }: RequestCardProps) => {
status={
requestData.media[requestData.is4k ? 'status4k' : 'status']
}
downloadItem={
requestData.media[
requestData.is4k ? 'downloadStatus4k' : 'downloadStatus'
]
}
downloadItem={requestDownloadStatus}
title={isMovie(title) ? title.title : title.name}
inProgress={
(
requestData.media[
requestData.is4k ? 'downloadStatus4k' : 'downloadStatus'
] ?? []
).length > 0
}
inProgress={requestDownloadStatus.length > 0}
is4k={requestData.is4k}
tmdbId={requestData.media.tmdbId}
mediaType={requestData.type}
@@ -10,7 +10,10 @@ import useToasts from '@app/hooks/useToasts';
import { Permission, useUser } from '@app/hooks/useUser';
import globalMessages from '@app/i18n/globalMessages';
import defineMessages from '@app/utils/defineMessages';
import { refreshIntervalHelper } from '@app/utils/refreshIntervalHelper';
import {
getRequestDownloadStatus,
refreshIntervalHelper,
} from '@app/utils/refreshIntervalHelper';
import {
ArrowPathIcon,
CheckIcon,
@@ -80,6 +83,15 @@ const RequestItemError = ({
iOSPlexUrl4k: requestData?.media?.iOSPlexUrl4k,
});
const requestDownloadStatus = getRequestDownloadStatus(
requestData?.media?.[
requestData?.is4k ? 'downloadStatus4k' : 'downloadStatus'
],
requestData?.type === 'tv'
? (requestData?.seasons ?? []).map((season) => season.seasonNumber)
: []
);
return (
<div className="flex h-64 w-full flex-col justify-center rounded-xl bg-gray-800 py-4 text-gray-400 shadow-md ring-1 ring-red-500 xl:h-28 xl:flex-row">
<div className="flex w-full flex-col justify-between overflow-hidden sm:flex-row">
@@ -139,21 +151,9 @@ const RequestItemError = ({
requestData.is4k ? 'status4k' : 'status'
]
}
downloadItem={
requestData.media[
requestData.is4k ? 'downloadStatus4k' : 'downloadStatus'
]
}
downloadItem={requestDownloadStatus}
title={intl.formatMessage(messages.unknowntitle)}
inProgress={
(
requestData.media[
requestData.is4k
? 'downloadStatus4k'
: 'downloadStatus'
] ?? []
).length > 0
}
inProgress={requestDownloadStatus.length > 0}
is4k={requestData.is4k}
mediaType={requestData.type}
plexUrl={requestData.is4k ? plexUrl4k : plexUrl}
@@ -414,6 +414,13 @@ const RequestItem = ({ request, revalidateList }: RequestItemProps) => {
);
}
const requestDownloadStatus = getRequestDownloadStatus(
requestData.media[requestData.is4k ? 'downloadStatus4k' : 'downloadStatus'],
requestData.type === 'tv'
? requestData.seasons.map((season) => season.seasonNumber)
: []
);
return (
<>
<RequestModal
@@ -540,19 +547,9 @@ const RequestItem = ({ request, revalidateList }: RequestItemProps) => {
status={
requestData.media[requestData.is4k ? 'status4k' : 'status']
}
downloadItem={
requestData.media[
requestData.is4k ? 'downloadStatus4k' : 'downloadStatus'
]
}
downloadItem={requestDownloadStatus}
title={isMovie(title) ? title.title : title.name}
inProgress={
(
requestData.media[
requestData.is4k ? 'downloadStatus4k' : 'downloadStatus'
] ?? []
).length > 0
}
inProgress={requestDownloadStatus.length > 0}
is4k={requestData.is4k}
tmdbId={requestData.media.tmdbId}
mediaType={requestData.type}
+127
View File
@@ -0,0 +1,127 @@
import { MediaType } from '@server/constants/media';
import type { DownloadingItem } from '@server/lib/downloadtracker';
import assert from 'node:assert/strict';
import { describe, it } from 'node:test';
import { getRequestDownloadStatus } from './refreshIntervalHelper';
const createDownloadItem = (
overrides: Partial<DownloadingItem> & Pick<DownloadingItem, 'downloadId'>
): DownloadingItem => ({
mediaType: MediaType.TV,
externalId: 1,
size: 1000,
sizeLeft: 500,
status: 'downloading',
timeLeft: '00:05:00',
estimatedCompletionTime: new Date('2026-01-01T00:00:00Z'),
title: 'Test Show',
...overrides,
});
const getRequestStatus = (
downloadStatus: DownloadingItem[] | undefined,
seasonNumbers: number[]
) => {
const downloadItem = getRequestDownloadStatus(downloadStatus, seasonNumbers);
return {
downloadItem,
inProgress: downloadItem.length > 0,
};
};
describe('getRequestDownloadStatus', () => {
it('returns an empty array when downloadStatus is undefined', () => {
const status = getRequestStatus(undefined, [1]);
assert.deepEqual(status.downloadItem, []);
assert.equal(status.inProgress, false);
});
it('returns all items when seasonNumbers is empty', () => {
const movieDownload = createDownloadItem({
downloadId: 'movie-1',
mediaType: MediaType.MOVIE,
});
const tvDownload = createDownloadItem({
downloadId: 'tv-1',
episode: {
id: 1,
seasonNumber: 2,
episodeNumber: 3,
absoluteEpisodeNumber: 3,
},
});
const downloads = [movieDownload, tvDownload];
const status = getRequestStatus(downloads, []);
assert.deepEqual(status.downloadItem, downloads);
assert.equal(status.inProgress, true);
});
it('returns only items whose episode season matches the request seasons', () => {
const seasonOne = createDownloadItem({
downloadId: 's1',
episode: {
id: 1,
seasonNumber: 1,
episodeNumber: 1,
absoluteEpisodeNumber: 1,
},
});
const seasonThree = createDownloadItem({
downloadId: 's3',
episode: {
id: 3,
seasonNumber: 3,
episodeNumber: 1,
absoluteEpisodeNumber: 10,
},
});
const downloads = [seasonOne, seasonThree];
const status = getRequestStatus(downloads, [3]);
assert.deepEqual(status.downloadItem, [seasonThree]);
assert.equal(status.inProgress, true);
});
it('returns an empty array when no items match the request seasons', () => {
const seasonThree = createDownloadItem({
downloadId: 's3',
episode: {
id: 3,
seasonNumber: 3,
episodeNumber: 1,
absoluteEpisodeNumber: 10,
},
});
const status = getRequestStatus([seasonThree], [1, 2]);
assert.deepEqual(status.downloadItem, []);
assert.equal(status.inProgress, false);
});
it('excludes items without episode metadata when filtering by season', () => {
const withoutEpisode = createDownloadItem({
downloadId: 'missing-episode',
});
const seasonTwo = createDownloadItem({
downloadId: 's2',
episode: {
id: 2,
seasonNumber: 2,
episodeNumber: 4,
absoluteEpisodeNumber: 4,
},
});
const downloads = [withoutEpisode, seasonTwo];
const status = getRequestStatus(downloads, [2]);
assert.deepEqual(status.downloadItem, [seasonTwo]);
assert.equal(status.inProgress, true);
});
});
+15
View File
@@ -1,5 +1,20 @@
import type { DownloadingItem } from '@server/lib/downloadtracker';
export const getRequestDownloadStatus = (
downloadStatus: DownloadingItem[] | undefined,
seasonNumbers: number[]
): DownloadingItem[] => {
const items = downloadStatus ?? [];
if (!seasonNumbers.length) {
return items;
}
return items.filter(
(item) => item.episode && seasonNumbers.includes(item.episode.seasonNumber)
);
};
export const refreshIntervalHelper = (
downloadItem: {
downloadStatus: DownloadingItem[] | undefined;