Files
UHD_Media_Tool/packages/domain/README.md
T

59 lines
8.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# @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`,非法迁移抛错即拒绝写入。