feat(subscriptions): 按 MP 收敛订阅默认值与表单
- 定时主动搜索默认关(DEFAULT_SEARCH_INTERVAL_HOURS = 0),新建 / 手动照常各搜一次,之后靠 RSS 与站点快照 - 多版本自动补全系统缺省改为关 - 表单:基础页 = 作品与范围、下载、洗版;收录版本、优先级、自动补全折叠到进阶页「多版本」 - 对照文档 §7.3 记录落实范围 Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
709414778e
commit
ed083fd138
12 files changed
+111
-74
No files matched your search
@@ -7,6 +7,11 @@
|
|||||||
### 修复
|
### 修复
|
||||||
- OurBits 详情页:简介是页面里未渲染的 BBCode(站点靠前端脚本渲染),之前原样显示 `[quote]`、`[img]` 且换行全丢;改为按 BBCode 解析,保留换行、去掉标记,图片进截图,引用块里的 MediaInfo 单独取;文件列表按需取 `viewfilelist.php`。已缓存的详情在过期或手动刷新后生效。
|
- OurBits 详情页:简介是页面里未渲染的 BBCode(站点靠前端脚本渲染),之前原样显示 `[quote]`、`[img]` 且换行全丢;改为按 BBCode 解析,保留换行、去掉标记,图片进截图,引用块里的 MediaInfo 单独取;文件列表按需取 `viewfilelist.php`。已缓存的详情在过期或手动刷新后生效。
|
||||||
|
|
||||||
|
### 调整(订阅,对照 MP)
|
||||||
|
- 定时主动搜索默认关闭:新建与手动搜索照常各搜一次,之后靠 RSS 与站点最新资源追更;需要时在订阅里填搜索间隔。没填过间隔的旧订阅也随之不再定时搜。
|
||||||
|
- 多版本自动补全默认关闭(订阅或规则组显式打开仍生效)。
|
||||||
|
- 订阅表单收敛:基础页只留作品与范围、下载、洗版;收录版本、优先级、自动补全折叠到进阶页的「多版本」。
|
||||||
|
|
||||||
### 新增
|
### 新增
|
||||||
- 下载器可以单独设水位(硬阈值 / 恢复水位 / 清理目标)和保种天数,不填用全局;多个下载器共用一块缓存盘时取最严格的。公网 BT 专用的 qB 可设保种 0 天(站点 H&R 仍遵守)。
|
- 下载器可以单独设水位(硬阈值 / 恢复水位 / 清理目标)和保种天数,不填用全局;多个下载器共用一块缓存盘时取最严格的。公网 BT 专用的 qB 可设保种 0 天(站点 H&R 仍遵守)。
|
||||||
- 内置公开 BT 站:Nyaa、蜜柑计划、AniBT、AnimeGarden(聚合动漫花园 / 蜜柑 / ANi 等)、萌番组。添加站点 → 公开 BT 站里一键添加,不需要账号;支持资源搜索、定时抓最新资源、按资源 ID 取种子或磁力(蜜柑取剧集页的种子,AnimeGarden 只有磁力)。默认不进资源搜索的默认站点集合;萌番组部分网络直连不通,打开站点的「代理」即可。原来手填 RSS 的方式保留。
|
- 内置公开 BT 站:Nyaa、蜜柑计划、AniBT、AnimeGarden(聚合动漫花园 / 蜜柑 / ANi 等)、萌番组。添加站点 → 公开 BT 站里一键添加,不需要账号;支持资源搜索、定时抓最新资源、按资源 ID 取种子或磁力(蜜柑取剧集页的种子,AnimeGarden 只有磁力)。默认不进资源搜索的默认站点集合;萌番组部分网络直连不通,打开站点的「代理」即可。原来手填 RSS 的方式保留。
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
type AcquisitionSlot,
|
type AcquisitionSlot,
|
||||||
Api,
|
Api,
|
||||||
|
DEFAULT_SEARCH_INTERVAL_HOURS,
|
||||||
type SlotTemplate,
|
type SlotTemplate,
|
||||||
type Subscription,
|
type Subscription,
|
||||||
type SubscriptionGroup,
|
type SubscriptionGroup,
|
||||||
@@ -181,7 +182,7 @@ function overviewItem(row: SubscriptionRow & { media_entity_title?: string | nul
|
|||||||
missing: primary?.missing ?? 0,
|
missing: primary?.missing ?? 0,
|
||||||
slots,
|
slots,
|
||||||
lastUpdatedAt: lastUpdatedOf(m).toISOString(),
|
lastUpdatedAt: lastUpdatedOf(m).toISOString(),
|
||||||
searchEnabled: p.searchIntervalHours !== 0,
|
searchEnabled: (p.searchIntervalHours ?? DEFAULT_SEARCH_INTERVAL_HOURS) > 0,
|
||||||
sites: [] as { id: string; name: string }[],
|
sites: [] as { id: string; name: string }[],
|
||||||
ruleGroups: [] as { id: string; name: string }[],
|
ruleGroups: [] as { id: string; name: string }[],
|
||||||
defaultRuleGroups: m.defaultRuleGroups,
|
defaultRuleGroups: m.defaultRuleGroups,
|
||||||
@@ -477,7 +478,7 @@ export function registerSubscriptionRoutes(app: ModuleApp, d: AoDeps) {
|
|||||||
extra: { nextSearchNow?: boolean } = {},
|
extra: { nextSearchNow?: boolean } = {},
|
||||||
) => {
|
) => {
|
||||||
const now = d.ports.now();
|
const now = d.ports.now();
|
||||||
const searchOn = subParams(sub).searchIntervalHours !== 0;
|
const searchOn = (subParams(sub).searchIntervalHours ?? DEFAULT_SEARCH_INTERVAL_HOURS) > 0;
|
||||||
const r = await db()
|
const r = await db()
|
||||||
.updateTable("subscription")
|
.updateTable("subscription")
|
||||||
.set({
|
.set({
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ const segOf = (u: { segment: string } | undefined) =>
|
|||||||
const editionKey = (e: string) => `edition:${e}`;
|
const editionKey = (e: string) => `edition:${e}`;
|
||||||
const ymd = (d: Date) => d.toISOString().slice(0, 10);
|
const ymd = (d: Date) => d.toISOString().slice(0, 10);
|
||||||
|
|
||||||
/** 订阅有效自动补全:params.autoFill → 规则链上后者优先的 auto_fill → 系统缺省开 */
|
/** 订阅有效自动补全:params.autoFill → 规则链上后者优先的 auto_fill → 系统缺省关 */
|
||||||
export function subscriptionAutoFill(
|
export function subscriptionAutoFill(
|
||||||
cat: RuleCatalog,
|
cat: RuleCatalog,
|
||||||
sub: Pick<SubscriptionRow, "id" | "params" | "media_kind" | "media_entity_id">,
|
sub: Pick<SubscriptionRow, "id" | "params" | "media_kind" | "media_entity_id">,
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { DEFAULT_SEARCH_INTERVAL_HOURS } from "@umt/contracts";
|
||||||
import { defineTask, type OutboxRouteFn, type TaskRegistry } from "@umt/tasks";
|
import { defineTask, type OutboxRouteFn, type TaskRegistry } from "@umt/tasks";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import type { SourcesRuntime } from "./runtime.ts";
|
import type { SourcesRuntime } from "./runtime.ts";
|
||||||
@@ -123,8 +124,8 @@ export function registerSourcesTasks(registry: TaskRegistry, rt: () => SourcesRu
|
|||||||
|
|
||||||
// ── 订阅主动搜索(W5-C,对照 MP SubscribeSearchOwner):按作品标题/原名/别名在已开启站点搜索 ──
|
// ── 订阅主动搜索(W5-C,对照 MP SubscribeSearchOwner):按作品标题/原名/别名在已开启站点搜索 ──
|
||||||
|
|
||||||
/** 主动搜索周期缺省 24h;subscription.params.searchIntervalHours 覆盖(0 = 关闭周期搜索) */
|
/** 周期主动搜索缺省值见 contracts DEFAULT_SEARCH_INTERVAL_HOURS(0 = 关);新建与手动搜索照常各搜一次 */
|
||||||
export const DEFAULT_SEARCH_INTERVAL_HOURS = 24;
|
export { DEFAULT_SEARCH_INTERVAL_HOURS };
|
||||||
/** 每次最多用几个检索词(标题、原名、别名),控制站点请求量 */
|
/** 每次最多用几个检索词(标题、原名、别名),控制站点请求量 */
|
||||||
export const SEARCH_MAX_KEYWORDS = 3;
|
export const SEARCH_MAX_KEYWORDS = 3;
|
||||||
|
|
||||||
|
|||||||
@@ -50,7 +50,13 @@ describe.skipIf(!hasPg)("订阅增强(真实 PG + mock qB + 临时目录)", () =
|
|||||||
|
|
||||||
async function seriesSlot(
|
async function seriesSlot(
|
||||||
title: string,
|
title: string,
|
||||||
opts: { mode?: "once" | "continuous"; status?: "active" | "pending"; hard?: object; upgrade?: object } = {},
|
opts: {
|
||||||
|
mode?: "once" | "continuous";
|
||||||
|
status?: "active" | "pending";
|
||||||
|
hard?: object;
|
||||||
|
upgrade?: object;
|
||||||
|
params?: object;
|
||||||
|
} = {},
|
||||||
) {
|
) {
|
||||||
const entityId = await seedEntity(h.db, { kind: "series", title, year: 2024 });
|
const entityId = await seedEntity(h.db, { kind: "series", title, year: 2024 });
|
||||||
const sub = await db()
|
const sub = await db()
|
||||||
@@ -60,6 +66,7 @@ describe.skipIf(!hasPg)("订阅增强(真实 PG + mock qB + 临时目录)", () =
|
|||||||
media_kind: "series",
|
media_kind: "series",
|
||||||
mode: opts.mode ?? "continuous",
|
mode: opts.mode ?? "continuous",
|
||||||
status: opts.status ?? "active",
|
status: opts.status ?? "active",
|
||||||
|
...(opts.params ? { params: JSON.stringify(opts.params) } : {}),
|
||||||
})
|
})
|
||||||
.returning("id")
|
.returning("id")
|
||||||
.executeTakeFirstOrThrow();
|
.executeTakeFirstOrThrow();
|
||||||
@@ -275,7 +282,7 @@ describe.skipIf(!hasPg)("订阅增强(真实 PG + mock qB + 临时目录)", () =
|
|||||||
}, 90_000);
|
}, 90_000);
|
||||||
|
|
||||||
it("多个目标版本按各自已接收单元完成,单版本入库不提前完成订阅", async () => {
|
it("多个目标版本按各自已接收单元完成,单版本入库不提前完成订阅", async () => {
|
||||||
const { entityId, subId } = await seriesSlot("双版本剧", { mode: "once" });
|
const { entityId, subId } = await seriesSlot("双版本剧", { mode: "once", params: { autoFill: "on" } });
|
||||||
await db()
|
await db()
|
||||||
.insertInto("acquisition_slot")
|
.insertInto("acquisition_slot")
|
||||||
.values({
|
.values({
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ describe.skipIf(!up)("订阅主动搜索与季总集数", () => {
|
|||||||
|
|
||||||
it("按标题在已开启站点搜索:结果 upsert 为 source_release(新结果发 release.observed),排下次搜索并入队订阅重评", async () => {
|
it("按标题在已开启站点搜索:结果 upsert 为 source_release(新结果发 release.observed),排下次搜索并入队订阅重评", async () => {
|
||||||
const e = await entity("Dune");
|
const e = await entity("Dune");
|
||||||
const subId = await subscribe(e);
|
const subId = await subscribe(e, { params: JSON.stringify({ searchIntervalHours: 24 }) });
|
||||||
const searches = () => mock.requests.filter((r) => r.path.includes("search=")).length;
|
const searches = () => mock.requests.filter((r) => r.path.includes("search=")).length;
|
||||||
const before = searches();
|
const before = searches();
|
||||||
const r = await searchSubscription(h.rt, subId);
|
const r = await searchSubscription(h.rt, subId);
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ describe("coverageRow", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("自动补全开关与补全预算", () => {
|
describe("自动补全开关与补全预算", () => {
|
||||||
it("订阅显式值优先;inherit 取规则链上最后一个设了 autoFill 的组;都没设为开", () => {
|
it("订阅显式值优先;inherit 取规则链上最后一个设了 autoFill 的组;都没设为关(MP 没有多版本补全)", () => {
|
||||||
expect(resolveAutoFill("off", [{ id: "g", autoFill: true }])).toMatchObject({
|
expect(resolveAutoFill("off", [{ id: "g", autoFill: true }])).toMatchObject({
|
||||||
enabled: false,
|
enabled: false,
|
||||||
source: "subscription",
|
source: "subscription",
|
||||||
@@ -99,7 +99,7 @@ describe("自动补全开关与补全预算", () => {
|
|||||||
]),
|
]),
|
||||||
).toEqual({ enabled: false, mode: "inherit", source: "rule_group", ruleGroupId: "b" });
|
).toEqual({ enabled: false, mode: "inherit", source: "rule_group", ruleGroupId: "b" });
|
||||||
expect(resolveAutoFill(undefined, [{ id: "a", autoFill: null }])).toEqual({
|
expect(resolveAutoFill(undefined, [{ id: "a", autoFill: null }])).toEqual({
|
||||||
enabled: true,
|
enabled: false,
|
||||||
mode: "inherit",
|
mode: "inherit",
|
||||||
source: "default",
|
source: "default",
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -309,7 +309,10 @@ describe.skipIf(!hasPg)("订阅重做(真实 PG)", () => {
|
|||||||
const e1 = await seedEntity(db(), { kind: "series", title: `${tag}甲`, year: 2025 });
|
const e1 = await seedEntity(db(), { kind: "series", title: `${tag}甲`, year: 2025 });
|
||||||
await season(e1, 1, 3);
|
await season(e1, 1, 3);
|
||||||
const e1u = await episode(e1, 1, 1);
|
const e1u = await episode(e1, 1, 1);
|
||||||
const s1 = await sub(e1, { lastProgressAt: new Date("2026-01-03T00:00:00Z"), params: { siteIds: [base.siteId] } });
|
const s1 = await sub(e1, {
|
||||||
|
lastProgressAt: new Date("2026-01-03T00:00:00Z"),
|
||||||
|
params: { siteIds: [base.siteId], searchIntervalHours: 24 },
|
||||||
|
});
|
||||||
const s1p = await slot(e1, s1, "main");
|
const s1p = await slot(e1, s1, "main");
|
||||||
const a1 = await asset(e1);
|
const a1 = await asset(e1);
|
||||||
await cover(a1, [e1u]);
|
await cover(a1, [e1u]);
|
||||||
@@ -349,7 +352,7 @@ describe.skipIf(!hasPg)("订阅重做(真实 PG)", () => {
|
|||||||
missing: 2,
|
missing: 2,
|
||||||
sites: [{ id: base.siteId, name: "测试站" }],
|
sites: [{ id: base.siteId, name: "测试站" }],
|
||||||
defaultRuleGroups: true,
|
defaultRuleGroups: true,
|
||||||
autoFill: { enabled: true, mode: "inherit", source: "default" },
|
autoFill: { enabled: false, mode: "inherit", source: "default" },
|
||||||
searchEnabled: true,
|
searchEnabled: true,
|
||||||
lastUpdatedAt: "2026-01-03T00:00:00.000Z",
|
lastUpdatedAt: "2026-01-03T00:00:00.000Z",
|
||||||
});
|
});
|
||||||
@@ -391,7 +394,7 @@ describe.skipIf(!hasPg)("订阅重做(真实 PG)", () => {
|
|||||||
const e = await seedEntity(db(), { kind: "series", title: "Fill Show", year: 2025 });
|
const e = await seedEntity(db(), { kind: "series", title: "Fill Show", year: 2025 });
|
||||||
await season(e, 1, 10);
|
await season(e, 1, 10);
|
||||||
const u1 = await episode(e, 1, 1);
|
const u1 = await episode(e, 1, 1);
|
||||||
const s = await sub(e);
|
const s = await sub(e, { params: { autoFill: "on" } });
|
||||||
const p = await slot(e, s, "4K", { position: 0, hard: { resolutions: ["2160p"] } });
|
const p = await slot(e, s, "4K", { position: 0, hard: { resolutions: ["2160p"] } });
|
||||||
const q = await slot(e, s, "1080p", { position: 1, hard: { resolutions: ["1080p"] } });
|
const q = await slot(e, s, "1080p", { position: 1, hard: { resolutions: ["1080p"] } });
|
||||||
const a = await asset(e, { quality: { resolution: "2160p", source: "REMUX" } });
|
const a = await asset(e, { quality: { resolution: "2160p", source: "REMUX" } });
|
||||||
@@ -613,7 +616,7 @@ describe.skipIf(!hasPg)("订阅重做(真实 PG)", () => {
|
|||||||
|
|
||||||
it("暂停 / 恢复 / 重新订阅 / 批量 / 收录版本排序", async () => {
|
it("暂停 / 恢复 / 重新订阅 / 批量 / 收录版本排序", async () => {
|
||||||
const e = await seedEntity(db(), { kind: "movie", title: "动作片", year: 2024 });
|
const e = await seedEntity(db(), { kind: "movie", title: "动作片", year: 2024 });
|
||||||
const s = await sub(e, { kind: "movie", mode: "once" });
|
const s = await sub(e, { kind: "movie", mode: "once", params: { searchIntervalHours: 24 } });
|
||||||
const a = await slot(e, s, "A", { position: 0 });
|
const a = await slot(e, s, "A", { position: 0 });
|
||||||
const b = await slot(e, s, "B", { position: 1 });
|
const b = await slot(e, s, "B", { position: 1 });
|
||||||
|
|
||||||
|
|||||||
@@ -671,47 +671,11 @@ export function SubscriptionFormDialog({
|
|||||||
</FormField>
|
</FormField>
|
||||||
</Section>
|
</Section>
|
||||||
|
|
||||||
<Section title="收录版本">
|
<Section title="洗版">
|
||||||
{editing ? (
|
|
||||||
slotsQ.isPending ? (
|
|
||||||
<p className="text-caption text-muted-foreground">读取收录版本…</p>
|
|
||||||
) : subSlots.length === 0 ? (
|
|
||||||
<p className="text-caption text-muted-foreground">还没有收录版本。在对比视图里新建。</p>
|
|
||||||
) : (
|
|
||||||
<FormField label="顺序" htmlFor={f("order")} hint="第一个是主版本;其他版本按自动补全设置补齐">
|
|
||||||
<SlotOrderList
|
|
||||||
items={order ?? subSlots.map((s) => ({ id: s.id, name: s.name }))}
|
|
||||||
onChange={setOrderState}
|
|
||||||
disabled={pending}
|
|
||||||
/>
|
|
||||||
</FormField>
|
|
||||||
)
|
|
||||||
) : (
|
|
||||||
<FormField
|
|
||||||
label="模板"
|
|
||||||
htmlFor={f("tpl")}
|
|
||||||
hint={
|
|
||||||
target?.kind === "entity"
|
|
||||||
? "作品还没有收录版本时按模板顺序创建,第一个是主版本"
|
|
||||||
: "作品加入媒体库后按模板创建收录版本"
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Pick
|
|
||||||
id={f("tpl")}
|
|
||||||
value={d.policyRevisionId}
|
|
||||||
onChange={(v) => patch({ policyRevisionId: v })}
|
|
||||||
placeholder={defaultTpl ? `按已发布收录策略(${defaultTpl.title})` : "按已发布收录策略"}
|
|
||||||
options={templates.map((p) => ({
|
|
||||||
value: p.id,
|
|
||||||
label: `${p.title} r${p.revision}:${tplNames(p.id).join(" → ") || "无模板"}`,
|
|
||||||
}))}
|
|
||||||
/>
|
|
||||||
</FormField>
|
|
||||||
)}
|
|
||||||
<FormField
|
<FormField
|
||||||
label="升级策略"
|
label="洗版"
|
||||||
htmlFor={f("upgrade")}
|
htmlFor={f("upgrade")}
|
||||||
hint="作用于这个订阅的全部收录版本;已入库低于目标档位时继续找更好的"
|
hint="已入库的低于规则组最高档时继续找更好的,拿到最高档即完成"
|
||||||
>
|
>
|
||||||
<Pick
|
<Pick
|
||||||
id={f("upgrade")}
|
id={f("upgrade")}
|
||||||
@@ -826,18 +790,18 @@ export function SubscriptionFormDialog({
|
|||||||
</FormField>
|
</FormField>
|
||||||
</Section>
|
</Section>
|
||||||
|
|
||||||
<Section title="搜索与优先级">
|
<Section title="搜索">
|
||||||
<div className="grid gap-3 sm:grid-cols-3">
|
<div className="grid gap-3 sm:grid-cols-2">
|
||||||
<FormField
|
<FormField
|
||||||
label="主动搜索间隔(小时)"
|
label="定时搜索间隔(小时)"
|
||||||
htmlFor={f("interval")}
|
htmlFor={f("interval")}
|
||||||
error={err("searchIntervalHours")}
|
error={err("searchIntervalHours")}
|
||||||
hint="0 表示不主动搜索"
|
hint="空或 0 = 不定时搜索:新建时搜一次,之后靠 RSS 和站点最新资源追更"
|
||||||
>
|
>
|
||||||
<Input
|
<Input
|
||||||
id={f("interval")}
|
id={f("interval")}
|
||||||
inputMode="decimal"
|
inputMode="decimal"
|
||||||
placeholder="24"
|
placeholder="0"
|
||||||
value={d.searchIntervalHours}
|
value={d.searchIntervalHours}
|
||||||
aria-invalid={!!err("searchIntervalHours")}
|
aria-invalid={!!err("searchIntervalHours")}
|
||||||
onChange={(e) => patch({ searchIntervalHours: e.target.value })}
|
onChange={(e) => patch({ searchIntervalHours: e.target.value })}
|
||||||
@@ -851,20 +815,65 @@ export function SubscriptionFormDialog({
|
|||||||
onChange={(e) => patch({ stopAfter: e.target.value })}
|
onChange={(e) => patch({ stopAfter: e.target.value })}
|
||||||
/>
|
/>
|
||||||
</FormField>
|
</FormField>
|
||||||
<FormField label="优先级" htmlFor={f("prio")} hint="只影响资源分配顺序">
|
|
||||||
<Pick
|
|
||||||
id={f("prio")}
|
|
||||||
value={d.priority}
|
|
||||||
onChange={(v) => patch({ priority: (v || "tracking") as AcquisitionPriority })}
|
|
||||||
options={AcquisitionPriorityEnum.options.map((v) => ({ value: v, label: PRIORITY[v] }))}
|
|
||||||
/>
|
|
||||||
</FormField>
|
|
||||||
</div>
|
</div>
|
||||||
</Section>
|
</Section>
|
||||||
|
|
||||||
<Section title="自动补全其他版本">
|
<details className="group min-w-0 border-t border-border/60 pt-4">
|
||||||
<AutoFillFields d={d} patch={patch} errors={errors} idPrefix={id} />
|
<summary className="cursor-pointer text-meta font-semibold tracking-wide text-muted-foreground">
|
||||||
</Section>
|
多版本(一般不用改)
|
||||||
|
</summary>
|
||||||
|
<div className="mt-3 space-y-5">
|
||||||
|
<Section title="收录版本">
|
||||||
|
{editing ? (
|
||||||
|
slotsQ.isPending ? (
|
||||||
|
<p className="text-caption text-muted-foreground">读取收录版本…</p>
|
||||||
|
) : subSlots.length === 0 ? (
|
||||||
|
<p className="text-caption text-muted-foreground">还没有收录版本。在对比视图里新建。</p>
|
||||||
|
) : (
|
||||||
|
<FormField label="顺序" htmlFor={f("order")} hint="第一个是主版本;其他版本按自动补全设置补齐">
|
||||||
|
<SlotOrderList
|
||||||
|
items={order ?? subSlots.map((s) => ({ id: s.id, name: s.name }))}
|
||||||
|
onChange={setOrderState}
|
||||||
|
disabled={pending}
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
<FormField
|
||||||
|
label="模板"
|
||||||
|
htmlFor={f("tpl")}
|
||||||
|
hint={
|
||||||
|
target?.kind === "entity"
|
||||||
|
? "作品还没有收录版本时按模板顺序创建,第一个是主版本"
|
||||||
|
: "作品加入媒体库后按模板创建收录版本"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Pick
|
||||||
|
id={f("tpl")}
|
||||||
|
value={d.policyRevisionId}
|
||||||
|
onChange={(v) => patch({ policyRevisionId: v })}
|
||||||
|
placeholder={defaultTpl ? `按已发布收录策略(${defaultTpl.title})` : "按已发布收录策略"}
|
||||||
|
options={templates.map((p) => ({
|
||||||
|
value: p.id,
|
||||||
|
label: `${p.title} r${p.revision}:${tplNames(p.id).join(" → ") || "无模板"}`,
|
||||||
|
}))}
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
)}
|
||||||
|
<FormField label="优先级" htmlFor={f("prio")} hint="只影响下载排队先后">
|
||||||
|
<Pick
|
||||||
|
id={f("prio")}
|
||||||
|
value={d.priority}
|
||||||
|
onChange={(v) => patch({ priority: (v || "tracking") as AcquisitionPriority })}
|
||||||
|
options={AcquisitionPriorityEnum.options.map((v) => ({ value: v, label: PRIORITY[v] }))}
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
</Section>
|
||||||
|
<Section title="自动补全其他版本">
|
||||||
|
<AutoFillFields d={d} patch={patch} errors={errors} idPrefix={id} />
|
||||||
|
</Section>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
|
|||||||
@@ -309,7 +309,7 @@ MP 手动添加入口识别失败时返回 `requires_confirmation`;前端 `Add
|
|||||||
|
|
||||||
这是为本项目资产管理补齐的行为,参考 MP 就地刮削语义,并非声称 MP 有同名模式。未改动自动接管配置或加入链接方式。实现与验证边界见发布记录和整理手册。
|
这是为本项目资产管理补齐的行为,参考 MP 就地刮削语义,并非声称 MP 有同名模式。未改动自动接管配置或加入链接方式。实现与验证边界见发布记录和整理手册。
|
||||||
|
|
||||||
## 7 订阅:追更、选片、洗版与完成(待讨论,2026-10-02)
|
## 7 订阅:追更、选片、洗版与完成(2026-10-02)
|
||||||
|
|
||||||
MP 依据 §1 固定提交 `app/chain/subscribe/*`,旧版逐函数记录见 [订阅移植对照](moviepilot-subscribe.md)(基线 `e6820a8`,本轮抽查完成判定、整理后事件、状态机未见行为变化)。本项目依据 `acquisition-organize/services/{subscriptions,acquisition,coverage}.ts`、`contracts/src/policy.ts`、订阅表单。测试环境现有 5 条订阅均已取消,收录版本(Slot)0 个:多版本相关机制尚无实际使用记录。
|
MP 依据 §1 固定提交 `app/chain/subscribe/*`,旧版逐函数记录见 [订阅移植对照](moviepilot-subscribe.md)(基线 `e6820a8`,本轮抽查完成判定、整理后事件、状态机未见行为变化)。本项目依据 `acquisition-organize/services/{subscriptions,acquisition,coverage}.ts`、`contracts/src/policy.ts`、订阅表单。测试环境现有 5 条订阅均已取消,收录版本(Slot)0 个:多版本相关机制尚无实际使用记录。
|
||||||
|
|
||||||
@@ -340,4 +340,12 @@ MP 依据 §1 固定提交 `app/chain/subscribe/*`,旧版逐函数记录见 [
|
|||||||
3. 第 5、11 项:延后重评、持续后续季是否保留。
|
3. 第 5、11 项:延后重评、持续后续季是否保留。
|
||||||
4. 第 3 项:主动搜索默认开关。
|
4. 第 3 项:主动搜索默认开关。
|
||||||
|
|
||||||
以上均为对照与建议,尚未授权实施。
|
### 7.3 本轮落实(用户授权自主收尾,v0.3.17)
|
||||||
|
|
||||||
|
只改默认值和表单布局,不动数据模型与评估逻辑:
|
||||||
|
|
||||||
|
- 第 3 项:定时主动搜索默认关(`DEFAULT_SEARCH_INTERVAL_HOURS = 0`)。新建、手动搜索照常各搜一次,之后靠 RSS/站点快照追更;单个订阅填间隔才定时搜。旧订阅没填间隔的随之变为不定时搜索。
|
||||||
|
- 第 8 项:多版本自动补全系统缺省改为关(订阅或规则组显式打开仍生效),代码保留。
|
||||||
|
- 第 1、4、12 项:表单收敛为「基础」=作品与范围、下载、洗版;「进阶」=过滤(规则组、包含/排除、分辨率、质量、特效、关键词)、搜索;收录版本模板/顺序、优先级、自动补全折叠到「多版本(一般不用改)」。
|
||||||
|
- 第 7 项:洗版已是 MP 三种(电影洗版、按集洗版、全集洗版),文案改为「拿到规则组最高档即完成」,不再另做。
|
||||||
|
- 第 5、10、11、13、14 项维持现状;第 15 项不做。
|
||||||
@@ -76,7 +76,7 @@ export const Subscription = EntityBase.extend({
|
|||||||
statusReason: z.string().nullable(),
|
statusReason: z.string().nullable(),
|
||||||
lastCheckedAt: Timestamp.nullable(),
|
lastCheckedAt: Timestamp.nullable(),
|
||||||
createdBy: z.string().nullable(),
|
createdBy: z.string().nullable(),
|
||||||
/** 上次主动搜索时间 / 下次到期(按 params.searchIntervalHours,缺省 24h) */
|
/** 上次主动搜索时间 / 下次到期(按 params.searchIntervalHours,缺省不周期搜索) */
|
||||||
lastSearchedAt: Timestamp.nullable(),
|
lastSearchedAt: Timestamp.nullable(),
|
||||||
nextSearchAt: Timestamp.nullable(),
|
nextSearchAt: Timestamp.nullable(),
|
||||||
/** 上次进展:任一收录版本的单元转为已入库、或总集数变化时写入;总览"上次更新"另合并计划预留时间 */
|
/** 上次进展:任一收录版本的单元转为已入库、或总集数变化时写入;总览"上次更新"另合并计划预留时间 */
|
||||||
@@ -126,6 +126,9 @@ export type SubscriptionEffect = z.infer<typeof SubscriptionEffect>;
|
|||||||
* 写入时兼容 MP 字段与旧键(sites/downloader/save_path/start_episode/filter_groups/keyword/autoFillVersions…),
|
* 写入时兼容 MP 字段与旧键(sites/downloader/save_path/start_episode/filter_groups/keyword/autoFillVersions…),
|
||||||
* 服务端规范化后只存本表列出的键,见 docs/runbook/subscriptions.md。
|
* 服务端规范化后只存本表列出的键,见 docs/runbook/subscriptions.md。
|
||||||
*/
|
*/
|
||||||
|
/** 周期主动搜索缺省关闭(对照 MP:订阅默认靠站点 RSS / 首页刷新匹配);params.searchIntervalHours > 0 才按周期搜 */
|
||||||
|
export const DEFAULT_SEARCH_INTERVAL_HOURS = 0;
|
||||||
|
|
||||||
export const SubscriptionParams = z
|
export const SubscriptionParams = z
|
||||||
.object({
|
.object({
|
||||||
siteIds: z.array(Id).optional(),
|
siteIds: z.array(Id).optional(),
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// 多版本自动补全(docs/design/subscriptions.md §2.2):非主收录版本在主版本已满足的单元上补下其他版本。
|
// 多版本自动补全(docs/design/subscriptions.md §2.2):非主收录版本在主版本已满足的单元上补下其他版本。
|
||||||
// 开关按 订阅 → 规则组(链上后者优先)→ 系统缺省(开)取值;补全预算按订阅、按自然日计,超出记延后。
|
// 开关按 订阅 → 规则组(链上后者优先)→ 系统缺省(关,MP 没有多版本补全)取值;补全预算按订阅、按自然日计,超出记延后。
|
||||||
|
|
||||||
export type AutoFillModeValue = "inherit" | "on" | "off";
|
export type AutoFillModeValue = "inherit" | "on" | "off";
|
||||||
|
|
||||||
@@ -15,7 +15,7 @@ export interface AutoFillResolution {
|
|||||||
export function resolveAutoFill(
|
export function resolveAutoFill(
|
||||||
mode: AutoFillModeValue | undefined,
|
mode: AutoFillModeValue | undefined,
|
||||||
groupFlags: readonly { id: string; autoFill: boolean | null }[] = [],
|
groupFlags: readonly { id: string; autoFill: boolean | null }[] = [],
|
||||||
systemDefault = true,
|
systemDefault = false,
|
||||||
): AutoFillResolution {
|
): AutoFillResolution {
|
||||||
if (mode === "on" || mode === "off") return { enabled: mode === "on", mode, source: "subscription" };
|
if (mode === "on" || mode === "off") return { enabled: mode === "on", mode, source: "subscription" };
|
||||||
for (let k = groupFlags.length - 1; k >= 0; k--) {
|
for (let k = groupFlags.length - 1; k >= 0; k--) {
|
||||||
|
|||||||
Reference in new issue
Block a user