merge: main(v0.3.43)进 identify-words
Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Cxwgnn2pbzQ7rXv3rq7fei
This commit is contained in:
commit
edbfdde29f
15 files changed
+98
-50
No files matched your search
@@ -219,6 +219,18 @@ export async function ensureUnits(
|
||||
parsed: ParsedRelease | null,
|
||||
): Promise<{ unitIds: string[]; primary: string | null }> {
|
||||
if (!parsed || kind === "movie" || parsed.season === undefined) return { unitIds: [], primary: null };
|
||||
// 作品已有元数据季集结构(TMDB 默认季或剧集组建的正式季)时,只挂结构里的季:资源写的季在结构外
|
||||
// (发布者把第二期写成 S02、TMDB 却连续编在第 1 季)就不建单元,等规则换算或停在待确认,不凭标题造季
|
||||
const structured = await exec
|
||||
.selectFrom("content_unit")
|
||||
.select(["season_number"])
|
||||
.where("media_entity_id", "=", entityId)
|
||||
.where("kind", "=", "season")
|
||||
.where("status", "<>", "retired")
|
||||
.where((eb) => eb.or([eb("status", "=", "confirmed"), eb("source", "in", ["tmdb", "user"])]))
|
||||
.execute();
|
||||
const known = new Set(structured.map((s) => s.season_number));
|
||||
if (known.size && !known.has(parsed.season)) return { unitIds: [], primary: null };
|
||||
// 季范围(S01-S03 季包)只给第一季找或建单元,后面的季只挂已有的(元数据建的或单季资源建的):
|
||||
// 发布者把「全 24 集」写成 S01-S24 时,不再凭空建出 23 个空季(药屋少女的呢喃 S04–S24 即此)
|
||||
const seasonIds: string[] = [await findOrCreateUnit(exec, entityId, "season", parsed.season, null, null)];
|
||||
@@ -232,6 +244,13 @@ export async function ensureUnits(
|
||||
.where("status", "<>", "retired")
|
||||
.where("season_number", ">", parsed.season)
|
||||
.where("season_number", "<=", last)
|
||||
.$if(known.size > 0, (q) =>
|
||||
q.where(
|
||||
"season_number",
|
||||
"in",
|
||||
[...known].filter((n): n is number => n !== null),
|
||||
),
|
||||
)
|
||||
.orderBy("season_number")
|
||||
.orderBy("created_at")
|
||||
.execute();
|
||||
|
||||
@@ -379,9 +379,10 @@ async function writeOrigin(exec: Exec, unitId: string, origin: Coord, now: Date)
|
||||
}
|
||||
|
||||
/**
|
||||
* 退役空的临时季:不在 TMDB 当前结构里、下面没有未退役的集、没被资源 / 文件 / 收录版本引用的临时季单元。
|
||||
* 来源是旧数据(资源把「全 24 集」写成 S01-S24 时按季范围建出的空季)和切换季集结构后留下的孤儿季;刷新元数据时顺带清掉。
|
||||
* 返回退役的季号。
|
||||
* 收掉结构外的临时季:不在 TMDB 当前结构(默认季或剧集组)里的临时季单元及其临时集单元。
|
||||
* 来源:资源按标题懒建的季(发布者把第二期写成 S02、TMDB 连续编在第 1 季)、季范围误建的空季、切换结构后留下的孤儿季。
|
||||
* 资源上指向这些单元的 content_unit_id 解开(资源仍挂在作品上,集数按规则换算或停在待确认);
|
||||
* 有文件覆盖或收录版本引用的单元留着不动(那是已经落盘的东西,要人看)。返回退役的季号。
|
||||
*/
|
||||
export async function retireOrphanSeasons(
|
||||
exec: Exec,
|
||||
@@ -389,42 +390,49 @@ export async function retireOrphanSeasons(
|
||||
keepSeasons: ReadonlySet<number>,
|
||||
now: Date,
|
||||
): Promise<number[]> {
|
||||
const rows = await exec
|
||||
.selectFrom("content_unit as s")
|
||||
.select(["s.id", "s.season_number"])
|
||||
.where("s.media_entity_id", "=", entityId)
|
||||
.where("s.kind", "=", "season")
|
||||
.where("s.status", "=", "provisional")
|
||||
.where("s.season_number", "is not", null)
|
||||
.where(({ not, exists, selectFrom }) =>
|
||||
not(
|
||||
exists(
|
||||
selectFrom("content_unit as c")
|
||||
.select("c.id")
|
||||
.whereRef("c.media_entity_id", "=", "s.media_entity_id")
|
||||
.where("c.kind", "=", "episode")
|
||||
.where("c.status", "<>", "retired")
|
||||
.whereRef("c.season_number", "=", "s.season_number"),
|
||||
),
|
||||
),
|
||||
)
|
||||
const units = await exec
|
||||
.selectFrom("content_unit")
|
||||
.select(["id", "kind", "season_number", "status"])
|
||||
.where("media_entity_id", "=", entityId)
|
||||
.where("status", "<>", "retired")
|
||||
.where("season_number", "is not", null)
|
||||
.execute();
|
||||
const candidates = rows.filter((r) => !keepSeasons.has(r.season_number!));
|
||||
if (!candidates.length) return [];
|
||||
const refs = await referencedUnits(
|
||||
exec,
|
||||
candidates.map((r) => r.id),
|
||||
const orphanSeasons = new Set(
|
||||
units
|
||||
// 季 0(特别篇)不收:剧集组常常不含特别篇,切到组时它只是暂时不在结构里
|
||||
.filter(
|
||||
(u) =>
|
||||
u.kind === "season" &&
|
||||
u.status === "provisional" &&
|
||||
u.season_number! > 0 &&
|
||||
!keepSeasons.has(u.season_number!),
|
||||
)
|
||||
.map((u) => u.season_number!),
|
||||
);
|
||||
const retire = candidates.filter((r) => !refs.has(r.id));
|
||||
if (!orphanSeasons.size) return [];
|
||||
// 孤儿季里还有正式集(不该发生)就整季不动
|
||||
for (const u of units)
|
||||
if (u.kind === "episode" && u.status !== "provisional" && orphanSeasons.has(u.season_number!))
|
||||
orphanSeasons.delete(u.season_number!);
|
||||
const ids = units.filter((u) => orphanSeasons.has(u.season_number!)).map((u) => u.id);
|
||||
if (!ids.length) return [];
|
||||
const pinned = await sql<{ id: string; season: number }>`
|
||||
select u.id, u.season_number as season from content_unit u where u.id = any(${ids}::uuid[]) and (
|
||||
exists (select 1 from asset_unit_coverage c where c.content_unit_id = u.id)
|
||||
or exists (select 1 from slot_unit s where s.content_unit_id = u.id)
|
||||
or exists (select 1 from acquisition_slot a where a.content_unit_id = u.id))`.execute(exec);
|
||||
for (const p of pinned.rows) orphanSeasons.delete(p.season);
|
||||
const retire = units.filter((u) => orphanSeasons.has(u.season_number!)).map((u) => u.id);
|
||||
if (!retire.length) return [];
|
||||
await exec
|
||||
.updateTable("source_release")
|
||||
.set({ content_unit_id: null })
|
||||
.where("content_unit_id", "in", retire)
|
||||
.execute();
|
||||
await exec
|
||||
.updateTable("content_unit")
|
||||
.set({ status: "retired", updated_at: now })
|
||||
.where(
|
||||
"id",
|
||||
"in",
|
||||
retire.map((r) => r.id),
|
||||
)
|
||||
.where("id", "in", retire)
|
||||
.execute();
|
||||
return retire.map((r) => r.season_number!).sort((a, b) => a - b);
|
||||
return [...orphanSeasons].sort((a, b) => a - b);
|
||||
}
|
||||
Reference in new issue
Block a user