| name | anime-library-organize |
| description | 将散乱的番剧/影视资源整理为 Plex / Jellyfin / Emby 标准库结构。
当用户提到「整理番剧」「番剧库整理」「anime 整理」「Plex 库」「Jellyfin 整理」
「新番剧入库」「番剧命名」「季文件夹」「AniList ID」「tvdb-id」「anilist-id」
「.specials 文件夹」「NCOP NCED」「SxxExx 命名」「季和 1 命名」时必须使用本 skill。
即使用户只说「帮我整理一下番剧」「新下了个 anime 怎么放」也应触发。
本 skill 包含完整的工作流:dryrun → 确认 → 节流执行 → 清理空目录 → 备份重复。
|
Anime Library Organizer
整理散落的番剧/影视资源到 Plex / Jellyfin / Emby 标准库结构。
🎯 目标
将散乱命名的番剧/影视资源整理为以下标准结构:
<中文名> (<English Name>) {anilist-<id>}/
Season 01/
S01E01.mp4
S01E02.mkv
...
Season 02/
S02E01.mp4
...
.specials/ ← 隐藏目录,放 NCOP/NCED/CM/Menu/PV/OVA/剧场版
S00E01.mkv ← S00 表示 specials 季 (Plex 约定)
NCOP__xxx.mkv
NCED__xxx.mkv
COMPLETE__xxx.mkv ← 无法解析 episode 的整集文件
可被 Plex / Jellyfin / Emby / Kodi 等媒体服务器直接识别。
📦 资源文件
本 skill 包含 4 个 Python 脚本(均放在 scripts/ 目录):
| 脚本 | 作用 |
|---|
dryrun.py | 核心: 扫描源目录, 生成整理计划 (plan.json + summary.txt), 不修改任何文件 |
execute.py | 按 plan.json 执行 move 操作, 带节流防速率限制, 支持断点续传 (state.json) |
cleanup.py | 一次性补充处理的模板 (按需复制并修改) |
remove_empty.py | 递归清理空目录 |
⚙️ 配置
通过环境变量配置路径(CLI 参数可覆盖):
| 变量 | 默认值 | 说明 |
|---|
ANIME_SRC | ./anime-library | 源目录(散乱文件所在的根) |
ANIME_OUT | ./.dryrun-output | dry-run 输出目录(plan.json 等) |
export ANIME_SRC=/path/to/your/messy/anime
export ANIME_OUT=./plans/run-1
$env:ANIME_SRC = "D:\anime\messy"
$env:ANIME_OUT = "D:\anime\plans\run-1"
python scripts/dryrun.py /path/to/anime ./plans/run-1
🚀 快速开始 (3 步)
步骤 1: Dry-Run 生成计划
python scripts/dryrun.py
会生成:
plan.json - 完整移动计划(机器可读)
summary.txt - 人类可读的整理报告
先看 summary.txt 确认无误再执行。
第一次运行如果 MANIFEST 还是空的, 所有源文件夹都会进 summary.txt 的
SKIPS 列表. 在 dryrun.py 顶部按需添加条目后再跑.
步骤 2: 节流执行
python scripts/execute.py 200 50
python scripts/execute.py 200 0
python scripts/execute.py 100 0
参数: [throttle_ms] [limit_count]。0 表示不限制。
支持中断恢复: 进度保存在 state.json, 重跑自动跳过已完成。
步骤 3: 清理空目录
python scripts/remove_empty.py
📋 命名规范
1. 顶层目录
格式: <中文名> (<English Name/Romaji>) {anilist-<id>}/
- 例:
示例番剧 (Example Show) {anilist-12345}/
- 例:
示例番剧 (Example Show)/ (无 AniList 时省略 {anilist-XXX})
- 找不到 AniList ID 时传
"?" 占位, 文件夹名仍合规 (脚本会剥离 ?)
2. 季文件夹
Season 01/, Season 02/, ... (两位数补零)
- 单季/OVA/剧场版集合:
Season 01/
- NCOP/NCED/CM/Menu/PV/OVA:
.specials/ (隐藏目录)
3. 文件名
S01E01.mp4 (季集号, 两位数补零)
S00E01.mkv (specials 季)
NCOP__xxx.mkv (无字幕 OP), 标签: NCOP / NCED / MENU / CM / PV / EXTRA
COMPLETE__xxx.mkv (无法解析集号的整集文件, 如单集电影)
4. 多季合一 (Plex 兼容模式)
将多季全部放入 Season 01, 集数连号; 在 dryrun.py 的 MANIFEST 中用 ep_offset 实现:
- S1 (1-25) + S2 (26-50) + S3 (51-66) → Season 01 / S01E01..S01E66
- 每季的
ep_offset = 前面所有季的累计集数
5. 不允许的字符
以下字符会被 T() 函数自动剥离(远程挂载 / FAT / NTFS 不支持):
? * < > | " \ : / (换行)
建议源文件名也避免使用。
🔧 扩展 MANIFEST
dryrun.py 顶部的 MANIFEST 字典是核心配置。脚本里以注释模板形式列出了
4 种典型模式 (单季 / 多季 / 多季合一 / 合集展开), 复制匹配的模板, 取消注释,
把 <占位符> 替换成你自己的数据即可。
MANIFEST 的 key 必须精确匹配源文件夹在磁盘上的目录名。
可以在 ALIASES 字典中做 GB 编码纠错 / 别名映射。
简单形式 (单季)
MANIFEST["<source-folder-name>"] = SeriesSpec(
"<中文名>", "<English Name>", "<anilist-id>",
T("<中文名>", "<English Name>", "<anilist-id>"),
note="<free-form note>",
season_map={"__root__": "Season 01"},
)
多季形式
MANIFEST["<source-folder-name>"] = SeriesSpec(
"<中文名>", "<English Name>", "<anilist-id>",
T("<中文名>", "<English Name>", "<anilist-id>"),
season_map={
"<S1-subfolder-name>": "Season 01",
"<S2-subfolder-name>": "Season 02",
"<S3-subfolder-name>": "Season 03",
"<specials-subfolder-name>": ".specials",
"<movie-subfolder-name>": ".specials",
},
)
多季合一 (ep_offset)
MANIFEST["<S1-source-folder>"] = SeriesSpec(
..., ep_offset=0, season_map={"__root__": "Season 01"})
MANIFEST["<S2-source-folder>"] = SeriesSpec(
..., ep_offset=25, season_map={"__root__": "Season 01"})
MANIFEST["<S3-source-folder>"] = SeriesSpec(
..., ep_offset=50, season_map={"__root__": "Season 01"})
父目录含集号
MANIFEST["<bundle-folder-name>"] = SeriesSpec(
"<中文名>", "<English Name>", "<anilist-id>",
T("<中文名>", "<English Name>", "<anilist-id>"),
season_map={
"<child-season-1-folder>": "Season 01",
"<child-season-2-folder>": "Season 01",
"<child-season-3-folder>": "Season 01",
},
)
合集展开 (子 spec)
把整个合集当作容器, 子目录各自映射为独立 anime。
MANIFEST["<bundle-folder-name>"] = SeriesSpec(
"<bundle中文名>", "<bundle English Name>", "?",
T("<bundle中文名>", "<bundle English Name>", "?"),
treat_as_collection=True,
season_map={
"<child1-subfolder>": "__child_spec_1__",
"<child2-subfolder>": "__child_spec_2__",
},
)
MANIFEST["__child_spec_1__"] = SeriesSpec(
"<child1中文名>", "<child1 English>", "<child1-anilist-id>",
T("<child1中文名>", "<child1 English>", "<child1-anilist-id>"),
season_map={"__root__": "Season 01"},
)
🛠️ 智能剧集号提取 (extract_episode)
按以下优先级提取集号:
| 优先级 | 模式 | 示例 |
|---|
| 1 | SxxExx (anime 标准) | MyShow_S01E05.mkv → 5 |
| 2 | E01 / EP01 前缀 | MyShow - 12 E01.mkv → 1 |
| 3 | 第X话/集/回/話 (中/繁) | 第01話「副标题」.mkv → 1 |
| 4 | [XX] 方括号 (1-100) | [Group][05][1080p].mkv → 5, [Group][17v2].mkv → 17 |
| 5 | - XX/_ XX + 分隔符 | MyShow_S3 - 01.mkv → 1, MyShow (2023) 32].mkv → 32 |
| 6 | 简单文件名 | 01.mkv → 1 |
⚙️ 分辨率/画质优先级
自动检测文件名中的 4K/2160p/1080p/720p, 重复集数保留最高画质:
- 4K (2160p) > 1440p > 1080p > 720p > 480p
- 同画质时: 文件大小 / 编码格式 由
dedupe_by_target_episode() 处理
🚦 特殊文件自动归类
文件名含以下关键字 → .specials/:
NCOP / NCED (无字幕 OP/ED)
OP / ED (空格包围, 避免误判)
PV / CM / Menu / Creditless / Trailer
Extra / OVA / OAD / SP / Offline
例: MyShow [02 Long Trailer] → .specials/EXTRA__...
📝 已知特殊 case 与处理
| 情况 | 解决方案 |
|---|
| 重复剧集不同画质 | dedupe_by_target_episode() 自动保留最高画质 |
| 同一合集有 S1-S5 | 放入 Season 01 + 季间 ep_offset |
| 嵌套两层子目录 (release 风格) | rglob("*") 递归遍历 |
| 父目录含集号, 文件含季内编号 | 优先用父目录的 episode |
文件名带 v2/v3 修正版 | [17v2] 解析为 17 |
| 多版本 (高帧率 vs 普通) | 保留高帧率, 其他被 dedup |
| 季和 1 命名 | ep_offset 机制 |
远程挂载不支持 ? * : | | 自动剥离 (T() 函数) |
| I/O 错误的损坏文件 | 跳过, 不删除 (云盘挂载常见) |
🆕 新增番剧工作流
当有新番剧文件夹要加入时:
-
先 dry-run 探索:
python scripts/dryrun.py
看输出 summary.txt 的 SKIPS 部分, 找到未识别的文件夹名。
-
在 dryrun.py 的 MANIFEST 中添加:
MANIFEST["<新番剧源文件夹名>"] = SeriesSpec(
"<中文名>", "<English Name>", "<anilist-id>",
T("<中文名>", "<English Name>", "<anilist-id>"),
season_map={"__root__": "Season 01"},
)
-
重新 dry-run 确认:
python scripts/dryrun.py
-
执行:
python scripts/execute.py 200 0
-
清理:
python scripts/remove_empty.py
🔍 AniList ID 查询
⚠️ 远程挂载特殊注意
- 节流: 至少 100ms/move, 推荐 200ms
- 不可用字符:
? * : | < > " \ / 必须剥离
- I/O 错误: 某些文件可能损坏, 跳过不删除
- 空目录缓存:
rmdir 可能报 "No such file" 但目录还在, 重新 ls 即可
- 断点续传:
state.json 自动保存, 中断后重跑会跳过已完成的
📁 目录结构示例 (整理后)
<你的番剧库根目录>/
├── 番剧A (Show A) {anilist-10001}/
│ └── Season 01/
│ ├── S01E01.mp4
│ ├── S01E02.mp4
│ └── COMPLETE__第11.5话.mp4
│
├── 番剧B (Show B) {anilist-10002}/
│ └── Season 01/
│ ├── S01E01.mp4 - S01E25.mp4 (S1)
│ ├── S01E26.mp4 - S01E50.mp4 (S2)
│ └── S01E51.mp4 - S01E66.mp4 (S3)
│
├── 番剧C (Show C) {anilist-10003}/
│ ├── Season 01/...S01E12.mkv
│ ├── Season 02/...S02E12.mkv
│ ├── Season 03/...S03E12.mkv
│ └── .specials/
│ ├── NCOP__xxx.mkv
│ ├── NCED__xxx.mkv
│ └── COMPLETE__剧场版.mkv
│
└── 番剧D (Show D) {anilist-10004}/
└── Season 01/
├── S01E01.mp4 - S01E24.mp4 (S1+S2)
├── S01E25.mp4 - S01E36.mp4 (S3)
├── S01E37.mkv - S01E48.mkv (S4)
├── S01E49.mkv (S5 4K)
└── S01E50.mp4 - S01E59.mp4 (S5 1080p)
🐛 故障排查
| 症状 | 原因 | 解决 |
|---|
PermissionError: [Errno 1] mkdir | 文件夹名含 ? * : 等 | 检查 T() 函数的字符剥离 |
大量文件移到 .specials/ | 文件名误判为 special | 检查 is_special_filename 关键字 |
| SxxExx 集号错乱 | 正则匹配到了 release tag | 加 S\d{1,2}E 必须紧跟数字, 不带 "Season" |
| 跨合集去重失败 | 不同合集有不同 AniList ID | 检查 target name 是否一致 |
| 远程挂载 rmdir 失败 | 缓存问题 | 重新 ls 一次, 或重启挂载 |
Plan file not found | ANIME_OUT 路径不一致 | 确保 dryrun / execute 用同一路径 |
📋 配置速查
环境变量:
ANIME_SRC - 源根目录
ANIME_OUT - dryrun 输出目录
execute.py CLI 参数:
THROTTLE_MS - 默认 200ms, 远程挂载请勿低于 100ms
LIMIT - 默认 0 (不限制)