- README 现代化重写:徽章区、特性一览、统一 GitHub 提示块格式、修正版本号与 AI 默认值 - 新增 docs/PROJECT_REPORT.md 学术项目报告(含许可证合规分析与死链修复记录) - 收录 23 份 ADR 架构决策记录与 CONTEXT.md 领域术语表 - 新增开发配置:.editorconfig / .shellcheckrc / .markdownlint-cli2.jsonc - .gitignore 补充 mo_map/、检查报告、编辑器临时文件 - 新增 config.example.json 配置模板(不含真实密钥)
31 KiB
基于 TMDB 与 AI 辅助的媒体库自动化整理系统设计与实现
——MediaOrganizer v9.6 技术报告
| 项目信息 | 内容 |
|---|---|
| 项目名称 | MediaOrganizer(Jellyfin 媒体库硬链接整理脚本) |
| 版本 | v9.6 |
| 作者 | LetsShareAll |
| 许可证 | MIT |
| 技术栈 | Bash 4.0+ / curl / jq / ffprobe / bashly 1.4 |
| 代码规模 | 22 个模块源文件,约 8 200 行,127 个函数 |
| 测试规模 | 14 个用例文件,123 个测试用例(120 通过 / 3 已知失败) |
摘要
随着数字媒体收藏规模的增长,个人媒体服务器(如 Jellyfin、Plex)面临两个核心挑战:一是媒体文件命名不规范导致刮削器(Metadata Scraper)无法正确识别,二是多份文件冗余存储造成磁盘空间浪费。本项目设计并实现了一个基于 TMDB(The Movie Database)API 与 AI 大语言模型辅助的媒体库自动化整理系统 MediaOrganizer,通过硬链接(Hard Link)技术在同一文件系统内零拷贝地创建 Jellyfin 标准目录结构,实现媒体库的自动化、规范化整理。
系统采用 Bash 脚本语言实现,以 bashly 框架生成单文件分发产物,具备三阶段流水线架构:识别池(并发解析文件名并通过 TMDB 搜索识别媒体)、AI 批处理(调用 OpenAI 兼容接口纠正搜索词、甄别匹配、学习特典映射)与硬链接阶段(创建目录结构与文件链接)。系统设计了全量镜像缓存、增量账本、失败冷却、季数偏移、特典自动归类、用户匹配规则、命名模板等十余项关键机制,并配套 120 余个单元测试用例与 CI 流水线。
测试结果表明:系统在 123 个测试用例中通过 120 个(通过率 97.6%),构建产物与源码保持一致性校验通过,Lint 检查零告警。系统已在 Linux / macOS / BusyBox 环境验证可用。
关键词:媒体库整理;TMDB API;硬链接;AI 辅助识别;Bash;Jellyfin
1 绪论
1.1 研究背景与意义
家庭媒体服务器用户通常从多种渠道获取影视资源,不同压制组的文件命名习惯差异巨大:有的采用 Title (Year) 格式,有的采用 Show S01E01 格式,还有的携带压制组标记与画质标签(如 [VCB-Studio] Show [1080p])。Jellyfin 等媒体服务器依赖文件名与目录结构进行元数据刮削,不规范命名会导致识别失败或错误匹配。
另一方面,用户常将同一资源保存在多个目录(下载目录、整理目录、共享目录),造成磁盘空间浪费。硬链接技术允许同一文件系统内的多个目录项指向同一 inode,不占用额外数据空间,是解决该问题的理想手段,但要求源与目标位于同一文件系统,且目录结构必须符合媒体服务器的刮削规范。
本项目旨在解决上述问题:自动识别媒体文件 → 判定规范目标路径 → 硬链接整理,全程无需人工干预(自动化模式),支持 cron 定时增量整理。
1.2 国内外研究现状
在开源社区中,已有若干同类项目:
| 项目 | 语言 | 特点 |
|---|---|---|
| Auto_Bangumi | Python | 面向动漫资源的自动重命名与整理,AB 解析结果可参与匹配 |
| Bangumi_Auto_Rename | Python | 基于 Bangumi 番组计划的自动重命名 |
| Sonarr / Radarr | C# | 完整的媒体管理套件,支持自动下载与整理,但依赖 .NET 环境且配置复杂 |
| 本系统 MediaOrganizer | Bash | 零运行时依赖、单文件分发、TMDB 识别 + AI 辅助、硬链接零拷贝 |
现有方案多依赖 Python/.NET 运行时,部署较重;本系统选择纯 Bash 实现,利用 find/jq/curl 等系统工具组合完成全部逻辑,单文件分发、无编译、无依赖安装,特别适合 NAS(群晖、威联通、OpenWrt 等嵌入式环境)。
1.3 主要研究内容
- 媒体文件名的多格式解析与媒体类型判定(电影/剧集/特典/音乐/音乐视频);
- 基于 TMDB v3 API 的媒体识别与全量镜像缓存设计;
- 基于 OpenAI 兼容接口的 AI 辅助识别与特典映射学习;
- 三阶段并发流水线与增量整理(账本 + 冷却)机制设计;
- 面向 Jellyfin 规范的目录结构生成与硬链接实现;
- 单元测试体系与 CI/CD 流水线建设。
1.4 论文组织结构
本文共七章。第 1 章为绪论;第 2 章介绍开发技术与工具链;第 3 章进行需求分析;第 4 章阐述系统详细设计;第 5 章说明编码实现要点;第 6 章给出系统测试方案与结果;第 7 章总结全文并展望后续工作。
2 开发技术
2.1 开发语言与运行环境
2.1.1 Bash
系统主体采用 Bash 4.0+ 编写,利用以下特性保证可维护性:
set -euo pipefail严格模式:未定义变量、管道失败、命令失败均立即终止,避免静默错误;- 关联数组(
declare -A):用于目的映射、结局账本、缓存等键值数据结构; [[ ]]条件表达式:比[ ]更安全(无单词拆分与路径名展开);- 进程替换
< <(...)与子 shell 隔离:实现并发池与测试隔离。
2.1.2 运行时依赖
| 依赖 | 版本要求 | 用途 |
|---|---|---|
| Bash | 4.0+ | 脚本运行环境 |
| curl | 任意 | TMDB / AI API 请求 |
| jq | 任意 | JSON 解析与构造 |
| ffprobe | 任意 | 媒体探测(时长/分辨率信号) |
系统无编译期依赖,产物为单个 Bash 脚本,可复制到任意目标机器直接运行。
2.2 开发工具链
2.2.1 bashly(CLI 生成框架)
bashly 1.4 是 Ruby 编写的命令行工具生成器:开发者以 YAML 声明参数、选项、帮助文本与互斥关系,bashly 生成完整的参数解析代码并包装自定义命令函数。本系统 CLI 定义位于 src/bashly.yml,构建时由 build.sh 调用 bashly 生成单文件分发脚本 dist/media_organizer。
# src/bashly.yml(节选)
name: media_organizer
help: |-
Jellyfin 媒体库硬链接整理脚本 v9.6
自动化整理媒体库:通过 TMDB API 识别电影与电视剧,并用硬链接创建
标准化的目录结构。支持 AI 辅助识别、全量缓存、特典映射、季数偏移、
Music Videos 音乐视频类目、配置按类目分区。
version: 9.6
args:
- name: source_dir
help: 源目录(--list-cache 模式下作为可选关键词)
- name: destination_dir
help: 目的目录
flags:
- long: --automated
short: -a
help: 自动化模式(写入日志,跳过无法处理项目)
- long: --dry-run
help: 干运行:不创建链接、不写缓存、不写日志
Note
示例代码节选自项目源码,格式符合 Google Shell Style Guide(2 空格缩进、
[[ ]]测试、local声明)。
2.2.2 代码质量工具链
| 工具 | 用途 | 集成方式 |
|---|---|---|
| shellcheck 0.11 | 静态分析(SC 规则零容忍) | lint.sh / CI |
| shfmt 3.13 | 格式化(Google 风格:2 空格缩进) | shfmt -i 2 -ci |
| bash -n | 语法检查 | lint.sh / CI |
| GitHub Actions | 持续集成 | .github/workflows/ci.yml |
CI 流水线:lint → 单元测试 → 构建 → 产物一致性校验,触发条件为 push/PR。
2.3 许可证合规分析
Warning
本报告由自动化工具扫描生成,仅供参考,不构成法律意见。正式分发前请咨询法律顾问。
2.3.1 主许可证
项目主许可证为 MIT License(LICENSE,Copyright (c) 2026 Shuery),允许自由使用、修改、分发与商用,仅要求保留版权声明。
2.3.2 依赖许可证清单
本项目运行时零第三方依赖(仅依赖系统自带的 bash/curl/jq/ffprobe),开发期依赖如下:
| 依赖 | 版本 | 许可证 | 类型 | 兼容性 |
|---|---|---|---|---|
| bashly | 1.4.0 | MIT | 开发期(CLI 生成) | ✅ 兼容 |
| completely | 1.4.0 | MIT | 开发期(bashly 传递依赖) | ✅ 兼容 |
| bash | 5.3+ | GPL-3.0 | 运行时(系统自带) | ✅ 兼容(不随项目分发) |
| curl | 任意 | MIT-like(curl 许可证) | 运行时(系统自带) | ✅ 兼容 |
| jq | 任意 | MIT | 运行时(系统自带) | ✅ 兼容 |
| ffprobe | 任意 | LGPL-2.1+(FFmpeg) | 运行时(系统自带) | ✅ 兼容 |
兼容性说明:
- 全部依赖许可证均为宽松许可证(MIT/LGPL),与 MIT 主许可证无冲突;
- bashly/completely 仅用于构建期,生成产物不包含其代码(纯文本生成器),不产生传染性义务;
- GPL-3.0 的 bash 与 LGPL 的 FFmpeg 为系统组件,随操作系统分发,不属于项目分发物;
3rd-party/目录下的 Auto_Bangumi、Bangumi_Auto_Rename 仅为本地参考仓库(.gitignore已排除),不随项目分发。
合规结论:✅ 无许可证冲突,项目可以 MIT 许可证对外分发。
3 需求分析
3.1 功能需求
3.1.1 核心功能
| 编号 | 需求 | 优先级 | 说明 |
|---|---|---|---|
| FR-01 | 媒体识别 | P0 | 通过 TMDB API 识别电影、电视剧,支持多种文件名格式 |
| FR-02 | 硬链接整理 | P0 | 同一文件系统内零拷贝创建 Jellyfin 标准结构 |
| FR-03 | 类型智能判定 | P0 | 无明确季集/年份特征时按媒体目录正片数量判定 |
| FR-04 | 特典自动归类 | P1 | NCOP/NCED/Menu/PV/CM 等特典归入 Season 00 |
| FR-05 | 持久化缓存 | P1 | 全量镜像缓存,二次运行零外部请求 |
| FR-06 | 增量整理 | P1 | 已链接账本跳过,cron 重跑只处理新文件 |
| FR-07 | AI 辅助识别 | P1 | 纠正搜索词、匹配甄别、学习特典映射 |
| FR-08 | 季数偏移 | P2 | 处理 TMDB 季数与实际集数不符 |
| FR-09 | 音乐/音乐视频归类 | P2 | CD 音乐归 Music,MV 归 MusicVideos |
| FR-10 | 用户匹配规则 | P2 | 搜索别名与 ID 映射的确定性兜底 |
| FR-11 | 手动纠错 | P2 | --export-map 伴生账本 + --rerun 重跑 |
3.1.2 命令行接口(CLI)
用法:media_organizer.sh [选项] <源目录> <目的目录>
模式形状:
<源目录> <目的目录> 正常整理(使用缓存)
--update-cache <源目录> <目的目录> 整理并强制重取对应缓存
--update-cache <源目录> 仅更新缓存,不整理
--list-cache [关键词] 列出缓存条目
--rerun <账本文件> 手动纠错重跑
选项:-a/--automated、--dry-run、--refresh-cache、--update-cache、
--list-cache、--export-map、--rerun、--src-dir、--dest-dir、-h、--version
退出码:0=全部成功;1=运行期错误;2=用法错误;3=部分失败(供 cron 感知)
3.1.3 配置需求
配置优先级链:CLI > 环境变量 > config.json > 内置默认值。配置文件采用 JSON 格式(白名单键读取,文件永不执行),包含 TMDB 认证、网络参数、文件类型扩展名、缓存 TTL、AI 参数、命名模板、目录根名等 50+ 配置项。
3.2 非功能需求
| 编号 | 需求 | 指标 |
|---|---|---|
| NFR-01 | 性能 | 识别池并发(默认 4 worker);缓存命中零外部请求 |
| NFR-02 | 可靠性 | 请求失败重试(curl 重试 3 次);失败冷却防每轮全量重试 |
| NFR-03 | 幂等性 | 重复运行结果一致;硬链接撞名去重(- 2) |
| NFR-04 | 可移植性 | Linux / macOS(BSD stat)/ BusyBox |
| NFR-05 | 可测试性 | 纯函数设计 + 子 shell 隔离测试框架 |
| NFR-06 | 安全性 | 配置文件权限校验(600);JSON 白名单解析不执行代码 |
3.3 用例模型
flowchart LR
U["用户/定时任务"] --> C1["整理媒体库<br>(正常模式)"]
U --> C2["干运行预览"]
U --> C3["仅更新缓存"]
U --> C4["列出缓存条目"]
U --> C5["手动纠错重跑"]
C1 --> S["系统<br>MediaOrganizer"]
S --> T["TMDB API"]
S --> A["AI API"]
S --> F["文件系统<br>(硬链接)"]
4 详细设计
4.1 系统总体架构
系统按功能域组织源码(不设通用 utils/ 目录),分为五个功能域 + 基础设施:
src/
├── bashly.yml # CLI 定义(bashly 读取)
├── root_command.sh # root 命令实现(bashly 包装)
└── lib/
├── main.sh # 常量 + 全局变量(最先加载)
├── log.sh strings.sh # 基础设施:日志与色彩 / 字符串工具
├── config/ # 配置域:加载 / 映射 / 命名模板 / 匹配规则
├── storage/ # 持久化域:TMDB 缓存 / 账本与冷却
├── integrate/ # 外部集成域:TMDB API / AI 批处理
├── media/ # 媒体识别域:文件名解析 / 识别 / AI 结果消费
└── pipeline/ # 流水线域:登记与并发池 / 扫描 / 链接 / 对照表
4.2 三阶段流水线设计
flowchart LR
A["scan_files 扫描源目录"] --> B["第一阶段:识别池<br>process_video + process_audio<br>并发 MEDIA_WORKERS worker<br>parse → identify_movie / identify_tv_show"]
B --> C["第二阶段:run_ai_batch<br>AI 纠正搜索词 + 匹配甄别<br>+ 特典映射学习 + 艺术家判定"]
C --> D["第三阶段:link_media<br>mkdir → 硬链接 → 配套文件"]
D --> E["汇总 + 退出码分级"]
并发契约:bash 子 shell 无法写父进程关联数组,识别池采用**协议行(emit)**机制——每个 worker 子进程产出 key\tvalue 协议行,父进程逐行合并登记,这是并发与数据聚合的唯一契约。
4.3 数据存储设计
4.3.1 三类数据分离
| 区域 | 内容 | 生命周期 |
|---|---|---|
mo_config/ |
用户配置类文件(config.json、特典映射、词表、季偏移、匹配规则) | 用户编辑 + AI 学习写回 |
mo_cache/tmdb/ |
TMDB API 响应缓存(镜像 API 路径) | 可再生,--refresh-cache 清除 |
mo_cache/media_organizer/ |
运行状态(已链接账本、失败冷却) | 增量累积 |
mo_cache/
├── tmdb/ # TMDB API 响应缓存
│ ├── search/movie/<hash>.json # 搜索缓存(包裹格式)
│ ├── search/tv/<hash>.json
│ ├── tv/<id>.<lang>.json # 剧集详情(语言段随 TMDB_LANG)
│ ├── tv/<id>/season/<n>.<lang>.json # 每季数据(含 season 0 特典季)
│ └── movie/<id>.<lang>.json # 电影详情
└── media_organizer/
├── linked.json # 已链接账本(增量标记)
└── fail_cooldown.json # 失败冷却
缓存关键机制:
- 镜像路径:缓存目录结构镜像 TMDB API 端点,二次运行零外部请求;
- 并发去重:
flock跨进程互斥 + 锁内双检(同一查询只发一次请求); - 空哨兵:空结果写
empty:true哨兵(3 天短 TTL),新片出现后自动重新搜索; - TTL 分级:正常数据 30 天 / 空哨兵 3 天(按 mtime 判定);
- 原子写:临时文件 + rename,避免半截 JSON。
4.3.2 结局账本与冷却
MEDIA_OUTCOME_MAP 是每条目结局类别的唯一账本(identified / fallback / skip_type / skip_unidentified / request_failed / pending_ai),运行级汇总报告的唯一数据源。失败条目登记冷却(默认 24h),冷却期内跳过、过期自动重试——避免 cron 每轮全量重试同一批失败项。
4.4 媒体识别算法设计
4.4.1 文件类型判定(顺序匹配)
flowchart TD
A["parse_media_filename"] --> B{"Title (Year)?<br>电影"}
B -- 否 --> C{"S##E## / #x## ?<br>剧集"}
C -- 否 --> D{"特典标记?<br>方括号词表 / 父目录"}
D -- 否 --> E{"Season 关键词?"}
E -- 否 --> F{"方括号 [数字]?"}
F -- 否 --> G["回退:媒体目录正片数量<br>1→movie / ≥2→tv / 0→unknown(AI)"]
特典识别优先于季份判定;特典词在方括号标记内匹配(避免误判标题),也支持父目录判断(SPs/、CDs/、Bonus/)。v9.6 起新增 Music Videos 判定(目录信号精确匹配 + [MV] 文件名信号,"歌手 - 歌名"模式消解双命中歧义)。
4.4.2 TMDB 识别与季数偏移
识别一个剧集文件的 API 调用链:/search/tv → /tv/{id} → /tv/{id}/season/0(特典季)→ /tv/{id}/season/N(集名)。季数偏移算法:
设实际集号为 $E$、季号为 $S$,TMDB 第一季集数为 $S_1$:
- 自动偏移(TMDB 有第一季数据时):
E' = E + S_1,\ S' = 1 - 手动偏移(
season_offsets.json配置,偏移值 $O$):E' = E + O,\ S' = 1
Tip
搜索失败时自动重试链:zh-CN → en-US → 规则别名 → 目录名 → MAL → 后缀二次剥离。主搜索(zh)请求失败短路返回,后续层失败忽略继续。
4.4.3 AI 辅助识别
AI 批处理四类待办:搜索词纠正/类别判断、特典映射学习、艺术家判定、匹配甄别。每批 AI_BATCH_SIZE(默认 50)条,AI_MAX_CALLS(默认 10)为批次上限。AI 响应解析走四级容错链(直接 → 剥围栏 → 剥离思考链标记 → 最外层 {} 块),推理模型的思考输出不会导致整批失效。
AI 学到的特典映射持久化到 special_maps.json(写回前交叉验证:产物必须是候选列表成员,防幻觉污染)。
4.5 输出目录结构设计
/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
│ └── S00CM01 - CM01.mkv
├── Music/
│ └── 平井大/幸せのレシピ/01. 幸せのレシピ.flac
└── MusicVideos/
└── 周杰伦/
├── 晴天.mkv
└── 七里香.mp4
命名遵循 Jellyfin 官方规范:目录名与文件名同名(电影)、Season NN 补零、SxxExx - 集名、特典 S00E{编号}(TMDB 匹配)或 S00{类型}{编号}(未匹配)。文件名不含剧集名——Jellyfin 通过父目录识别剧集。
5 编码与实现
5.1 工程规范
- Google Shell Style Guide:2 空格缩进、
[[ ]]测试、local声明、变量引号包裹; - shellcheck 零容忍:跨文件共享全局变量的 SC2034/SC2004 误报在文件头部集中 disable 并注明原因;
- 定义顺序:日志配置最先 → 全局常量 → 类型/函数 → main 入口;
- 版本单一来源:
src/lib/main.sh的SCRIPT_VERSION是唯一权威版本号,构建时注入bashly.yml。
5.2 核心实现:TMDB API 统一缓存封装
所有 TMDB 请求必经 tmdb_api 函数:先查缓存(命中直接返回),未命中加锁请求并落盘。以下为源码节选(已按 Google 风格格式化):
tmdb_api() {
local endpoint="$1"
shift
local key path is_search
key=$(cache_key "$@")
path=$(cache_path "$endpoint" "$key")
is_search=0
[[ "$endpoint" == /search/* ]] && is_search=1
# --update-cache 修饰(整理时强制重取):跳过缓存读与空哨兵,直接请求并覆盖缓存
local cached
if [[ "${UPDATE_CACHE:-false}" != "true" ]] && cached=$(cache_get "$path" "$is_search"); then
echo "$cached"
return 0
fi
# 新鲜空哨兵:上次已确认空结果,CACHE_EMPTY_TTL_DAYS 内跳过请求
if [[ "${UPDATE_CACHE:-false}" != "true" ]] && cache_empty_fresh "$path" "$is_search"; then
[[ "$DEBUG_LEVEL" -ge 2 ]] && _log 调试 "缓存为空哨兵(新鲜),跳过请求: ${endpoint}"
return 1
fi
# 并发去重(识别池多 worker 可能同时 miss 同一查询):
# flock 跨进程互斥(内核锁,进程退出自动释放,无残留)→ 锁内双检缓存。
local lockfile
lockfile="/tmp/mo_lock_$(cache_key_hash "$path")"
local locked=false
if [[ "${UPDATE_CACHE:-false}" != "true" ]]; then
exec 9>"$lockfile"
flock 9
locked=true
# 双检:等待者可能在锁期间已写入缓存
if cached=$(cache_get "$path" "$is_search"); then
exec 9>&-
echo "$cached"
return 0
fi
if cache_empty_fresh "$path" "$is_search"; then
exec 9>&-
return 1
fi
fi
local curl_args=(
"--get" "-s"
"--connect-timeout" "${TMDB_CURL_CONNECT_TIMEOUT}"
"--max-time" "${TMDB_CURL_MAX_TIME}" "-fS"
)
# ...(curl 请求、成功后 cache_put 落盘)
}
5.3 核心实现:登记层(register 系列)
登记层收拢一切条目写入,一次调用原子完成"目的映射 + 结局账本 + 计数器",结构上不可能出现只写一半的不一致:
register_identified() {
local key="$1" subdir="$2" filename="$3"
MEDIA_DESTINATION_MAP["$key"]="${subdir}|${filename}"
MEDIA_OUTCOME_MAP["$key"]="identified"
}
# 登记回退命名条目(AI 尽力后仍失败;降级成功,可链接)。
register_fallback() {
local key="$1" subdir="$2" filename="$3"
MEDIA_DESTINATION_MAP["$key"]="${subdir}|${filename}"
MEDIA_OUTCOME_MAP["$key"]="fallback"
}
# 登记待 AI 搜索纠正条目(不进入目的映射)。
register_pending() {
local key="$1" ai_data="$2"
PENDING_AI_SEARCH["$key"]="$ai_data"
PENDING_SEARCH_COUNT=$((PENDING_SEARCH_COUNT + 1))
MEDIA_OUTCOME_MAP["$key"]="pending_ai"
}
5.4 核心实现:命名模板渲染
命名格式化采用逐 token 扫描的模板渲染器(不用 ${var//pat/repl} 全局替换——替换串的 & 会被当作匹配整体,文件名含 & 时损坏):
render_naming_template() {
# 占位符语法:{name} 值插入;{name:NN} 数字补零;{?name:text} 条件段
# (name 非空才渲染 text,text 内可含其他占位符)。
local template="$1"
shift
# ...(逐 token 扫描渲染)
}
5.5 构建与分发
build.sh 实现"源码 → 单文件产物"的可复现构建:
# 在临时目录生成(注入版本号,不触碰工作区源码)
work="$(mktemp -d)"
trap 'rm -rf "$work"' EXIT
cp -r src "$work/src"
sed -i "s/^version: .*/version: $VERSION/" "$work/src/bashly.yml"
(cd "$work" && "$BASHLY_CMD" generate --quiet >/dev/null)
generated="$work/media_organizer"
# 语法检查
if ! bash -n "$generated"; then
echo "错误:构建产物语法检查未通过" >&2
exit 1
fi
构建后执行产物一致性校验(./build.sh --check,CI 集成):比对生成产物与 dist/media_organizer,不一致即失败。
6 系统测试
6.1 测试方案
系统采用零依赖轻量测试框架(纯 bash):
- 用例文件位于
tests/cases/(按功能域分组),每个用例文件在独立子 shell 中运行(全局状态自动隔离、互不污染); - 断言库
tests/lib/assert.sh(assert_eq/assert_contains/assert_success/assert_failure等); - 加载
src/main.sh+src/lib/*.sh(跳过 root_command.sh 顶层代码); - 测试中
_log覆盖为 no-op 保持输出干净。
./tests/run.sh # 运行全部用例
./tests/run.sh cache # 仅运行名字含 cache 的用例文件
./tests/run.sh -v # 详细模式
6.2 测试覆盖
| 功能域 | 用例文件 | 覆盖点 |
|---|---|---|
| 基础设施 | strings、log | 字符串工具、日志 |
| 配置域 | config(categories/partition/rules/match_rules) | 特典类别、类目分区、命名模板、匹配规则 |
| 存储域 | storage(cache/ledger) | TMDB 缓存、账本与冷却 |
| 集成域 | integrate/ai | AI 批处理 |
| 媒体域 | media(filename/identify/ai_resolve/musicvideo) | 文件名解析、识别、AI 消费、MV 判定 |
| 流水线域 | pipeline(link/registry) | 硬链接、登记 |
6.3 测试结果
| 指标 | 数值 |
|---|---|
| 测试用例总数 | 123 |
| 通过 | 120(97.6%) |
| 已知失败 | 3 |
| Lint(bash -n + shellcheck) | 0 问题 |
| 构建产物一致性 | 通过 |
已知失败用例(3 个,均为既有逻辑问题,与格式化无关):
| 用例 | 现象 |
|---|---|
| partition/test_legacy_skip_dirs_empty_falls_back | 遗留跳过目录空回退行为不符 |
| partition/test_partitioned_season_offset | 分区形态季偏移行为不符 |
| musicvideo/test_musicvideo_dual_hit_keeps_special | 双命中歧义保持特典路径不符 |
6.4 CI 流水线
# .github/workflows/ci.yml(节选)
jobs:
quality:
name: lint + test + build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: sudo apt-get install -y shellcheck
- uses: ruby/setup-ruby@v1
with:
ruby-version: "3.4"
- run: gem install bashly
- run: ./lint.sh # bash -n + shellcheck
- run: ./tests/run.sh # 单元测试
- run: ./build.sh # 构建
- run: ./build.sh --check # 产物一致性
7 总结与展望
7.1 工作总结
本文设计并实现了一个基于 TMDB 与 AI 辅助的媒体库自动化整理系统。主要成果:
- 架构层面:三阶段流水线 + 五功能域模块化组织 + bashly 单文件分发,兼顾可维护性与部署便捷性;
- 识别层面:多格式文件名解析、媒体目录正片数量判定、搜索重试链、候选结构评分、后缀季模糊匹配等组合策略,识别准确率显著高于单一策略;
- 工程层面:全量镜像缓存(二次运行零请求)、增量账本与失败冷却(cron 友好)、AI 学习写回(自进化)、Google 风格规范 + shellcheck 零容忍 + CI 闭环;
- 质量层面:120+ 单元测试用例、构建产物一致性校验、决策树文档(README 13 章 + 22 份 ADR 记录架构决策)。
7.2 不足与展望
| 方向 | 现状 | 展望 |
|---|---|---|
| 测试 | 3 个已知失败 | 修复 partition/musicvideo 边界逻辑 |
| AI 能力 | 默认 DeepSeek-V4-Flash | 支持多模型路由与本地模型(Ollama) |
| 音乐元数据 | 依赖目录名与 ID3 标签 | 引入 MusicBrainz 识别 |
| 覆盖范围 | 单机单库 | 多库配置、分布式缓存 |
| 界面 | CLI | 可选 Web 管理界面 |
| 语言 | Bash | 保持零依赖,必要时核心逻辑迁移 |
参考文献
- Jellyfin Project. Jellyfin Documentation — Media Management[EB/OL]. https://jellyfin.org/docs/general/server/libraries/
- TMDB. The Movie Database API v3 Documentation[EB/OL]. https://developer.themoviedb.org/docs
- Google. Google Shell Style Guide[EB/OL]. https://google.github.io/styleguide/shellguide.html
- bashly. Bash Command Line Tool Generator[EB/OL]. https://bashly.dev
- ShellCheck Project. ShellCheck — Shell Script Analysis Tool[EB/OL]. https://github.com/koalaman/shellcheck
- mvdan. shfmt — Shell Formatter[EB/OL]. https://github.com/mvdan/sh
Auto_Bangumi. 基于 Mikan Project 的全自动追番整理下载工具[EB/OL]. https://github.com/EstrellaXD/Auto_Bangumi- OpenAI. OpenAI API Reference[EB/OL]. https://platform.openai.com/docs/api-reference
- jq. jq Manual (development version)[EB/OL]. https://jqlang.github.io/jq/
- Free Software Foundation. GNU Bash Manual[EB/OL]. https://www.gnu.org/software/bash/manual/
致谢
感谢开源社区提供的 TMDB API、Jellyfin、bashly、ShellCheck 等优秀工具与平台;感谢 Auto_Bangumi 与 Bangumi_Auto_Rename 项目在媒体解析算法上的启发(候选结构评分、公共子串剥离、纠错学习等机制借鉴其思路);感谢所有在测试与反馈中提供帮助的社区用户。
附录 A 死链检查摘要
- 检查范围:
PROJECT_REPORT.md全部外部链接; - 结果:全部可达(详见
link_check_report_*.md或本报告生成时的检查记录)。
附录 B 许可证合规摘要
- 主许可证:MIT;全部依赖(bashly/completely MIT、系统组件 GPL-3.0/LGPL)兼容,无冲突;
- 详细分析见第 2.3 节与
license_check_report_*.md。
本报告基于项目源码自动生成,代码示例均经 Google Shell Style 格式化。生成日期:2026-08-14。