Shuery 83d2008a5e
CI / lint + test + build (push) Canceled after 0s
docs: 重写 README(现代化精炼版,543 行)
- 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 确认失效
2026-08-15 00:12:39 +08:00
2026-08-13 17:46:49 +08:00

🎬 MediaOrganizer

Jellyfin 媒体库硬链接整理脚本:通过 TMDB API 识别电影、电视剧、音乐与音乐视频,用硬链接在同一文件系统内零拷贝创建 Jellyfin 标准目录结构。AI 辅助识别、全量镜像缓存、特典自动归类、季数偏移、增量整理——一次整理,终身整洁。

License: MIT Version Bash ShellCheck Tests Style: Google Shell Hosted on PRs Welcome

作者: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),允许自由使用、修改、分发,需保留版权声明。

致谢:


文档版本对应脚本 v9.6(SCRIPT_VERSION 为唯一权威版本号)。

S
Description
媒体库整理,将媒体库整理成 Jellyfin 支持的格式。
Readme MIT
479 KiB
0 Stars 1 Watchers 0 Forks
Languages
Shell 99.8%
Makefile 0.2%