- 8 枚 shields.io 徽章(MIT/v9.6/Bash/ShellCheck/120 tests/Google Shell/Gitea/PRs) - 5 幅 Mermaid 图:三阶段流水线、配置优先级链、搜索重试链、AI 判断/执行分离、增量账本决策流 - 6 处 GitHub 提示块(NOTE/TIP/WARNING,规范格式防 Prettier 折叠) - 结构精简:1438 行 → 543 行,保留模式/退出码/配置/缓存/特典/AI/纠错等全部关键事实 - 质量验证:Prettier 3 + markdownlint-cli2 0 问题 + 死链检查 0 确认失效
35 KiB
🎬 MediaOrganizer
Jellyfin 媒体库硬链接整理脚本:通过 TMDB API 识别电影、电视剧、音乐与音乐视频,用硬链接在同一文件系统内零拷贝创建 Jellyfin 标准目录结构。AI 辅助识别、全量镜像缓存、特典自动归类、季数偏移、增量整理——一次整理,终身整洁。
作者:LetsShareAll | 许可:MIT | 语言:Bash(零运行时依赖,单文件分发)
📑 目录
- ✨ 特性一览
- 🚀 快速开始
- 📦 获取项目
- 🧭 命令行模式与选项
- 📖 使用示例
- 📊 架构与执行流程
- 🗂️ 输出目录结构
- 🛠️ 配置详解
- 🧠 核心机制
- 🧪 测试与开发
- 🐛 故障排除
- 🤝 贡献指南
- 📄 许可证与致谢
✨ 特性一览
| 特性 | 说明 |
|---|---|
| 🎬 智能识别 | TMDB API 匹配电影与电视剧,支持多种文件名格式,媒体类型自动判定 |
| 🔗 硬链接整理 | 同一文件系统内零拷贝,不复制数据、不占额外空间 |
| 🤖 AI 辅助识别 | OpenAI 兼容接口(默认 DeepSeek-V4-Flash):纠正搜索词、匹配甄别(选候选 id + 季映射)、判定专辑艺术家、学习特典映射;分批处理、失败可逆跳过 |
| 📚 全量镜像缓存 | mo_cache/tmdb/ 镜像 TMDB API 路径缓存全部请求,二次运行零外部请求;正常 30 天 / 查无此片 3 天 TTL;并发 flock 去重 |
| ⭐ 特典自动识别 | NCOP/NCED/Menu/PV/CM 等特典归入 Season 00;TMDB 匹配用 S00E{编号},未匹配用 S00{类型}{编号};AI 学习写回映射 |
| 🎵 音乐与音乐视频 | CD 音乐归 Music/歌手/专辑/曲目;Music Videos(v9.6)识别与 MusicVideos/{artist}/{title} 命名 |
| ♻️ 增量标记 | 已链接账本(inode 校验):cron 重跑只处理新文件;目标被删/源被替换自动失效重新链接 |
| ⏳ 失败冷却 | 尽力后失败的条目登记冷却(默认 24h),冷却期内跳过、过期自动重试,不重复打 API |
| ✏️ 手动纠错 | --export-map 伴生 .ledger.json:改目标路径 + --rerun 重跑;修正结果自动学习(corrections.json) |
| 🔎 搜索重试链 | zh-CN → en-US → 规则别名 → 目录名兜底 → MAL(可选)→ 后缀季二次剥离 |
| 🧮 季数偏移 | 处理 TMDB 季数与实际集数不符的剧集(自动 / 手动两种偏移,含后缀季 Railgun T) |
| 🧭 命名模板 | 电影/剧集/特典/音乐/音乐视频命名格式全部可配置(NAMING_*),默认与旧版输出逐字节一致 |
| 🎛️ 用户匹配规则 | match_rules.json:搜索别名 + ID 映射,确定性兜底,优先于 AI,零 AI 轮次消耗 |
| 🗂️ 配置按类目分区 | 特典映射/词表/跳过目录/季偏移支持 movie/tv/music/musicvideo 分区(v9.6) |
| 📏 AI 多维信号 | AI 甄别输入附带 ffprobe 时长/分辨率 + 同目录兄弟文件列表——区分剧场版/OVA/正片 |
| 🧩 AI 响应容错 | 四级 JSON 容错提取(直接解析 → 剥围栏 → 剥离思考链 → 最外层 {}),推理模型不再整批失效 |
| 🖥️ 跨平台 | Linux / macOS(BSD stat 兼容)/ BusyBox(NAS / OpenWrt) |
| 📦 单文件分发 | dist/media_organizer 自包含全部配置模板,无需携带任何配套文件 |
处理流程(三阶段)
flowchart LR
A["scan_files 扫描源目录<br>视频 VIDEO_FILES + 音频 AUDIO_FILES"] --> B["第一阶段 识别池<br>process_video + process_audio<br>并发 MEDIA_WORKERS 个 worker<br>parse → TMDB 识别 → register 登记"]
B --> C["第二阶段 AI 批处理<br>run_ai_batch(AI_BATCH_SIZE 条/批)<br>纠正搜索词 / 匹配甄别 / 判定艺术家 / 学习特典<br>AI 失败 → 剩余显式跳过(可逆)"]
C --> D["第三阶段 硬链接<br>link_media:mkdir → ln(幂等 / 撞名去重)<br>→ 伴随文件(字幕 / 音轨 / 歌词 / 封面)"]
D --> E["运行级汇总 + 退出码分级<br>0 全部成功 / 3 部分失败"]
🚀 快速开始
Note
零依赖设计:
dist/media_organizer是自包含单文件产物(bashly 生成),仅需系统自带的bash/curl/jq/ffprobe,无需安装 Ruby 或任何语言运行时。构建工具链(bashly)仅在从源码二次构建时需要。
# 1. 获取分发脚本(单文件即用)
curl -O https://gitea.ppuc.lssa.fun/Shuery/MediaOrganizer/raw/branch/main/dist/media_organizer
chmod +x media_organizer
# 2. 首次运行:自动生成配置模板(询问时输入 y)
./media_organizer /downloads /media
# 3. 填写 TMDB 密钥
chmod 600 mo_config/config.json # 配置含密钥,权限收紧
# 编辑 mo_config/config.json,至少填写 TMDB_API_RA_TOKEN
# 4. 干运行预览(零副作用:不建链接、不写缓存、不写日志)
./media_organizer --dry-run /downloads /media
# 5. 正式运行
./media_organizer /downloads /media
Warning
源目录与目的目录必须在同一文件系统上,否则无法创建硬链接(脚本启动时会自动检测并报错)。
Tip
需要 TMDB API 密钥?前往 TMDB 官网 免费申请;AI 密钥可选用 DeepSeek 等任意 OpenAI 兼容端点(
AI_BASE_URL/AI_MODEL可配)。
📦 获取项目
# 自托管 Gitea:SSH 或 HTTPS 克隆
git clone [email protected]:Shuery/MediaOrganizer.git
# 或
git clone https://gitea.ppuc.lssa.fun/Shuery/MediaOrganizer.git
- 免构建即用:
dist/media_organizer随仓库提交,Gitea 可 raw 直接下载,无需任何工具链 - 从源码构建(约 1 秒,需 Ruby + bashly):
./build.sh - 开发配套文档:
CONTEXT.md(领域术语表)、docs/PROJECT_REPORT.md(技术报告)、docs/adr/(23 份架构决策记录)
🧭 命令行模式与选项
一次调用对应一种模式(mode),由目录参数个数与修饰标志共同决定:
| 模式形状 | 调用形式 | 行为 |
|---|---|---|
organize |
[选项] <源目录> <目的目录> |
正常整理(使用缓存) |
organize + update |
--update-cache <源目录> <目的目录> |
整理并强制重取对应缓存 |
cache-only |
--update-cache <源目录> |
仅更新缓存,不整理(更新完退出) |
list |
--list-cache [关键词] |
列出缓存条目(可选关键词按类型/路径/参数过滤) |
export-map |
--export-map [目录] |
生成源→目标对照表 markdown + 伴生纠错账本 .ledger.json |
rerun |
--rerun <账本文件> |
手动纠错重跑:只做链接层,不重新识别(可叠加 --dry-run 预览) |
选项:
| 选项 | 说明 |
|---|---|
-a, --automated |
自动化模式:写入日志文件,跳过无法处理的项目(适合 cron) |
--no-automated |
取消自动化模式(覆盖环境变量 / config.json 设置) |
--dry-run |
干运行:不创建链接、不写缓存、不写日志;网络请求照常但不落盘 |
--no-dry-run |
取消干运行(覆盖环境变量 / config.json 设置) |
--refresh-cache |
清空 TMDB 缓存后执行(保留特典映射等脚本学习数据) |
--update-cache |
强制重取对应缓存条目(与 --list-cache 互斥) |
--src-dir <目录> / --dest-dir <目录> |
以选项形式指定源/目的目录(与位置参数互斥,二选一通道) |
-h, --help / --version |
帮助 / 版本(退出码 0) |
Note
退出码分级:
0= 全部成功;1= 运行期错误;2= 用法错误(未知选项、参数个数、模式冲突);3= 部分失败(非预期跳过 / 请求失败 / 链接失败 > 0,供 cron 感知)。
📖 使用示例
# 基础整理
./dist/media_organizer /downloads /media
# 干运行预览(推荐先跑一次)
./dist/media_organizer --dry-run /downloads /media
# 自动化模式 + cron 定时增量整理(已链接/冷却条目自动跳过,只处理新文件)
0 3 * * * /path/to/dist/media_organizer -a /downloads /media
# 缓存维护
./dist/media_organizer --list-cache # 列出全部缓存条目
./dist/media_organizer --list-cache 刀剑 # 关键词过滤
./dist/media_organizer --update-cache /downloads # 仅刷新缓存
./dist/media_organizer --update-cache /downloads /media # 刷新并整理
./dist/media_organizer --refresh-cache /downloads /media # 清空 TMDB 缓存后整理
# 手动纠错闭环:导出对照表 → 编辑账本 → 重跑
./dist/media_organizer --export-map /downloads /media # 生成 mo_map/*.md + *.ledger.json
# 编辑 *.ledger.json:修改 dest 路径,将 action 改为 "rerun"
./dist/media_organizer --rerun mo_map/downloads-xxxx.ledger.json # 按账本重跑(不重新识别)
Tip
纠错结果会被学习到
corrections.json:同命名系列文件下次识别直接命中你指定的目标,无需再次修正。
📊 架构与执行流程
配置优先级链
配置值统一按 CLI > 环境变量 > config.json > 内置默认值 解析,同一键只取最高优先级来源;CLI 显式设置(含 --no-* 反选)覆盖一切。
flowchart LR
A["CLI 参数<br>--dry-run / --src-dir ..."] --> P{"最终配置值"}
B["环境变量<br>TMDB_LANG / AI_MODEL ..."] --> P
C["config.json<br>mo_config/ 白名单键"] --> P
D["内置默认值<br>CONFIG_DEFS 单一数据源"] --> P
识别搜索重试链
flowchart TD
A["识别搜索<br>identify_tv_show / identify_movie"] --> B["zh-CN 主搜索"]
B -- "无结果" --> C["en-US 重搜"]
C -- "无结果" --> D["规则别名 search_aliases"]
D -- "无结果" --> E["目录名兜底<br>(清洗 + 剥季后缀)"]
E -- "无结果" --> F["MAL 候选回搜<br>(SEARCH_FALLBACK_MAL=true,可选)"]
F -- "无结果" --> G["后缀季二次剥离<br>(Railgun T / II / 2nd)"]
G -- "仍失败" --> H["交 AI 待处理<br>(失败可逆跳过,下次自动重试)"]
Note
ID 映射优先于一切搜索:
match_rules.json的id_maps标题命中后直接使用指定 TMDB ID、完全跳过搜索(适合 TMDB 多候选易选错、同名动画/真人版场景);search_aliases仅在常规搜索无结果时介入。
AI:判断引擎与执行引擎分离
flowchart TD
subgraph AI["🤖 AI 只做判断"]
A["输入:文件信息 + 目录链 +<br>TMDB 搜索结果 + 时长/分辨率<br>+ 同目录兄弟文件"] --> B["输出:choice / season_shift<br>/ search_term / artist /<br>特典匹配关键字"]
end
subgraph SCRIPT["📜 脚本负责执行"]
C["命名格式化(年份 / Season 补零<br>/ SxxExx / 目录结构)"]
D["硬链接 / 账本 / 汇总"]
end
B --> C --> D
- AI 输出交叉验证:特典学习产物必须是 TMDB 候选列表成员,防幻觉污染
- 失败语义:AI 请求失败 → 剩余条目显式跳过(可逆,下次运行自动重试);仅"AI 成功且判断无解"才走回退命名
🗂️ 输出目录结构
脚本在目的目录创建 Jellyfin 官方规范的结构(根目录名默认跟随系统语言,FOLDER_* 可配):
/media
├── Movies/ # 🎬 电影:目录名与文件名同名
│ └── Inception (2010)/
│ └── Inception (2010).mkv
├── Shows/ # 📺 剧集
│ ├── Breaking Bad (2008)/
│ │ └── Season 01/
│ │ ├── S01E01 - Pilot.mkv
│ │ └── S01E01 - Pilot.srt ← 伴随文件(保留语言标签)
│ └── Some Anime (2020)/
│ └── Season 00/ # ⭐ 特典
│ ├── S00E01 - 迷你动画「猫猫的独语」第1话:白粉.mkv ← TMDB 匹配(S00E 编号)
│ ├── S00CM01 - CM01.mkv ← 未匹配(S00{类型}{编号})
│ └── S00Menu01 - Menu01.mkv
├── Music/ # 🎵 CD 音乐(flac/mp3)
│ └── 平井大/
│ └── 幸せのレシピ/
│ └── 01. 幸せのレシピ.flac
└── MusicVideos/ # 🎤 音乐视频(v9.6)
└── 周杰伦/
├── 晴天.mkv
└── 七里香.mp4
命名规则:
| 类型 | 规则 |
|---|---|
| 电影 | Movies/标题 (年份)/标题 (年份).扩展名 |
| 剧集 | Shows/标题 (年份)/Season NN/S{季}E{集} - 集名.扩展名(多集区间 S01E01-E02 - 首集名 - 末集名) |
| 特典(TMDB 匹配) | Shows/标题 (年份)/Season 00/S00E{集号} - TMDB集名.扩展名 |
| 特典(未匹配) | Shows/标题 (年份)/Season 00/S00{类型}{编号} - 片段.扩展名(如 S00CM01、S00Menu01) |
| 音乐 | Music/歌手/专辑/[子碟]/曲目.扩展名(一夹一专辑) |
| 音乐视频 | MusicVideos/歌手/歌名.扩展名(artist 元数据 → 目录名 → FOLDER_UNKNOWN) |
| 伴随文件 | 与主视频同名,保留语言标签(.zh.srt、.jp.ass);视频 > 音频 归属判定 |
Tip
剧集文件名不含剧集名——Jellyfin 通过父目录(
Shows/标题 (年份)/)识别剧集,不影响刮削。撞名按官方多版本格式去重(文件名追加- 2),同源旧链接按 inode 清理。
🛠️ 配置详解
配置文件体系
三类数据严格分离,各自独立生命周期:
mo_config/ # 用户配置类文件(可编辑 + 脚本可写回)
├── config.json # 主配置(含 API 密钥,权限 600)
├── special_maps.json # 特典映射(AI 学习写回)
├── special_keywords.json # 特典识别词表(AI 学习写回)
├── skip_directories.json # 跳过目录词表(AI 学习写回)
├── season_offsets.json # 季数偏移(运行时生成)
├── special_categories.json # 特典类别判定表(v9.6 外置)
└── match_rules.json # 用户匹配规则(搜索别名 / ID 映射)
mo_cache/ # 缓存根目录(可再生)
├── tmdb/ # TMDB API 响应缓存(--refresh-cache 仅清空此区)
│ ├── search/movie|tv/<hash>.json # 搜索缓存(包裹格式,含空哨兵)
│ ├── tv/<id>.<lang>.json # 剧集详情(语言段隔离)
│ ├── tv/<id>/season/<n>.<lang>.json # 每季数据(含 season 0 特典季)
│ └── movie/<id>.<lang>.json # 电影详情
└── media_organizer/ # 脚本运行状态(不清除)
├── linked.json # 已链接账本(增量标记)
├── fail_cooldown.json # 失败冷却
├── corrections.json # 纠错学习
├── mal/ # MAL 搜索缓存
└── ai_cases/ # AI 用例落盘(AI_SAVE_CASES=true 时)
Note
旧版本配置文件(旧文件名 / 旧位置)在首次运行时自动迁移到上述布局,无需手工处理。
config.json 主要配置项
参考 config.example.json 模板(不含真实密钥)。值统一为字符串,文件权限 600:
| 配置项 | 默认值 | 说明 |
|---|---|---|
TMDB_API_RA_TOKEN / TMDB_API_KEY |
(空,必填之一) | TMDB 认证:Bearer Token(推荐)或 API Key |
TMDB_LANG |
zh-CN |
API 查询语言;缓存按语言段隔离,切换自动重取 |
TMDB_DELAY / TMDB_CURL_RETRY |
1 / 3 |
请求间延迟(防限流)/ 重试次数 |
VIDEO_EXTS / AUDIO_EXTS / SUB_EXTS |
常见媒体扩展名 | 视频 / 音频 / 字幕扩展名清单 |
CACHE_TTL_DAYS / CACHE_EMPTY_TTL_DAYS |
30 / 3 |
正常缓存 / 空哨兵 TTL(查无此片短 TTL,新片出现后自动发现) |
AI_API_KEY |
(空,留空禁用 AI) | AI API 密钥(OpenAI 兼容) |
AI_BASE_URL / AI_FULL_URL |
https://api.deepseek.com / 空 |
AI 端点(AI_FULL_URL 优先,默认 {BASE}/v1/chat/completions) |
AI_MODEL |
DeepSeek-V4-Flash |
AI 模型 |
AI_MAX_CALLS / AI_BATCH_SIZE |
10 / 50 |
AI 批次上限 / 每批条目数(成本控制) |
AI_DRY_RUN / AI_SAVE_CASES |
false |
AI 干运行(不产生费用)/ 用例落盘(复盘 AI 输入输出) |
SEARCH_FALLBACK_MAL / MAL_BASE_URL |
false / https://api.jikan.moe/v4 |
可选 MAL 兜底搜索(外部 API 依赖,默认关闭) |
FAIL_RETRY_COOLDOWN_HOURS |
24 |
失败冷却时长(0 禁用) |
MEDIA_WORKERS |
4 |
识别池并发数 |
FOLDER_MOVIES / FOLDER_SHOWS / FOLDER_MUSIC / FOLDER_MUSICVIDEOS / FOLDER_UNKNOWN |
跟随系统语言 | 目的目录根名(对齐 Jellyfin 官方库名 Movies/Shows/Music/MusicVideos) |
LOG_FILE / SKIP_LOG_FILE / LOG_ROTATE_MB |
/var/log/... / 10 |
自动化日志路径 / 跳过记录 / 轮转阈值(超阈值滚动保留 5 份) |
命名模板(NAMING_*)
命名格式化外置为可配置模板(v9.5),默认模板与旧版输出逐字节一致:
| 语法 | 含义 | 示例 |
|---|---|---|
{name} |
变量值原样插入 | {title} → Sword Art Online |
{name:NN} |
数字补零至 NN 位 | {season:02} → 01 |
{?name:text} |
条件段:name 非空才渲染 text(text 内可含其他占位符) | {?year: ({year})} → (2012) 或空 |
{
"NAMING_MOVIE": "{title}{?year: ({year})}",
"NAMING_SHOW": "{title}{?year: ({year})}",
"NAMING_SEASON": "Season {season:02}",
"NAMING_EPISODE": "S{season:02}E{episode:02}{range}{?episode_name: - {episode_name}}",
"NAMING_SPECIAL": "S00{tag} - {fragment}",
"NAMING_MUSIC": "{artist}/{album}",
"NAMING_MUSICVIDEO": "{artist}/{title}",
}
Warning
Season NN与SxxExx是 Jellyfin 解析格式——修改NAMING_SEASON/NAMING_EPISODE默认结构可能导致刮削失败,请仅在了解后果时自定义。未知占位符渲染为空并打印警告。
用户匹配规则(match_rules.json)
{
"search_aliases": {
"俺妹": "Ore no Imouto ga Konna ni Kawaii Wake ga Nai",
"路人女主剧场版": { "term": "Saekano the Movie", "year": "2019" }
},
"id_maps": {
"movie": { "刀剑神域": 20982 },
"tv": { "魔法禁书目录": 4654 }
}
}
- 搜索别名:主搜索(zh → en)无结果时用重搜词兜底——适合本地俗称/简称(TMDB 搜不到"俺妹"但能搜到全名)
- ID 映射:标题命中直接使用指定 TMDB ID,完全跳过搜索(最高优先级)——适合同名动画/真人版易选错的场景
Tip
匹配规则优先级:ID 映射 > 常规搜索链(zh → en → 别名 → 目录名 → MAL)> AI。规则全部命中即走确定性路径,不消耗 AI 轮次。
类目分区(v9.6)
special_maps / special_keywords / skip_directories / season_offsets 四个配置类文件支持分区形态:顶层按类目分键,未配置回退 default 分区 → 全局表:
{
"tv": ["menu", "ncop", "nced", "pv", "cm"],
"musicvideo": ["mv", "music video", "live", "concert", "演唱会"],
"default": ["特典", "特番"]
}
Note
分区语义因文件而异:
skip_directories分区形态 = 完整语义(不叠加内置默认);special_keywords分区 = 叠加语义(积累型词表)。AI 学习写回自动适配分区(写 tv / video 分区)。
🧠 核心机制
缓存设计
- 镜像缓存:
mo_cache/tmdb/路径结构镜像 TMDB API 端点,一切请求数据全量落盘;详情/季缓存带语言段(tv/<id>.<lang>.json),切换TMDB_LANG自动 miss 重取 - 并发去重:识别池多 worker 同时 miss 同一查询时,
flock互斥 + 锁内双检——同一查询只发一次 TMDB 请求 - 空哨兵:搜索"查无此片"写
empty:true包裹缓存(3 天短 TTL),新片出现后自动重新搜索;请求失败不写缓存,下次运行重试 - 原子写:先写临时文件再
rename,避免并发/中断产生半截 JSON
增量账本与失败冷却
flowchart TD
A["识别池入口 process_one_file"] --> B{"纠错学习命中?<br>corrections.json"}
B -- "是" --> C["直接用用户指定目标<br>(跳过 parse/识别/AI)"]
B -- "否" --> D{"已链接账本命中?<br>源存在 + 目标存在 + 同 inode"}
D -- "是" --> E["结局 already_linked 跳过"]
D -- "否" --> F{"失败冷却中?<br>request_failed / skip_unidentified"}
F -- "是" --> G["结局 cooldown 跳过<br>(过期自动重试)"]
F -- "否" --> H["正常识别 + 链接<br>账本运行末尾统一落盘"]
- 任一账本失效(目标被删 / 源被替换)自动重新识别 + 链接,可逆语义完整
- 干运行不落盘、不启用跳过(展示全貌)
AI 辅助识别
- 批量:每批
AI_BATCH_SIZE条(默认 50),AI_MAX_CALLS为批次上限(默认 10),jq安全转义构造输入 - 四类任务合并为一次请求:搜索词纠正 / 特典映射学习 / 专辑艺术家判定 / 匹配甄别
- 判断与执行分离:AI 只输出判断(
choice/season_shift/search_term),命名格式化始终由脚本构建——确定性职责不交给 AI - 失败可逆:AI 请求失败 → 剩余条目显式
skip_unidentified(下次自动重试);只有"AI 成功且判断无解"才走回退命名(降级成功,单列一类显示) - 防幻觉:特典学习产物必须 ∈ TMDB 候选列表(交叉验证);跳过目录学习产物必须实际出现在输入目录链中
特典识别(Season 00)
判定信号 = 文件名方括号标记(含特典词)+ 特典父目录(SPs//CDs//Bonus/ 等)。归属判断:媒体目录仅 1 个正片 → 电影特典直接跳过(TMDB/Jellyfin 不收录);多个正片 → 剧集特典 Season 00。匹配用多语言别称(本地关键字 → keymap 多语言值 → TMDB season 0 候选,三级匹配:精确 > 最短前缀 > contains)。
E' = E + S_1,\qquad S' = 1 \quad\text{(自动偏移:TMDB 第一季集数 } S_1 \text{)}
🧪 测试与开发
一键命令
make check # lint + 单元测试 + 产物一致性(等同 CI 全部关卡)
make test # 单元测试
make lint # bash -n + ShellCheck 静态检查(零容忍)
make build # 构建 dist/media_organizer
单元测试
零依赖纯 bash 测试框架:加载全部模块,每个用例文件在独立子 shell 运行(全局状态自动隔离):
./tests/run.sh # 运行全部用例
./tests/run.sh cache # 仅运行名字含 cache 的用例文件
./tests/run.sh -v # 详细模式
Note
当前 120 通过 / 3 已知失败(分区回退、季偏移分区、音乐视频双命中——见
docs/PROJECT_REPORT.md的记录)。CI 流水线(.github/workflows/ci.yml)执行:ShellCheck 静态检查 → 单元测试 → 构建 →./build.sh --check产物一致性校验(源码变更后必须重新构建并提交dist/media_organizer,否则 CI 失败)。
源码结构
build.sh # 构建脚本:bashly generate → 单文件分发脚本 dist/media_organizer
dist/ # 构建产物(media_organizer,随仓库提交,Gitea 可 raw 直接下载)
src/
bashly.yml # CLI 定义(选项/位置参数/互斥/帮助文本),bashly 读取
root_command.sh # root 命令实现(参数映射/形状校验/主流水线),由 bashly 包装
lib/
main.sh # 常量 + 全局变量 + 配置模板(最先加载)
log.sh strings.sh # 基础设施:日志与色彩 / 字符串工具
config/ # 配置与规则域:config.sh / maps.sh / rules.sh
storage/ # 数据持久化域:cache.sh / ledger.sh
integrate/ # 外部集成域:tmdb.sh / ai.sh
media/ # 媒体识别域:filename.sh / identify.sh / ai_resolve.sh
pipeline/ # 执行流水线域:registry.sh / process.sh / link.sh / link_media.sh / report.sh
tests/
run.sh # 测试运行器(零依赖)
cases/ # 用例文件(按功能域同名分组)
lib/assert.sh # 断言库
Warning
不要直接编辑
dist/media_organizer——下次构建会覆盖你的修改。修改流程:编辑src/下对应模块 →./tests/run.sh→./build.sh→ 提交源码与产物。
🐛 故障排除
| 问题 | 解决方法 |
|---|---|
Permission denied config.json |
chmod 600 mo_config/config.json |
| TMDB API 请求超时 | 增大 TMDB_CURL_MAX_TIME(如 60)、TMDB_DELAY=2 |
| 识别准确度低 | 配置 AI_API_KEY 启用 AI 辅助,或添加 match_rules.json 规则 |
| AI 成本过高 | 减小 AI_MAX_CALLS(如 5),或先用 AI_DRY_RUN=true 测试 |
| 无法创建硬链接 | 确认源/目标在同一文件系统 |
特典全部未匹配(S00xxx 而非 S00E) |
--list-cache 确认 tmdb/tv/<id>/season/0.json 是否存在;缺失则 --update-cache 强制重取或 --refresh-cache 清空重跑;可在 special_maps.json 增强匹配 |
| 季数错乱 | 在 season_offsets.json 添加偏移值(键 = 剧名小写或 id:TMDB_ID) |
目标文件被覆盖产生 .bak_* |
v9.1 起不再备份(直接替换),遗留 .bak_* 可手动清理 |
运行中报 Argument list too long |
旧版特典季 JSON 作为命令行参数所致;使用新版(独立文件存储) |
🤝 贡献指南
欢迎任何形式的贡献——Bug 报告、功能建议、文档改进、Pull Request!
- 提出问题:在 Issues 提交,请附上运行环境、复现步骤与相关日志
- 架构约定:动手前先读
CONTEXT.md(领域术语表)与docs/adr/(架构决策记录)——命名与职责边界是项目的一等公民 - 修改流程:编辑
src/模块 →./tests/run.sh跑相关测试 →./build.sh重新生成产物 → 提交源码与dist/media_organizer - 代码风格:遵循 Google Shell Style Guide + ShellCheck 零容忍(
./lint.sh) - 测试要求:新功能/修复请配套
tests/cases/下的用例
Note
本项目配套了完整的自动化质量闭环:
make check一条命令覆盖 lint + 测试 + 产物一致性,CI 强制dist/media_organizer与源码同步。
📄 许可证与致谢
本项目基于 MIT License 开源(见 LICENSE),允许自由使用、修改、分发,需保留版权声明。
致谢:
- TMDB(The Movie Database) —— 媒体元数据与搜索 API
- bashly —— Bash CLI 生成器(开发期工具)
- Jellyfin —— 开源媒体服务器,目录结构规范参照
- MyAnimeList / Jikan —— 可选兜底搜索 API
- Auto_Bangumi 等开源项目 —— 部分识别思路借鉴(详见 ADR 记录)
文档版本对应脚本 v9.6(SCRIPT_VERSION 为唯一权威版本号)。