- metadata.includeSpecials (default true): full refresh and the language backfill fetch TMDB season 0 (specials) when present, like MP; season 0 stays out of the episode-total ledger - refreshMetadata promotes provisional season/episode units that match the TMDB season/episode layout to confirmed (source=tmdb), keeping ids and all references; non-matching units stay provisional - content_unit.source (migration 0053): release / file / tmdb / user - duplicate units (same entity/kind/season/episode/content type/segment) are merged as a refresh step and via `scripts/identity-cleanup.ts --merge-duplicates` (dry-run by default); keeper = most referenced, then confirmed, then earliest; references auto-discovered via FKs and repointed - ensureUnits takes a per-unit advisory xact lock to stop concurrent lazy creation from producing duplicates - runbooks updated; user DB read-only dry-run: 征途 S01E09/E10 4 units each -> 1 each (6 removed), not applied Co-Authored-By: Claude Opus 5.5 <[email protected]>
156 lines
5.9 KiB
TypeScript
156 lines
5.9 KiB
TypeScript
import { createMockNexusPhpSite, createMockTmdb, type MockNexusPhpSite, type MockTmdb } from "@umt/testkit";
|
|
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
|
import { refreshMetadata } from "../../src/modules/sources-identity/service/metadata.ts";
|
|
import { trackedSeasonsForAirDates } from "../../src/modules/sources-identity/service/metadata-totals.ts";
|
|
import { searchSubscription } from "../../src/modules/sources-identity/tasks.ts";
|
|
import { createHarness, type Harness, json, pgAvailable } from "../sources-identity/harness.ts";
|
|
|
|
// C1-A:订阅搜索关键词追加到主动搜索;元数据刷新按 TMDB 季详情给追踪中订阅的季写集播出日期(订阅日历数据源)。
|
|
|
|
const up = await pgAvailable();
|
|
|
|
describe.skipIf(!up)("订阅搜索关键词与集播出日期", () => {
|
|
let h: Harness;
|
|
let site: MockNexusPhpSite;
|
|
let tmdb: MockTmdb;
|
|
let siteId: string;
|
|
|
|
beforeAll(async () => {
|
|
site = await createMockNexusPhpSite();
|
|
tmdb = await createMockTmdb();
|
|
h = await createHarness("subsc1a", { tmdbUrl: tmdb.url });
|
|
const ok = await json(
|
|
await h.req("/sites", {
|
|
body: {
|
|
key: "audiences",
|
|
name: "观众",
|
|
adapter: "nexusphp",
|
|
baseUrl: `${site.baseUrl}/`,
|
|
rateLimit: { minIntervalMs: 0, concurrency: 4 },
|
|
},
|
|
}),
|
|
);
|
|
expect(ok.status).toBe(200);
|
|
siteId = ok.body.data.id;
|
|
await h.req("/site-accounts", { body: { siteId, label: "主号", authKind: "cookie", credential: site.cookie } });
|
|
});
|
|
afterAll(async () => {
|
|
await h?.close();
|
|
await site?.close();
|
|
await tmdb?.close();
|
|
});
|
|
|
|
it("params.keywords 追加在标题之后(去重,不占标题检索词上限),每个站点每个词搜一次", async () => {
|
|
const e = (
|
|
await h.db
|
|
.insertInto("media_entity")
|
|
.values({ kind: "movie", title: "Dune", original_title: "Dune", year: 2021, status: "active" })
|
|
.returning("id")
|
|
.executeTakeFirstOrThrow()
|
|
).id;
|
|
const sub = (
|
|
await h.db
|
|
.insertInto("subscription")
|
|
.values({
|
|
media_entity_id: e,
|
|
media_kind: "movie",
|
|
mode: "once",
|
|
params: JSON.stringify({ keywords: ["Dune Part One", "dune", " "] }),
|
|
})
|
|
.returning("id")
|
|
.executeTakeFirstOrThrow()
|
|
).id;
|
|
const r = await searchSubscription(h.rt, sub);
|
|
expect(r).toMatchObject({ status: "ok", keywords: ["Dune", "Dune Part One"], sites: [siteId], searches: 2 });
|
|
const snaps = await h.db
|
|
.selectFrom("site_snapshot")
|
|
.select("query")
|
|
.where("kind", "=", "live_search")
|
|
.where("query", "=", "Dune Part One")
|
|
.execute();
|
|
expect(snaps).toHaveLength(1);
|
|
});
|
|
|
|
it("元数据刷新:追踪中订阅的季按 TMDB 季详情建集单元并写播出日期;已有集单元只补日期;没有追踪订阅不拉季详情", async () => {
|
|
const e = (
|
|
await h.db
|
|
.insertInto("media_entity")
|
|
.values({ kind: "series", title: "The Bear", original_title: "The Bear", year: 2022, status: "active" })
|
|
.returning("id")
|
|
.executeTakeFirstOrThrow()
|
|
).id;
|
|
await h.db
|
|
.insertInto("external_identity")
|
|
.values({
|
|
media_entity_id: e,
|
|
provider: "tmdb",
|
|
entity_type: "tv",
|
|
external_id: "136315",
|
|
status: "accepted",
|
|
})
|
|
.execute();
|
|
expect(await trackedSeasonsForAirDates(h.db, e, [1, 2, 3])).toEqual([]);
|
|
const seasonRequests = () => tmdb.callsTo(/\/season\/\d+/).length;
|
|
const before = seasonRequests();
|
|
await refreshMetadata(h.rt, e);
|
|
expect(seasonRequests()).toBe(before);
|
|
|
|
const s2 = (
|
|
await h.db
|
|
.insertInto("content_unit")
|
|
.values({ media_entity_id: e, kind: "season", season_number: 2, status: "confirmed" })
|
|
.returning("id")
|
|
.executeTakeFirstOrThrow()
|
|
).id;
|
|
// 已有的 E01(发布懒建,没有日期)
|
|
const e1 = (
|
|
await h.db
|
|
.insertInto("content_unit")
|
|
.values({ media_entity_id: e, kind: "episode", season_number: 2, episode_number: 1, status: "provisional" })
|
|
.returning("id")
|
|
.executeTakeFirstOrThrow()
|
|
).id;
|
|
await h.db
|
|
.insertInto("subscription")
|
|
.values({
|
|
media_entity_id: e,
|
|
media_kind: "series",
|
|
mode: "continuous",
|
|
season_unit_id: s2,
|
|
season_scope: "following_seasons",
|
|
})
|
|
.execute();
|
|
expect(await trackedSeasonsForAirDates(h.db, e, [1, 2, 3])).toEqual([2, 3]);
|
|
const r = await refreshMetadata(h.rt, e);
|
|
expect(r.status).toBe("refreshed");
|
|
const eps = await h.db
|
|
.selectFrom("content_unit")
|
|
.select(["id", "season_number", "episode_number", "status", "title"])
|
|
.select((eb) => eb.fn<string>("to_char", [eb.ref("air_date"), eb.val("YYYY-MM-DD")]).as("air"))
|
|
.where("media_entity_id", "=", e)
|
|
.where("kind", "=", "episode")
|
|
.orderBy("season_number")
|
|
.orderBy("episode_number")
|
|
.execute();
|
|
expect(eps.filter((x) => x.season_number === 2)).toHaveLength(10);
|
|
expect(eps.filter((x) => x.season_number === 3)).toHaveLength(10);
|
|
expect(eps.filter((x) => x.season_number === 1)).toHaveLength(0);
|
|
const first = eps.find((x) => x.season_number === 2 && x.episode_number === 1)!;
|
|
// 已有的临时集单元:id 不变,补日期,对上 TMDB 季集结构后转正式(DH-1)
|
|
expect(first).toMatchObject({ id: e1, status: "confirmed", air: "2023-06-22" });
|
|
expect(eps.find((x) => x.season_number === 2 && x.episode_number === 2)).toMatchObject({
|
|
status: "confirmed",
|
|
air: "2023-06-29",
|
|
});
|
|
// 再刷新幂等:不重复建
|
|
await refreshMetadata(h.rt, e);
|
|
const n = await h.db
|
|
.selectFrom("content_unit")
|
|
.select((eb) => eb.fn.countAll<number>().as("n"))
|
|
.where("media_entity_id", "=", e)
|
|
.where("kind", "=", "episode")
|
|
.executeTakeFirstOrThrow();
|
|
expect(Number(n.n)).toBe(20);
|
|
});
|
|
});
|