Files
Shuery c1ed1795c2
CI / lint + test + build (push) Canceled after 0s
docs: 重写 README、新增项目报告与开发工具配置
- README 现代化重写:徽章区、特性一览、统一 GitHub 提示块格式、修正版本号与 AI 默认值
- 新增 docs/PROJECT_REPORT.md 学术项目报告(含许可证合规分析与死链修复记录)
- 收录 23 份 ADR 架构决策记录与 CONTEXT.md 领域术语表
- 新增开发配置:.editorconfig / .shellcheckrc / .markdownlint-cli2.jsonc
- .gitignore 补充 mo_map/、检查报告、编辑器临时文件
- 新增 config.example.json 配置模板(不含真实密钥)
2026-08-14 22:41:40 +08:00

31 KiB
Raw Permalink Blame History

基于 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 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             # 失败冷却

缓存关键机制:

  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 文件类型判定(顺序匹配)

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 辅助的媒体库自动化整理系统。主要成果:

  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。