# 基于 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 主要研究内容 1. 媒体文件名的多格式解析与媒体类型判定(电影/剧集/特典/音乐/音乐视频); 2. 基于 TMDB v3 API 的媒体识别与全量镜像缓存设计; 3. 基于 OpenAI 兼容接口的 AI 辅助识别与特典映射学习; 4. 三阶段并发流水线与增量整理(账本 + 冷却)机制设计; 5. 面向 Jellyfin 规范的目录结构生成与硬链接实现; 6. 单元测试体系与 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](https://bashly.dev) 1.4 是 Ruby 编写的命令行工具生成器:开发者以 YAML 声明参数、选项、帮助文本与互斥关系,bashly 生成完整的参数解析代码并包装自定义命令函数。本系统 CLI 定义位于 `src/bashly.yml`,构建时由 `build.sh` 调用 bashly 生成单文件分发脚本 `dist/media_organizer`。 ```yaml # 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](https://google.github.io/styleguide/shellguide.html)(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) ```text 用法: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 用例模型 ```mermaid flowchart LR U["用户/定时任务"] --> C1["整理媒体库
(正常模式)"] U --> C2["干运行预览"] U --> C3["仅更新缓存"] U --> C4["列出缓存条目"] U --> C5["手动纠错重跑"] C1 --> S["系统
MediaOrganizer"] S --> T["TMDB API"] S --> A["AI API"] S --> F["文件系统
(硬链接)"] ``` --- ## 4 详细设计 ### 4.1 系统总体架构 系统按功能域组织源码(不设通用 `utils/` 目录),分为五个功能域 + 基础设施: ```text 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 三阶段流水线设计 ```mermaid flowchart LR A["scan_files 扫描源目录"] --> B["第一阶段:识别池
process_video + process_audio
并发 MEDIA_WORKERS worker
parse → identify_movie / identify_tv_show"] B --> C["第二阶段:run_ai_batch
AI 纠正搜索词 + 匹配甄别
+ 特典映射学习 + 艺术家判定"] C --> D["第三阶段:link_media
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/` | 运行状态(已链接账本、失败冷却) | 增量累积 | ```text mo_cache/ ├── tmdb/ # TMDB API 响应缓存 │ ├── search/movie/.json # 搜索缓存(包裹格式) │ ├── search/tv/.json │ ├── tv/..json # 剧集详情(语言段随 TMDB_LANG) │ ├── tv//season/..json # 每季数据(含 season 0 特典季) │ └── movie/..json # 电影详情 └── media_organizer/ ├── linked.json # 已链接账本(增量标记) └── fail_cooldown.json # 失败冷却 ``` **缓存关键机制**: 1. **镜像路径**:缓存目录结构镜像 TMDB API 端点,二次运行零外部请求; 2. **并发去重**:`flock` 跨进程互斥 + 锁内双检(同一查询只发一次请求); 3. **空哨兵**:空结果写 `empty:true` 哨兵(3 天短 TTL),新片出现后自动重新搜索; 4. **TTL 分级**:正常数据 30 天 / 空哨兵 3 天(按 mtime 判定); 5. **原子写**:临时文件 + 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 文件类型判定(顺序匹配) ```mermaid flowchart TD A["parse_media_filename"] --> B{"Title (Year)?
电影"} B -- 否 --> C{"S##E## / #x## ?
剧集"} C -- 否 --> D{"特典标记?
方括号词表 / 父目录"} D -- 否 --> E{"Season 关键词?"} E -- 否 --> F{"方括号 [数字]?"} F -- 否 --> G["回退:媒体目录正片数量
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 输出目录结构设计 ```text /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 风格格式化): ```bash 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 系列) 登记层收拢一切条目写入,一次调用原子完成"目的映射 + 结局账本 + 计数器",结构上不可能出现只写一半的不一致: ```bash 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}` 全局替换——替换串的 `&` 会被当作匹配整体,文件名含 `&` 时损坏): ```bash render_naming_template() { # 占位符语法:{name} 值插入;{name:NN} 数字补零;{?name:text} 条件段 # (name 非空才渲染 text,text 内可含其他占位符)。 local template="$1" shift # ...(逐 token 扫描渲染) } ``` ### 5.5 构建与分发 `build.sh` 实现"源码 → 单文件产物"的可复现构建: ```bash # 在临时目录生成(注入版本号,不触碰工作区源码) 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 保持输出干净。 ```bash ./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 流水线 ```yaml # .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 辅助的媒体库自动化整理系统。主要成果: 1. **架构层面**:三阶段流水线 + 五功能域模块化组织 + bashly 单文件分发,兼顾可维护性与部署便捷性; 2. **识别层面**:多格式文件名解析、媒体目录正片数量判定、搜索重试链、候选结构评分、后缀季模糊匹配等组合策略,识别准确率显著高于单一策略; 3. **工程层面**:全量镜像缓存(二次运行零请求)、增量账本与失败冷却(cron 友好)、AI 学习写回(自进化)、Google 风格规范 + shellcheck 零容忍 + CI 闭环; 4. **质量层面**:120+ 单元测试用例、构建产物一致性校验、决策树文档(README 13 章 + 22 份 ADR 记录架构决策)。 ### 7.2 不足与展望 | 方向 | 现状 | 展望 | | ---------- | ---------------------- | ---------------------------------- | | 测试 | 3 个已知失败 | 修复 partition/musicvideo 边界逻辑 | | AI 能力 | 默认 DeepSeek-V4-Flash | 支持多模型路由与本地模型(Ollama) | | 音乐元数据 | 依赖目录名与 ID3 标签 | 引入 MusicBrainz 识别 | | 覆盖范围 | 单机单库 | 多库配置、分布式缓存 | | 界面 | CLI | 可选 Web 管理界面 | | 语言 | Bash | 保持零依赖,必要时核心逻辑迁移 | --- ## 参考文献 1. Jellyfin Project. _Jellyfin Documentation — Media Management_[EB/OL]. https://jellyfin.org/docs/general/server/libraries/ 2. TMDB. _The Movie Database API v3 Documentation_[EB/OL]. https://developer.themoviedb.org/docs 3. Google. _Google Shell Style Guide_[EB/OL]. https://google.github.io/styleguide/shellguide.html 4. bashly. _Bash Command Line Tool Generator_[EB/OL]. https://bashly.dev 5. ShellCheck Project. _ShellCheck — Shell Script Analysis Tool_[EB/OL]. https://github.com/koalaman/shellcheck 6. mvdan. _shfmt — Shell Formatter_[EB/OL]. https://github.com/mvdan/sh 7. `Auto_Bangumi`. _基于 Mikan Project 的全自动追番整理下载工具_[EB/OL]. https://github.com/EstrellaXD/Auto_Bangumi 8. OpenAI. _OpenAI API Reference_[EB/OL]. https://platform.openai.com/docs/api-reference 9. jq. _jq Manual (development version)_[EB/OL]. https://jqlang.github.io/jq/ 10. 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。_