fix(acquisition): 集数待确认的资源在订阅侧写候选记录
评估跳过 episode_unresolved 时,给仍在追踪的收录版本各写一条 skipped 候选评估 (原因 episode_unresolved),作品「为什么选 / 不选」与订阅记录能看到"集数待确认"。 确认后重发评估按 (收录版本, 资源) 覆盖;换算到别的季不再进入原收录版本时, 先清掉旧的待确认记录,不留陈旧原因。 Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Cxwgnn2pbzQ7rXv3rq7fei
This commit is contained in:
1 parent
00045a24cb
commit
4d24edd94c
2 files changed
+82
-1
No files matched your search
@@ -34,6 +34,7 @@ import { hashJson, jsonb, startOfDay, toApi } from "../util.ts";
|
|||||||
import {
|
import {
|
||||||
type AoDeps,
|
type AoDeps,
|
||||||
automationBlock,
|
automationBlock,
|
||||||
|
CANDIDATE_ARTIFACT_TITLE,
|
||||||
downloaderPathOf,
|
downloaderPathOf,
|
||||||
type Exec,
|
type Exec,
|
||||||
enqueueTracked,
|
enqueueTracked,
|
||||||
@@ -364,7 +365,13 @@ async function evaluateRelease(c: Ctx, rel: SourceReleaseRow, eventUnits: readon
|
|||||||
if (rel.availability !== "active") return skip(`release_${rel.availability}`);
|
if (rel.availability !== "active") return skip(`release_${rel.availability}`);
|
||||||
const entity = await resolveEntity(db, rel.media_entity_id);
|
const entity = await resolveEntity(db, rel.media_entity_id);
|
||||||
if (!entity || entity.status === "archived") return skip("entity_unavailable");
|
if (!entity || entity.status === "archived") return skip("entity_unavailable");
|
||||||
if (entity.kind !== "movie" && episodeUnresolved(rel)) return skip("episode_unresolved");
|
if (entity.kind !== "movie" && episodeUnresolved(rel)) {
|
||||||
|
// 订阅侧可见:相关收录版本各记一条候选评估;确认后重发 release.resolved 正常评估时按 (收录版本, 资源) 覆盖
|
||||||
|
await noteEpisodeUnresolved(c, rel, entity.id, onlySlotId);
|
||||||
|
return skip("episode_unresolved");
|
||||||
|
}
|
||||||
|
// 已确认的资源:先清掉之前的"集数待确认"记录(换算到别的季时下面可能不再进入该收录版本,不会被覆盖)
|
||||||
|
if (entity.kind !== "movie" && episodeResolutionOf(rel)) await clearEpisodeUnresolvedNotes(c, rel.id, entity.id);
|
||||||
const parsed = parsedOf(rel);
|
const parsed = parsedOf(rel);
|
||||||
const units = await releaseUnits(
|
const units = await releaseUnits(
|
||||||
db,
|
db,
|
||||||
@@ -523,6 +530,37 @@ async function evaluateRelease(c: Ctx, rel: SourceReleaseRow, eventUnits: readon
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 集数待确认:给仍在追踪的收录版本各写一条 skipped 候选评估(原因 episode_unresolved) */
|
||||||
|
async function noteEpisodeUnresolved(c: Ctx, rel: SourceReleaseRow, entityId: string, onlySlotId?: string) {
|
||||||
|
let q = c.d.ctx.db
|
||||||
|
.selectFrom("acquisition_slot as s")
|
||||||
|
.leftJoin("subscription as sub", "sub.id", "s.subscription_id")
|
||||||
|
.select("s.id")
|
||||||
|
.where("s.media_entity_id", "=", entityId)
|
||||||
|
.where((eb) => eb.or([eb("s.subscription_id", "is", null), eb("sub.status", "in", [...TRACKING_SUB_STATUSES])]));
|
||||||
|
if (onlySlotId) q = q.where("s.id", "=", onlySlotId);
|
||||||
|
for (const s of await q.execute())
|
||||||
|
await note(c, {
|
||||||
|
slotId: s.id,
|
||||||
|
sourceReleaseId: rel.id,
|
||||||
|
title: rel.title,
|
||||||
|
planId: null,
|
||||||
|
decision: "skipped",
|
||||||
|
mode: null,
|
||||||
|
rejected: [{ code: "episode_unresolved", message: "集数待确认,确认后自动重新评估" }],
|
||||||
|
reasons: ["episode_unresolved"],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function clearEpisodeUnresolvedNotes(c: Ctx, sourceReleaseId: string, entityId: string) {
|
||||||
|
await sql`
|
||||||
|
delete from artifact
|
||||||
|
where title = ${CANDIDATE_ARTIFACT_TITLE} and subject_type = 'acquisition_slot'
|
||||||
|
and subject_id in (select id from acquisition_slot where media_entity_id = ${entityId})
|
||||||
|
and content->>'sourceReleaseId' = ${sourceReleaseId}
|
||||||
|
and content->'reasons' ? 'episode_unresolved'`.execute(c.d.ctx.db);
|
||||||
|
}
|
||||||
|
|
||||||
/** 站点字段 → 评估上下文(做种数、促销下载系数、副标题/标签、站点 id、发布后分钟数) */
|
/** 站点字段 → 评估上下文(做种数、促销下载系数、副标题/标签、站点 id、发布后分钟数) */
|
||||||
export function contextOf(rel: SourceReleaseRow, now: Date = new Date()): EvaluationContext {
|
export function contextOf(rel: SourceReleaseRow, now: Date = new Date()): EvaluationContext {
|
||||||
const promo = (rel.promotion ?? {}) as { kind?: string; raw?: { downloadFactor?: number | null } };
|
const promo = (rel.promotion ?? {}) as { kind?: string; raw?: { downloadFactor?: number | null } };
|
||||||
|
|||||||
@@ -156,4 +156,47 @@ describe.skipIf(!hasPg)("订阅评估跳过集数待确认的资源", () => {
|
|||||||
expect(ev.plans).toHaveLength(0);
|
expect(ev.plans).toHaveLength(0);
|
||||||
expect(ev.skipped).toEqual([{ sourceReleaseId: id, reason: "episode_unresolved" }]);
|
expect(ev.skipped).toEqual([{ sourceReleaseId: id, reason: "episode_unresolved" }]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("集数待确认在订阅侧可见:写候选记录 episode_unresolved;确认后正常评估并覆盖", async () => {
|
||||||
|
const slot = await h.db
|
||||||
|
.selectFrom("acquisition_slot")
|
||||||
|
.select("id")
|
||||||
|
.where("media_entity_id", "=", entityId)
|
||||||
|
.executeTakeFirstOrThrow();
|
||||||
|
const candidate = async (releaseId: string) =>
|
||||||
|
(
|
||||||
|
await h.db
|
||||||
|
.selectFrom("artifact")
|
||||||
|
.select("content")
|
||||||
|
.where("title", "=", "candidate_evaluation")
|
||||||
|
.where("subject_id", "=", slot.id)
|
||||||
|
.execute()
|
||||||
|
)
|
||||||
|
.map(
|
||||||
|
(r) => r.content as { sourceReleaseId: string; decision: string; reasons: string[]; planId: string | null },
|
||||||
|
)
|
||||||
|
.find((c) => c.sourceReleaseId === releaseId);
|
||||||
|
const id = await seed(LILITH_40);
|
||||||
|
await evaluateAcquisition(h.d, { sourceReleaseId: id });
|
||||||
|
expect(await candidate(id)).toMatchObject({ decision: "skipped", reasons: ["episode_unresolved"] });
|
||||||
|
|
||||||
|
// 人工确认为 S02E12(与 `- 24` 的换算结果相同),重发评估:正常建计划,记录被覆盖
|
||||||
|
const fixed = resolved("[Lilith-Raws] Kusuriya no Hitorigoto - 24 [Baha][WEB-DL][1080p][AVC AAC][CHT][MP4]");
|
||||||
|
const cols = toParsedColumns(fixed.parsed);
|
||||||
|
await h.db
|
||||||
|
.updateTable("source_release")
|
||||||
|
.set({
|
||||||
|
parsed: JSON.stringify(cols.parsed),
|
||||||
|
parser_version: cols.parserVersion,
|
||||||
|
episode_resolution: JSON.stringify(fixed.resolution),
|
||||||
|
})
|
||||||
|
.where("id", "=", id)
|
||||||
|
.execute();
|
||||||
|
const ev = await evaluateAcquisition(h.d, { sourceReleaseId: id });
|
||||||
|
expect(ev.plans, JSON.stringify(ev)).toHaveLength(1);
|
||||||
|
const c = await candidate(id);
|
||||||
|
expect(c?.decision).toBe("accept");
|
||||||
|
expect(c?.planId).toBe(ev.plans[0]);
|
||||||
|
expect(c?.reasons).not.toContain("episode_unresolved");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
Reference in new issue
Block a user