Files
UHD_Media_Tool/packages/domain/test/scrape.test.ts
T
rootandClaude Opus 5.5 861f799615 feat(domain): 刮削 NFO 生成与目标文件布局(SCR-1)
电影 / 剧集 / 季 / 集 NFO(元素与顺序对照 MP TmdbScraper,缺值不写),
poster / fanart / 季海报 / 剧照命名(Emby / Jellyfin / Kodi 兼容)。

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-30 22:36:14 +08:00

202 lines
7.5 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { episodeNfo, inferWorkDir, planScrapeTargets, seasonNfo, seasonPosterName, workNfo } from "../src/index.ts";
describe("NFO 生成", () => {
it("电影:MP 元素顺序,缺值不写,IMDb 为 default", () => {
const xml = workNfo({
kind: "movie",
title: "星际穿越",
originalTitle: "Interstellar",
year: 2014,
premiered: "2014-11-05",
overview: "穿越虫洞。",
rating: 8.46,
genres: ["科幻", "剧情", "科幻"],
countries: ["美国"],
runtime: 169,
directors: [{ tmdbId: 525, name: "克里斯托弗·诺兰" }],
actors: [{ tmdbId: 10297, name: "马修·麦康纳", role: "Cooper", profilePath: "/mm.jpg" }],
ids: { tmdb: "157336", imdb: "tt0816692" },
});
expect(xml).toBe(
[
'<?xml version="1.0" encoding="utf-8"?>',
"<movie>",
" <tmdbid>157336</tmdbid>",
' <uniqueid type="tmdb" default="false">157336</uniqueid>',
" <imdbid>tt0816692</imdbid>",
' <uniqueid type="imdb" default="true">tt0816692</uniqueid>',
" <plot><![CDATA[穿越虫洞。]]></plot>",
" <outline><![CDATA[穿越虫洞。]]></outline>",
' <director tmdbid="525">克里斯托弗·诺兰</director>',
" <actor>",
" <name>马修·麦康纳</name>",
" <type>Actor</type>",
" <role>Cooper</role>",
" <tmdbid>10297</tmdbid>",
" <thumb>https://image.tmdb.org/t/p/original/mm.jpg</thumb>",
" <profile>https://www.themoviedb.org/person/10297</profile>",
" </actor>",
" <genre>科幻</genre>",
" <genre>剧情</genre>",
" <rating>8.5</rating>",
" <country>美国</country>",
" <runtime>169</runtime>",
" <title>星际穿越</title>",
" <originaltitle>Interstellar</originaltitle>",
" <premiered>2014-11-05</premiered>",
" <year>2014</year>",
"</movie>",
"",
].join("\n"),
);
});
it("剧集 tvshow.nfo:season/episode = -1,豆瓣 id、标语、平台", () => {
const xml = workNfo({
kind: "tvshow",
title: "人生切割术",
premiered: "2022-02-17",
tagline: "工作与生活,彻底分开",
studios: ["Apple TV+"],
ids: { tmdb: "95396", tvdb: "371980", douban: "30448896" },
});
expect(xml).toContain('<uniqueid type="tmdb" default="true">95396</uniqueid>');
expect(xml).toContain('<uniqueid type="tvdb">371980</uniqueid>');
expect(xml).toContain('<uniqueid type="douban">30448896</uniqueid>');
expect(xml).toContain("<studio>Apple TV+</studio>");
expect(xml).toMatch(/<year>2022<\/year>\n {2}<season>-1<\/season>\n {2}<episode>-1<\/episode>\n {2}<tagline>/);
expect(xml).not.toContain("<plot>");
expect(xml).not.toContain("<rating>");
});
it("转义与 CDATA:& < > 转义,]]> 拆段,控制字符删除", () => {
const xml = workNfo({
kind: "movie",
title: 'A & <B> "c"\u0001',
overview: "x]]>y",
ids: { tmdb: "1" },
});
expect(xml).toContain('<title>A &amp; &lt;B&gt; "c"</title>');
expect(xml).toContain("<plot><![CDATA[x]]]]><![CDATA[>y]]></plot>");
});
it("季与集", () => {
expect(seasonNfo({ season: 1, title: "第 1 季", airDate: "2022-02-17" })).toBe(
[
'<?xml version="1.0" encoding="utf-8"?>',
"<season>",
" <title>第 1 季</title>",
" <premiered>2022-02-17</premiered>",
" <releasedate>2022-02-17</releasedate>",
" <year>2022</year>",
" <seasonnumber>1</seasonnumber>",
"</season>",
"",
].join("\n"),
);
const ep = episodeNfo({ season: 1, episode: 2, overview: "参观。", airDate: "2022-02-18", rating: 8, runtime: 53 });
expect(ep).toContain("<title>第 2 集</title>");
expect(ep).toContain("<rating>8.0</rating>");
expect(ep).toMatch(/<season>1<\/season>\n {2}<episode>2<\/episode>/);
expect(ep).toContain("<runtime>53</runtime>");
});
});
describe("刮削目标布局", () => {
const images = {
posterPath: "/p.jpg",
backdropPath: "/b.png",
seasonPosters: new Map([[1, "/s1.jpg"]]),
stills: new Map([
["1:1", "/e1.jpg"],
["1:2", null],
]),
};
it("作品目录推断", () => {
expect(inferWorkDir("tv", "人生切割术 (2022)/Season 1/a.mkv")).toBe("人生切割术 (2022)");
expect(inferWorkDir("tv", "剧集/人生切割术 (2022)/S01/a.mkv")).toBe("剧集/人生切割术 (2022)");
expect(inferWorkDir("movie", "星际穿越 (2014)/a.mkv")).toBe("星际穿越 (2014)");
expect(inferWorkDir("movie", "a.mkv")).toBeNull();
expect(seasonPosterName(0)).toBe("season-specials-poster.jpg");
});
it("剧集:tvshow / 季 / 集,季海报写两处,缺剧照不写", () => {
const t = planScrapeTargets({
kind: "tv",
workDir: "人生切割术 (2022)",
videos: [
{ path: "人生切割术 (2022)/Season 1/人生切割术 - S01E02 - 第 2 集.mkv", season: 1, episode: 2 },
{ path: "人生切割术 (2022)/Season 1/人生切割术 - S01E01 - 第 1 集.mkv", season: 1, episode: 1 },
],
images,
options: { images: true, episodeThumbs: true },
});
expect(t.map((x) => x.path)).toEqual([
"人生切割术 (2022)/tvshow.nfo",
"人生切割术 (2022)/poster.jpg",
"人生切割术 (2022)/fanart.png",
"人生切割术 (2022)/Season 1/season.nfo",
"人生切割术 (2022)/season01-poster.jpg",
"人生切割术 (2022)/Season 1/poster.jpg",
"人生切割术 (2022)/Season 1/人生切割术 - S01E01 - 第 1 集.nfo",
"人生切割术 (2022)/Season 1/人生切割术 - S01E01 - 第 1 集-thumb.jpg",
"人生切割术 (2022)/Season 1/人生切割术 - S01E02 - 第 2 集.nfo",
]);
});
it("关图片 / 关剧照", () => {
const base = {
kind: "tv" as const,
workDir: "S",
videos: [{ path: "S/Season 1/a.mkv", season: 1, episode: 1 }],
images,
};
expect(planScrapeTargets({ ...base, options: { images: false, episodeThumbs: true } }).map((x) => x.role)).toEqual([
"tvshow_nfo",
"season_nfo",
"episode_nfo",
]);
expect(
planScrapeTargets({ ...base, options: { images: true, episodeThumbs: false } }).some(
(x) => x.role === "episode_thumb",
),
).toBe(false);
});
it("电影:作品目录里写 poster/fanart;在库根时用 <视频名>-poster", () => {
const inDir = planScrapeTargets({
kind: "movie",
workDir: "星际穿越 (2014)",
videos: [{ path: "星际穿越 (2014)/星际穿越 (2014) - 1080p.mkv" }],
images,
options: { images: true, episodeThumbs: true },
});
expect(inDir.map((x) => x.path)).toEqual([
"星际穿越 (2014)/星际穿越 (2014) - 1080p.nfo",
"星际穿越 (2014)/poster.jpg",
"星际穿越 (2014)/fanart.png",
]);
const atRoot = planScrapeTargets({
kind: "movie",
workDir: null,
videos: [{ path: "x.mkv" }],
images,
options: { images: true, episodeThumbs: true },
});
expect(atRoot.map((x) => x.path)).toEqual(["x.nfo", "x-poster.jpg", "x-fanart.png"]);
});
it("剧集视频在库根:不写 tvshow / 季文件,只写集 NFO", () => {
const t = planScrapeTargets({
kind: "tv",
workDir: null,
videos: [{ path: "a.S01E01.mkv", season: 1, episode: 1 }],
images,
options: { images: true, episodeThumbs: true },
});
expect(t.map((x) => x.path)).toEqual(["a.S01E01.nfo", "a.S01E01-thumb.jpg"]);
});
});