Files
UHD_Media_Tool/docs/runbook/subscriptions.md
T

11 KiB

订阅运行手册(W9-E)

实现:apps/server/src/modules/acquisition-organize/{routes/subscriptions.ts, services/coverage.ts, services/evaluate-autofill.ts, services/subscriptions-params.ts, services/subscriptions.ts}、 packages/domain/src/{slot/overview.ts, acquisition/fill.ts}、contracts policy.ts(SubscriptionOverviewItem SubscriptionCoverage 等)、迁移 0035_subscriptions_v2。 设计:docs/design/subscriptions.md §1–§3、§5;规则组见 docs/runbook/rules.md;W5-C 账本/完成/搜索见 docs/plan/05-subscription-gap.md §4。

数据

列 含义
subscription.last_progress_at 上次进展:核验入库写分集账本时(afterSlotVerified)、季总集数变化时(applyEpisodeTotal)写入;迁移时按账本/计划时间回填
acquisition_slot.position 同一订阅内的顺序,最小者为主版本;同值按 created_at、id。模板建目标时按模板顺序编号;迁移按 created_at 编号

总览"上次更新" = max(last_progress_at ?? created_at, 该订阅版本目标最近一次非草案计划的 created_at)。

总览 GET /subscriptions

查询:status mediaEntityId mediaKind q group sort order siteId ruleGroupId origin upgrading cursor limit。 不带 status 时不返回 cancelled(只在"订阅历史"里看);status=cancelled 时 group = null。

分组(服务端按顺序判定,group 字段 + counts,计数是当前筛选去掉 group 后的数量):

group 条件
paused 暂停 status = paused
pending 待定 status = pending,或剧集/综艺范围内任一季总集数未知(剧集没有任何季也算)
completed 已完成 status = completed,或所有版本目标在范围内全覆盖且总集数已知(continuous 剧集此时 awaitingNewSeason = true)
following 追更中 库内任一版本有已入库或在途单元
waiting 待搜索 没有已入库/在途单元;何时搜索看 nextSearchAt(searchEnabled = false 表示周期搜索已关)

排序 sort:lastUpdated(缺省,倒序)/ nextSearch(正序,没排期的在最后)/ missing(主版本缺失数倒序)/ created(倒序)/ name(正序);order 可反转。 分组与排序是派生的,所以在内存里做:一次最多取 2000 条订阅,游标是偏移量(不透明串)。siteId 筛选包含"站点范围为空 = 全部站点"的订阅。

条目 = 订阅行 + group year seasonNumber progress{have,total,inflight} missing slots[] lastUpdatedAt searchEnabled sites[] ruleGroups[] defaultRuleGroups autoFill{enabled,mode,source,budget} upgrading awaitingNewSeason。 progress 取主版本;total 在任一季总集数未知时为 null。slots[] 每个版本目标一条:have inflight missing total upgrading minPri targetPri autoFill primary ruleGroupIds。 ruleGroups = 订阅 params.ruleGroupIds → 各版本目标 ruleGroupIds 的并集 → 订阅默认链(defaultRuleGroups = true)。

查询量固定:订阅 1 条 + 版本目标/作品/内容单元/资产/覆盖/分集账本/在途计划/最近计划/延后各 1 条,和订阅数无关;站点名只查当前页。

覆盖矩阵 GET /subscriptions/{id}/coverage

  • seasons[]:剧集按季分段(seasonNumber total totalKnown source locked columns[]);综艺只有播出日期的单元另成一段(seasonNumber = null,列键 d{YYYYMMDD});电影一段,列 = 内容版本(edition:default、edition:Extended…,取各版本目标 completion.cuts 与库内资产版本的并集)。
  • 列:unitKey episode airDate contentUnitId temporary segment edition。temporary = 没有内容单元行(合成键)或内容单元仍是 provisional;segment = 综艺分段(上/中/下,整集为 null)。 总集数已知列到 total;未知列到已见最高集(已入库/在途/已建集单元)。
  • rows[]:首行 slotId = null "库内任一版本"(作品全部资产 + 全部分集账本、全部在途),其后每个版本目标一行(按 position,primary 标主版本,autoFill 为非主版本当前是否自动补全),summary{have,inflight,missing,skipped}。
  • 格 cells[i] 与展平后的列一一对应:
state 含义 附带
have 已入库 assetId quality{resolution,source,label} pri upgradable(开了升级且 pri < 目标 pri)
inflight 在途(计划单元键与列相交,季包整季通配 s{季}* 覆盖该季全部未入库集) planId planStage(reserved / downloading / verifying)
missing 缺失 reason = suspected(总集数未知)、deferred(该单元有延后资源)
skipped 不需要 reason = out_of_range(集范围外)/ not_aired(播出日期在今天之后)/ not_required(电影:该版本目标不要求这个内容版本)

覆盖口径与完成判定一致(slotCoveredUnits):版本目标覆盖 = 该目标计划产出的资产 + 当前资产 + 分集账本;作品只有一个版本目标时作品下全部资产都算。 进程内缓存 30s(?refresh=true 强制重算);订阅写入(PATCH、暂停/恢复、排序、补全、设总集数)会清掉该订阅的缓存。/progress 保留一版兼容。

多版本自动补全

  • 开关 params.autoFill: inherit | on | off(缺省 inherit):inherit 取订阅生效规则链(私有组 → params.ruleGroupIds 或订阅默认链)上最后一个设了 auto_fill 的组,都没设为开。总览 autoFill.source 标来源(subscription / rule_group / default)。
  • 预算 params.fillBudget {unitsPerDay?, bytesPerDay?}:按订阅、按自然日,只计自动补全计划(acquisition_plan.evaluation.autoFill = true,草案/取消/失败/过期不计),同时仍受全局每日预算与缓存卷阈值约束。
  • 评估(evaluateForSlot 建计划前 autoFillCheck):
    • 主版本、没有订阅的版本目标、只有一个版本目标:照旧;
    • 非主版本 + 开关关:不建计划,候选记 deferred(deferred:auto_fill_off),不进 slot_deferral;
    • 非主版本 + 开 + 计划单元都已被主版本满足(电影:主版本有当前资产):补全计划,priority = upgrade(低于追更与新作品),evaluation.autoFill = true;超出补全预算 → 延后(slot_deferral.reason = fill_budget),由 slots.reevaluate 周期重评,次日预算重置后补上;
    • 非主版本 + 开 + 主版本也缺:照常追更(priority = tracking)。
  • 评估时同一发布按 position 先评主版本(evaluateRelease 版本目标按 position、created_at 排序)。
  • 手动补全 POST /subscriptions/{id}/slots/{slotId}/fill {unitKeys?}:入队 evaluateAcquisition{acquisitionSlotId, forceFill},不受开关与补全预算限制(全局预算/缓存卷照常),unitKeys 限定只补这些单元;订阅须在追踪中(active/pending)。
  • 定时:不新增任务。slots.reevaluate(到期 Slot 重评)覆盖补全与预算延后;subscriptions.search 按 next_search_at 到期搜索。

编辑表单字段(POST / PATCH params)

写入时规范化,库里只存规范键(未知键原样保留)。PATCH 的 params 仍是整体替换。

规范键 兼容写法(MP / 旧键) 说明
siteIds sites(站点 id 或 key) 空 = 全部启用站点;不存在 400
downloaderInstanceId downloader(实例 id 或名称) 不存在 400
downloadSavePath save_path / savePath 已存,下载提交暂未使用(P2)
episodeRange {start,end} start_episode / end_episode;旧 episodeRange.locked 丢弃 end < start 400;电影丢弃
—(季总集数) total_episode + manual_total_episode 保存后写订阅季(无季单元按第 1 季)的总集数,manual_total_episode 为真时锁定
include[] / exclude[] 字符串或数组;开头的 (?i) 去掉 正则白名单(validateKeywordPattern)+ 语法校验,不过 400;评估时并入每个版本目标的硬条件
keywords[] keyword 已存;主动搜索任务(sources-identity)尚未追加这些检索词
ruleGroupIds filter_groups(组名或 id) 不存在/已删 400
—(版本目标升级) best_version / best_version_full 写到该订阅的版本目标 upgrade_threshold:关 → enabled:false;开 → mode = per_unit(剧集)/ full_pack(全集洗版)/ movie(电影),revision+1
autoFill 布尔;autoFillVersions{enabled} → on / off
fillBudget {unitsPerDay, bytesPerDay} fillBudget{perDay, bytes}、autoFillVersions{budget{perDay, bytes}} 负数 400
libraryRootId — 库根须存在;整理去向暂未使用(organize 侧待接)
searchIntervalHours stopAfter — 同 W5-C;stopAfter 须是时间

新建:同作品同季(season_unit_id 同为空也算)已有 active / pending / paused 订阅 → 409,消息里带已有订阅 id。PATCH 把状态改回追踪、重新订阅也做同样检查。

状态动作

端点 说明
POST /subscriptions/{id}/pause active / pending → paused(status_reason = user_paused,清 next_search_at);已暂停幂等;其他 409
POST /subscriptions/{id}/resume paused → active(TMDB 未就绪回 pending),next_search_at = now(周期搜索开着时),立即重评;追踪中幂等;已完成/已取消 409
POST /subscriptions/{id}/resubscribe completed / cancelled → active / pending(resubscribed),历史保留,写审计,立即重评 + 主动搜索
POST /subscriptions/batch {ids, action} search / pause / resume / delete;逐条返回 {id, ok, error};delete = 取消(cancelled,user_deleted)并写审计 subscription.deleted
PUT /subscriptions/{id}/slot-order {slotIds} 必须恰好是该订阅的全部版本目标;第一个成为主版本
POST /subscriptions/{id}/search 同 W5-C

PATCH status 仍可直接改(旧行为),完成写 subscription_history 的逻辑不变。

测试

  • apps/server/test/subscriptions/overview-domain.test.ts:分组顺序、矩阵行格状态(通配在途、范围、未播出、suspected、可升级、延后)、自动补全取值、补全预算。
  • apps/server/test/subscriptions/subscriptions-v2.test.ts(真实 PG):矩阵(季包 + 单集 + 双版本 + 在途 + 整季通配 + 临时单元 + 未播出 + 缓存)、集范围 / 总集数未知 / 电影版本、总览分组计数排序分页、自动补全(补全计划优先级、主版本也缺照常追更、关闭记延后、手动补全)、规则组继承与补全预算、表单兼容与校验 / 409 / best_version、暂停恢复重新订阅批量排序。
  • E7 / E9 / 规则组 / 订阅增强测试保持通过。

遗留

  • 日历(§4,P2)、POST /subscriptions/{id}/reset(差距 #24)未做。
  • keywords 未接主动搜索(sources-identity/tasks.ts searchSubscription);libraryRootId / downloadSavePath 已存未接整理与下载提交。
  • include/exclude 以订阅 params 存、评估时并入硬条件,没有建订阅私有规则组(设计 §3 的写法);分辨率/来源/特效(MP resolution/quality/effect)表单字段未映射。
  • 综艺分段列(同一播出日的上/中/下拆成独立列)等身份层按分段建单元后启用;现在只在列上标 segment。
  • 版本目标新建(POST /acquisition-slots)position 缺省 0,与主版本同值时按 created_at 排在后面;需要调整顺序用 slot-order。