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
54 changes: 54 additions & 0 deletions apps/internal-site/components/web-uploader/source-scanner.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,60 @@ describe("scanBrowserUploadFiles", () => {
expect(plan.frames.map((frame) => frame.title)).toEqual(["00-2183", "12-27240"]);
});

it("sorts structured flat volumes numerically when episode and frame match", () => {
const plan = scanBrowserUploadFiles(
[
image("TITLE_VOL10_00000.m2ts-27240-src.png"),
image("TITLE_VOL10_00000.m2ts-27240-output.png"),
image("TITLE_VOL2_00000.m2ts-27240-src.png"),
image("TITLE_VOL2_00000.m2ts-27240-output.png"),
],
"20260702",
);

expect(plan.frames).toHaveLength(2);
expect(plan.frames.map((frame) => frame.before.source.relativePath)).toEqual([
"TITLE_VOL2_00000.m2ts-27240-src.png",
"TITLE_VOL10_00000.m2ts-27240-src.png",
]);
});

it("keeps unrelated flat titles grouped before comparing volume numbers", () => {
const plan = scanBrowserUploadFiles(
[
image("BBB_VOL2_00000.m2ts-27240-src.png"),
image("BBB_VOL2_00000.m2ts-27240-output.png"),
image("AAA_VOL10_00000.m2ts-27240-src.png"),
image("AAA_VOL10_00000.m2ts-27240-output.png"),
],
"20260702",
);

expect(plan.frames).toHaveLength(2);
expect(plan.frames.map((frame) => frame.before.source.relativePath)).toEqual([
"AAA_VOL10_00000.m2ts-27240-src.png",
"BBB_VOL2_00000.m2ts-27240-src.png",
]);
});

it("sorts structured nested volumes numerically when episode and frame match", () => {
const plan = scanBrowserUploadFiles(
[
image("case/before/TITLE_VOL10_00000.m2ts-27240.png"),
image("case/after/TITLE_VOL10_00000.m2ts-27240.png"),
image("case/before/TITLE_VOL2_00000.m2ts-27240.png"),
image("case/after/TITLE_VOL2_00000.m2ts-27240.png"),
],
"case",
);

expect(plan.frames).toHaveLength(2);
expect(plan.frames.map((frame) => frame.before.source.relativePath)).toEqual([
"before/TITLE_VOL2_00000.m2ts-27240.png",
"before/TITLE_VOL10_00000.m2ts-27240.png",
]);
});

it("recognizes structured encode filenames with m2ts markers and no fps prefix", () => {
const plan = scanBrowserUploadFiles(
[
Expand Down
56 changes: 54 additions & 2 deletions apps/internal-site/components/web-uploader/source-scanner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,15 @@ const MATCH_KEY_SUFFIX_RE = new RegExp(
"i",
);
const NON_ALNUM_RE = /[^0-9a-z]+/g;
const VOLUME_HINT_RE = /(?:^|[^a-zA-Z0-9])(VOL|BOX)[ _-]*(\d+)(?=$|[^a-zA-Z0-9])/i;
const MAX_AFTER_ASSETS = 4;

interface VolumeSortKey {
kind: "VOL" | "BOX";
number: number;
label: string;
}

interface SourceCandidate {
entry: BrowserUploadFile;
originalName: string;
Expand Down Expand Up @@ -125,6 +132,45 @@ function titleCase(input: string) {
.replace(/\b\w/g, (letter) => letter.toUpperCase());
}

function volumeSortKey(input: string): VolumeSortKey | null {
const match = input.match(VOLUME_HINT_RE);
if (!match) {
return null;
}

const kind = match[1].toUpperCase() as VolumeSortKey["kind"];
const number = Number(match[2]);
return { kind, number, label: `${kind}${number}` };
}

function compareVolumeHints(left: string, right: string) {
const leftKey = volumeSortKey(left);
const rightKey = volumeSortKey(right);
const kindRank = (key: VolumeSortKey | null) => key ? (key.kind === "VOL" ? 0 : 1) : 2;

return (
kindRank(leftKey) - kindRank(rightKey) ||
(leftKey?.number ?? Number.MAX_SAFE_INTEGER) - (rightKey?.number ?? Number.MAX_SAFE_INTEGER)
);
}

function rootHintWithoutVolume(input: string) {
return input
.replace(VOLUME_HINT_RE, " ")
.toLowerCase()
.replace(NON_ALNUM_RE, " ")
.trim();
}

function compareRootHints(left: string, right: string) {
// Keep unrelated titles grouped first; volume numbers only break ties within the same root title.
return (
rootHintWithoutVolume(left).localeCompare(rootHintWithoutVolume(right)) ||
compareVolumeHints(left, right) ||
left.localeCompare(right)
);
}

function variantLabel(variant: string) {
const normalized = variant.toLowerCase();
if (PRIMARY_AFTER_VARIANTS.has(normalized)) {
Expand Down Expand Up @@ -576,7 +622,7 @@ function buildFlatPlan(sourceRootName: string, entries: BrowserUploadFile[], ign
return (
Number(leftCandidate.episode) - Number(rightCandidate.episode) ||
leftCandidate.frameNumber - rightCandidate.frameNumber ||
leftCandidate.rootHint.localeCompare(rightCandidate.rootHint)
compareRootHints(leftCandidate.rootHint, rightCandidate.rootHint)
);
});
const fallbackWidth = Math.max(
Expand Down Expand Up @@ -643,7 +689,13 @@ function buildNestedPlan(sourceRootName: string, entries: BrowserUploadFile[], i
const miscAssignments = assignCandidatesToBeforeKeys(miscParsed, beforeByKey);
const allBefore = [...beforeByKey.entries()]
.map(([matchKey, candidates]) => [matchKey, candidates[0]] as const)
.sort((left, right) => Number(left[1].episode) - Number(right[1].episode) || left[1].frameNumber - right[1].frameNumber || left[0].localeCompare(right[0]));
.sort(
(left, right) =>
Number(left[1].episode) - Number(right[1].episode) ||
left[1].frameNumber - right[1].frameNumber ||
compareRootHints(left[1].rootHint, right[1].rootHint) ||
left[0].localeCompare(right[0]),
);
const fallbackWidth = Math.max(4, String(Math.max(0, ...allBefore.map(([, candidate]) => candidate.frameNumber))).length);
const episodeWidth = structuredEpisodeWidth(beforeParsed.candidates);
const frames: WebUploadFramePlan[] = [];
Expand Down
Loading