import { describe, expect, it } from "vitest"; import { episodeNfo, inferWorkDir, isSeasonDirName, multiEpisodeNfo, planScrapeTargets, scrapeKindOf, seasonNfo, seasonOfDirName, 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( [ '', "", " 157336", ' 157336', " tt0816692", ' tt0816692', " ", " ", ' 克里斯托弗·诺兰', " ", " 马修·麦康纳", " Actor", " Cooper", " 10297", " https://image.tmdb.org/t/p/original/mm.jpg", " https://www.themoviedb.org/person/10297", " ", " 科幻", " 剧情", " 8.5", " 美国", " 169", " 星际穿越", " Interstellar", " 2014-11-05", " 2014", "", "", ].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('95396'); expect(xml).toContain('371980'); expect(xml).toContain('30448896'); expect(xml).toContain("Apple TV+"); expect(xml).toMatch(/2022<\/year>\n {2}-1<\/season>\n {2}-1<\/episode>\n {2}/); expect(xml).not.toContain(""); expect(xml).not.toContain(""); }); it("转义与 CDATA:& < > 转义,]]> 拆段,控制字符删除", () => { const xml = workNfo({ kind: "movie", title: 'A & "c"\u0001', overview: "x]]>y", ids: { tmdb: "1" }, }); expect(xml).toContain('A & <B> "c"'); expect(xml).toContain("y]]>"); }); it("季与集", () => { expect(seasonNfo({ season: 1, title: "第 1 季", airDate: "2022-02-17" })).toBe( [ '', "", " 第 1 季", " 2022-02-17", " 2022-02-17", " 2022", " 1", "", "", ].join("\n"), ); const ep = episodeNfo({ season: 1, episode: 2, overview: "参观。", airDate: "2022-02-18", rating: 8, runtime: 53 }); expect(ep).toContain("第 2 集"); expect(ep).toContain("8.0"); expect(ep).toMatch(/1<\/season>\n {2}2<\/episode>/); expect(ep).toContain("53"); }); }); 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"]); }); it("SCR-2 季目录名:Season 1 / S01 / Specials / SPs / 第 1 季", () => { for (const n of ["Season 1", "season 12", "S01", "Specials", "SPs", "特别篇", "第 2 季"]) expect(isSeasonDirName(n)).toBe(true); for (const n of ["人生切割术 (2022)", "Extras", "Season"]) expect(isSeasonDirName(n)).toBe(false); expect(seasonOfDirName("Season 3")).toBe(3); expect(seasonOfDirName("S01")).toBe(1); expect(seasonOfDirName("Specials")).toBe(0); expect(seasonOfDirName("第 2 季")).toBe(2); expect(seasonOfDirName("Extras")).toBeNull(); }); it("SCR-2 范围:选中季目录只写季级与各集(季海报仍写上级),选中单集只写该集", () => { const videos = [{ path: "人生切割术 (2022)/Season 1/a.S01E01.mkv", season: 1, episode: 1 }]; const season = planScrapeTargets({ kind: "tv", workDir: "人生切割术 (2022)", videos, images, options: { images: true, episodeThumbs: true }, parts: { work: false }, }); expect(season.map((x) => `${x.role}:${x.path}`)).toEqual([ "season_nfo:人生切割术 (2022)/Season 1/season.nfo", "season_poster:人生切割术 (2022)/season01-poster.jpg", "season_poster:人生切割术 (2022)/Season 1/poster.jpg", "episode_nfo:人生切割术 (2022)/Season 1/a.S01E01.nfo", "episode_thumb:人生切割术 (2022)/Season 1/a.S01E01-thumb.jpg", ]); const ep = planScrapeTargets({ kind: "tv", workDir: null, videos, images, options: { images: true, episodeThumbs: true }, parts: { work: false, season: false }, }); expect(ep.map((x) => x.role)).toEqual(["episode_nfo", "episode_thumb"]); }); }); describe("作品类型 → 刮削类型", () => { it("剧集 / 综艺按剧集,电影 / 合集 / 其他按电影", () => { expect(["series", "variety", "tv"].map(scrapeKindOf)).toEqual(["tv", "tv", "tv"]); expect(["movie", "collection", "other"].map(scrapeKindOf)).toEqual(["movie", "movie", "movie"]); }); }); describe("多集文件", () => { it("集 NFO 每集一个 块,只有一个 XML 声明;一集时与 episodeNfo 相同", () => { const xml = multiEpisodeNfo([ { season: 1, episode: 1, title: "上", airDate: "2022-02-17" }, { season: 1, episode: 2, title: "下", airDate: "2022-02-17" }, ]); expect(xml.match(/<\?xml /g)).toHaveLength(1); expect(xml.match(//g)).toHaveLength(2); expect(xml).toContain(" 上\n 2022-02-17"); expect(xml.indexOf("1")).toBeLessThan(xml.indexOf("2")); const one = { season: 1, episode: 3, title: "单集" }; expect(multiEpisodeNfo([one])).toBe(episodeNfo(one)); }); it("布局:多集文件只写一个 NFO(按视频名)与首集剧照,目标带全部集号", () => { const t = planScrapeTargets({ kind: "tv", workDir: "剧 (2020)", videos: [{ path: "剧 (2020)/Season 1/剧 - S01E01-E02.mkv", season: 1, episode: 1, episodes: [2, 1] }], images: { stills: new Map([["1:1", "/e1.jpg"]]) }, options: { images: true, episodeThumbs: true }, parts: { work: false, season: false }, }); expect(t).toEqual([ expect.objectContaining({ role: "episode_nfo", path: "剧 (2020)/Season 1/剧 - S01E01-E02.nfo", episodes: [1, 2], }), expect.objectContaining({ role: "episode_thumb", path: "剧 (2020)/Season 1/剧 - S01E01-E02-thumb.jpg" }), ]); }); });