Files
UHD_Media_Tool/docs/runbook/scrape.md
T
2026-09-30 22:36:14 +08:00

7.3 KiB

刮削运行手册(SCR-1:NFO 与图片写进媒体库目录)

「刮削」= 把作品的最终字段写成 NFO,并下载海报、背景图、季海报、剧照,放进媒体库目录,Emby / Jellyfin / Kodi 直接读取。 「刷新元数据」只更新本工具库里的字段,不写媒体库目录;两者分开。

实现位置

位置 内容
packages/domain/src/scrape/nfo.ts NFO 生成(电影 / 剧集 / 季 / 集),纯函数
packages/domain/src/scrape/layout.ts 目标文件清单与命名 planScrapeTargets、作品目录推断 inferWorkDir
apps/server/src/modules/acquisition-organize/services/scrape-input.ts 最终字段(media_entity.metadata / content_unit.metadata)→ NFO 输入
apps/server/src/modules/acquisition-organize/services/scrape.ts 排任务 enqueueScrape、执行 runScrape、状态 loadScrapeState、入库后钩子 scrapeAfterImport
handlers.ts 任务 scrapeMedia 文件道;不重试(失败的文件在作品详情里重新刮削)
迁移 0057_media_scrape 每部作品最近一次刮削:状态、覆盖方式、逐文件结果
端口 AoPorts.scrapeImage TMDB 图片路径 → 字节:ImageCache.bytes("original", path),缓存目录 DATA_DIR/cache/images,出口按环境变量代理(externalFetch)

触发

触发 做法
整理入库成功(finalizeRecord 结果为已入库 / 部分入库) 设置 organize.scrapeAfterImport 打开时排 scrapeMedia,幂等键 scrapeMedia:record:<整理记录 id>(同一记录只排一次;季包一次整理只刮一次)
作品详情「重新刮削」「覆盖重写」 POST /library/{id}/scrape {mode?}(organize.execute,202);没有已入库文件 412
文件管理「刮削」(单项菜单、多选、详情按钮) POST /files/scrape-batch {write: true},按作品去重各排一次
POST /files/scrape {writeNfo / images} 在原有 refreshMetadata 之外再排一次 scrapeMedia,返回 queued 与 scrapeTaskId

同一作品已有排队中(未开始,30 分钟内)的刮削时直接复用(deduplicated: true)。

设置(设置 → 媒体库与目录 →「刮削」)

字段 缺省 说明
organize.scrapeAfterImport 关 入库后自动刮削(MP 目录「刮削」缺省同样关)
organize.scrapeMode missing missing 只补缺失(MP「仅缺失」)/ overwrite 全部覆盖(替换别的工具写的 NFO 和图片)
organize.scrapeImages 开 海报、背景图、季海报
organize.scrapeEpisodeThumbs 开 每集剧照;scrapeImages 关时不生效

写哪些文件

文件名跟随已入库的视频(视频名本身由命名模板生成);作品目录优先取作品的路径绑定,没有时从视频路径推(剧集父目录像 Season 1 / S01 / Specials 时取上一级)。

人生切割术 (2022)/
├── tvshow.nfo
├── poster.jpg
├── fanart.jpg
├── season01-poster.jpg            # 特别篇:season-specials-poster.jpg
└── Season 1/
    ├── season.nfo
    ├── poster.jpg                 # 季海报(与作品目录那张同一张)
    ├── 人生切割术 - S01E01 - 第 1 集.mkv
    ├── 人生切割术 - S01E01 - 第 1 集.nfo
    └── 人生切割术 - S01E01 - 第 1 集-thumb.jpg
星际穿越 (2014)/
├── 星际穿越 (2014) - 1080p.mkv
├── 星际穿越 (2014) - 1080p.nfo
├── poster.jpg
└── fanart.jpg
  • 电影视频直接放在库根(没有作品目录)时,图片改为 <视频名>-poster.jpg / <视频名>-fanart.jpg,不往库根放 poster.jpg; 剧集视频直接在库根时只写每集文件,不写 tvshow / 季文件(结果里记 scrape_no_work_dir)。
  • 多集文件(S01E01-E02)按首集写 NFO 与剧照(同 MP begin_episode)。蓝光原盘目录不刮削。
  • 只写 managed 库根;只登记(register_only)库根整体跳过(scrape_root_read_only)。库根离线整体失败(library_root_offline)。
  • 目标路径必须仍在库根内;已存在的目标是目录 / 符号链接时跳过。

NFO 内容

对照 MP app/modules/themoviedb/scraper.py,元素名、顺序、属性一致,格式同 minidom toprettyxml(indent=" ", encoding="utf-8"):

  • 公共(电影 / 剧集):tmdbid + uniqueid type=tmdb;有 TVDB / IMDb 时追加(有 IMDb 时 IMDb 为 default);plot / outline(CDATA); director(带 tmdbid);actor{name,type,role,tmdbid,thumb,profile};genre;rating(一位小数);studio;country;runtime; title、originaltitle、premiered、year;剧集另有 season=-1、episode=-1。
  • 季:plot、outline、title(没有时「季 N」)、premiered、releasedate、year、seasonnumber。
  • 集:title(没有或 TMDB 占位名时「第 N 集」)、plot、outline、aired、year、season、episode、rating。
  • 取值:标题 / 简介 / 标语按元数据语言链(见 metadata.md);字段锁与人工覆盖优先(用的是投影后的最终值)。

与 MP 的差异(差分登记 apps/server/test/scrape/nfo-deviations.json):

差异 理由
缺值不写元素(MP 写 <premiered/>、评分 0、空 CDATA) 空元素不带信息;评分 0 会被当成真实评分
额外写豆瓣 id(doubanid + uniqueid type=douban)、tagline、集 runtime 本地有这些字段
制片国取 TMDB origin_country(代码转中文名);剧集 studio 写播出平台 本地不存 production_countries / production_companies
电影不写 studio 同上
集 NFO 不写 uniqueid / tmdbid 本地不存 TMDB 集 id;媒体服务器按季号集号匹配
简介含 ]]> 时拆成两段 CDATA MP 的 minidom 直接抛错
不写 backdrop.jpg 副本,只写 fanart.jpg 三家都认 fanart.jpg
剧照 <视频名>-thumb.jpg(MP <视频名>.jpg) Kodi 约定,Jellyfin / Emby 同样识别

重新生成 MP 输出(改了样例或跟进上游后):

cd /opt/moviepilot-plus/upstream/MoviePilot && PYTHONPATH=. \
  .venv/bin/python /opt/uhd_media_tool/apps/server/test/scrape/gen-scrape-mp.py

结果与排查

  • 作品详情「刮削」卡片:状态(排队中 / 刮削中 / 已完成 / 部分完成 / 失败)、写入 / 跳过 / 失败计数、逐文件结果;排队或进行中时每 3 秒刷新。
  • GET /library/{id} 的 scrape:status、trigger(auto / manual)、mode、written / skipped / failed、reason、files[{path, kind, role, action, reason}]、recordId、taskId。
  • 审计 organize.scrape(目标作品;载荷含写入与失败的文件,最多 200 条);写入后发 files.changed,文件管理自动刷新。
  • 原因码(packages/contracts/src/reasons.ts):scrape_no_files、scrape_no_metadata、scrape_root_read_only、scrape_exists、 scrape_write_failed、scrape_image_failed、scrape_no_work_dir、library_root_offline、entity_not_found。
现象 看哪里
入库后没有 NFO 设置里「入库后自动刮削」是否打开;作品详情「刮削」是否有记录;引擎运行 scrapeMedia
全部「已存在」 覆盖方式是只补缺失;要替换用「覆盖重写」
图片失败、NFO 正常 TMDB 图片访问(出口代理);稍后「重新刮削」只补缺失的图片
「作品还没有元数据」 作品没有 TMDB 字段,先刷新元数据或指定 TMDB 条目