7.3 KiB
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 条目 |