fix(identity): 自动识别新建作品后排一次元数据刷新
bindAuto 经 createEntityFromTmdb 新建作品时,事务提交后入队 full refreshMetadata (幂等键 refreshMetadata:created:<作品>,与人工指定 / 整理接管同一做法)。新建档没有季表时 资源按原号放行(unverified),刷新补齐季表后重算为换算结果。 Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Cxwgnn2pbzQ7rXv3rq7fei
This commit is contained in:
1 parent
c16f8a51e9
commit
b02b4a500f
2 files changed
+117
-14
No files matched your search
@@ -35,6 +35,7 @@ import {
|
|||||||
looksLikeVariety,
|
looksLikeVariety,
|
||||||
resolveEntityId,
|
resolveEntityId,
|
||||||
} from "./entities.ts";
|
} from "./entities.ts";
|
||||||
|
import { enqueueNewEntityRefresh } from "./entity-from-candidate.ts";
|
||||||
import {
|
import {
|
||||||
canonicalParsed,
|
canonicalParsed,
|
||||||
clearReleaseEpisodes,
|
clearReleaseEpisodes,
|
||||||
@@ -714,8 +715,10 @@ async function bindAuto(
|
|||||||
const site = ((release.raw as Record<string, unknown>)?._site ?? {}) as { categoryName?: string };
|
const site = ((release.raw as Record<string, unknown>)?._site ?? {}) as { categoryName?: string };
|
||||||
const variety = looksLikeVariety(parsed, site.categoryName ?? release.category_raw);
|
const variety = looksLikeVariety(parsed, site.categoryName ?? release.category_raw);
|
||||||
for (let attempt = 0; ; attempt++) {
|
for (let attempt = 0; ; attempt++) {
|
||||||
|
// 本次新建的作品:事务提交后排一次 full 元数据刷新(季表、集单元),刷新完成会重算待确认 / 放行资源的集数
|
||||||
|
let created: string | null = null;
|
||||||
try {
|
try {
|
||||||
return await ctx.db.transaction().execute(async (trx) => {
|
const result = await ctx.db.transaction().execute(async (trx) => {
|
||||||
let entityId: string | null = null;
|
let entityId: string | null = null;
|
||||||
if (best.candidate.localEntityId) entityId = await resolveEntityId(trx, best.candidate.localEntityId);
|
if (best.candidate.localEntityId) entityId = await resolveEntityId(trx, best.candidate.localEntityId);
|
||||||
if (!entityId && best.candidate.provider === "tmdb") {
|
if (!entityId && best.candidate.provider === "tmdb") {
|
||||||
@@ -725,19 +728,19 @@ async function bindAuto(
|
|||||||
externalId: best.candidate.externalId,
|
externalId: best.candidate.externalId,
|
||||||
});
|
});
|
||||||
if (!entityId && tmdbDetail) {
|
if (!entityId && tmdbDetail) {
|
||||||
entityId = (
|
const r = await createEntityFromTmdb(trx, tmdbDetail, {
|
||||||
await createEntityFromTmdb(trx, tmdbDetail, {
|
level,
|
||||||
level,
|
evidence: {
|
||||||
evidence: {
|
sourceReleaseId: release.id,
|
||||||
sourceReleaseId: release.id,
|
rules: best.evidence,
|
||||||
rules: best.evidence,
|
titleMatch: best.titleMatch,
|
||||||
titleMatch: best.titleMatch,
|
yearMatch: best.yearMatch,
|
||||||
yearMatch: best.yearMatch,
|
},
|
||||||
},
|
actor: SYSTEM,
|
||||||
actor: SYSTEM,
|
variety,
|
||||||
variety,
|
});
|
||||||
})
|
entityId = r.entityId;
|
||||||
).entityId;
|
if (r.created) created = r.entityId;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!entityId)
|
if (!entityId)
|
||||||
@@ -848,6 +851,8 @@ async function bindAuto(
|
|||||||
reason: resolution.reason,
|
reason: resolution.reason,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
if (created) await enqueueNewEntityRefresh(ctx, created);
|
||||||
|
return result;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof IdentityConflictError && attempt < 2) continue;
|
if (e instanceof IdentityConflictError && attempt < 2) continue;
|
||||||
throw e;
|
throw e;
|
||||||
|
|||||||
@@ -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<string, unknown>;
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in new issue
Block a user