From b02b4a500f48c6c25dd2d8d88197c10bf1bd06a5 Mon Sep 17 00:00:00 2001 From: root Date: Sat, 3 Oct 2026 20:38:50 +0800 Subject: [PATCH] =?UTF-8?q?fix(identity):=20=E8=87=AA=E5=8A=A8=E8=AF=86?= =?UTF-8?q?=E5=88=AB=E6=96=B0=E5=BB=BA=E4=BD=9C=E5=93=81=E5=90=8E=E6=8E=92?= =?UTF-8?q?=E4=B8=80=E6=AC=A1=E5=85=83=E6=95=B0=E6=8D=AE=E5=88=B7=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit bindAuto 经 createEntityFromTmdb 新建作品时,事务提交后入队 full refreshMetadata (幂等键 refreshMetadata:created:<作品>,与人工指定 / 整理接管同一做法)。新建档没有季表时 资源按原号放行(unverified),刷新补齐季表后重算为换算结果。 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Cxwgnn2pbzQ7rXv3rq7fei --- .../sources-identity/service/resolve.ts | 33 ++++--- .../auto-created-entity-refresh.test.ts | 98 +++++++++++++++++++ 2 files changed, 117 insertions(+), 14 deletions(-) create mode 100644 apps/server/test/sources-identity/auto-created-entity-refresh.test.ts diff --git a/apps/server/src/modules/sources-identity/service/resolve.ts b/apps/server/src/modules/sources-identity/service/resolve.ts index 1ad8dab2..98b18813 100644 --- a/apps/server/src/modules/sources-identity/service/resolve.ts +++ b/apps/server/src/modules/sources-identity/service/resolve.ts @@ -35,6 +35,7 @@ import { looksLikeVariety, resolveEntityId, } from "./entities.ts"; +import { enqueueNewEntityRefresh } from "./entity-from-candidate.ts"; import { canonicalParsed, clearReleaseEpisodes, @@ -714,8 +715,10 @@ async function bindAuto( const site = ((release.raw as Record)?._site ?? {}) as { categoryName?: string }; const variety = looksLikeVariety(parsed, site.categoryName ?? release.category_raw); for (let attempt = 0; ; attempt++) { + // 本次新建的作品:事务提交后排一次 full 元数据刷新(季表、集单元),刷新完成会重算待确认 / 放行资源的集数 + let created: string | null = null; try { - return await ctx.db.transaction().execute(async (trx) => { + const result = await ctx.db.transaction().execute(async (trx) => { let entityId: string | null = null; if (best.candidate.localEntityId) entityId = await resolveEntityId(trx, best.candidate.localEntityId); if (!entityId && best.candidate.provider === "tmdb") { @@ -725,19 +728,19 @@ async function bindAuto( externalId: best.candidate.externalId, }); if (!entityId && tmdbDetail) { - entityId = ( - await createEntityFromTmdb(trx, tmdbDetail, { - level, - evidence: { - sourceReleaseId: release.id, - rules: best.evidence, - titleMatch: best.titleMatch, - yearMatch: best.yearMatch, - }, - actor: SYSTEM, - variety, - }) - ).entityId; + const r = await createEntityFromTmdb(trx, tmdbDetail, { + level, + evidence: { + sourceReleaseId: release.id, + rules: best.evidence, + titleMatch: best.titleMatch, + yearMatch: best.yearMatch, + }, + actor: SYSTEM, + variety, + }); + entityId = r.entityId; + if (r.created) created = r.entityId; } } if (!entityId) @@ -848,6 +851,8 @@ async function bindAuto( reason: resolution.reason, }; }); + if (created) await enqueueNewEntityRefresh(ctx, created); + return result; } catch (e) { if (e instanceof IdentityConflictError && attempt < 2) continue; throw e; diff --git a/apps/server/test/sources-identity/auto-created-entity-refresh.test.ts b/apps/server/test/sources-identity/auto-created-entity-refresh.test.ts new file mode 100644 index 00000000..ebf00fff --- /dev/null +++ b/apps/server/test/sources-identity/auto-created-entity-refresh.test.ts @@ -0,0 +1,98 @@ +import { createMockTmdb, type MockTmdb, TMDB_FIXTURES, type TmdbFixture } from "@umt/testkit"; +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 { createHarness, type Harness, pgAvailable, seedSite } from "./harness.ts"; + +// 自动识别新建作品后排一次 full 元数据刷新:新建档没有季表,绝对集号资源按原号滞后放行(unverified); +// 刷新补齐季表后重算为换算结果。已有作品的再次识别不重复排刷新。 + +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(ANi 连续编号 25 → S02E13) +const SHOW: TmdbFixture = { + type: "tv", + id: 990101, + original_title: "Hoshikuzu Telepath", + original_language: "ja", + origin_country: ["JP"], + date: "2023-10-06", + titles: { "zh-CN": "星屑心灵感应", "en-US": "Hoshikuzu Telepath" }, + popularity: 30, + alternative_titles: [], + external_ids: {}, + rich: { status: "Ended" }, + seasons: [season(1, 12, "2023-10-06"), season(2, 13, "2025-01-10")], +}; + +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("auto_created_entity_refresh", { tmdbUrl: tmdb.url }); + siteId = (await seedSite(h, { baseUrl: "https://acgrip.example/" })).site.id; + }); + afterAll(async () => { + await h?.close(); + await tmdb?.close(); + }); + + const resolve = async (title: string) => { + const r = await h.db + .insertInto("source_release") + .values({ site_id: siteId, site_release_id: String(810000 + ++n), title, subtitle: "星屑心灵感应" }) + .returningAll() + .executeTakeFirstOrThrow(); + const res = await resolveRelease(h.rt, { sourceReleaseId: r.id, trigger: "manual" }); + expect(res.outcome, JSON.stringify(res)).toBe("auto"); + return { id: r.id, entityId: res.mediaEntityId as string }; + }; + const resolution = async (id: string) => + ( + await h.db + .selectFrom("source_release") + .select("episode_resolution") + .where("id", "=", id) + .executeTakeFirstOrThrow() + ).episode_resolution as Record; + const refreshJobs = () => h.engine.enqueued.filter((e) => e.task === "refreshMetadata"); + + it("新建作品后入队一次 full 刷新;刷新后 unverified 的 `- 25` 重算为 S02E13;再次识别不重复入队", async () => { + const first = await resolve("[ANi] Hoshikuzu Telepath - 25 [1080P][Baha][WEB-DL][AAC AVC][CHT][MP4]"); + expect(await resolution(first.id)).toMatchObject({ method: "unverified" }); + expect(refreshJobs()).toEqual([ + { + task: "refreshMetadata", + input: { mediaEntityId: first.entityId, full: true }, + key: `refreshMetadata:created:${first.entityId}`, + }, + ]); + + // 执行入队的刷新任务(同 refreshMetadata 任务处理器的调用) + const r = await refreshMetadata(h.rt, first.entityId, { full: true }); + expect(r.status).toBe("refreshed"); + expect(await resolution(first.id)).toMatchObject({ + method: "absolute_cross_season", + canonical: { season: 2, episodes: { start: 13, end: 13 } }, + }); + + const second = await resolve("[ANi] Hoshikuzu Telepath - 24 [1080P][Baha][WEB-DL][AAC AVC][CHT][MP4]"); + expect(second.entityId).toBe(first.entityId); + expect(refreshJobs()).toHaveLength(1); + }); +});