feat(organize): record filters, stats and delete with files (HIS-1)

List query gains kind/since/organizeOnly and wildcard/hash search; new
GET /organization-records/stats and POST /organization-records/delete
(optional source/library files to trash via files delete). Public record
adds posterPath, sizeBytes and download summary.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
rootandClaude Opus 5.5 committed 2026-09-30 23:57:19 +08:00
1 parent f60254e265
commit 51e82f1cd9
8 files changed
+850 -17

No files matched your search

+16
View File
@@ -1431,6 +1431,22 @@ export const Api = {
query: organization.OrganizationChangesQuery,
response: organization.OrganizationChangesResponse,
}),
stats: ep({
method: "get",
path: "/organization-records/stats",
scope: "organize.read",
summary: "整理记录按状态计数(筛选同列表)",
query: organization.OrganizationStatsQuery,
response: organization.OrganizationStats,
}),
remove: ep({
method: "post",
path: "/organization-records/delete",
scope: "organize.execute",
summary: "删除整理记录,可选把源文件 / 媒体库文件移入回收站(dryRun 只核对)",
body: organization.OrganizationDeleteRequest,
response: organization.OrganizationDeleteResult,
}),
create: ep({
method: "post",
path: "/organization-records",
+86
View File
@@ -228,6 +228,8 @@ export const PublicOrganizationRecord = z.object({
episodeNumber: z.number().int().nullable(),
/** 已确认的外部身份,如 tmdb:movie:693134 */
externalIds: z.array(z.object({ provider: z.string(), entityType: z.string(), externalId: z.string() })),
/** HIS-1:作品海报(TMDB poster_path,如 /abc.jpg);没有为 null */
posterPath: z.string().nullable().optional(),
})
.nullable(),
trigger: OrganizationTrigger,
@@ -240,6 +242,13 @@ export const PublicOrganizationRecord = z.object({
target: PublicPathRef,
reason: PublicReason.nullable(),
counts: z.object({ total: NonNegInt, succeeded: NonNegInt, failed: NonNegInt, skipped: NonNegInt }),
/** HIS-1:逐文件源大小之和(字节);没有文件明细为 null */
sizeBytes: NonNegInt.nullable().optional(),
/** HIS-1:来源下载任务摘要(下载器名称、种子 hash);手动整理本地路径时为 null */
download: z
.object({ taskId: Id, name: z.string(), hash: z.string(), downloaderName: z.string().nullable() })
.nullable()
.optional(),
/** includeFiles=false 时为 null */
files: z.array(PublicOrganizationFile).nullable(),
createdAt: Timestamp,
@@ -280,10 +289,87 @@ export const OrganizationListQuery = z.object({
trigger: OrganizationTrigger.optional(),
mediaEntityId: Id.optional(),
downloadTaskId: Id.optional(),
/** 作品标题、源/目标路径、种子 hash;支持 * 与 ? 通配 */
q: z.string().max(200).optional(),
/** HIS-1:作品类型;none = 没关联作品(未识别) */
kind: z.enum(["movie", "series", "none"]).optional(),
/** HIS-1:只看这之后创建的记录 */
since: Timestamp.optional(),
/** HIS-1:只看整理类记录(复制/移动/硬链接),不含文件管理的改名、删除、还原 */
organizeOnly: z.union([z.boolean(), z.enum(["true", "false"]).transform((v) => v === "true")]).optional(),
});
export type OrganizationListQuery = z.infer<typeof OrganizationListQuery>;
/** HIS-1:统计与列表同一套筛选(不含状态,状态按计数分开给) */
export const OrganizationStatsQuery = OrganizationListQuery.omit({ cursor: true, limit: true, status: true });
export type OrganizationStatsQuery = z.infer<typeof OrganizationStatsQuery>;
export const OrganizationStats = z.object({
total: NonNegInt,
succeeded: NonNegInt,
partial: NonNegInt,
failed: NonNegInt,
/** 排队 + 执行中 */
running: NonNegInt,
cancelled: NonNegInt,
});
export type OrganizationStats = z.infer<typeof OrganizationStats>;
/**
* HIS-1:删除整理记录,可选同时删源文件 / 媒体库里的文件(对照 MP deletesrc / deletedest)。
* 文件一律移入回收站(同文件管理删除,可在回收站还原);下载器里的任务不动。
* 删文件只允许管理员(控制台会话或 DEV key);API Key 删文件请走 /files/delete(一次性审批)。
*/
export const OrganizationDeleteRequest = z.object({
ids: z.array(Id).min(1).max(200),
deleteSource: z.boolean().default(false),
deleteTarget: z.boolean().default(false),
/** true 只核对会删什么,不动文件与记录 */
dryRun: z.boolean().default(false),
});
export type OrganizationDeleteRequest = z.infer<typeof OrganizationDeleteRequest>;
/** 单条记录不能删的原因:running 还在整理 / not_found 记录不存在 / 其余同文件管理删除的 blocked 码 */
export const OrganizationDeleteBlock = z.enum([
"running",
"not_found",
"download_active",
"needs_unbind",
"is_root",
"root_unavailable",
"escape",
"outside_roots",
"failed",
]);
export type OrganizationDeleteBlock = z.infer<typeof OrganizationDeleteBlock>;
export const OrganizationDeleteResult = z.object({
dryRun: z.boolean(),
items: z.array(
z.object({
id: Id,
/** 会移入回收站的源文件 / 媒体库文件数(已不存在的不算) */
sourceFiles: NonNegInt,
targetFiles: NonNegInt,
bytes: NonNegInt,
blocked: OrganizationDeleteBlock.nullable(),
/** blocked 时的一句说明(路径 + 原因) */
message: z.string().nullable(),
}),
),
/** 执行后真正删掉的记录 */
removed: z.array(Id),
sourceFiles: NonNegInt,
targetFiles: NonNegInt,
bytes: NonNegInt,
/** 删掉的媒体库文件里已入库的文件数(同时从媒体库移除) */
assets: NonNegInt,
/** 删记录后会被自动整理重新处理的下载任务数(该下载任务没有别的整理记录了) */
autoRetry: NonNegInt,
/** 文件移入回收站时写的「移入回收站」整理记录 */
trashRecordId: Id.nullable(),
});
export type OrganizationDeleteResult = z.infer<typeof OrganizationDeleteResult>;
/** 手动发起整理(organize.execute):返回 record/task ID,由任务页追踪 */
export const OrganizeRequest = z
.object({