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:
1 parent
52402864d6
commit
70d56645fe
20 files changed
+1663
-44
No files matched your search
@@ -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",
|
||||
|
||||
@@ -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 = 不表态 */
|
||||
|
||||
Reference in new issue
Block a user