fix(scrape): 没有 TMDB id 的作品季 / 集缺元数据时照旧写占位 NFO

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Cxwgnn2pbzQ7rXv3rq7fei
This commit is contained in:
rootandClaude Opus 5.5 committed 2026-10-03 21:37:14 +08:00
1 parent 5b023ad682
commit 871031d5ed
4 files changed
+23 -7

No files matched your search

@@ -57,7 +57,8 @@ import {
// - 覆盖:update(缺省)只覆盖本工具上次写入且没被改过的文件(逐文件 sha256 记在 scrape_record.files,
// 改过的记 scrape_user_modified)/ missing 只补缺失 / overwrite 全部覆盖;写入走同目录临时文件 + rename;
// 目标必须在登记目录内。
// - 季 / 集缺元数据时先按 TMDB id 现取季详情补齐,仍缺就跳过(scrape_metadata_missing,记录 partial),不写占位 NFO。
// - 季 / 集缺元数据时先按 TMDB id 现取季详情补齐,仍缺就跳过(scrape_metadata_missing,记录 partial),不写占位 NFO;
// 没有 TMDB id 的作品(Local)无处可补,照旧写占位。
// - 每次刮削一条 scrape_record(目录、识别结果、逐文件结果、原因);审计 organize.scrape;SSE files.changed。
export const SCRAPE_TASK = "scrapeMedia";
@@ -957,6 +958,8 @@ export async function runScrapeRecord(
}
};
// 没有 TMDB id 的作品(Local)没有别的来源可补,季 / 集缺元数据时照旧写「季 N」「第 N 集」占位
const hasTmdbId = !!(match.externalId ?? fields.ids.tmdb);
// NFO 先写(不依赖网络)
for (const t of targets.filter((x) => x.kind === "nfo")) {
if (signal?.aborted) throw new Error("aborted");
@@ -969,14 +972,14 @@ export async function runScrapeRecord(
if (t.role === "movie_nfo" || t.role === "tvshow_nfo") xml = workNfo(work);
else if (t.role === "season_nfo") {
const u = seasonUnit.get(t.season!);
if (seasonMetadataMissing(u)) {
if (seasonMetadataMissing(u) && hasTmdbId) {
files.push(missingMeta(t));
continue;
}
xml = seasonNfo(nfoSeason(t.season!, u));
} else {
const u = episodeUnit.get(`${t.season}:${t.episode}`);
if (episodeMetadataMissing(u)) {
if (episodeMetadataMissing(u) && hasTmdbId) {
files.push(missingMeta(t));
continue;
}
@@ -1,4 +1,4 @@
import { readFile, stat, utimes, writeFile } from "node:fs/promises";
import { mkdir, readFile, stat, utimes, writeFile } from "node:fs/promises";
import type { TmdbRichSeasonDetail } from "@umt/adapters";
import type { ScrapeFileResult, ScrapeMatch } from "@umt/contracts";
import { createTempTree, type TempTree } from "@umt/testkit";
@@ -295,6 +295,19 @@ describe.skipIf(!hasPg)("刮削准确度:更新模式、缺元数据不写占位
expect(r2.status).toBe("partial");
});
it("没有 TMDB id 的作品(Local)季 / 集缺元数据时照旧写占位", async () => {
const local = await entity("series", "本地剧", { title: f("本地剧") });
const dir = "TV/本地剧 (2020)";
await mkdir(lib.path(`${dir}/Season 1`), { recursive: true });
await writeFile(lib.path(`${dir}/Season 1/Local.S01E01.mkv`), "video");
const calls = seasonCalls.length;
const r = await scrape(local, "tv", dir, "update", null);
expect(seasonCalls.length).toBe(calls);
expect(r.files.find((x) => x.path.endsWith("S01E01.nfo"))).toMatchObject({ action: "written" });
expect(await readFile(lib.path(`${dir}/Season 1/Local.S01E01.nfo`), "utf8")).toContain("第 1 集");
expect(r.files.some((x) => x.reason === "scrape_metadata_missing")).toBe(false);
});
it("合集 / 其他类作品按电影刮削,NFO 根元素是 <movie>", async () => {
expect(nfoWork({ kind: "collection", title: "x", original_title: null, year: null, metadata: {} }, {}).kind).toBe(
"movie",