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
@@ -49,8 +49,13 @@ export interface ScrapeVideo {
|
||||
/** 库根内相对路径 */
|
||||
path: string;
|
||||
season?: number | null;
|
||||
/** 首集集号(多集文件按首集,同 MP begin_episode) */
|
||||
/** 首集集号(同 MP begin_episode;剧照、文件命名按首集) */
|
||||
episode?: number | null;
|
||||
/**
|
||||
* 文件覆盖的全部集号(多集文件 E01-E02 / 覆盖多个集单元),含首集、升序;缺省 = 只有首集。
|
||||
* 集 NFO 按 Kodi 约定每集写一个 `<episodedetails>` 块(MP 只写首集)。
|
||||
*/
|
||||
episodes?: readonly number[] | null;
|
||||
}
|
||||
|
||||
export interface ScrapeImages {
|
||||
@@ -83,6 +88,8 @@ export interface ScrapeTarget {
|
||||
role: ScrapeTargetRole;
|
||||
season?: number;
|
||||
episode?: number;
|
||||
/** 集 NFO:文件覆盖的全部集号(多集文件多于一个) */
|
||||
episodes?: number[];
|
||||
/** 这个文件对应的视频(电影 / 集 NFO、剧照) */
|
||||
video?: string;
|
||||
/** 图片:TMDB 图片路径 */
|
||||
@@ -227,12 +234,15 @@ export function planScrapeTargets(input: ScrapeLayoutInput): ScrapeTarget[] {
|
||||
if (v.season === null || v.season === undefined || v.episode === null || v.episode === undefined) continue;
|
||||
const dir = normDir(posix.dirname(v.path));
|
||||
const stem = stemOf(v.path);
|
||||
const first = v.episode;
|
||||
const eps = [...new Set([first, ...(v.episodes ?? [])])].sort((a, b) => a - b);
|
||||
add({
|
||||
path: join(dir, `${stem}.nfo`),
|
||||
kind: "nfo",
|
||||
role: "episode_nfo",
|
||||
season: v.season,
|
||||
episode: v.episode,
|
||||
episodes: eps,
|
||||
video: v.path,
|
||||
});
|
||||
const still = img.stills?.get(`${v.season}:${v.episode}`);
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
// - 缺值不写元素(MP 写空元素 `<premiered/>`、评分缺省写 0、空 CDATA);
|
||||
// - 简介里的 `]]>` 拆成两段 CDATA(MP 的 minidom 直接抛错);
|
||||
// - 额外写 豆瓣 id(doubanid + uniqueid type=douban)、标语 tagline、集时长 runtime;
|
||||
// - 集 NFO 没有 TMDB 集 id(本地不存),不写 uniqueid / tmdbid,媒体服务器按季号集号匹配。
|
||||
// - 集 NFO 没有 TMDB 集 id(本地不存),不写 uniqueid / tmdbid,媒体服务器按季号集号匹配;
|
||||
// - 多集文件(E01-E02)按 Kodi 约定每集一个 `<episodedetails>` 块写进同一个 NFO(MP 只写首集)。
|
||||
|
||||
export const TMDB_IMAGE_BASE = "https://image.tmdb.org/t/p/original";
|
||||
|
||||
@@ -92,9 +93,9 @@ function render(n: Node, indent: string, out: string[]) {
|
||||
else out.push(`${indent}<${n.tag}${attrs}/>`);
|
||||
}
|
||||
|
||||
function toXml(root: Node): string {
|
||||
function toXml(...roots: Node[]): string {
|
||||
const out = ['<?xml version="1.0" encoding="utf-8"?>'];
|
||||
render(root, "", out);
|
||||
for (const root of roots) render(root, "", out);
|
||||
return `${out.join("\n")}\n`;
|
||||
}
|
||||
|
||||
@@ -248,6 +249,19 @@ export function seasonNfo(s: NfoSeason): string {
|
||||
|
||||
/** 每集 `<视频名>.nfo` */
|
||||
export function episodeNfo(e: NfoEpisode): string {
|
||||
return toXml(episodeNode(e));
|
||||
}
|
||||
|
||||
/**
|
||||
* 多集文件的 `<视频名>.nfo`:Kodi 约定(Jellyfin / Emby 同样读取)每集一个 `<episodedetails>` 块,依次写在同一个文件里,
|
||||
* 只有一个 XML 声明。只有一集时与 episodeNfo 相同。
|
||||
*/
|
||||
export function multiEpisodeNfo(list: readonly NfoEpisode[]): string {
|
||||
if (!list.length) throw new Error("multiEpisodeNfo: 至少一集");
|
||||
return toXml(...list.map(episodeNode));
|
||||
}
|
||||
|
||||
function episodeNode(e: NfoEpisode): Node {
|
||||
const b = new Builder();
|
||||
b.text("title", has(e.title) ? e.title : `第 ${e.episode} 集`);
|
||||
b.plot(e.overview);
|
||||
@@ -259,5 +273,5 @@ export function episodeNfo(e: NfoEpisode): string {
|
||||
b.directors(e.directors);
|
||||
b.actors(e.actors, false);
|
||||
if (num(e.runtime) && e.runtime > 0) b.text("runtime", e.runtime);
|
||||
return toXml({ tag: "episodedetails", children: b.nodes });
|
||||
return { tag: "episodedetails", children: b.nodes };
|
||||
}
|
||||
Reference in new issue
Block a user