162 lines
4.7 KiB
TypeScript
162 lines
4.7 KiB
TypeScript
import { Database } from "bun:sqlite";
|
|
import { describe, expect, test } from "bun:test";
|
|
import { SCHEMA } from "../../db/schema";
|
|
import { buildReviewGroups } from "../review";
|
|
|
|
function makeDb(): Database {
|
|
const db = new Database(":memory:");
|
|
for (const stmt of SCHEMA.split(";")) {
|
|
const trimmed = stmt.trim();
|
|
if (trimmed) db.run(trimmed);
|
|
}
|
|
return db;
|
|
}
|
|
|
|
interface SeedOpts {
|
|
id: number;
|
|
type: "Movie" | "Episode";
|
|
name?: string;
|
|
seriesName?: string | null;
|
|
seriesJellyfinId?: string | null;
|
|
seasonNumber?: number | null;
|
|
episodeNumber?: number | null;
|
|
confidence?: "high" | "low";
|
|
}
|
|
|
|
function seed(db: Database, opts: SeedOpts) {
|
|
const {
|
|
id,
|
|
type,
|
|
name = `Item ${id}`,
|
|
seriesName = null,
|
|
seriesJellyfinId = null,
|
|
seasonNumber = null,
|
|
episodeNumber = null,
|
|
confidence = "high",
|
|
} = opts;
|
|
db
|
|
.prepare(
|
|
"INSERT INTO media_items (id, jellyfin_id, type, name, series_name, series_jellyfin_id, season_number, episode_number, file_path) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
|
)
|
|
.run(id, `jf-${id}`, type, name, seriesName, seriesJellyfinId, seasonNumber, episodeNumber, `/x/${id}.mkv`);
|
|
db
|
|
.prepare(
|
|
"INSERT INTO review_plans (item_id, status, is_noop, confidence, apple_compat, job_type, notes) VALUES (?, 'pending', 0, ?, 'direct_play', 'copy', NULL)",
|
|
)
|
|
.run(id, confidence);
|
|
}
|
|
|
|
describe("buildReviewGroups", () => {
|
|
test("returns a complete series with every pending episode", () => {
|
|
const db = makeDb();
|
|
for (let i = 1; i <= 30; i++) {
|
|
seed(db, {
|
|
id: i,
|
|
type: "Episode",
|
|
seriesName: "Breaking Bad",
|
|
seriesJellyfinId: "bb",
|
|
seasonNumber: 1,
|
|
episodeNumber: i,
|
|
});
|
|
}
|
|
|
|
const { groups, totalItems } = buildReviewGroups(db);
|
|
|
|
expect(groups).toHaveLength(1);
|
|
const series = groups[0];
|
|
expect(series.kind).toBe("series");
|
|
if (series.kind !== "series") throw new Error("expected series");
|
|
expect(series.episodeCount).toBe(30);
|
|
expect(series.seasons).toHaveLength(1);
|
|
expect(series.seasons[0].episodes).toHaveLength(30);
|
|
expect(totalItems).toBe(30);
|
|
});
|
|
|
|
test("buckets episodes by season with null ordered last", () => {
|
|
const db = makeDb();
|
|
for (let ep = 1; ep <= 3; ep++) {
|
|
seed(db, {
|
|
id: ep,
|
|
type: "Episode",
|
|
seriesName: "Lost",
|
|
seriesJellyfinId: "lost",
|
|
seasonNumber: 1,
|
|
episodeNumber: ep,
|
|
});
|
|
}
|
|
for (let ep = 1; ep <= 2; ep++) {
|
|
seed(db, {
|
|
id: 10 + ep,
|
|
type: "Episode",
|
|
seriesName: "Lost",
|
|
seriesJellyfinId: "lost",
|
|
seasonNumber: 2,
|
|
episodeNumber: ep,
|
|
});
|
|
}
|
|
seed(db, { id: 99, type: "Episode", seriesName: "Lost", seriesJellyfinId: "lost", seasonNumber: null });
|
|
|
|
const { groups } = buildReviewGroups(db);
|
|
expect(groups).toHaveLength(1);
|
|
const lost = groups[0];
|
|
if (lost.kind !== "series") throw new Error("expected series");
|
|
expect(lost.seasons.map((s) => s.season)).toEqual([1, 2, null]);
|
|
expect(lost.seasons[0].episodes).toHaveLength(3);
|
|
expect(lost.seasons[1].episodes).toHaveLength(2);
|
|
expect(lost.seasons[2].episodes).toHaveLength(1);
|
|
});
|
|
|
|
test("sorts groups: high-confidence first, then by name", () => {
|
|
const db = makeDb();
|
|
seed(db, { id: 1, type: "Movie", name: "Zodiac", confidence: "high" });
|
|
seed(db, { id: 2, type: "Movie", name: "Arrival", confidence: "low" });
|
|
seed(db, { id: 3, type: "Movie", name: "Blade Runner", confidence: "high" });
|
|
|
|
const { groups } = buildReviewGroups(db);
|
|
const names = groups.map((g) => (g.kind === "movie" ? g.item.name : g.seriesName));
|
|
expect(names).toEqual(["Blade Runner", "Zodiac", "Arrival"]);
|
|
});
|
|
|
|
test("minConfidence is low when any episode in the series is low", () => {
|
|
const db = makeDb();
|
|
seed(db, {
|
|
id: 1,
|
|
type: "Episode",
|
|
seriesName: "Show",
|
|
seriesJellyfinId: "s",
|
|
seasonNumber: 1,
|
|
episodeNumber: 1,
|
|
confidence: "high",
|
|
});
|
|
seed(db, {
|
|
id: 2,
|
|
type: "Episode",
|
|
seriesName: "Show",
|
|
seriesJellyfinId: "s",
|
|
seasonNumber: 1,
|
|
episodeNumber: 2,
|
|
confidence: "low",
|
|
});
|
|
|
|
const { groups } = buildReviewGroups(db);
|
|
expect(groups).toHaveLength(1);
|
|
if (groups[0].kind !== "series") throw new Error("expected series");
|
|
expect(groups[0].minConfidence).toBe("low");
|
|
});
|
|
|
|
test("excludes plans that are not pending or are is_noop=1", () => {
|
|
const db = makeDb();
|
|
seed(db, { id: 1, type: "Movie", name: "Pending" });
|
|
seed(db, { id: 2, type: "Movie", name: "Approved" });
|
|
db.prepare("UPDATE review_plans SET status = 'approved' WHERE item_id = ?").run(2);
|
|
seed(db, { id: 3, type: "Movie", name: "Noop" });
|
|
db.prepare("UPDATE review_plans SET is_noop = 1 WHERE item_id = ?").run(3);
|
|
|
|
const { groups, totalItems } = buildReviewGroups(db);
|
|
expect(groups).toHaveLength(1);
|
|
expect(totalItems).toBe(1);
|
|
if (groups[0].kind !== "movie") throw new Error("expected movie");
|
|
expect(groups[0].item.name).toBe("Pending");
|
|
});
|
|
});
|