Files
UHD_Media_Tool/docs/design/subscriptions.md
T

21 KiB
Raw Blame History

订阅重做 + 规则页(对标 MoviePilot v3)

需求来源:docs/plan/00-first-version-plan.md §8 第 3、4 条(订阅重做、收录策略 + 偏好标签合一)。命名模板另见 naming-template.md。 上游行为账本:docs/porting/moviepilot-subscribe.md;差距清单:docs/plan/05-subscription-gap.md;规则关键字核对:docs/porting/mp-rust-logic-parity.md §6。上游 jxxghp/MoviePilot v3 @ e6820a8,只记行为,不复制源码。

0. MP 要点与我们的现状

MP 订阅页(pages/subscribe.vue、views/subscribe/SubscribeListView.vue、components/cards/SubscribeCard.vue)

  • 按类型分路由:电影 / 电视剧 / 音乐;每类 Tab「我的订阅 / 热门 / 分享」。
  • 筛选(剧集):全部 / 洗版 / 未开始 / 订阅中 / 待定 / 暂停 / 已完成。归类顺序:洗版 → P 待定 → S 暂停 → lack_episode==0 已完成 → completed_episode>0 订阅中 → 未开始。没有分组,只有筛选。
  • 排序:自定义拖拽 / 上次更新 / 添加时间 / 缺集数(剧集),按类型记在本地。批量:全选、启用、暂停、删除。
  • 卡片:背景图 + 海报、年份、名称 + 季(S0 显示"特别篇")、进度 total−lack / total、洗版进度条(已洗版/已下载双段)、洗版角标、状态角标(只显示 暂停 / 待定)、用户名、上次更新(相对时间)。不显示站点、下载器、规则组。
  • 菜单:编辑 / 搜索 / 暂停·启用 / 重置 / 分享 / 媒体详情 / 文件统计 / 取消订阅。
  • 编辑对话框两 Tab。基础:搜索关键词、总集数、开始集数、质量/分辨率/特效、订阅站点、下载器、保存路径、洗版、全集洗版、用 ImdbID 搜索。进阶:包含、排除、优先级规则组、剧集组、指定季、自定义类别、识别词。"默认订阅规则"用同一对话框,按类型存 Default{Movie|Tv}SubscribeConfig。
  • 文件统计对话框:按集两 Tab「下载文件 / 媒体库文件」。选季对话框:每季显示集数、缺失/已订阅,可选 普通 / 分集洗版 / 全集洗版。日历:月视图,事件按 air_date 聚合,入库状态 无 / 部分 / 完整。
  • 进度口径:lack_episode 存库(普通 = 媒体库缺集;洗版 = 范围内 note 与 pri 都没有的集),completed_episode 响应时派生(洗版按 pri==100 计)。

MP 规则(app/application/rules.py:510-656、modules/filter/__init__.py、views/setting/AccountSettingRule.vue)

  • 内置规则 = 正则原子,默认匹配 标题+副标题+标签,可限定字段;可带 tmdb 条件(命中即放行,CNSUB/CNVOI 用 original_language)。自定义规则字段:id、name、include、exclude、size_range(MB,剧集按每集)、seeders、publish_time,同 id 覆盖内置。
  • 规则组 = name + rule_string + media_type + category;> 分层,层内 ! > & > | 与括号;第 1 层 pri=100 逐层 −1,一层不中即过滤。多组按 media_type/category 取适用组后串联 AND,pri 取最后一组。
  • 默认组:订阅默认、洗版默认、搜索默认,各为组名列表;订阅 filter_groups 非空则覆盖默认。
  • UI:三张卡片(自定义规则 / 优先级规则组 / 下载优先规则);规则组编辑 = 组名 + 媒体类型 + 分类 + 可拖拽"优先级 N"层卡片,每层多选 chips(内置 + 自定义 + "排除:X"),保存时层内 &、层间 >;| 与括号只能靠导入字符串。JSON 导入/分享。

我们

  • 订阅 = 本地作品 + 季单元 + season_scope + params;条件在版本目标(Slot);状态 active/paused/pending/completed/cancelled;已有 /progress、/search、/episode-total、历史。订阅页是表格 + 详情抽屉,进度只算第一个版本目标(subscriptionProgress 取 slots[0])。
  • 版本目标已有:硬条件(SlotHardConditions,含 include/exclude、站点、做种、免费)、优先级阶梯 soft.tiers[](层内字段 AND)、三种洗版模式、分集质量账本 slot_unit。
  • 收录策略(policy_revision kind=acquisition,草案→发布→回滚,含 newTitle / defaultSlots)与偏好标签(preference_tag + 抑制记录)是两页;偏好标签没接入评估(差距 #17)。
  • 阶梯层内只能 AND 结构化字段;MP 的 SPECSUB/CNVOI/HKVOI/GZ/ATMOS/60FPS/3D/UHD、跨字段 OR 表达不了(parity §6:43 条样例 ✔22 ~6 ✗15)。

1. 订阅总览

路由 /console/subscriptions,页头 Tab:剧集 / 电影 / 综艺 / 日历(P2),选中项写 URL(?kind=)。视图切换 卡片 / 列表(记本地)。

1.1 分组(派生,不新增状态值)

分组 条件(按顺序判定) 对应 MP
暂停 status=paused S
待定 status=pending(未建档 / 无 TMDB 身份),或任一范围内季 totalKnown=false P
已完成 status=completed,或 continuous 订阅所有版本目标在范围内全覆盖且 total 已知(卡片仍追下一季时显示"等待新季") lack=0
追更中 active 且至少一个单元已入库或在途 订阅中
待搜索 active 且没有任何已入库/在途单元 未开始

"洗版中"不单列分组,作为徽标(任一版本目标开升级且未达顶档)与筛选项。cancelled 不在总览,只在"订阅历史"。

默认按分组折叠展示(待定、追更中、待搜索、已完成、暂停),组内按上次更新倒序。也可切成平铺 + 排序:上次更新 / 下次搜索 / 缺集数 / 添加时间 / 名称。筛选:分组、洗版中、站点、规则组、来源(用户/Agent/策略)、关键词;全部进 URL query(use-query-state.ts)。

上次更新 = subscription.last_progress_at(新列):任一关联版本目标的单元转为 已入库 / 在途,或总集数变化时写入;为空取 created_at。与 MP last_update 同义(MP 在下载添加时更新)。

1.2 卡片字段

区 字段 数据
封面 占位块:类型图标 + 标题首字(暂无海报;有 TMDB poster_path 后再接) —
标题行 名称、年份、类型标签;剧集加季(S1,S0 显示"特别篇",following_seasons 显示"S2 起") 实体、季单元
进度 已入库/总,总未知显示 ?;下方细条:已入库、在途两段;多版本时显示主版本数字,悬浮列出每个版本 overview.progress
版本徽标 每个版本目标一个:名称 + 状态色(已齐 / 缺 N / 在途 N / 洗版中 pri a→目标 b) overview.slots
元信息 上次更新(相对时间)、下次搜索(或"搜索已关")、站点范围(≤2 个名 + "+N";空 = 全部站点)、规则组(同样折叠)
状态角标 仅 暂停 / 待定 / 洗版中(同 MP 只标异常态)
操作菜单 立即搜索、查看对比、编辑、暂停/启用、重置(P2)、删除;点卡片 = 查看对比

批量模式:勾选后 立即搜索 / 暂停 / 启用 / 删除(删除走确认,写审计)。空态:"还没有订阅。从媒体库或 Agent 添加。"

2. 对比视图(订阅详情)

路由 /console/subscriptions/:id(取代抽屉;抽屉保留为列表页快速预览)。页头:标题、状态、进度、操作按钮;下方三块:覆盖矩阵、版本目标、记录(搜索记录、候选评估、订阅历史)。

2.1 覆盖矩阵

  • 列 = 集,按季分段(季标题显示 已入库/总 · 总集数来源 · 锁定,可点"设置总集数");行 = 版本目标,每个一行;首行"库内任一版本"汇总。
  • 电影:每个版本目标一格。
格状态 显示 来源
已入库·达标 实心,文字 2160p REMUX slot_unit + asset
已入库·可升级 实心 + 角点,悬浮 pri 97 → 目标 100 slot_unit.pri < targetPri
在途 斜纹,计划状态(已预留/下载中/核验中) 在途计划 unit_keys
延后 空心 + 时钟 slot_deferral
缺失 空心红框 computeUnitGaps
疑似缺失 虚线红框 total 未知时的集号空洞
未播出 灰,显示播出日期 content_unit.air_date > 今天
范围外 淡灰 episodeRange / season_scope

点格打开侧栏:资产位置(链到文件管理)、质量与 pri、来源资源与下载任务、该单元的候选评估(接受/拒绝理由 trace)、操作"搜索此集""为此版本补全"。

2.2 多版本与自动补全

  • 每个版本目标一行,行尾:缺失数、在途数、"补全此版本"按钮、自动补全状态。
  • 自动补全其他版本:订阅级开关 params.autoFill: "inherit" | "on" | "off",缺省 inherit → 取引用规则组/订阅默认值的 autoFill(系统缺省 on)。
    • on:非主版本目标像主版本一样独立评估、搜索、下载缺失单元。
    • off:非主版本目标只登记缺失,不自动建计划;RSS 来的合格资源记为"延后(未开自动补全)";手动"补全此版本"或 Agent 调 download.plan 仍可下。
    • 主版本 = 版本目标排序第一个(可在版本目标区拖动)。
  • 补全预算 params.fillBudget {bytesPerDay?, unitsPerDay?}:只约束非主版本的自动补全计划,计入全局每日预算;超出记延后,次日由 slots.reevaluate 重评。计划优先级用 upgrade 档(低于追更)。

3. 新建 / 编辑订阅表单

对话框两 Tab,同 MP 分"基础 / 进阶"。"订阅默认值"(按类型)用同一表单的默认模式,存规则页 §6.4。

区 字段(界面) MP 字段 我们存放 默认 / 显示条件
基础 作品(搜索本地作品或 TMDB) media_id / name mediaEntityId / titleQuery 必填
类型 type mediaKind 由作品带出
季 / 季范围 season seasonUnitId + seasonScope 剧集;缺省最新季 + 接续后续季(Agent 来源缺省仅本季)
开始集 / 结束集 start_episode params.episodeRange {start,end} 剧集
总集数(可锁定) total_episode / manual_total_episode POST /episode-total 剧集;显示来源
方式 — mode once / continuous 电影缺省 once
优先级 — priority tracking
站点范围 sites params.siteIds 空 = 全部启用站点
下载器 downloader params.downloaderInstanceId 空 = 默认实例
库根 save_path(整理去向部分) params.libraryRootId(新) 空 = 按类型默认库根
版本目标 best_version / best_version_full 子表:名称 + 规则组 + 升级(关 / 按集 / 整季 / 电影)+ 目标 pri 缺省取订阅默认值
进阶 规则组 filter_groups params.ruleGroupIds(作用于全部版本目标)+ 版本目标 rule_group_ids 空 = 订阅默认组
包含 / 排除 include / exclude 订阅私有规则组(scope.subscriptionId,不在列表页显示) 正则白名单校验
分辨率 / 来源 / 特效 resolution / quality / effect 同上,写进私有规则组 hard
搜索关键词 keyword params.keywords[](新,追加到标题/别名检索词)
主动搜索间隔 —(全局) params.searchIntervalHours 24;0 = 关
自动补全 / 补全预算 — params.autoFill、params.fillBudget inherit
截止时间 — params.stopAfter
下载保存路径 save_path params.downloadSavePath(新,P2) 空 = 实例默认
不做 剧集组 episode_group P2(需 TMDB episode group 适配)
ImdbID 搜索 search_imdbid P2 params.searchByImdb
自定义类别 media_category 用命名模板 category
识别词 custom_words 放身份层(实体别名/集偏移),表单给链接

保存:创建时同实体同季已有 active 订阅 → 返回 409 并给"打开已有订阅"(差距 #18)。

4. 日历(P2)

/console/subscriptions?kind=calendar:月视图,事件 = 追踪中订阅范围内集单元的 air_date(同日同剧聚合为 E03–E04),电影取上映日期;颜色 = 该日事件 已入库 / 部分 / 未入库 / 未播出。数据来自 refreshMetadata 写入的集单元 air_date(需补:目前只写季单元总集数,不建集单元)。接口 GET /subscriptions/calendar?from&to。

5. 后端与 contracts

端点 用途 新/改
GET /subscriptions/overview?kind&group&sort&siteId&ruleGroupId&q&cursor 卡片数据:分组、进度、每版本摘要、上次更新、下次搜索、站点、规则组 新
GET /subscriptions/{id}/coverage 覆盖矩阵:季 × 集 × 版本目标 新(取代 /progress,后者保留一版兼容)
POST /subscriptions/batch {ids, action: search|pause|resume|delete} 新
POST /subscriptions/{id}/slots/{slotId}/fill 手动补全某版本(可带 unitKeys) 新
POST /subscriptions/{id}/reset 清在途预留、重评(保留资产) 新(差距 #24)
GET /subscriptions/calendar 日历 新,P2
PATCH /subscriptions/{id} params 新键:ruleGroupIds keywords libraryRootId autoFill fillBudget downloadSavePath 改

contracts(policy.ts):SubscriptionOverviewItem、SubscriptionCoverage {seasons[{seasonNumber,total,totalKnown,source,locked,episodes[{episode,airDate}]}], rows[{slotId|null,name,primary,autoFill,cells[{unitKey,state,quality,pri,planId,assetId}]}]}、SubscriptionBatch、SubscriptionParams 扩键;规则页见 §6.6。

覆盖矩阵查询:一次取 作品全部集单元 → 左连 asset_unit_coverage(库内任一版本)、slot_unit(按版本目标)、在途计划 unit_keys、slot_deferral;在服务里用 computeUnitGaps 逐版本目标算状态。总览用一条聚合 SQL 出每订阅 × 版本目标的 已入库/在途/总 计数,避免 N+1;超 200 条订阅走游标分页。

DB:subscription.last_progress_at;acquisition_slot.rule_group_ids uuid[]、acquisition_slot.position int(主版本排序);rule_group、rule_atom 两表(§6);偏好标签迁移。

定时任务(沿用注册表):subscriptions.search、subscriptions.metadata(补:建集单元与 air_date;完成前强制刷新 = 差距 #8)、slots.reevaluate(补:按 autoFill 与 fillBudget 过滤非主版本)。不新增任务。

6. 规则页(收录策略 + 偏好标签合一)

路由 /console/rules,页名"规则"。Tab:规则组 / 自定义规则 / 默认值 / 收录策略 / 抑制记录。/console/preferences 重定向到 /console/rules?tab=groups(带 origin 筛选时透传);术语表加"规则组",弃用"偏好标签"(对话与 Agent 产生的规则组以"来源"区分)。

6.1 规则组结构

字段 含义
name、description 名称必填唯一;说明一句
hard 硬条件(必须 / 排除),即 SlotHardConditions,再加 atoms {require[], exclude[]} 引用原子
ladder 优先级阶梯:有序层,每层一个布尔表达式 expr(原子 + `! &
ladderRequired 一层不中即拒绝(MP 语义,缺省 true;从 MP 串导入时固定 true)
scope mediaKinds[]、entityIds[]、siteIds[]、subscriptionId(私有组);空 = 全部
origin manual / conversation / agent_inferred(沿用偏好标签来源)
status、revision、fingerprint、evidence 同偏好标签;每次修改 revision+1 并写审计,评估 trace 记 组id@revision
autoFill 引用本组的订阅缺省是否自动补全其他版本

原子(rule_atom):内置(只读)+ 自定义。自定义原子字段对齐 MP 自定义规则:id(大写字母数字)、name、include[]、exclude[]、matchFields(title/subtitle/labels,缺省全部)、unitSizeMB {min,max}(剧集按每集)、minSeeders、publishDelayMinutes {min,max}、free。同 id 不允许覆盖内置(偏离 MP,避免静默改义)。

6.2 内置原子(对齐 MP 关键字)

原子 语义 我们的判定
4K / 1080P / 720P 分辨率 resolution 字段
BLU 蓝光原盘 source = BluRay-Disc(先修 parity our_bug:Blu-ray+AVC/HEVC 无 x26x 判原盘)
BLURAY 标题含 Blu-ray 来源属蓝光族(原盘/REMUX/压制)
REMUX / WEBDL Remux;WEB-DL 或 WEBRip source 字段
UHD 标题含 UHD/UltraHD parsed.uhd
H265 / H264 编码 videoCodec
HDR / DOLBY / SDR HDR 类;杜比视界;SDR hdr 字段(HDR = 任一非 DV HDR,同 MP 正则口径做差分)
ATMOS 全景声 audio 含 Atmos
60FPS / 3D 帧率;3D fps>=60;正则 3D
CNSUB 中字 subtitles 含 zh(补 CHS/CHT/GB/BIG5);作品原语言 zh 时直接满足(MP tmdb 放行)
SPECSUB 特效字幕 正则 特效(标题+副标题+标签)
CNVOI / HKVOI 国语配音 / 粤语 正则;CNVOI 作品原语言 zh 时满足
GZ 官种 只看站点标签 官方/官种/官组
FREE 免费 downloadVolumeFactor = 0
音乐 13 条(HIRES/FLAC/…) — 不做

6.3 MP 规则串 ↔ 结构化

  • 规则组编辑页有"规则串"输入框(高级):输入 SPECSUB & CNSUB > CNSUB & 4K > 1080P & !BLU 实时解析,下方同步画出层卡片;反过来编辑层卡片时规则串随之更新(规范化:原子大写、层内按 | 优先级加最少括号)。
  • 解析错误逐条给位置:未知原子(给相近名)、括号不配对、空层。音乐原子报"不支持"。
  • 层卡片编辑:每层是 chips 行,chip = 原子或取反原子;层内 chips 之间可切换 且/或,支持"加括号组";拖动层即改优先级(同 MP),层标题显示 优先级 1 · pri 100。
  • 测试:"用最近资源测试"对 200 条近期资源跑本组,显示每条命中层/被拒原因,和 MP trace 同粒度;也可粘贴标题测试。
  • JSON 导入/导出兼容 MP 格式(name/rule_string/media_type,自定义规则 7 字段),media_type 映射到 mediaKinds,category 丢弃并提示。

6.4 引用关系与默认值

  • 版本目标 rule_group_ids(有序)+ 订阅 params.ruleGroupIds;评估时编译为 SlotSpec:hard 取交集;各组按 scope 过滤后全部须命中某层,pri 取最后一个带阶梯的组(同 MP),界面在该组上标"决定优先级"。
  • 版本目标自身的 hard/soft 字段保留,视为一个匿名组排在最前。
  • 默认值 Tab:按类型设 订阅默认规则组、洗版默认规则组(版本目标开升级时额外叠加)、搜索默认规则组(手动搜索与 Agent release.search)、订阅默认表单(§3 默认模式:站点、下载器、库根、版本目标模板、自动补全)。
  • 收录策略 Tab:现有策略工作区原样搬入(草案→预览→发布→回滚、newTitle、defaultSlots);SlotTemplate 增加 ruleGroupIds。
  • Agent:能力 rules.list 读规则组;新增/修改走 rules.propose(origin=agent_inferred,生成待处理事项,批准后生效);命中有效抑制记录的指纹直接拒绝。对话中用户明确说的偏好 origin=conversation,直接生效并可撤销。

6.5 偏好标签迁移

旧 kind 新规则组
hard hard 条件,ladder 空
soft ladder 单层(或按 rule 顺序多层),ladderRequired=false
version 规则组 + 在"默认值"里挂到对应版本目标模板
target 转收录策略 newTitle scope,原标签留一条只读规则组作说明

指纹、来源、证据、修订历史、抑制记录原样迁移(抑制按新组指纹重算一次,旧指纹保留作别名)。迁移后偏好标签接入评估,关闭差距 #17。

6.6 规则接口

GET/POST /rule-groups、GET/PATCH/DELETE /rule-groups/{id}、GET /rule-groups/{id}/revisions、POST /rule-groups/parse {ruleString} → {ladder, errors[]}、POST /rule-groups/render {ladder} → {ruleString}、POST /rule-groups/{id}/test {sourceReleaseIds?|titles?}、GET/POST/PATCH/DELETE /rule-atoms、GET/PUT /rule-defaults、POST /rule-groups/import、GET /rule-groups/export。抑制记录接口沿用 /preference-suppressions(改名 alias /rule-suppressions)。

domain:slot/rule-expr.ts(词法/解析/求值/规范化输出)、slot/atoms.ts(内置原子表)、SlotTierRule.expr?;mapping/slot.ts 编译规则组 → SlotSpec。差分:filter-cases.json#rules 43 条全部重跑,目标 ✗ 从 15 降到 1(只剩音乐)。

7. 实施拆分与估算

包 内容 估 依赖
R1 domain rule-expr 解析/求值/串互转、内置原子(含 BLU/CHS 修正)、多组编译、差分样例 M 2 天 —
R2 server 规则 迁移(rule_group、rule_atom、slot.rule_group_ids)、CRUD/parse/render/test/import、偏好标签迁移、评估接入、Agent rules.propose L 3 天 R1
R3 web 规则页 5 个 Tab、层卡片编辑 + 规则串双向、测试面板、/preferences 重定向、术语表 L 3 天 R2
S1 server 订阅 last_progress_at、overview 聚合、coverage 矩阵、batch、fill、reset、params 新键、autoFill/fillBudget 调度、查重 409、集单元与 air_date L 3 天 R2(ruleGroupIds)
S2 web 总览 类型 Tab、分组/排序/筛选、卡片、批量 M 1.5 天 S1
S3 web 详情 覆盖矩阵 + 格侧栏、版本目标区(排序、补全、自动补全)、编辑表单(两 Tab、默认模式) L 2.5 天 S1
S4 日历(P2) 接口 + 月视图 M 1 天 S1

并行:R1 与 S1 的非规则部分可同时开工;R3 与 S2/S3 可并行。合计约 15 人日(不含 P2 约 14)。 验收:subscription.test.ts 加 覆盖矩阵、自动补全 off 不建计划、补全预算延后、多组 pri 取最后组;e2e 加"双版本订阅一版齐一版缺 → 矩阵正确 → 手动补全"。