feat(scrape): 多集文件写多个 episodedetails 块,综艺日期期数按播出日期对集
- 多集文件(文件名 E01-E02 或覆盖多个集单元)按 Kodi 约定在一个 NFO 里每集写一块,MP 只写首集,偏离登记在 nfo-deviations.json; - 综艺按日期命名的期数按本地季表或 TMDB 季详情的播出日期对上集,写季号、集号与 aired,不再 scrape_no_episode 跳过; - 认不出集号、集数待确认的跳过让记录记部分完成(已存在 / 没变 / 被人改过仍算完成)。 Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Cxwgnn2pbzQ7rXv3rq7fei
This commit is contained in:
1 parent
0085e45ca7
commit
8c4c7d028a
8 files changed
+513
-61
No files matched your search
@@ -65,5 +65,11 @@
|
||||
"ours": "<视频名>-thumb.jpg",
|
||||
"reason": "Kodi 约定 -thumb 后缀;Jellyfin / Emby 同样识别,且不会和同名海报规则冲突"
|
||||
}
|
||||
]
|
||||
],
|
||||
"multiEpisode": {
|
||||
"case": "s01e01-e02(一个文件覆盖第 1、2 集)",
|
||||
"mp": "只按首集(begin_episode)写一个 <episodedetails>,与 s01e01 相同",
|
||||
"ours": "按 Kodi 多集约定写两个 <episodedetails> 块:第一块同 s01e01,第二块同 s01e02(各自的已登记差异照旧)",
|
||||
"reason": "只写首集时 Kodi 只显示第一集,第二集在媒体库里缺失;Jellyfin / Emby 也读多块 NFO"
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { fetchRichDetail, fetchRichSeason } from "@umt/adapters";
|
||||
import { episodeNfo, planScrapeTargets, seasonNfo, seasonPosterName, workNfo } from "@umt/domain";
|
||||
import { episodeNfo, multiEpisodeNfo, planScrapeTargets, seasonNfo, seasonPosterName, workNfo } from "@umt/domain";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { nfoEpisode, nfoSeason, nfoWork } from "../../src/modules/acquisition-organize/services/scrape-input.ts";
|
||||
import { richEntityFields } from "../../src/modules/sources-identity/service/metadata-rich.ts";
|
||||
@@ -12,7 +12,8 @@ import { richEntityFields } from "../../src/modules/sources-identity/service/met
|
||||
const read = (f: string) => JSON.parse(readFileSync(new URL(f, import.meta.url), "utf8"));
|
||||
const raw = read("./tmdb-raw.json") as Record<string, unknown>;
|
||||
const mp = read("./scrape-mp.json") as Record<string, string>;
|
||||
const devs = read("./nfo-deviations.json").deviations as { case: string; tag: string }[];
|
||||
const devFile = read("./nfo-deviations.json");
|
||||
const devs = devFile.deviations as { case: string; tag: string }[];
|
||||
|
||||
const LANGS = ["zh-CN", "zh-SG", "zh-TW", "zh-HK", "en-US"];
|
||||
const client = {
|
||||
@@ -63,23 +64,23 @@ async function ours(): Promise<Record<string, string>> {
|
||||
metadata: { name: { value: sd.name }, overview: { value: sd.overview }, airDate: { value: sd.airDate } },
|
||||
}),
|
||||
);
|
||||
for (const e of sd.episodes) {
|
||||
out[`s01e${String(e.episodeNumber).padStart(2, "0")}`] = episodeNfo(
|
||||
nfoEpisode(1, e.episodeNumber, {
|
||||
season_number: 1,
|
||||
episode_number: e.episodeNumber,
|
||||
title: e.name,
|
||||
air_date: e.airDate,
|
||||
metadata: {
|
||||
name: { value: e.name },
|
||||
overview: { value: e.overview },
|
||||
airDate: { value: e.airDate },
|
||||
runtime: { value: e.runtime },
|
||||
voteAverage: { value: e.voteAverage },
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
const eps = sd.episodes.map((e) =>
|
||||
nfoEpisode(1, e.episodeNumber, {
|
||||
season_number: 1,
|
||||
episode_number: e.episodeNumber,
|
||||
title: e.name,
|
||||
air_date: e.airDate,
|
||||
metadata: {
|
||||
name: { value: e.name },
|
||||
overview: { value: e.overview },
|
||||
airDate: { value: e.airDate },
|
||||
runtime: { value: e.runtime },
|
||||
voteAverage: { value: e.voteAverage },
|
||||
},
|
||||
}),
|
||||
);
|
||||
for (const e of eps) out[`s01e${String(e.episode).padStart(2, "0")}`] = episodeNfo(e);
|
||||
out["s01e01-e02"] = multiEpisodeNfo(eps.slice(0, 2));
|
||||
return out;
|
||||
}
|
||||
|
||||
@@ -109,6 +110,24 @@ describe("NFO 与 MP 差分", async () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("多集文件:按登记的偏离写多个 <episodedetails> 块,每块与 MP 对应单集一致(MP 只写首集)", () => {
|
||||
expect(devFile.multiEpisode?.reason).toBeTruthy();
|
||||
const xml = mine["s01e01-e02"]!;
|
||||
const [decl, ...body] = xml.split("\n");
|
||||
expect(decl).toBe(mp.s01e01!.split("\n")[0]);
|
||||
const blocks = body
|
||||
.join("\n")
|
||||
.split(/(?=<episodedetails>)/)
|
||||
.filter((b) => b.trim());
|
||||
expect(blocks).toHaveLength(2);
|
||||
for (const [i, c] of ["s01e01", "s01e02"].entries()) {
|
||||
const skip = new Set(devs.filter((d) => d.case === c).map((d) => d.tag));
|
||||
const a = normalize(mp[c]!).filter((l) => !skip.has(tagOf(l) ?? "") && !l.startsWith("<?xml"));
|
||||
const b = normalize(blocks[i]!).filter((l) => !skip.has(tagOf(l) ?? ""));
|
||||
expect(b).toEqual(a);
|
||||
}
|
||||
});
|
||||
|
||||
it("编码与声明:UTF-8、两空格缩进、末尾换行,和 MP 一致", () => {
|
||||
for (const c of cases) {
|
||||
expect(mine[c]!.split("\n")[0]).toBe(mp[c]!.split("\n")[0]);
|
||||
|
||||
Reference in new issue
Block a user