feat(subscriptions): private rule groups, MP quality presets, save path, reset and calendar API

- include/exclude sync to a per-subscription private rule group (rule_group.hard, migration 0036);
  evaluation merges chain hard conditions; private groups hidden from GET /rules, soft-deleted with
  the subscription and rebuilt on resubscribe
- MP resolution / quality / effect map to presets written onto the subscription's own slots
  (default slot template on create); UHD / Atmos go to the private group include
- downloadSavePath mapped to the downloader path and passed as qB savepath; libraryRootId used
  for the first organize when the work has no path binding; keywords appended to active search
- POST /subscriptions/{id}/reset releases stale in-flight plans, clears deferrals, re-evaluates
- GET /subscriptions/calendar from episode air dates; refreshMetadata writes TMDB season
  episode air dates for tracked seasons

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
rootandClaude Opus 5.5 committed 2026-09-28 20:14:40 +08:00
1 parent 52402864d6
commit 70d56645fe
20 files changed
+1663 -44

No files matched your search

+22
View File
@@ -629,6 +629,28 @@ export const Api = {
},
R,
),
/** 日历(静态路径,须先于 /subscriptions/{id} 注册) */
calendar: ep({
method: "get",
path: "/subscriptions/calendar",
scope: "read",
summary:
"订阅日历:追踪中订阅范围内集的播出日期(同日同季聚合)与入库状态,电影取上映日期;没有播出日期的列在 unknown。from/to 为 YYYY-MM-DD,跨度不超过 62 天",
query: z.object({
from: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
to: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
mediaKind: policy.SubscriptionMediaKind.optional(),
}),
response: policy.SubscriptionCalendar,
}),
reset: ep({
method: "post",
path: "/subscriptions/{id}/reset",
scope: "admin",
summary: "重置进度:取消已预留未提交、下载任务已不在的计划,清延后记录,立即重评;仍在下载的计划与已入库资产保留",
params: IdParam,
response: policy.SubscriptionResetResult,
}),
/** 总览:在 crud 列表上加分组/排序/筛选,条目带卡片派生字段,另给各分组计数(docs/design/subscriptions.md §1) */
list: ep({
method: "get",
+77 -1
View File
@@ -97,12 +97,32 @@ export const FillBudget = z
.strict();
export type FillBudget = z.infer<typeof FillBudget>;
/** MP 分辨率(resolution)→ 版本目标硬条件 resolutions */
export const SubscriptionResolution = z.enum(["4k", "1080p", "720p"]);
export type SubscriptionResolution = z.infer<typeof SubscriptionResolution>;
/** MP 资源质量(quality / resource_type)→ 版本目标硬条件 sources / codecs;uhd 写进订阅私有规则组的包含 */
export const SubscriptionSourcePreset = z.enum([
"bluray_disc",
"remux",
"bluray",
"uhd",
"web",
"hdtv",
"h265",
"h264",
]);
export type SubscriptionSourcePreset = z.infer<typeof SubscriptionSourcePreset>;
/** MP 特效(effect)→ 版本目标硬条件 hdr + 软偏好 hdr 顺序;atmos 写进订阅私有规则组的包含 */
export const SubscriptionEffect = z.enum(["dv", "hdr", "sdr", "atmos"]);
export type SubscriptionEffect = z.infer<typeof SubscriptionEffect>;
/**
* subscription.params 的已知键(JSON 列,其余键原样保留):
* siteIds 站点范围;episodeRange 集范围(对照 MP start/total_episode);searchIntervalHours 主动搜索周期(0 = 关);
* stopAfter 截止时间;downloaderInstanceId 指定下载器;ruleGroupIds 规则组;keywords 追加检索词;
* include/exclude 订阅级关键词正则(作用于全部版本目标);libraryRootId 库根;downloadSavePath 下载保存路径;
* autoFill/fillBudget 多版本自动补全。
* autoFill/fillBudget 多版本自动补全;resolution/source/effect 质量预设(MP resolution/quality/effect)。
* include/exclude 保存时同步为订阅私有规则组(scope.subscriptionId,随订阅删除),评估只读规则组。
* 写入时兼容 MP 字段与旧键(sites/downloader/save_path/start_episode/filter_groups/keyword/autoFillVersions…),
* 服务端规范化后只存本表列出的键,见 docs/runbook/subscriptions.md。
*/
@@ -130,6 +150,10 @@ export const SubscriptionParams = z
downloadSavePath: z.string().min(1).max(1000).optional(),
autoFill: AutoFillMode.optional(),
fillBudget: FillBudget.optional(),
/** MP resolution / quality / effect:保存时写到该订阅自己的版本目标(硬条件 + 软偏好) */
resolution: SubscriptionResolution.optional(),
source: SubscriptionSourcePreset.optional(),
effect: SubscriptionEffect.optional(),
})
.passthrough();
export type SubscriptionParams = z.infer<typeof SubscriptionParams>;
@@ -424,6 +448,56 @@ export const SubscriptionBatchResult = z.object({
});
export type SubscriptionBatchResult = z.infer<typeof SubscriptionBatchResult>;
/** 重置进度:清在途标记(未提交/已失联的计划)与延后记录,重新评估;不删资产 */
export const SubscriptionResetResult = z.object({
subscriptionId: Id,
/** 被取消的计划(已预留未提交,或下载任务已不在) */
releasedPlans: z.array(Id),
/** 下载器里仍在下载的计划:保留 */
keptPlans: z.array(Id),
clearedDeferrals: z.number().int(),
reevaluated: z.boolean(),
});
export type SubscriptionResetResult = z.infer<typeof SubscriptionResetResult>;
/** 日历条目状态:have 已入库 / partial 部分 / missing 未入库 / upcoming 未播出 */
export const CalendarEntryState = z.enum(["have", "partial", "missing", "upcoming"]);
export type CalendarEntryState = z.infer<typeof CalendarEntryState>;
export const SubscriptionCalendarEntry = z.object({
/** YYYY-MM-DD */
date: z.string(),
subscriptionId: Id,
mediaEntityId: Id.nullable(),
title: z.string(),
mediaKind: SubscriptionMediaKind,
/** 电影为 null */
seasonNumber: z.number().int().nullable(),
episodes: z.array(z.number().int()),
/** 如 "S01E03–E04";电影 "上映" */
label: z.string(),
state: CalendarEntryState,
have: z.number().int(),
total: z.number().int(),
});
export type SubscriptionCalendarEntry = z.infer<typeof SubscriptionCalendarEntry>;
export const SubscriptionCalendar = z.object({
from: z.string(),
to: z.string(),
entries: z.array(SubscriptionCalendarEntry),
/** 范围内季集没有播出日期(TMDB 未给或未刷新)的订阅:播出日期未知 */
unknown: z.array(
z.object({
subscriptionId: Id,
title: z.string(),
mediaKind: SubscriptionMediaKind,
seasonNumber: z.number().int().nullable(),
/** 没有播出日期的集;电影为空 */
episodes: z.array(z.number().int()),
}),
),
});
export type SubscriptionCalendar = z.infer<typeof SubscriptionCalendar>;
/** 手动补全某版本:不受自动补全开关与补全预算限制(仍受全局预算/缓存卷阈值) */
export const SlotFillRequest = z.object({ unitKeys: z.array(z.string().min(1)).max(500).optional() });
export type SlotFillRequest = z.infer<typeof SlotFillRequest>;
@@ -776,6 +850,8 @@ export const RuleGroup = EntityBase.extend({
/** 语义指纹(kind + scope + 层 + 硬排除),停用/删除后写抑制记录 */
fingerprint: z.string(),
evidence: JsonObject,
/** 结构化硬条件(include / exclude 正则等),评估时并入版本目标硬条件;订阅私有组用它存表单的包含 / 排除 */
hard: JsonObject.optional(),
/** 非 null = 属于该类默认链,按此排序 */
defaultOrder: z.number().int().nullable(),
/** 引用本组的订阅缺省是否自动补全其他版本;null = 不表态 */