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

@@ -0,0 +1,50 @@
import type { Kysely } from "kysely";
import { execDdl } from "../src/migration-helpers.ts";
// C1-A 订阅遗留收尾(docs/design/subscriptions.md §3、§6.1):
// - rule_group.hard:规则组的结构化硬条件(SlotHardConditions 子集,目前用 include / exclude 正则),
// 评估时并入版本目标硬条件。订阅表单的"包含 / 排除"写成订阅私有规则组(scope.subscriptionId),
// 不在规则页列表显示,随订阅删除(软删)。
// - 存量:params 里已有 include / exclude 的未取消订阅各建一个私有组(params 原样保留,表单回显用)。
// biome-ignore lint/suspicious/noExplicitAny: 迁移不依赖应用表类型
export async function up(db: Kysely<any>): Promise<void> {
await execDdl(
db,
`
alter table rule_group add column hard jsonb not null default '{}'::jsonb check (jsonb_typeof(hard) = 'object');
create index rule_group_private_idx on rule_group ((scope ->> 'subscriptionId'))
where deleted_at is null and scope ? 'subscriptionId';
insert into rule_group (name, description, kind, scope, ladder_required, source, fingerprint, hard)
select '订阅私有 ' || left(s.id::text, 8) || '-' || substr(s.id::text, 10, 4),
'订阅表单的包含 / 排除,随订阅删除',
'acquisition',
jsonb_build_object('subscriptionId', s.id::text),
false,
'manual',
'private:' || s.id::text || ':' || md5(coalesce(s.params -> 'include', '[]'::jsonb)::text || coalesce(s.params -> 'exclude', '[]'::jsonb)::text),
jsonb_strip_nulls(jsonb_build_object(
'include', case when jsonb_typeof(s.params -> 'include') = 'array' and jsonb_array_length(s.params -> 'include') > 0
then s.params -> 'include' end,
'exclude', case when jsonb_typeof(s.params -> 'exclude') = 'array' and jsonb_array_length(s.params -> 'exclude') > 0
then s.params -> 'exclude' end))
from subscription s
where s.status <> 'cancelled'
and ((jsonb_typeof(s.params -> 'include') = 'array' and jsonb_array_length(s.params -> 'include') > 0)
or (jsonb_typeof(s.params -> 'exclude') = 'array' and jsonb_array_length(s.params -> 'exclude') > 0));
`,
);
}
// biome-ignore lint/suspicious/noExplicitAny: 同上
export async function down(db: Kysely<any>): Promise<void> {
await execDdl(
db,
`
delete from rule_group where scope ? 'subscriptionId' and fingerprint like 'private:%';
drop index if exists rule_group_private_idx;
alter table rule_group drop column if exists hard;
`,
);
}
+1
View File
@@ -1190,6 +1190,7 @@ export interface RuleGroupTable {
deleted_at: Timestamptz | null;
created_at: GeneratedTimestamptz;
updated_at: GeneratedTimestamptz;
hard: Generated<Json>;
}
export interface ScheduledJobTable {