Files
UHD_Media_Tool/packages/domain
rootandClaude Opus 5.5 18ee466664 fix(parser): 原盘判定参考副标题线索与 PCM 音轨
标题写成 `Bluray H.264` 这类压制字样、但副标题写「自购自抓自扫」「BDMV」「原盘」「DIY」
「Blu-ray Disc」的,或带 LPCM / PCM 音轨又没有 x264 / x265 / BDRip / 压制说法的,判为原盘。
只在来源是 BluRay(或没写来源但有分辨率)时补判;「原盘压制」「自压」、WEB-DL、Remux、单个视频文件不受影响。
用「药屋少女的呢喃」真实搜索结果补正例与反例。

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Cxwgnn2pbzQ7rXv3rq7fei
2026-10-06 15:36:15 +08:00
..
2026-10-06 12:39:07 +08:00

@umt/domain

纯业务逻辑(无 IO):发布标题解析、身份匹配、Slot 满足度、收录模式/预算/磁盘阈值、路径绑定与命名、整理计划、下载状态映射。

  • 禁止 import 任何网络/文件/数据库库,也不读全局配置与时钟;时间(now)、设备号、文件清单、已存在目标等事实一律由调用方传入。
  • 不依赖 @umt/contracts:这里的类型是领域类型,W2 在 server/tasks 层映射到 zod 合同。
  • 唯一入口 src/index.ts(export * 各模块)。测试:cd packages/domain && deno task test。

模块地图

目录 主要导出 说明
release-parser/ parseRelease(title, opts)、parseReleasePath(path | segments, opts)、mergeParsed、scanCnMarkers、evalEpisodeOffset、cnToInt、fileKindOf、expandEpisodes、RELEASE_GROUP_SEED、PARSER_VERSION 标题/文件名/目录名 → ParsedRelease。行为对照 MP MetaInfo/MetaInfoPath,偏离点见下
identity/ scoreCandidate、rankCandidates、resolveIdentity、normalizeTitle、stripSeasonSuffix、toSimplified 候选(TMDB 或本地别名召回,统一为 IdentityCandidate)按证据等级排序;年份容差 ±1;只给等级不给概率
episode-resolution/ resolveEpisodes、validateEpisodes、convertEpisodes、selectRule、applyRule、suggestRule 解析出的季集号 → 媒体库规范季集:规则 → 原号校验(元数据滞后放行)→ 绝对集号 / 播出日期换算 → 待确认。见 docs/plan/episode-resolution.md
slot/ evaluateCandidate(spec, parsed, current?, ctx?)、rankForSlot、checkHardConditions、softRank、qualityTier、priorityLadder/currentPri、improvableUnits、describeViolation、validateKeywordPattern、transition/canTransition、transitionWithInflight、movieCompletion、unitCompletion、checkDistinctAssets、resolveEpisodeTotal、computeUnitGaps/seasonProgress/unitsOverlap 硬条件(区分缺字段与不满足;含排除来源、HDR/编码集合、每集体积、关键词正则、站点/做种/免费,站点事实经 EvaluationContext)、软偏好排序、优先级阶梯(MP > 分层 → pri 0–100 + 逐层 trace)、升级阈值与洗版模式(movie/per_unit/full_pack)、状态机(含 Slot×单元多在途聚合)、完成条件、季总集数账本(只升、回退不低于已覆盖最高集)、缺集 = 单元 ∩ 范围 − 已覆盖 − 在途
rules/ parseRuleString/parseExpr、serializeLayers/serializeExpr、normalizeExpr、BUILTIN_ATOMS、customAtomFromMp、evaluateRuleGroup/evaluateRuleChain、testAtom MP 规则串 ↔ 结构化层(> 分层,层内 ! & | 与括号,错误带位置);内置关键字对齐 MP(能用解析字段的用字段,其余沿用 MP 正则,音乐 13 条不支持);多组串联取交集、pri 取最后一个带层的组;经 SlotSpec.rules 接入 evaluateCandidate。差分见 test/differential/rules.test.ts 与 docs/runbook/rules.md
acquisition/ determineMode、sortByPriority/priorityOf、checkBudget、evaluateCacheVolume 新作品/追更补缺/升级判定;即时>追更>新作品>升级(可配置);每日预算;缓存卷硬阈值与恢复水位
path/ resolveTargetDirectory、renderMediaFileName、renderTemplate/compileTemplate、buildNamingContext、sanitizeSegment/sanitizeFileName、detectPathConflicts、toHostPath/toDownloaderPath、validatePathMappings 路径绑定(已有优先、不可变)、受限模板命名、非法字符、冲突结构、下载器↔宿主路径映射
organize/ planOrganize(input) → OrganizePlan、sidecarStem、sidecarLanguageSuffix 主视频 + 随行字幕/音轨、样例排除、目标存在策略、跨卷标记、H&R 前置条件
download-state/ mapQbState、applyObservation、isDownloadComplete、detectNoProgress qB 状态 → download_task 状态机;完成判定;无进展只产出证据

不变量(对应 REQ §2.1,改动这些行为必须先改需求)

  1. 身份不等于名字,路径不等于外部 ID。 identity 只输出候选与证据等级,不写入任何绑定;文件名里的 [tmdbid=…] 只提升证据一级,不直接视为已确认身份。命名一律用本地实体的标题(buildNamingContext(entity, parsed)),发布标题只提供季集/版本等事实。
  2. 已建立的路径绑定不随元数据重新计算。 resolveTargetDirectory 在存在有效绑定时直接返回 existing,模板与译名不参与;只有首次建立作品/季目录才渲染模板并带 templateVersion;同一作品多个历史目录 → conflict,不自动合并。
  3. 模型不是业务事实来源,也不产生概率。 解析与匹配只给 rule_confirmed / multi_evidence / needs_confirm(及 rejected)等级和 evidence[] 规则名;Slot 评估与优先级都是确定性代码。
  4. 失败不等于不存在。 字段缺失与字段不满足分开(unknown_field vs reject);磁盘状态读不到 → block;体积未知 → 预算 unknown;qB 返回未知状态 → 沿用上次并标记 uncertain;mapQbState(null)(=removed)只能在 qB 可达且确认列表中没有时调用;Slot 只在"确认缺失"事件下进 attention。
  5. 清理/覆盖按明确授权。 整理计划默认 onExisting: "conflict";替换需 grants.replace 且旧文件先隔离;move 在 H&R 未满足或未知时是阻塞冲突,不静默改 copy;跨卷只标记 crossDevice,由执行层走 copy→verify→delete source;孤儿字幕不搬不删;无进展只 record_evidence,从不换源。

与 MoviePilot 的偏离(测试里以 [deviation: …] 标注)

场景 MP 本项目
综艺日期 2024.05.03 / 20240503 删除或忽略 airDate(YYYYMMDD),有日期即单期,绝不判整季包
第N期 当 episode issue,与 episode 分开
第八季 全12期 季号丢失 season=8,totalEpisodes=12
未知发布组 None 末段 -GROUP 兜底,releaseGroup.source = "suffix";动漫开头方括号 bracket
剪辑版 Extended/Director's Cut/IMAX… 当干扰词删除 独立 cut 字段(内容版本),与交付版本分开
文件无剧集信号、目录是剧集 不合并 合并名称/季号,证据 parent_dir:type_inferred,置信度 needs_confirm
辅助词文件名(简体.srt) 取到根目录名 auxiliaryOnly,名称取父目录,交给附属文件配对
附加内容目录(Featurettes/花絮) 识别成独立电影 contentType = "extra",归属父作品;整理计划中排除(M2)
分盘 CD1.mkv part 丢失 part = "CD1"
有集号无季号 季号为空 推定 1 并标 seasonInferred
集号偏移 eval() 白名单解析器 evalEpisodeOffset,负结果拒绝
多集文件 E01-E03 截成单集命名 保留区间 S02E01-E03
overwrite / latest 可静默覆盖、删同集其他版本 默认冲突待处理;跨 Slot 永不删除;替换需授权 + 隔离
move 跨盘 EXDEV 隐式降级复制,删源失败仍成功 只标记 crossDevice,执行层显式三步
Move 删种 不看 H&R H&R 未满足/未知 → 阻塞冲突

给 W2 的接入提示

  • 解析:下载完成后用 parseReleasePath({ file, parent, grandParent }) 或 planOrganize({ sourceRoot })(只在种子根内取目录名);RSS/站点标题用 parseRelease(title, { subtitle })。ParsedRelease.sizeBytes 可用站点字段覆盖({ ...parsed, sizeBytes })后再交给 Slot 评估。
  • 身份:本地别名召回(pg_trgm)与 TMDB 结果都转成 IdentityCandidate(本地用 provider: "local" + localEntityId + linkedExternalIds);resolveIdentity 为 auto 才自动绑定,其余进待识别池。
  • 选片:rankForSlot(spec, candidates, currentAssetFacts);unknown_field 表示需补查详情(如字幕/体积),不是拒绝。
  • 下载前:determineMode → priorityOf → checkBudget → evaluateCacheVolume(按卷汇总所有 qB 实例的在途任务,已提交计划的预留不要再传)。
  • 入库:resolveTargetDirectory(返回的 newBindings 与文件登记同事务写入)→ 库根 hostPath 拼成绝对目录 → planOrganize → 冻结计划;requiresApproval 为真时生成 decision/approval。
  • 状态:Slot 与下载任务的状态迁移在写库前先过 transition / applyObservation,非法迁移抛错即拒绝写入。