Skip to main content
Glama
README.md
# tmdb-media

TypeScript/Node implementation of the TMDB local media metadata tool described in
[`docs/spec-tmdb-media.md`](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

```bash
corepack pnpm install
corepack pnpm benchmark:parser
corepack pnpm test
corepack pnpm typecheck
```

Parse one filename or path:

```bash
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`](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` 举例。

```bash
# 列出某库(或子目录)的视频文件(只读)
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 刮削(模式①,不动视频)

1. **plan** — `plan_scrape({ library, subpath?, tmdb_id?, ... })` 规划,返回 `plan_id`、`expected_plan_hash` 和 `targets`(将写哪些文件、命中哪部作品、置信度)。不写任何文件。
2. **确认** — 把 `targets` 念给用户看,等用户核对。
3. **write** — 用户确认后 `write_artifacts({ plan_id, expected_plan_hash })` 原子写出(hash 不符会报 `PLAN_STALE`,重新 plan 即可)。

### 工作流 B:organize 整理(模式②,会动真实视频)

1. **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 文件夹、无分集)。
2. **确认(重点)** — organize **会重命名文件夹、移动真实视频**,所以必须把文件夹改名和每集移动一条条念给用户,明确告知这会改动真实文件,等用户确认。
3. **apply** — 用户确认后 `apply_organize({ plan_id, expected_plan_hash })` 执行,返回一个 `run_id`。
4. **undo(必要时)** — 出问题或用户反悔,用 `undo_organize({ run_id })` 整次回滚。

> 安全红线:root 由配置固定,只传 `library` + `subpath`,**绝不传绝对路径**;organize 因为动真实文件,必须 plan → 给用户看 → 确认 → apply,必要时 undo;纯数字集号 / 被改名的文件夹用 `tmdb_id`(如 `"tv:272432"`)强制锁定作品。

# 如何集成 (Integration)

## 前置准备

```bash
# 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`](.mcp.json)(已提供示例,把占位 token 换成你自己的):

```json
{
  "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:命令行一键添加:

```bash
claude mcp add tmdb-media -- node dist/cli.js mcp
```

随仓库还附带一个 Claude Code skill [`.claude/skills/tmdb-scrape/SKILL.md`](.claude/skills/tmdb-scrape/SKILL.md),
覆盖 sidecar 刮削和 organize 整理两套安全工作流,用户提到「刮削 / scrape / organize /
Jellyfin / 重命名 / TMDB 元数据 / 识别媒体文件夹」等时会自动触发。

## 在 Codex 注册

在 `~/.codex/config.toml` 加:

```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`。

鉴权只走以上环境变量,**不要**写进配置文件,也不要在对话里打印 / 回显。