- 新增 isEntityWatched(service/watched.ts):有进行中订阅、用户搜索 / 手动识别 / 手动下载 / 人工确认身份、 正在下载或整理、已在媒体库的作品算关注中 - 自动新建作品后的 full 刷新只在关注范围内排(用户搜索 / 手动识别 / 接管照常);集数待确认事项只给关注中的作品开 - 进入关注范围(建订阅、用户搜索命中已有作品、开始下载)时对从没刷新过的作品补排刷新,刷新重算后再开事项 - 确定性的集数校验 / 换算对所有已绑定作品照做 Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Cxwgnn2pbzQ7rXv3rq7fei
151 lines
6.8 KiB
TypeScript
151 lines
6.8 KiB
TypeScript
import { createMockTmdb, type MockTmdb, TMDB_FIXTURES, type TmdbFixture } from "@umt/testkit";
|
|
import { sql } from "kysely";
|
|
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
|
import { refreshMetadata } from "../../src/modules/sources-identity/service/metadata.ts";
|
|
import { resolveRelease } from "../../src/modules/sources-identity/service/resolve.ts";
|
|
import { enqueueWatchedRefresh, isEntityWatched } from "../../src/modules/sources-identity/service/watched.ts";
|
|
import { createHarness, type Harness, pgAvailable, seedSite } from "./harness.ts";
|
|
|
|
// 集数识别分两层(2026-10-06):
|
|
// 确定性校验 / 换算对所有已绑定作品的资源照做;拉 TMDB(自动建档后的元数据刷新)、开集数待确认只对关注中的作品做。
|
|
// RSS 扫进来自动建档、没人关心的作品:不排刷新、不开事项;进入关注范围(建订阅等)补排刷新,刷新重算后才开内部事项。
|
|
|
|
const pg = await pgAvailable();
|
|
|
|
const season = (n: number, eps: number, start: string) => ({
|
|
season_number: n,
|
|
air_date: start,
|
|
names: { "zh-CN": `第 ${n} 季`, "en-US": `Season ${n}` },
|
|
episodes: Array.from({ length: eps }, (_, k) => ({
|
|
episode_number: k + 1,
|
|
air_date: new Date(Date.parse(start) + k * 7 * 86_400_000).toISOString().slice(0, 10),
|
|
names: { "zh-CN": `第 ${k + 1} 集`, "en-US": `Episode ${k + 1}` },
|
|
})),
|
|
});
|
|
|
|
// 已完结:S1 = 12、S2 = 13;绝对集号 40 超出全部季,刷新后换算不了 → 集数待确认
|
|
const SHOW: TmdbFixture = {
|
|
type: "tv",
|
|
id: 990301,
|
|
original_title: "Yoru no Kurage wa Oyogenai",
|
|
original_language: "ja",
|
|
origin_country: ["JP"],
|
|
date: "2024-04-06",
|
|
titles: { "zh-CN": "夜晚的水母不会游泳", "en-US": "Yoru no Kurage wa Oyogenai" },
|
|
popularity: 30,
|
|
alternative_titles: [],
|
|
external_ids: {},
|
|
rich: { status: "Ended" },
|
|
seasons: [season(1, 12, "2024-04-06"), season(2, 13, "2025-04-05")],
|
|
};
|
|
|
|
describe.skipIf(!pg)("集数识别两层:只对关注中的作品拉元数据、开集数待确认", () => {
|
|
let h: Harness;
|
|
let tmdb: MockTmdb;
|
|
let siteId: string;
|
|
let n = 0;
|
|
|
|
beforeAll(async () => {
|
|
tmdb = await createMockTmdb({ fixtures: [...TMDB_FIXTURES, SHOW] });
|
|
h = await createHarness("episode_watch_scope", { tmdbUrl: tmdb.url });
|
|
siteId = (await seedSite(h, { baseUrl: "https://acgrip.example/" })).site.id;
|
|
// 不靠订阅也查 TMDB(旧行为):RSS 扫进来的资源会自动建档,但作品不在关注范围
|
|
await sql`insert into settings (key, value) values ('identity', ${JSON.stringify({ eagerTmdbLookup: true })}::jsonb)
|
|
on conflict (key) do update set value = excluded.value`.execute(h.db);
|
|
});
|
|
afterAll(async () => {
|
|
await h?.close();
|
|
await tmdb?.close();
|
|
});
|
|
|
|
const observe = async (title: string) => {
|
|
const r = await h.db
|
|
.insertInto("source_release")
|
|
.values({ site_id: siteId, site_release_id: String(830000 + ++n), title, subtitle: "夜晚的水母不会游泳" })
|
|
.returningAll()
|
|
.executeTakeFirstOrThrow();
|
|
const res = await resolveRelease(h.rt, { sourceReleaseId: r.id, trigger: "observed" });
|
|
expect(res.outcome, JSON.stringify(res)).toBe("auto");
|
|
return { id: r.id, entityId: res.mediaEntityId as string };
|
|
};
|
|
const method = async (id: string) =>
|
|
(
|
|
(
|
|
await h.db
|
|
.selectFrom("source_release")
|
|
.select("episode_resolution")
|
|
.where("id", "=", id)
|
|
.executeTakeFirstOrThrow()
|
|
).episode_resolution as { method: string }
|
|
).method;
|
|
const refreshJobs = () => h.engine.enqueued.filter((e) => e.task === "refreshMetadata");
|
|
const episodeDecisions = () =>
|
|
h.db
|
|
.selectFrom("decision")
|
|
.selectAll()
|
|
.where("kind", "=", "episode_mapping")
|
|
.where("status", "in", ["open", "investigating", "awaiting_user"])
|
|
.execute();
|
|
|
|
let entityId: string;
|
|
let r40: string;
|
|
|
|
it("RSS 扫进来自动建档:确定性换算照做,但不排元数据刷新、不开集数待确认", async () => {
|
|
const a = await observe("[ANi] Yoru no Kurage wa Oyogenai - 03 [1080P][Baha][WEB-DL][AAC AVC][CHT][MP4]");
|
|
entityId = a.entityId;
|
|
expect(await method(a.id)).toBe("unverified");
|
|
expect(await isEntityWatched(h.db, entityId)).toBe(false);
|
|
const b = await observe("[Lilith-Raws] Yoru no Kurage wa Oyogenai - 40 [Baha][WEB-DL][1080p][AVC AAC][CHT][MP4]");
|
|
r40 = b.id;
|
|
expect(b.entityId).toBe(entityId);
|
|
expect(refreshJobs()).toEqual([]);
|
|
expect(await episodeDecisions()).toEqual([]);
|
|
// 季详情一次都没拉(识别只用搜索与详情)
|
|
expect(tmdb.callsTo(/\/season\//)).toHaveLength(0);
|
|
});
|
|
|
|
it("即使季表已知、资源换算不了,非关注作品也不开事项", async () => {
|
|
// 直接刷新一次(模拟别处拉过季表),非关注作品的待确认资源照样只记在资源上
|
|
expect((await refreshMetadata(h.rt, entityId, { full: true })).status).toBe("refreshed");
|
|
expect(await method(r40)).toBe("unresolved");
|
|
expect(await episodeDecisions()).toEqual([]);
|
|
await h.db.updateTable("media_entity").set({ last_metadata_refresh_at: null }).where("id", "=", entityId).execute();
|
|
});
|
|
|
|
it("进入关注范围(建订阅):补排一次 full 刷新;刷新重算后对换算不了的资源开内部事项", async () => {
|
|
await h.db
|
|
.insertInto("subscription")
|
|
.values({ media_entity_id: entityId, media_kind: "series", status: "active" })
|
|
.execute();
|
|
expect(await isEntityWatched(h.db, entityId)).toBe(true);
|
|
expect(await enqueueWatchedRefresh(h.ctx, entityId, "subscription")).toBe(true);
|
|
expect(refreshJobs()).toEqual([
|
|
{
|
|
task: "refreshMetadata",
|
|
input: { mediaEntityId: entityId, full: true },
|
|
key: `refreshMetadata:watched:${entityId}`,
|
|
},
|
|
]);
|
|
// 执行入队的刷新(同 refreshMetadata 任务处理器)
|
|
expect((await refreshMetadata(h.rt, entityId, { full: true })).status).toBe("refreshed");
|
|
const ds = await episodeDecisions();
|
|
expect(ds).toHaveLength(1);
|
|
expect((ds[0]!.evidence as { sourceReleaseIds: string[] }).sourceReleaseIds).toEqual([r40]);
|
|
// 已刷新过的不再补排
|
|
expect(await enqueueWatchedRefresh(h.ctx, entityId, "download")).toBe(false);
|
|
});
|
|
|
|
it("用户搜索 / 手动识别出的新作品照常排刷新", async () => {
|
|
const r = await h.db
|
|
.insertInto("source_release")
|
|
.values({ site_id: siteId, site_release_id: String(830000 + ++n), title: "Oppenheimer.2023.2160p.UHD.BluRay" })
|
|
.returningAll()
|
|
.executeTakeFirstOrThrow();
|
|
const before = refreshJobs().length;
|
|
const res = await resolveRelease(h.rt, { sourceReleaseId: r.id, trigger: "search" });
|
|
expect(res.outcome, JSON.stringify(res)).toBe("auto");
|
|
expect(await isEntityWatched(h.db, res.mediaEntityId as string)).toBe(true);
|
|
expect(refreshJobs().length).toBe(before + 1);
|
|
});
|
|
});
|