tmdb-media
TypeScript/Node implementation of the TMDB local media metadata tool described in docs/spec-tmdb-media.md.
Current status: iteration 1 (read-only pipeline) in progress, building on the parser benchmark spike. It does not write media metadata, rename files, move files, delete files, or touch a media library.
The iteration provides these read-only commands:
scan— discover media files under a configured library root.search— query TMDB for candidate matches.match— score and rank candidates against a parsed file.details— fetch canonical metadata for a chosen candidate.scrape --dry-run— run the full pipeline and report what would be written,
without writing anything to the media library.
Run
corepack pnpm install
corepack pnpm benchmark:parser
corepack pnpm test
corepack pnpm typecheck
Parse one filename or path:
corepack pnpm exec tsx src/cli.ts parse "/Volumes/Media/TV/庆余年/Season 1/第01集.mkv" --json
Parser Spike Result
The fixture at tests/fixtures/parser-samples.json currently shows:
- PTT + normalization baseline: 9/14 required cases.
- Enhanced parser: 14/14 required cases.
The enhanced parser layers Chinese season/episode handling and parent-directory title inference on top of parse-torrent-title. Known-hard cases are also tracked in the fixture but do not gate the command.
如何使用 (Usage)
本项目有两种用法:CLI(你在终端手动跑)和 MCP(AI 客户端如 Claude Code / Codex 通过对话调用)。两者底层是同一套管线。
刮削分两种模式,区别在「动不动真实视频文件」:
| 模式 | 做什么 | 动视频吗 | CLI | MCP 工具 | | --- | --- | --- | --- | --- | | ① 轻量 sidecar | 在视频旁边写 metadata.json / metadata.md | 不动,只新增旁车文件 | scrape | plan_scrape → write_artifacts | | ② 完整 organize | tinyMediaManager / Jellyfin 式整理:重命名文件夹、把分集移进 Season N/、写 tvshow.nfo + 分集 NFO、下载 poster/fanart/剧照 | 会重命名/移动真实视频 | organize(+ undo) | plan_organize → apply_organize(+ undo_organize) |
两种模式都很保守:默认 dry-run、默认不覆盖、执行前需二次确认;organize 还可整次回滚。
CLI 用法
CLI 既能用源码跑(corepack pnpm exec tsx src/cli.ts <cmd>),也能 build 后用 node dist/cli.js <cmd>。下面用配置示例里的库名 movies / tv 举例。
# 列出某库(或子目录)的视频文件(只读)
node dist/cli.js scan -l tv -s "庆余年"
# 搜 TMDB 候选
node dist/cli.js search "庆余年" -t tv
node dist/cli.js search "盗梦空间" -t movie -y 2010
# 取某 TMDB id 的规范元数据
node dist/cli.js details tv 272432
node dist/cli.js details movie 27205 --language zh-CN
# 发现/挑片(只读):趋势、推荐、相似
node dist/cli.js trending movie # 本周热门电影(默认 week)
node dist/cli.js trending tv -w day # 当日热门剧集
node dist/cli.js recommend movie 27205 # 与某电影相关的推荐
node dist/cli.js similar tv 272432 --language zh-CN # 与某剧集相似的作品
# 扫描 + 解析 + 匹配,看会命中什么(只读,不写)
node dist/cli.js match -l tv -s "庆余年"
node dist/cli.js match -l tv -s "某改名文件夹" --tmdb-id tv:272432 # 强制锁定作品
# 模式①:sidecar 刮削。默认 DRY-RUN,加 --yes 才真正写
node dist/cli.js scrape -l tv -s "庆余年" # dry-run,看会写什么
node dist/cli.js scrape -l tv -s "庆余年" --yes # 真正写出 metadata.json/.md
node dist/cli.js scrape -l tv -s "某文件夹" --tmdb-id tv:272432 --yes # 强制作品后写
node dist/cli.js scrape -l tv -s "庆余年" --yes --overwrite # 覆盖已有 sidecar(先备份 .bak.<ts>)
# 模式②:organize(Jellyfin 式整理,会重命名/移动真实视频)。默认 DRY-RUN
# 同时支持电影和剧集:--tmdb-id 接受 movie:603 / tv:272432 / 裸 id(默认 tv)
node dist/cli.js organize -l tv -s "低智商犯罪" --tmdb-id 272432 # 剧集 dry-run:打印改名 + 每集移动
node dist/cli.js organize -l tv -s "低智商犯罪" --tmdb-id 272432 --yes # 剧集真正执行,打印 run-id
node dist/cli.js organize -l tv -s "低智商犯罪" --tmdb-id 272432 --yes --no-images # 不下图片
node dist/cli.js organize -l tv -s "低智商犯罪" --tmdb-id 272432 --yes --overwrite # 覆盖已有 NFO/图片
node dist/cli.js organize -l movies -s "某电影文件夹" --tmdb-id movie:603 # 电影 dry-run
node dist/cli.js organize -l movies -s "某电影文件夹" --tmdb-id movie:603 --yes # 电影真正执行
# 回滚一次 organize(用 organize --yes 打印的 run-id)
node dist/cli.js undo <run-id>
关键点:
scrape/organize默认都是 dry-run,必须显式--yes才动磁盘。organize同时支持电影和剧集;--tmdb-id必填,接受movie:603/tv:272432/ 裸272432(裸 id 默认按 tv 处理)。- 剧集布局:文件夹改名为
片名 (年份),每集移进Season N/片名 - SxxExx - 标题,写tvshow.nfo+ 分集 NFO,下 poster/fanart/剧照。 - 电影布局:把文件夹与主视频都重命名为
片名 (年份)(如片名 (年份).mkv),写movie.nfo,下 poster/fanart——没有 Season 文件夹、没有分集。 - 每次
organize --yes都会落一个run-id,可用undo <run-id>整次回滚(移回视频、删除新建的 Season 目录与 NFO/图片、把文件夹名改回去)。 - 任何命令都不传绝对路径,只用
-l/--library库名 +-s/--subpath相对子路径。
MCP 用法
MCP server 跑的是编译后的 dist/cli.js mcp,共暴露 12 个工具:
| 工具 | 作用 | 破坏性 | | --- | --- | --- | | get_capabilities | 列出库、默认/回退语言、支持格式、minConfidence、视频扩展名、TMDB 署名 | 否(只读) | | scan_media_files | 分页列出某库的视频文件(含 existingArtifacts) | 否(只读) | | search_tmdb | 按标题(可带年份)搜 TMDB 候选 | 否(只读) | | get_tmdb_metadata | 取某 TMDB id 的规范元数据 | 否(只读) | | get_trending | 取趋势:{ type, window?(day/week,默认 week), language? },挑片/发现用 | 否(只读) | | get_recommendations | 取某 TMDB id 的推荐:{ type, tmdb_id, language? } | 否(只读) | | get_similar | 取某 TMDB id 的相似作品:{ type, tmdb_id, language? } | 否(只读) | | plan_scrape | 模式① 规划 sidecar 写入(不写),返回 plan_id + expected_plan_hash | 否(只读) | | write_artifacts | 模式① 按计划写出 sidecar,需 plan_id + expected_plan_hash | 是(destructive) | | plan_organize | 模式② 规划 Jellyfin 式整理(电影 / 剧集均可,tmdb_id 接受 movie:603 / tv:272432 / 裸 id),不动文件,返回 plan_id + expected_plan_hash | 否(只读) | | apply_organize | 模式② 执行整理:重命名/移动视频 + 写 NFO + 下图片,需 plan_id + expected_plan_hash,落 run_id | 是(destructive,动真实视频!) | | undo_organize | 按 run_id 整次回滚一次 apply_organize | 是(destructive) |
工作流 A:sidecar 刮削(模式①,不动视频)
- plan —
plan_scrape({ library, subpath?, tmdb_id?, ... })规划,返回plan_id、expected_plan_hash和targets(将写哪些文件、命中哪部作品、置信度)。不写任何文件。 - 确认 — 把
targets念给用户看,等用户核对。 - write — 用户确认后
write_artifacts({ plan_id, expected_plan_hash })原子写出(hash 不符会报PLAN_STALE,重新 plan 即可)。
工作流 B:organize 整理(模式②,会动真实视频)
- plan —
plan_organize({ library, subpath, tmdb_id, images?, overwrite? })规划,返回plan_id、expected_plan_hash、show_folder_rename(文件夹改名)、seasons、moves(每集要移到哪)、NFO/图片数量、unmatched。不动任何文件。tmdb_id同时支持电影和剧集,接受movie:603/tv:272432/ 裸272432(裸 id 默认 tv);电影布局只把文件夹+主视频重命名为「片名 (年份)」并写movie.nfo+ poster/fanart(无 Season 文件夹、无分集)。 - 确认(重点) — organize 会重命名文件夹、移动真实视频,所以必须把文件夹改名和每集移动一条条念给用户,明确告知这会改动真实文件,等用户确认。
- apply — 用户确认后
apply_organize({ plan_id, expected_plan_hash })执行,返回一个run_id。 - undo(必要时) — 出问题或用户反悔,用
undo_organize({ run_id })整次回滚。
安全红线:root 由配置固定,只传
library+subpath,绝不传绝对路径;organize 因为动真实文件,必须 plan → 给用户看 → 确认 → apply,必要时 undo;纯数字集号 / 被改名的文件夹用tmdb_id(如"tv:272432")强制锁定作品。
如何集成 (Integration)
前置准备
# 1. 安装依赖并构建(MCP / dist 用法都需要先 build)
corepack pnpm install
corepack pnpm build
# 2. 配置 config.toml(参考 config.example.toml,填好每个库的 root 与 type)
cp config.example.toml config.toml
# 3. 设置 TMDB 凭据(环境变量,不要写进配置文件)
export TMDB_ACCESS_TOKEN="<your-tmdb-v4-read-access-token>"
库根目录在配置里固定,AI / MCP 调用方只传库名 + 相对子路径,绝不传绝对路径。
在 Claude Code 注册
方式 A:项目级 .mcp.json(已提供示例,把占位 token 换成你自己的):
{
"mcpServers": {
"tmdb-media": {
"command": "node",
"args": ["dist/cli.js", "mcp"],
"env": {
"TMDB_ACCESS_TOKEN": "<your-tmdb-v4-read-access-token>",
"TMDB_MEDIA_CONFIG": "config.toml"
}
}
}
}
方式 B:命令行一键添加:
claude mcp add tmdb-media -- node dist/cli.js mcp
随仓库还附带一个 Claude Code skill .claude/skills/tmdb-scrape/SKILL.md, 覆盖 sidecar 刮削和 organize 整理两套安全工作流,用户提到「刮削 / scrape / organize / Jellyfin / 重命名 / TMDB 元数据 / 识别媒体文件夹」等时会自动触发。
在 Codex 注册
在 ~/.codex/config.toml 加:
[mcp_servers.tmdb-media]
command = "node"
args = ["dist/cli.js", "mcp"]
[mcp_servers.tmdb-media.env]
TMDB_ACCESS_TOKEN = "<your-tmdb-v4-read-access-token>"
TMDB_MEDIA_CONFIG = "config.toml"
环境变量说明
TMDB_ACCESS_TOKEN— TMDB v4 read access token(Bearer,优先)。TMDB_API_KEY— TMDB v3 api key(TMDB_ACCESS_TOKEN缺失时的回退)。TMDB_MEDIA_CONFIG—(可选)config.toml 路径;不设则用$XDG_CONFIG_HOME/tmdb-media/config.toml。TMDB_MEDIA_LOG_LEVEL—(可选)日志级别,默认info。
鉴权只走以上环境变量,不要写进配置文件,也不要在对话里打印 / 回显。











