- README 现代化重写:徽章区、特性一览、统一 GitHub 提示块格式、修正版本号与 AI 默认值 - 新增 docs/PROJECT_REPORT.md 学术项目报告(含许可证合规分析与死链修复记录) - 收录 23 份 ADR 架构决策记录与 CONTEXT.md 领域术语表 - 新增开发配置:.editorconfig / .shellcheckrc / .markdownlint-cli2.jsonc - .gitignore 补充 mo_map/、检查报告、编辑器临时文件 - 新增 config.example.json 配置模板(不含真实密钥)
This commit is contained in:
1 parent
0df07ad0ae
commit
c1ed1795c2
31 files changed
+1816
-234
No files matched your search
+228
@@ -0,0 +1,228 @@
|
||||
# Media Organizer Context
|
||||
|
||||
Jellyfin 媒体库硬链接整理脚本(`media_organizer.sh`)。本上下文记录该工具的领域术语——重点是其入口层(CLI 参数 + 配置加载)的语言约定。
|
||||
|
||||
## Language
|
||||
|
||||
**入口层 (entry layer)**:
|
||||
脚本的前门:命令行参数解析、位置参数校验、env/mo_env 配置加载的统称。讨论入口行为时用它,而不是具体指某个函数。
|
||||
_Avoid_: 参数解析(只指其中一部分)
|
||||
|
||||
**模式 (mode)**:
|
||||
互斥的入口动作,一次调用恰好选择其一:`list`(列出缓存)、`organize`(正常整理)。模式决定入口分发的分支;当 `update-cache` 修饰标志存在且只给了源目录时,退化为只刷缓存的形状(不整理,提前退出)。
|
||||
_Avoid_: 选项、动作
|
||||
|
||||
**修饰标志 (modifier)**:
|
||||
与模式正交的布尔开关,可自由附加到模式上:`dry-run`、`automated`、`refresh-cache`、`update-cache`。语义上属于"本次调用如何执行",而非"执行什么"。`update-cache` = 强制重取对应缓存条目(不信任陈旧缓存),是唯一会改变流程形状的修饰标志。
|
||||
_Avoid_: 选项、参数
|
||||
|
||||
**干运行 (dry-run)**:
|
||||
修饰标志;本次调用不产生任何持久化副作用:不建链接、不写缓存、不写日志;网络请求(TMDB/AI)照常执行但不落盘。与 `cache-only` 形状冲突(报错),与 `update+organize` 兼容。
|
||||
_Avoid_: 试运行、预览模式(语义含混)
|
||||
|
||||
**配置优先级链 (priority chain)**:
|
||||
配置值的解析顺序:**CLI > 环境变量 > mo_env 文件 > 内置默认值**。同一键只取最高优先级来源;CLI 显式设置(含 `--no-*` 反选)覆盖一切。
|
||||
_Avoid_: 覆盖顺序
|
||||
|
||||
**位置参数**:
|
||||
模式之外的目录参数:源目录、目的目录。`organize` 需要两个;`update-cache` 只需源目录(只给一个时仅刷缓存,给两个时刷新并整理);`list` 不接受任何目录参数。
|
||||
|
||||
**源目录 (source directory)**:
|
||||
被整理的媒体文件所在目录。英文标识统一用 `source`(`SOURCE_DIR`、`--src-dir`)。
|
||||
_Avoid_: src 的其他混用
|
||||
|
||||
**目的目录 (destination directory)**:
|
||||
硬链接的目标目录。英文标识统一用 `destination`(`DESTINATION_DIR`、`--dest-dir`),不再使用 `target`。
|
||||
_Avoid_: target, dst(作概念名时)
|
||||
|
||||
**过滤词 (filter keyword)**:
|
||||
`list` 模式的可选关键词,按类型/路径/参数过滤缓存条目。仅 CLI 提供(`--list-cache` 的值或 `=` 形式),不从配置读取。
|
||||
|
||||
## Specials
|
||||
|
||||
**特典类别词 (special category word)**:
|
||||
特典识别的判定词(`mo_special_words.json`,JSON 数组)——文件名方括号标记含这些词 → 判定为特典(Season 00)。语义 = 特典类别(Menu/CM/PV/NCOP/NCED 等)。匹配对空格不敏感(词表 `ncop` 可匹配文件里的 `nc op`);词表内容不当作正则(纯字符串子串匹配)。
|
||||
_Avoid_: 特典名("名"暗示具体条目,实际是类别判定词)
|
||||
|
||||
**匹配关键字 (match keyword)**:
|
||||
keymap 值元素与 AI 学习产物的统一术语——`["Menu","菜单","メニュー"]` 中的每个元素都是匹配关键字:用于与 TMDB 特典候选列表条目做匹配(精确/最短前缀/contains 三级)。用户可写 TMDB 条目名,也可写自定义别名。
|
||||
_Avoid_: 特典名、集名(这些词本质是特典类别/关键词,不是"名字")
|
||||
|
||||
**TMDB 特典候选列表 (season0 candidate list)**:
|
||||
`tv/{id}/season/0` 的条目名列表(`PENDING_AI_SPECIAL` 值中的 `编号:名称` 串)。AI 学习时作为候选:**AI 从候选中选择与本地片段匹配的项**(选择判定,与 AI 匹配轮同构),产物必须是候选列表成员(交叉验证,防幻觉污染)。
|
||||
_Avoid_: 特典集名(暗示 AI "返回名字",实际是"从候选中选择")
|
||||
|
||||
**特典映射 (special keymap)**:
|
||||
`mo_special_keymap.json`:`{"本地关键字符串": 匹配关键字数组 | 字符串引用}`——多键共享同一组匹配关键字用字符串引用(递归展开,防循环);键小写化。AI 学习写回前交叉验证(产物必须在候选列表)。
|
||||
_Avoid_: 特典名映射
|
||||
|
||||
## Custom
|
||||
|
||||
**命名模板 (naming template)**:
|
||||
`NAMING_MOVIE`/`NAMING_SHOW`/`NAMING_SEASON`/`NAMING_EPISODE`/`NAMING_SPECIAL`/`NAMING_MUSIC` 六个配置键,把"命名格式化"从硬编码外置为用户可配置模板。占位符语法:`{name}` 值插入、`{name:NN}` 数字补零、`{?name:text}` 条件段(name 非空才渲染 text,正文可含其他占位符);默认模板与旧版输出逐字节一致(仅格式外置,行为不变)。渲染实现 `render_naming_template` 逐 token 扫描(不用 `${var//pat/repl}` 全局替换——替换串的 `&` 会被当匹配整体,文件名含 `&` 时损坏);`render_naming` 按配置键渲染并在键未配置时回退内置默认(`naming_template_default` 集中定义)。加载期 `validate_naming_templates` 对未知占位符打印警告(渲染为空)。
|
||||
_Avoid_: 直接拼接字符串(命名格式必须走模板渲染,保证识别/回退两套路径一致)
|
||||
|
||||
**匹配规则 (match rules)**:
|
||||
`mo_config/match_rules.json`:用户手工维护的确定性匹配——`search_aliases`(搜索别名)与 `id_maps`(ID 映射),键 = 文件名清洗后的标题(`rule_key` 归一化:小写 + 空白折叠)。不参与 AI 学习写回(用户显式维护即权威)。
|
||||
_Avoid_: 匹配规则(泛指 keymap/季偏移等一切匹配类配置)
|
||||
|
||||
**搜索别名 (search alias)**:
|
||||
匹配规则的 `search_aliases` 条目:`{"<标题键>": "<重搜词>" | {"term": "...", "year": "..."}}`。主搜索(zh→en)**无结果**时用重搜词再搜(zh→en 各一次)——确定性兜底,优先于目录名兜底与 MAL/AI。适合本地俗称/简称(TMDB 搜不到"俺妹"但能搜到全名)。别名 year 仅电影生效。
|
||||
_Avoid_: 无条件用别名替换标题(主搜索成功时别名不介入,ID 映射才是一等优先级)
|
||||
|
||||
**ID 映射 (id map)**:
|
||||
匹配规则的 `id_maps` 条目:`{"movie": {"<标题键>": <id>}, "tv": {...}}`——标题命中直接使用指定 TMDB ID,**完全跳过搜索**(最高优先级,先于一切搜索)。movie/tv 按 parse 判定类型分表。适合 TMDB 多候选易选错(同名动画/真人版)、搜索命中错条目的场景。
|
||||
_Avoid_: 用别名模拟 ID 映射(别名仍需搜索验证,ID 映射是确定性绑定)
|
||||
|
||||
**类目分区 (category partition)**:
|
||||
四个配置类文件(`special_maps`/`special_keywords`/`skip_directories`/`season_offsets`,v9.6)支持分区形态:顶层全部键 ∈ `MO_CATEGORY_KEYS`(movie/tv/music/musicvideo/video/audio/default/global)→ 分区形态(`is_category_partitioned` 判定,混合形态警告并按全局处理)。查询链统一"**类目/流程分区 → default 分区 → 全局表 → 内置默认**"。分区键语义因文件而异:特典映射/词表用类目键(tv/musicvideo),跳过目录用**流程键**(video/music——目录语义判断发生在类目确定前);`skip_directories` 分区形态 = **完整语义**(不叠加内置默认,通用词放 default);`special_keywords` 分区 = 叠加语义(tv 分区词 + 内置兜底,词表是积累型)。AI 学习写回自动适配(分区形态写 tv/video 分区)。
|
||||
_Avoid_: 分区形态下叠加内置默认(`skip_directories` 的 music 流程误命中 video 侧内置词 "sps"——分区文件表达"只要这些词")
|
||||
|
||||
**音乐视频类目 (musicvideo category)**:
|
||||
v9.6 新增第四类目:识别信号 = musicvideo 判定词(`special_keywords.json` 的 `musicvideo` 分区 → default → 内置 `MUSICVIDEO_WORDS` 13 词,**不回退** tv 特典词表——"sp" 子串命中 "SPs" 目录等交叉误判)。两类信号:**目录信号**(目录名**精确匹配**归一化整名——"Music Videos"/"MV"/"Live"/"演唱会";精确匹配避免 "Muv-Luv"/"tmp.xxx" 含词误判)与**文件名信号**(方括号标记子串命中,如 [MV];双命中歧义用 "**歌手 - 歌名**" 模式消解——含 ` - ` → musicvideo,不含 → 特典保持现状)。命名 `MusicVideos/{artist}/{title}`(`NAMING_MUSICVIDEO` 模板),artist 链:元数据 ALBUMARTIST → ARTIST → 父目录名("歌手 - 歌名"取首段/整名)→ FOLDER_UNKNOWN。本地规则无网络,识别失败不消耗 AI。
|
||||
_Avoid_: 目录信号用子串匹配("Muv-Luv" 目录含 "mv" 误判——整目录视频被归类音乐视频的代价远超单个文件)、musicvideo 词表回退 tv 特典词表(SPs 目录稳定误判)
|
||||
|
||||
**特典类别判定表 (special category table)**:
|
||||
`mo_config/special_categories.json`(v9.6 外置):S00 未匹配特典的类别标签判定(`S00{类型} - {片段}` 的"类型"),`[{"match": "nced", "tag": "NCED"}]` **数组保序 = 优先级**(长词/特定词在前)。查询链:用户表(按序子串)→ 内置默认表(24 项,与 `SPECIAL_CATEGORIES_TEMPLATE` 一致)→ "Special"。此前硬编码在 `special_category_tag` 的 24 项类别词表外置为用户可编辑(增删/调序),`special_category_tag` 保留内置表作最后兜底。
|
||||
_Avoid_: 对象形态存类别表(JSON 对象无顺序,判定优先级丢失);把类别表并入 special_keywords(语义不同:词表判定"是否特典",类别表判定"S00 标签是什么")
|
||||
|
||||
## Pipeline
|
||||
|
||||
**识别池 (worker pool)**:
|
||||
`process_video` 与 `process_audio` 共用的并发 worker 池(`MEDIA_WORKERS`,默认 4)。子进程产出 `key\tvalue` 行,父进程合并——bash 子 shell 无法写父关联数组,这是唯一的并行契约。音频与视频走同一通用接口(识别函数 → 结果/结局)。
|
||||
_Avoid_: 并行处理(泛指)
|
||||
|
||||
**登记 (register)**:
|
||||
收拢一切条目写入的注册函数层(`register_identified` / `register_pending` / `register_fallback` / `register_skip` / `register_request_failed`)。一次调用原子完成"目的映射 + 结局账本 + 计数器",结构上不可能出现只写一半的不一致。命名取自"声明这条记录存在并处于什么状态",区别于赋值。
|
||||
_Avoid_: 赋值、写入(语义含混)
|
||||
|
||||
**结局账本 (outcome map)**:
|
||||
`MEDIA_OUTCOME_MAP`:每条目结局类别的唯一账本(identified / fallback / skip_type / skip_unidentified / request_failed / pending_ai)。运行级汇总报告的唯一数据源;跳过/失败条目不进入目的映射,仅登记于此。
|
||||
_Avoid_: 状态字段
|
||||
|
||||
**回退命名 (fallback naming)**:
|
||||
AI 尽力后仍无法识别时的降级出口(触发条件唯一,无 AI 密钥 → `skip_unidentified`,不回退)。命名不含年份占位(Jellyfin 年份可省略);空集名不加 ` - ` 段;特典回退复用识别路径的 `S00{tag} - {fragment}` 约定。
|
||||
_Avoid_: 兜底命名(与识别兜底混淆)
|
||||
|
||||
**降级成功 (degraded success)**:
|
||||
回退命名的条目在汇总中的归类:链接确实建立了(文件可访问),但名字是文件名推断的。计入成功而非跳过,但单列一类显示。
|
||||
_Avoid_: 成功(不区分)、警告
|
||||
|
||||
**伴随文件优先级**:
|
||||
同名文件归属判定:**视频 > 音频**。音频文件先查是否存在同名视频(任一视频扩展名)——存在则该音频是视频的伴随音轨(由视频的伴随逻辑处理,从独立音频处理中排除);不存在才是独立音频,再查它自己的伴随(歌词/封面)。当前脚本对 `Movie.zh.ac3` 类音轨会双重处理,需按此规则修复。
|
||||
_Avoid_: 按扩展名并列判断
|
||||
|
||||
**艺术家归类链 (artist resolution chain)**:
|
||||
音乐条目艺术家归属的逐级判定:专辑艺术家(元数据 ALBUMARTIST,唯一)→ 无则歌曲艺术家(ARTIST)单值视为专辑艺术家 → 多值交 AI(并入现有 AI 批处理,不新增请求类型)→ AI 判断不出用 ` & ` 拼接所有艺术家 → 元数据缺失回退目录名解析(`parse_cd_dir`)→ 仍无则 `Unknown` 文件夹。专辑名(ALBUM 标签 → 目录名)同构;封面等提取仅整理不增删文件,默认关闭。
|
||||
_Avoid_: 目录名优先(现状,将被替换为末级兜底)
|
||||
|
||||
**目的目录规范 (destination structure)**:
|
||||
按 Jellyfin 官方规范:`Movies/{title} ({year})/` 夹内同名文件;`Shows/{title} ({year})/Season NN/`(补零、不缩写,`Season` 为解析格式不可本地化);`SxxExx - 集名`;特典 `Season 00` 描述性命名;`Music/{artist}/{album}/`(一夹一专辑);`MusicVideos/{artist}/{title}`(根目录无空格,识别待样本驱动)。撞名按官方多版本格式去重(` - 2`);同源旧链接(迁移场景)按 inode 清理。根目录名本地化(默认系统语言,`FOLDER_MOVIES` 等可配)。
|
||||
_Avoid_: `S01`/`SE01` 季目录名
|
||||
|
||||
**已链接账本 (linked ledger)**:
|
||||
`mo_cache/media_organizer/linked.json`:`{src: {dest, inode, linked_at}}`。增量标记的唯一数据源——识别池入口命中且**源存在 + 目标存在 + 同 inode** 才跳过(结局 `already_linked`),任一失效(目标被删/源被替换)自动重新识别+链接。父进程加载一次(worker fork 复制内存),运行末尾 `ledger_flush` 统一落盘(原子写+惰性清理),干运行不落盘且不启用跳过(展示全貌)。
|
||||
_Avoid_: marker 文件(污染媒体目录)、每文件落盘(O(n²))
|
||||
|
||||
**失败冷却 (failure cooldown)**:
|
||||
`mo_cache/media_organizer/fail_cooldown.json`:`{src: {retry_at, reason}}`。`request_failed`/`skip_unidentified` 登记(`FAIL_RETRY_COOLDOWN_HOURS` 默认 24h,0=禁用),冷却期内识别池入口跳过(结局 `cooldown`),过期自动重试。仅"尽力后失败"冷却;`--rerun` 显式重跑绕过;冷却不计入退出码 3。
|
||||
_Avoid_: 每轮全量重试同一批失败项、永久跳过(破坏可逆语义)
|
||||
|
||||
**纠错账本 (correction ledger)**:
|
||||
`--export-map` 伴生同名 `.ledger.json`:`[{src, dest, outcome, status, action}]`(dest 为绝对路径;status 导出时现场校验 linked/pending)。用户编辑 dest + `action:"rerun"` 后 `--rerun <文件>` 重跑——只做链接层(mkdir+硬链接,目标已存在且非同 inode → 替换),不重新识别,不加载 TMDB 认证。
|
||||
_Avoid_: 重新识别纠错(与 AI 流程重叠)、Python WebUI(破坏纯 bash 定位)
|
||||
|
||||
**同集冲突 (episode conflict)**:
|
||||
`detect_conflicts` 按 `剧集目录||SxxExx`(含区间)聚合识别成功条目,同 key 多源 → `CONFLICT_MAP`,对照表「同集冲突」小节 + 汇总提示。仅检测+报告,不自动删(硬链接库场景自动替换风险大于收益);Season 00 特典/电影/音乐不参与;可经 `--rerun` 指定保留版本。
|
||||
_Avoid_: hold/replace 自动替换(AB 式,需下载器信息且破坏性)
|
||||
|
||||
**搜索重试链 (search fallback chain)**:
|
||||
识别搜索失败顺序重试:zh-CN 空 → `en-US`(`tmdb_api_lang` 局部覆盖,缓存按语言段隔离,默认)→ `SEARCH_FALLBACK_MAL=true` 时 MyAnimeList (jikan v4) 候选标题(罗马音/英/日,≤3 个)逐个回 TMDB en-US 重搜(`mo_cache/media_organizer/mal/` 独立缓存)。请求失败(rc=2)不重试;MAL 失败静默降级到 AI 路径。
|
||||
_Avoid_: 直接信任 MAL 标题入库(必须经 TMDB 验证)、默认开启 MAL(外部 API 依赖)
|
||||
|
||||
**AI 时长信号 (AI duration signal)**:
|
||||
`match_entries` 构造时为待甄别文件运行 ffprobe,附加 `duration`(分钟,<1min 视为空)与 `resolution`。AI 据此区分剧场版(~70-120min)/ OVA·特典(~20-30min)/ 正片(~24min);失败留空不阻塞。
|
||||
_Avoid_: 全量文件 ffprobe(仅需甄别条目)、hachoir 新依赖(ffprobe 已存在)
|
||||
|
||||
**候选结构评分 (candidate structure scoring)**:
|
||||
`pick_tv_show_id` 精确名匹配失败后的评分层:前 5 候选(前缀排除后)按 `季数覆盖文件季号 +40 / 名称 norm 互相包含 +30 / 特典候选含 Season 0 +20` 取最高,全不满足兜底 results[0]。评分请求走 `tmdb_api`(缓存命中免费)。精确匹配始终最高优先——评分层只在歧义场景介入。
|
||||
_Avoid_: 直接 results[0](歧义场景浪费 AI 轮次)、评分替代精确匹配(改变正常识别路径)
|
||||
|
||||
**目录名兜底 (directory fallback search)**:
|
||||
`tv_search_by_dir` / `movie_search_by_dir`:主搜索失败(zh→en 均空)后用父目录名(`clean_name`+`strip_season_suffix`)重搜。源根散放不兜底(共享目录误判);目录名与 query 相同/为空跳过;插在 MAL 之前(零外部依赖优先)。TV 复用结构评分,电影取 results[0]。
|
||||
_Avoid_: 目录名直接当标题(未经验证)、无条件用目录名(源根散放误判)
|
||||
|
||||
**纠错学习 (correction learning)**:
|
||||
`mo_cache/media_organizer/corrections.json`:`{clean_name(文件名)小写: {dest, learned_at}}`。`--rerun` 链接成功即学习(用户显式修正即权威,立即原子写盘);识别入口按同算法 key 前置命中 → 直接 DEST(跳过 parse/识别/AI/冷却)。学习源只有 `--rerun`(自动识别成功不学习——防固化脚本自身错误);dest 须位于当前 `DESTINATION_DIR` 下;目录参数已归一化绝对路径(`normalize_abs`)。
|
||||
_Avoid_: 学习 AI 识别结果(无用户确认)、按绝对路径学习(换目录失效)、冷却优先于纠错(用户修正不应被退避阻塞)
|
||||
|
||||
**后缀季模糊 (suffix-season fuzzy)**:
|
||||
后缀季映射词表全失败且后缀 ≥3 字符时:awk Levenshtein 只比较季名**末尾窗口**(末 flen+1 字符,完美命中距离 ≤1 与阈值分档 1/2/3 匹配),最短距离 ≤ 阈值命中。跨语言不指望命中(距离必然超阈值)——服务同文近似拼写。
|
||||
_Avoid_: 整名 Levenshtein(距离不可达,阈值形同虚设)、短后缀启用(词表/罗马数字已覆盖,误配风险高)
|
||||
|
||||
**公共子串剥离 (peer prefix/suffix extraction)**:
|
||||
`extract_episode_from_peers`:无特征剧集目录(≥2 视频)求最长公共前缀/后缀(bash 逐字符),当前文件中段取**末尾数字**作集号(排除分辨率 1080/720/480/2160/4320);无数字保持 episode=0(不猜)。仅"正片计数 → tv"回退分支启用。
|
||||
_Avoid_: difflib 全公共子串(python 新依赖)、中段无数字仍猜集号
|
||||
|
||||
**AI 响应容错 (AI response tolerance)**:
|
||||
`ai_extract_json` 四级容错链:① 直接解析 → ② 剥 ` ```json `/` ``` ` 围栏 → ③ 剥离思考链标记(`<thinking>` 块 + 「思考/分析/推理:」前缀)→ ④ 最外层 `{}` 块(花括号配平,字符串内误计仅致本级失败)。任何一级 jq 验证通过即返回;全失败保持原失败路径(条目留待下批)。纯 awk/sed,零新依赖。调用点 `ai_batch_request` 用 `||` 保护(set -e 安全)。
|
||||
_Avoid_: 严格校验整批丢弃(推理模型输出思考内容时浪费一轮+额度)、SDK 结构化输出(curl 直连兼容端点,能力不一)
|
||||
|
||||
**AI 用例落盘 (AI case persistence)**:
|
||||
`AI_SAVE_CASES=true` 时每次请求的输入 JSON(`build_ai_input_json` 产物)与原始响应存 `mo_cache/media_organizer/ai_cases/<序号>_<ts>_{request,response}.json`。序号复用 `AI_CALL_COUNT`;输入不含 API 密钥(key 只在 HTTP 头);响应存原始文本(非 JSON 也留档——正是排查价值);干运行不写。反馈闭环 + 防幻觉学习候选数据源。
|
||||
_Avoid_: 只存响应(缺输入侧无法重建上下文)、存 payload(无 key 但含 base_url 等配置信息,不必要)
|
||||
|
||||
**同目录上下文 (siblings)**:
|
||||
`build_ai_input_json` 的 match_entries 每项附 `siblings`(同目录其他视频文件名,≤20,排除自身,目录扫描零请求)。与时长信号互补:时长回答"这个文件多长",siblings 回答"它和什么在一起"——剧场版混 TV 场景(90min 文件旁 23 个 24min 文件 → season 0 剧场版)的判断依据。AI 仍只做判断,命名归脚本。
|
||||
_Avoid_: 整目录一次映射(BAR 模式,prompt/输出契约复杂化,与单文件判断流水线不兼容)、附路径/时长(体积膨胀,信息已由 file/directory/duration 提供)
|
||||
|
||||
**搜索链抽象 (search chain abstraction)**:
|
||||
`tv_search_once` / `movie_search_once` 统一尝试器:参数(query/季号/特典/语言/年份),返回码 0=命中(stdout=id)/1=无结果/2=请求失败;movie 版两行协议(id + 单行响应 JSON)回传响应供年份校验/需甄别检测(命令替换子 shell 丢全局赋值,两行协议是零新依赖回传)。识别链 = zh → en → 规则别名 → 目录 → MAL → 后缀二次剥离,逐层调用尝试器。失败语义:主搜索失败短路 return 2;后续层失败忽略;别名层失败不试 en 变体。`pick_tv_show_id` 调用点收敛到尝试器内部 1 处。
|
||||
_Avoid_: 逐层复制(每层 ~10 行且失败语义易漂移)、全局变量回传(子 shell 丢失)、整链编排(各层 query 生成逻辑异构)
|
||||
|
||||
**配置单点化 (config single source)**:
|
||||
`CONFIG_DEFS`(键|默认值数组,main.sh)→ `render_config_template()` 生成 config.json 模板 + `load_config` 循环加载(`printf -v` 动态赋值 + `${!key:-}` 间接引用,零 eval)。新增纯标量键只需改一处;FOLDER_* locale 默认/扩展名拆数组/数值校验/日志初始化等后处理保留手写。NAMING_* 字面值须与 `naming_template_default` 一致(后者为 render_naming 兜底权威)。
|
||||
_Avoid_: 模板/加载/默认值三处手写(漂移源)、全配置关联数组化(动所有消费点)
|
||||
|
||||
**已知缺口 (known gaps)**:
|
||||
① ~~多集单文件(`S01E01-E02.mkv`)~~ ——**已实现**(parse 输出契约 7 段含 `episode_end`;识别/回退命名 `S01E01-E02 - 首集名 - 末集名`);② Music Videos 识别信号——待真实样本驱动(多数文件规则补齐);③ 纯音频 mkv/mp4 容器改名(mka/m4a)——文档提示,不自动改文件;④ ~~电影/电视判断链~~ ——**已实现**(`(YYYY)` 任意位置提取进 year 字段;`count_main_videos` 扩展名读 `VIDEO_EXTS` + 跳过目录词表 `mo_skip_dirs.json` + 源根散放约束→AI);判断链的"文件名特征评分系统"挂账**正式关闭**——年份任意位置提取后无特征文件已收敛(有年份→movie、剧集目录多集→tv、源根散放→unknown→AI),评分系统价值趋零;⑤ ~~后缀季(`Railgun T`/`II`/`2nd`)~~ ——**已实现**(`strip_season_suffix_word` 剥后缀重搜 + base 剧 seasons 名匹配映射季号 + 罗马数字直映射 + Levenshtein 末尾窗口模糊兜底;AI 的 `season_shift` 仅兜底脚本剥离失败);⑥ ~~AI 使用~~ ——**已实现**(AI 失败→可逆 skip、jq 构造输入、分批、判断/执行职责分离;v9.4 起 match_entries 补充时长/分辨率信号)。
|
||||
|
||||
**多集区间 (multi-episode range)**:
|
||||
单文件含多集(`S01E01-E02`)的区间解析与命名规则。目标文件名保留区间:`{标题} - S01E01-E02 - {首集名} - {末集名}.{ext}`(集名段取**首尾两集**的 TMDB 集名,` - ` 连接);Jellyfin 据 `S01E01-E02` 识别为多集条目。解析支持 `E01-E02` 与扩展形式(`E01-E02E03`)。
|
||||
_Avoid_: 吞掉区间(现状行为,静默降级为单集)
|
||||
|
||||
## Cache
|
||||
|
||||
**空哨兵 (empty sentinel)**:
|
||||
搜索"查无此片"(`results:[]`)写入的 `empty:true` 包裹缓存,TTL 3 天(`CACHE_EMPTY_TTL_DAYS`)。TTL 内免重复请求,过期后自动重新搜索——新片出现后会被发现。与"请求失败"(不写缓存,下次运行重试)语义分离。
|
||||
_Avoid_: 空结果缓存(30 天 TTL 锁死,旧行为)
|
||||
|
||||
**缓存语言段 (cache language segment)**:
|
||||
id 类缓存路径(`tv/<id>.<lang>.json`、`tv/<id>/season/<n>.<lang>.json`、`movie/<id>.<lang>.json`)携带 `TMDB_LANG`——语言相关的详情/季数据按语言隔离,切换语言自动 miss 重新拉取。与搜索缓存的 `lang` 入 hash 同构。
|
||||
_Avoid_: 无语言维度的 id 缓存(切语言后 30 天旧数据)
|
||||
|
||||
**并发去重 (flock double-checked locking)**:
|
||||
识别池多 worker 同时 miss 同一查询时的互斥机制:`tmdb_api` 未命中后 `flock` 锁(锁文件在 `/tmp`,内核锁进程退出自动释放),锁内**双检**缓存——等待者直接命中先写者的结果,同一查询只发一次 TMDB 请求。
|
||||
_Avoid_: 无锁并发(同一剧多集并发识别会重复请求击穿限流)
|
||||
|
||||
## AI
|
||||
|
||||
**判断引擎 / 执行引擎 (judgment vs execution)**:
|
||||
AI 只做**判断**——输出 `{choice: <id> | "", season_shift: <N>, search_term: "重搜词"}`(选中候选 / 季映射 / 无匹配纠正词);**命名格式化(subdir|filename、年份、Season 补零、SxxExx)始终由脚本构建**。AI 是判断引擎,脚本是执行引擎——格式化是确定性职责,不交给 AI。
|
||||
_Avoid_: AI 直接输出目标文件名
|
||||
|
||||
**AI 批处理批次 (AI batch)**:
|
||||
`AI_BATCH_SIZE`(默认 50)条/批;`AI_MAX_CALLS` 语义为**批次上限**,达标后剩余 pending 显式 `skip_unidentified`。输入构造用 jq 安全转义(文件名含引号/反斜杠不破坏 JSON);`PENDING_AI_SEARCH` 分隔符用 `$'\t'`(文件名可含 `|`)。
|
||||
_Avoid_: 全量单批(数百条 prompt 超上下文)、字符串拼接 JSON
|
||||
|
||||
**AI 失败语义**:
|
||||
AI 请求失败(网络/非 JSON/上限达标)→ 剩余 pending 显式 `skip_unidentified`(**可逆**,下次运行自动重试);只有"AI 成功返回且判断无解"才走回退命名(不可逆)。与"无 AI 密钥"语义统一——AI 没尽力 ≠ AI 尽力后失败。
|
||||
_Avoid_: AI 失败 → 回退命名(伪命名被幂等锁死)
|
||||
|
||||
**AI 匹配输入契约**:
|
||||
文件信息(`file` 完整路径 + **目录链**——源根相对路径,最多 3 层)+ TMDB **search 响应原样**(缓存中,零额外请求)+ detail 的 **seasons 四字段提炼**(season_number/name/episode_count/air_date)——体积-信息平衡点。
|
||||
_Avoid_: 手工提炼候选集(字段选择错误风险)、detail 全量原样(单条 3-8KB,批量超上下文)
|
||||
|
||||
**后缀季剥离 (suffix-season stripping)**:
|
||||
脚本侧方案:搜索无结果或季数存疑时,剥标题末尾季后缀(`T`/`S`/`II`/`III`/`2nd`/`Season 2` 等词表)重搜 base 剧 → 遍历其 seasons 找**名字含被剥后缀**的季 → 文件季号映射到该 season_number。AI 的 `season_shift` 仅兜底脚本剥离失败的情况。
|
||||
_Avoid_: 把后缀季交给 AI(脚本可解,减少 AI 依赖)
|
||||
Reference in new issue
Block a user