真正入库取决于媒体服务器有没有扫到,本工具只负责整理(元数据、NFO 与图片、按规则归到目录)。 界面与面向用户的服务端文案里「已入库 / 未入库 / 部分入库 / 入库失败 / 入库中 / 入库后 / 原地入库…」 改为「已整理 / 未整理 / 部分整理 / 整理失败 / 整理中 / 整理后 / 原地整理…」;文件管理快速筛选「已整理」、 目录副行「n 个已整理 / 未整理」、季卡片「已整理 x/y」。代码标识符不改,「Emby 已有」这类媒体服务器状态保留。 glossary 加「整理」口径条目;check:copy 拦处理状态语境的「入库」搭配,其它线待替换的文件暂列 TERM_PENDING。 Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Cxwgnn2pbzQ7rXv3rq7fei
414 lines
14 KiB
TypeScript
414 lines
14 KiB
TypeScript
import { z } from "zod";
|
|
import { cursorPage, Id, IdParam } from "../common.ts";
|
|
import * as policy from "../policy.ts";
|
|
import { crud, del, ep, listQuery, R } from "./shared.ts";
|
|
|
|
export const rulesApi = {
|
|
// ── 规则 ──
|
|
subscriptions: {
|
|
...crud(
|
|
"/subscriptions",
|
|
"订阅",
|
|
{
|
|
row: policy.Subscription,
|
|
create: policy.SubscriptionCreate,
|
|
update: policy.SubscriptionUpdate,
|
|
filters: {
|
|
status: policy.SubscriptionStatus.optional(),
|
|
mediaEntityId: Id.optional(),
|
|
mediaKind: policy.SubscriptionMediaKind.optional(),
|
|
/** 作品标题 / 原名 / 别名 / 未建档检索词,模糊匹配 */
|
|
q: z.string().max(200).optional(),
|
|
},
|
|
},
|
|
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",
|
|
path: "/subscriptions",
|
|
scope: "read",
|
|
summary:
|
|
"订阅总览(分组、进度、各版本摘要、上次更新、下次搜索、站点、规则组;cancelled 只在显式 status=cancelled 时返回)",
|
|
query: listQuery({
|
|
status: policy.SubscriptionStatus.optional(),
|
|
mediaEntityId: Id.optional(),
|
|
mediaKind: policy.SubscriptionMediaKind.optional(),
|
|
/** 作品标题 / 原名 / 别名 / 未建档检索词,模糊匹配 */
|
|
q: z.string().max(200).optional(),
|
|
group: policy.SubscriptionGroup.optional(),
|
|
sort: policy.SubscriptionSort.optional(),
|
|
order: z.enum(["asc", "desc"]).optional(),
|
|
siteId: Id.optional(),
|
|
ruleGroupId: Id.optional(),
|
|
origin: policy.SubscriptionOrigin.optional(),
|
|
upgrading: z.enum(["true", "false"]).optional(),
|
|
}),
|
|
response: policy.SubscriptionOverviewPage,
|
|
}),
|
|
coverage: ep({
|
|
method: "get",
|
|
path: "/subscriptions/{id}/coverage",
|
|
scope: "read",
|
|
summary:
|
|
"覆盖矩阵:季 → 集 × 收录版本(电影:内容版本 × 收录版本),每格 have/inflight/missing/skipped;缓存 30s,refresh=true 强制重算",
|
|
params: IdParam,
|
|
query: z.object({ refresh: z.enum(["true", "false"]).optional() }),
|
|
response: policy.SubscriptionCoverage,
|
|
}),
|
|
pause: ep({
|
|
method: "post",
|
|
path: "/subscriptions/{id}/pause",
|
|
scope: "admin",
|
|
summary: "暂停订阅(不评估、不搜索)",
|
|
params: IdParam,
|
|
response: policy.Subscription,
|
|
}),
|
|
resume: ep({
|
|
method: "post",
|
|
path: "/subscriptions/{id}/resume",
|
|
scope: "admin",
|
|
summary: "恢复已暂停的订阅(TMDB 未就绪回到 pending),立即重评并排上搜索",
|
|
params: IdParam,
|
|
response: policy.Subscription,
|
|
}),
|
|
resubscribe: ep({
|
|
method: "post",
|
|
path: "/subscriptions/{id}/resubscribe",
|
|
scope: "admin",
|
|
summary: "重新订阅已完成/已取消的订阅(历史保留),立即重评并搜索",
|
|
params: IdParam,
|
|
response: policy.Subscription,
|
|
}),
|
|
batch: ep({
|
|
method: "post",
|
|
path: "/subscriptions/batch",
|
|
scope: "admin",
|
|
summary: "批量:立即搜索 / 暂停 / 恢复 / 删除(删除 = 取消,写审计)",
|
|
body: policy.SubscriptionBatch,
|
|
response: policy.SubscriptionBatchResult,
|
|
}),
|
|
fillSlot: ep({
|
|
method: "post",
|
|
path: "/subscriptions/{id}/slots/{slotId}/fill",
|
|
scope: "admin",
|
|
summary: "手动补全某收录版本(不受自动补全开关与补全预算限制)",
|
|
params: z.object({ id: Id, slotId: Id }),
|
|
body: policy.SlotFillRequest,
|
|
response: z.object({ runId: z.string(), deduplicated: z.boolean() }),
|
|
}),
|
|
slotOrder: ep({
|
|
method: "put",
|
|
path: "/subscriptions/{id}/slot-order",
|
|
scope: "admin",
|
|
summary: "收录版本排序(第一个为主版本)",
|
|
params: IdParam,
|
|
body: policy.SlotOrderSet,
|
|
response: z.array(policy.AcquisitionSlot),
|
|
}),
|
|
search: ep({
|
|
method: "post",
|
|
path: "/subscriptions/{id}/search",
|
|
scope: "admin",
|
|
summary: "手动主动搜索(按标题/别名在已开启站点搜索,结果走同一识别评估链)",
|
|
params: IdParam,
|
|
response: z.object({ runId: z.string(), deduplicated: z.boolean() }),
|
|
}),
|
|
progress: ep({
|
|
method: "get",
|
|
path: "/subscriptions/{id}/progress",
|
|
scope: "read",
|
|
summary: "订阅进度:季总集数账本、每集状态(已覆盖/在途/缺失/疑似缺集)、收录版本在途与延后",
|
|
params: IdParam,
|
|
response: policy.SubscriptionProgress,
|
|
}),
|
|
setEpisodeTotal: ep({
|
|
method: "post",
|
|
path: "/subscriptions/{id}/episode-total",
|
|
scope: "admin",
|
|
summary: "手动设置季总集数(只升;locked 锁定后刷新不再改写)",
|
|
params: IdParam,
|
|
body: policy.EpisodeTotalSet,
|
|
response: policy.SubscriptionProgress,
|
|
}),
|
|
},
|
|
subscriptionHistory: crud(
|
|
"/subscription-history",
|
|
"订阅完成历史",
|
|
{
|
|
row: policy.SubscriptionHistory,
|
|
filters: { mediaEntityId: Id.optional(), subscriptionId: Id.optional() },
|
|
},
|
|
R,
|
|
),
|
|
policyRevisions: {
|
|
...crud(
|
|
"/policy-revisions",
|
|
"策略版本",
|
|
{
|
|
row: policy.PolicyRevision,
|
|
create: policy.PolicyRevisionCreate,
|
|
update: policy.PolicyRevisionUpdate,
|
|
filters: { policyKey: z.string().optional(), status: policy.PolicyStatus.optional() },
|
|
},
|
|
R,
|
|
),
|
|
preview: ep({
|
|
method: "post",
|
|
path: "/policy-revisions/{id}/preview",
|
|
scope: "read",
|
|
summary: "历史回放预览",
|
|
params: IdParam,
|
|
response: policy.PolicyRevision,
|
|
}),
|
|
publish: ep({
|
|
method: "post",
|
|
path: "/policy-revisions/{id}/publish",
|
|
scope: "admin",
|
|
summary: "发布策略版本",
|
|
params: IdParam,
|
|
body: z.object({ expectedRevision: z.number().int().min(1) }),
|
|
response: policy.PolicyRevision,
|
|
}),
|
|
rollback: ep({
|
|
method: "post",
|
|
path: "/policy-revisions/{id}/rollback",
|
|
scope: "admin",
|
|
summary: "回滚到该版本",
|
|
params: IdParam,
|
|
response: policy.PolicyRevision,
|
|
}),
|
|
},
|
|
acquisitionSlots: crud(
|
|
"/acquisition-slots",
|
|
"收录版本",
|
|
{
|
|
row: policy.AcquisitionSlot,
|
|
create: policy.AcquisitionSlotCreate,
|
|
update: policy.AcquisitionSlotUpdate,
|
|
filters: {
|
|
mediaEntityId: Id.optional(),
|
|
status: policy.SlotStatus.optional(),
|
|
subscriptionId: Id.optional(),
|
|
},
|
|
},
|
|
R,
|
|
),
|
|
preferenceTags: {
|
|
...crud(
|
|
"/preference-tags",
|
|
"偏好标签",
|
|
{
|
|
row: policy.PreferenceTag,
|
|
create: policy.PreferenceTagCreate,
|
|
update: policy.PreferenceTagUpdate,
|
|
filters: { status: policy.PreferenceStatus.optional(), kind: policy.PreferenceKind.optional() },
|
|
},
|
|
R,
|
|
),
|
|
/** 删除 = 标记 deleted + 写抑制记录 */
|
|
remove: del("/preference-tags/{id}", "删除偏好标签(留抑制记录)"),
|
|
revisions: ep({
|
|
method: "get",
|
|
path: "/preference-tags/{id}/revisions",
|
|
scope: "read",
|
|
summary: "偏好标签修订历史(新→旧;取自审计事件)",
|
|
params: IdParam,
|
|
response: policy.PreferenceTagRevisionList,
|
|
}),
|
|
},
|
|
preferenceSuppressions: crud(
|
|
"/preference-suppressions",
|
|
"偏好抑制",
|
|
{
|
|
row: policy.PreferenceSuppression,
|
|
create: policy.PreferenceSuppressionCreate,
|
|
update: policy.PreferenceSuppressionUpdate,
|
|
filters: { active: z.enum(["true", "false"]).optional() },
|
|
},
|
|
R,
|
|
),
|
|
|
|
// ── 规则(W9-D):规则组 = 收录过滤条件 + 偏好标签合一。静态路径先于 /rules/{id} 注册 ──
|
|
rules: {
|
|
keywords: ep({
|
|
method: "get",
|
|
path: "/rules/keywords",
|
|
scope: "read",
|
|
summary: "关键字表:内置(对齐 MP,含判定依据与上游正则)+ 自定义条件",
|
|
response: policy.RuleKeywordList,
|
|
}),
|
|
parse: ep({
|
|
method: "post",
|
|
path: "/rules/parse",
|
|
scope: "read",
|
|
summary: "规则串 → 结构化层(`>` 分层,层内!& | 与括号);错误逐条给位置与相近名",
|
|
body: policy.RuleParseRequest,
|
|
response: policy.RuleParseResult,
|
|
}),
|
|
serialize: ep({
|
|
method: "post",
|
|
path: "/rules/serialize",
|
|
scope: "read",
|
|
summary: "结构化层 → 规范化规则串",
|
|
body: policy.RuleSerializeRequest,
|
|
response: policy.RuleSerializeResult,
|
|
}),
|
|
defaults: ep({
|
|
method: "get",
|
|
path: "/rules/defaults",
|
|
scope: "read",
|
|
summary: "三套默认规则组链:订阅 / 洗版 / 搜索",
|
|
response: policy.RuleDefaults,
|
|
}),
|
|
setDefaults: ep({
|
|
method: "put",
|
|
path: "/rules/defaults",
|
|
scope: "admin",
|
|
summary: "设置默认规则组链(按数组顺序串联,pri 取最后一个带层的组)",
|
|
body: policy.RuleDefaults,
|
|
response: policy.RuleDefaults,
|
|
}),
|
|
...crud(
|
|
"/rules",
|
|
"规则组",
|
|
{
|
|
row: policy.RuleGroup,
|
|
create: policy.RuleGroupCreate,
|
|
update: policy.RuleGroupUpdate,
|
|
filters: {
|
|
kind: policy.RuleGroupKind.optional(),
|
|
source: policy.RuleGroupSource.optional(),
|
|
enabled: z.enum(["true", "false"]).optional(),
|
|
includeDeleted: z.enum(["true", "false"]).optional(),
|
|
q: z.string().max(80).optional(),
|
|
},
|
|
},
|
|
R,
|
|
),
|
|
/** 删除 = 标记 deleted_at + 写抑制记录 */
|
|
remove: del("/rules/{id}", "删除规则组(留抑制记录;被引用时 409)"),
|
|
revisions: ep({
|
|
method: "get",
|
|
path: "/rules/{id}/revisions",
|
|
scope: "read",
|
|
summary: "规则组修订历史(新→旧;取自审计事件)",
|
|
params: IdParam,
|
|
response: policy.RuleGroupRevisionList,
|
|
}),
|
|
preview: ep({
|
|
method: "post",
|
|
path: "/rules/{id}/preview",
|
|
scope: "read",
|
|
summary: "试算:对最近 N 条已识别资源(或指定资源/标题)跑本组,给 pri、命中层与拒绝原因",
|
|
params: IdParam,
|
|
body: policy.RulePreviewRequest,
|
|
response: policy.RulePreviewResult,
|
|
}),
|
|
},
|
|
ruleAtoms: {
|
|
...crud(
|
|
"/rules/atoms",
|
|
"自定义条件",
|
|
{ row: policy.RuleAtom, create: policy.RuleAtomCreate, update: policy.RuleAtomUpdate },
|
|
R,
|
|
),
|
|
remove: del("/rules/atoms/{id}", "删除自定义条件(被规则组引用时 409)"),
|
|
},
|
|
/** /policy-revisions 的新路径(旧路径保留为别名,见 docs/runbook/rules.md) */
|
|
rulePolicies: {
|
|
list: ep({
|
|
method: "get",
|
|
path: "/rules/policies",
|
|
scope: "read",
|
|
summary: "收录策略版本列表",
|
|
query: listQuery({ policyKey: z.string().optional(), status: policy.PolicyStatus.optional() }),
|
|
response: cursorPage(policy.PolicyRevision),
|
|
}),
|
|
get: ep({
|
|
method: "get",
|
|
path: "/rules/policies/{id}",
|
|
scope: "read",
|
|
summary: "收录策略版本",
|
|
params: IdParam,
|
|
response: policy.PolicyRevision,
|
|
}),
|
|
create: ep({
|
|
method: "post",
|
|
path: "/rules/policies",
|
|
scope: "admin",
|
|
summary: "新建策略草案",
|
|
body: policy.PolicyRevisionCreate,
|
|
response: policy.PolicyRevision,
|
|
}),
|
|
update: ep({
|
|
method: "patch",
|
|
path: "/rules/policies/{id}",
|
|
scope: "admin",
|
|
summary: "修改策略草案",
|
|
params: IdParam,
|
|
body: policy.PolicyRevisionUpdate,
|
|
response: policy.PolicyRevision,
|
|
}),
|
|
preview: ep({
|
|
method: "post",
|
|
path: "/rules/policies/{id}/preview",
|
|
scope: "read",
|
|
summary: "预览策略草案",
|
|
params: IdParam,
|
|
response: policy.PolicyRevision,
|
|
}),
|
|
publish: ep({
|
|
method: "post",
|
|
path: "/rules/policies/{id}/publish",
|
|
scope: "admin",
|
|
summary: "发布策略版本",
|
|
params: IdParam,
|
|
body: z.object({ expectedRevision: z.number().int().min(1) }),
|
|
response: policy.PolicyRevision,
|
|
}),
|
|
rollback: ep({
|
|
method: "post",
|
|
path: "/rules/policies/{id}/rollback",
|
|
scope: "admin",
|
|
summary: "回滚到该策略版本",
|
|
params: IdParam,
|
|
response: policy.PolicyRevision,
|
|
}),
|
|
},
|
|
/** /preference-suppressions 的新路径(旧路径保留为别名) */
|
|
ruleSuppressions: crud(
|
|
"/rules/suppressions",
|
|
"规则抑制记录",
|
|
{
|
|
row: policy.PreferenceSuppression,
|
|
create: policy.PreferenceSuppressionCreate,
|
|
update: policy.PreferenceSuppressionUpdate,
|
|
filters: { active: z.enum(["true", "false"]).optional() },
|
|
},
|
|
R,
|
|
),
|
|
} as const;
|