Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -165,9 +165,12 @@ const ReleaseManagementPreviewModal = ({
show && !mixMatchSelection,
);

// Mix & Match always fetches with includeVariations=true, so a selection may legitimately
// include a variation's PlaceID - this must match or the endpoint 400s on an "unknown" ID.
const mixMatchPreviewQuery = useReleaseMixMatchDeletionPreviewQuery(
selectedSeries?.[0] ?? 0,
{ selectedPlaceIDs: mixMatchSelection ?? [] },
true,
show && !!mixMatchSelection && !!selectedSeries,
);
const mixMatchPreviewData = mixMatchPreviewQuery.data ? [mixMatchPreviewQuery.data] : undefined;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,9 +95,18 @@ const ReleaseManagementSeriesDetail = () => {
const [mixMatchSelection, setMixMatchSelection] = useImmer<Map<string, number>>(new Map());
const [mixMatchUnassignedCount, setMixMatchUnassignedCount] = useState(0);

// Candidates tab passes through the list page's includeVariations toggle, same as the batch
// preview. Mix & Match always wants every file - variations included - as a pickable option
// regardless of that toggle, so it fetches independently with includeVariations forced true.
const seriesQuery = useReleaseManagementSeriesDetailQuery(seriesId, includeVariations, seriesId > 0);
const series = seriesQuery.data;

const mixMatchSeriesQuery = useReleaseManagementSeriesDetailQuery(
seriesId,
true,
seriesId > 0 && activeTab === 'mixmatch',
);

useEffect(() => {
if (seriesQuery.isError) {
toast.error(`Series ${seriesId} is invalid or does not have multiple releases`);
Expand Down Expand Up @@ -219,10 +228,24 @@ const ReleaseManagementSeriesDetail = () => {
/>
)}

{series && activeTab === 'mixmatch' && (
{activeTab === 'mixmatch' && mixMatchSeriesQuery.isPending && (
<div className="flex h-32 items-center justify-center text-panel-text-primary">
<Icon path={mdiLoading} size={2} spin />
</div>
)}

{activeTab === 'mixmatch' && mixMatchSeriesQuery.isError && (
<div className="flex h-32 items-center justify-center">
<span className="text-panel-text-danger">
Failed to load Mix &amp; Match options.
</span>
</div>
)}

{activeTab === 'mixmatch' && mixMatchSeriesQuery.data && (
<MixAndMatchTab
selection={mixMatchSelection}
series={series}
series={mixMatchSeriesQuery.data}
setSelection={setMixMatchSelection}
setUnassignedCount={setMixMatchUnassignedCount}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -150,11 +150,7 @@ const ReleaseManagementSeriesList = ({
const lastRowIndex = useRef<number>(undefined);
const handleRowClick = (event: MouseEvent<HTMLDivElement>, index: number) => {
if (!autoDeleteMode) {
navigate(
`${series[index].SeriesID.toString()}?tab=candidates&includeVariations=${
searchParams.get('includeVariations') ?? 'true'
}`,
);
navigate(`${series[index].SeriesID.toString()}?tab=candidates&includeVariations=${includeVariations}`);
return;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,11 @@ const CandidateCard = ({
const [markVariationsPending, setMarkVariationsPending] = useState(false);

const coverageString = buildEpisodeCoverageString(candidate.Episodes);
const totalSize = candidate.Files.reduce((sum, file) => sum + file.FileSize, 0);
const realFiles = candidate.Files.filter(file => !file.IsVariation);
const variationFiles = candidate.Files.filter(file => file.IsVariation);
const totalSize = realFiles.reduce((sum, file) => sum + file.FileSize, 0);
const redundantEpisodeStr = buildEpisodeCoverageString(candidate.RedundantEpisodes);
const multiFileEpisodeCount = candidate.Episodes.filter(episode => episode.PlaceIDs.length > 1).length;

const isVersion = candidate.DecidingSignal === 'Version';

Expand All @@ -53,9 +56,9 @@ const CandidateCard = ({
const handleMarkAllAsVariations = async () => {
try {
setMarkVariationsPending(true);
await Promise.all(candidate.Files.map(file => markVariation({ fileId: file.VideoLocalID, variation: true })));
await Promise.all(realFiles.map(file => markVariation({ fileId: file.VideoLocalID, variation: true })));
resetQueries(['release-management']);
toast.success(`Marked ${candidate.Files.length} file${candidate.Files.length !== 1 ? 's' : ''} as variations`);
toast.success(`Marked ${realFiles.length} file${realFiles.length !== 1 ? 's' : ''} as variations`);
} catch (_error) {
toast.error('Failed to mark files as variations');
}
Expand Down Expand Up @@ -171,10 +174,23 @@ const CandidateCard = ({
<div className="border-t border-panel-border pt-3 text-sm">
Coverage:&nbsp;
<span className="font-semibold">{coverageString !== '' ? coverageString : 'None'}</span>
{multiFileEpisodeCount > 0 && (
<span
className="ml-2 text-panel-text-warning"
data-tooltip-id="tooltip"
data-tooltip-content="Multiple files legitimately cover the same episode - a multi-part release, or files with no detectable difference"
>
(
{multiFileEpisodeCount}
&nbsp;
{multiFileEpisodeCount === 1 ? 'episode has' : 'episodes have'}
&nbsp;multiple files)
</span>
)}

<div className="mt-1">
Files:&nbsp;
<span className="font-semibold">{candidate.Files.length}</span>
<span className="font-semibold">{realFiles.length}</span>
{candidate.RedundantFileCount > 0 && (
<>
&nbsp;(
Expand All @@ -185,6 +201,14 @@ const CandidateCard = ({
)
</>
)}
{variationFiles.length > 0 && (
<>
&nbsp;(+
{variationFiles.length}
&nbsp;
{variationFiles.length === 1 ? 'variation' : 'variations'})
</>
)}
&nbsp;,&nbsp;
<span className="font-semibold">{prettyBytes(totalSize, { binary: true })}</span>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,10 @@ const CandidateCardFileList = ({ candidate, isAiring, isPrimary, primaryEpisodes
const fileCoverage = buildEpisodeCoverageString(file.Episodes);
const fileStreamSummary = buildFileStreamSummary(file);

let fileState: 'redundant' | 'kept' | 'also-delete' | 'required' | 'unknown';
if (isPrimary) {
let fileState: 'redundant' | 'kept' | 'also-delete' | 'required' | 'unknown' | 'variation';
if (file.IsVariation) {
fileState = 'variation';
} else if (isPrimary) {
fileState = 'kept';
} else if (isAiring) {
fileState = file.IsRedundant ? 'redundant' : 'kept';
Expand Down Expand Up @@ -86,6 +88,7 @@ const CandidateCardFileList = ({ candidate, isAiring, isPrimary, primaryEpisodes
{fileState === 'also-delete' && <span className="opacity-65">Could also delete</span>}
{fileState === 'kept' && <span className="opacity-65">Kept</span>}
{fileState === 'required' && <span className="text-panel-text-warning">Required - no other copy</span>}
{fileState === 'variation' && <span className="opacity-65">Variation - shown for reference</span>}
{fileState === 'unknown' && (
<span
className="flex items-center gap-1 text-panel-text-warning"
Expand Down Expand Up @@ -120,6 +123,8 @@ const CandidateCardFileList = ({ candidate, isAiring, isPrimary, primaryEpisodes
)}

<div className="mt-1 flex flex-wrap gap-x-2 gap-y-1">
{file.IsVariation && <Badge className="bg-panel-input">Variation</Badge>}

{file.IsChaptered != null && (file.IsChaptered || isChapteredAnomaly) && (
<Badge
className={cx(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ const CandidatesTab = ({ primaryCandidate, series, setPrimaryCandidate }: Props)
const candidateRedundant = isSubsetOf(candidate.Episodes, primaryEpisodeSet);

const deletedFiles = candidate.Files.filter((fileEntry) => {
if (fileEntry.IsVariation) return false;
if (primaryPlaceIds.has(fileEntry.PlaceID)) return false;
if (candidateRedundant) return true;
return series.IsAiring
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import cx from 'classnames';
import prettyBytes from 'pretty-bytes';
import { useToggle } from 'usehooks-ts';

import { Badge } from '@/components/Badge';

export type FileOption = {
placeID: number;
absolutePath?: string;
Expand All @@ -13,6 +15,7 @@ export type FileOption = {
groupLabel: string;
version: number;
isChaptered?: boolean;
isVariation: boolean;
subtitleStreamCount: number;
source?: string;
resolution?: string;
Expand Down Expand Up @@ -88,6 +91,7 @@ export const MixAndMatchEpisode = ({
{selectedOption && (
<>
<span className="font-semibold">{selectedOption.groupLabel}</span>{' '}
{selectedOption.isVariation && <Badge className="bg-panel-input">Variation</Badge>}{' '}
<span className="opacity-65">
- {getOptionSummary(selectedOption)}, {prettyBytes(selectedOption.fileSize, { binary: true })}
</span>
Expand All @@ -106,7 +110,8 @@ export const MixAndMatchEpisode = ({
{isExpanded && (
<div className="border-t border-panel-border">
{episode.options.map((option) => {
const { absolutePath, audioLanguages, fileSize, groupLabel, placeID, subtitleLanguages } = option;
const { absolutePath, audioLanguages, fileSize, groupLabel, isVariation, placeID, subtitleLanguages } =
option;
const isSelected = selectedPlaceID === placeID;
const fileName = absolutePath?.split(/[/\\]/).pop() ?? `Place ${placeID}`;
const summary = getOptionSummary(option);
Expand All @@ -130,7 +135,10 @@ export const MixAndMatchEpisode = ({
/>

<div className="min-w-0 grow">
<div className="font-semibold">{groupLabel}</div>
<div className="flex items-center gap-x-2">
<div className="font-semibold">{groupLabel}</div>
{isVariation && <Badge className="bg-panel-input">Variation</Badge>}
</div>
<div className="text-xs opacity-65">{summary}</div>
{(audioLanguages.length > 0 || subtitleLanguages.length > 0) && (
<div className="text-xs opacity-65">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ const buildEpisodeMap = (items: ReleaseCandidateType[] | ReleaseOverrideType[])
groupLabel: item.Name,
version: file.Version,
isChaptered: file.IsChaptered,
// OverrideFileType (used for partial-coverage groups) has no IsVariation field.
isVariation: 'IsVariation' in file && file.IsVariation,
subtitleStreamCount: item.SubtitleStreamCount,
source: item.Source,
resolution: item.Resolution,
Expand Down
8 changes: 6 additions & 2 deletions src/core/react-query/release-management/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,14 @@ export const useReleaseDeletionPreviewQuery = (
export const useReleaseMixMatchDeletionPreviewQuery = (
seriesId: number,
body: ReleaseMixMatchDeletionPreviewRequestType,
includeVariations = false,
enabled = true,
) =>
useQuery<ReleaseDeletionPreviewType>({
queryKey: ['release-management', 'preview', 'mix-match', seriesId, body],
queryFn: () => axios.post(`ReleaseManagement/Series/${seriesId}/Override`, body),
queryKey: ['release-management', 'preview', 'mix-match', seriesId, body, includeVariations],
queryFn: () =>
axios.post(`ReleaseManagement/Series/${seriesId}/Override`, body, {
params: { includeVariations },
}),
enabled,
});
3 changes: 3 additions & 0 deletions src/core/types/api/release-management.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ export type EpisodeCoverageType = {
Type: EpisodeTypeEnum;
Number: number;
GroupShortName?: string;
/** PlaceIDs of every file covering this episode. Only populated on candidate-level `Episodes`; always empty on a file's own `Episodes`. More than one entry means multiple files legitimately cover this episode. */
PlaceIDs: number[];
};

export type ReleaseCandidateFileType = {
Expand All @@ -12,6 +14,7 @@ export type ReleaseCandidateFileType = {
AbsolutePath?: string;
FileSize: number;
Version: number;
IsVariation: boolean;
IsRedundant: boolean;
IsChaptered?: boolean;
IsCensored?: boolean;
Expand Down