真正入库取决于媒体服务器有没有扫到,本工具只负责整理(元数据、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
339 lines
12 KiB
TypeScript
339 lines
12 KiB
TypeScript
import { z } from "zod";
|
||
import * as classification from "../classification.ts";
|
||
import { cursorPage, IdParam } from "../common.ts";
|
||
import * as files from "../files.ts";
|
||
import * as organization from "../organization.ts";
|
||
import * as organizeSources from "../organize-sources.ts";
|
||
import { ep } from "./shared.ts";
|
||
|
||
export const organizeApi = {
|
||
// ── 整理(公开合同) ──
|
||
organization: {
|
||
list: ep({
|
||
method: "get",
|
||
path: "/organization-records",
|
||
scope: "organize.read",
|
||
summary: "整理记录列表",
|
||
query: organization.OrganizationListQuery,
|
||
response: cursorPage(organization.PublicOrganizationRecord),
|
||
}),
|
||
get: ep({
|
||
method: "get",
|
||
path: "/organization-records/{id}",
|
||
scope: "organize.read",
|
||
summary: "整理记录详情(含逐文件结果)",
|
||
params: IdParam,
|
||
response: organization.PublicOrganizationRecord,
|
||
}),
|
||
changes: ep({
|
||
method: "get",
|
||
path: "/organization-records/changes",
|
||
scope: "organize.read",
|
||
summary: "按 changeSeq 增量读取(含既有记录状态更新)",
|
||
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,
|
||
}),
|
||
dismissAttention: ep({
|
||
method: "post",
|
||
path: "/organization-records/{id}/dismiss-attention",
|
||
scope: "organize.execute",
|
||
summary: "忽略这条整理记录的问题:不再算需要处理,关联的未关闭事项一并关闭",
|
||
params: IdParam,
|
||
response: organization.PublicOrganizationRecord,
|
||
}),
|
||
agentIdentify: ep({
|
||
method: "post",
|
||
path: "/organization-records/{id}/agent-identify",
|
||
scope: "organize.execute",
|
||
summary: "让智能体识别这条整理记录的作品(没识别出作品的记录),认出来自动重新整理",
|
||
params: IdParam,
|
||
response: organization.OrganizationAgentIdentifyResult,
|
||
}),
|
||
create: ep({
|
||
method: "post",
|
||
path: "/organization-records",
|
||
scope: "organize.execute",
|
||
summary: "手动发起整理",
|
||
body: organization.OrganizeRequest,
|
||
response: organization.OrganizeAccepted,
|
||
}),
|
||
},
|
||
|
||
// ── 文件管理(docs/runbook/files.md;只浏览已登记根,写操作两段式 dryRun → planId) ──
|
||
classification: {
|
||
get: ep({
|
||
method: "get",
|
||
path: "/classification",
|
||
scope: "organize.read",
|
||
summary: "分类配置",
|
||
response: classification.ClassificationConfigView,
|
||
}),
|
||
put: ep({
|
||
method: "put",
|
||
path: "/classification",
|
||
scope: "admin",
|
||
summary: "保存分类配置",
|
||
body: classification.ClassificationPut,
|
||
response: classification.ClassificationConfigView,
|
||
}),
|
||
preview: ep({
|
||
method: "post",
|
||
path: "/classification/preview",
|
||
scope: "organize.read",
|
||
summary: "测试分类规则",
|
||
body: classification.ClassificationPreviewInput,
|
||
response: classification.ClassificationPreviewResult,
|
||
}),
|
||
},
|
||
organizeSources: {
|
||
list: ep({
|
||
method: "get",
|
||
path: "/organize-sources",
|
||
scope: "organize.read",
|
||
summary: "整理来源",
|
||
response: organizeSources.OrganizeSourceList,
|
||
}),
|
||
save: ep({
|
||
method: "put",
|
||
path: "/organize-sources/{id}",
|
||
scope: "admin",
|
||
summary: "保存整理来源",
|
||
params: IdParam,
|
||
body: organizeSources.OrganizeSourceInput,
|
||
response: organizeSources.OrganizeSourceView,
|
||
}),
|
||
remove: ep({
|
||
method: "delete",
|
||
path: "/organize-sources/{id}",
|
||
scope: "admin",
|
||
summary: "删除整理来源",
|
||
params: IdParam,
|
||
response: z.object({ deleted: z.boolean() }),
|
||
}),
|
||
stock: ep({
|
||
method: "post",
|
||
path: "/organize-sources/{id}/stock",
|
||
scope: "organize.execute",
|
||
summary: "扫描存量文件",
|
||
params: IdParam,
|
||
body: organizeSources.OrganizeStockInput,
|
||
response: organizeSources.OrganizeStockResult,
|
||
}),
|
||
},
|
||
files: {
|
||
roots: ep({
|
||
method: "get",
|
||
path: "/files/roots",
|
||
scope: "organize.read",
|
||
summary: "可浏览的根(库根 + 下载根),含实时健康与空间",
|
||
response: files.FileRootList,
|
||
}),
|
||
list: ep({
|
||
method: "get",
|
||
path: "/files",
|
||
scope: "organize.read",
|
||
summary: "列目录(分页/排序/过滤/截断,行内业务状态);根离线 503",
|
||
query: files.FilesListQuery,
|
||
response: files.FilesListResponse,
|
||
}),
|
||
stat: ep({
|
||
method: "get",
|
||
path: "/files/stat",
|
||
scope: "organize.read",
|
||
summary: "单项详情:realpath/inode/nlink/sidecars/路径绑定",
|
||
query: files.FileStatQuery,
|
||
response: files.FileStat,
|
||
}),
|
||
mkdir: ep({
|
||
method: "post",
|
||
path: "/files/mkdir",
|
||
scope: "organize.execute",
|
||
summary: "新建目录(非递归,已存在 409,仅 writable 根)",
|
||
body: files.FilesMkdirRequest,
|
||
response: files.FilesMkdirResult,
|
||
}),
|
||
rename: ep({
|
||
method: "post",
|
||
path: "/files/rename",
|
||
scope: "organize.execute",
|
||
summary: "同目录改名(不覆盖;同步 asset_location 与 path_binding 修订;做种中 409)",
|
||
body: files.FilesRenameRequest,
|
||
response: files.FilesRenameResult,
|
||
}),
|
||
move: ep({
|
||
method: "post",
|
||
path: "/files/move",
|
||
scope: "organize.execute",
|
||
summary: "移动/复制到目录:dryRun 返回 planId,执行回传 planId(源变更 409 plan_stale)",
|
||
body: files.FilesMoveRequest,
|
||
response: files.FilesTransferResult,
|
||
}),
|
||
copy: ep({
|
||
method: "post",
|
||
path: "/files/copy",
|
||
scope: "organize.execute",
|
||
summary: "复制到目录(同 /files/move mode=copy)",
|
||
body: files.FilesCopyRequest,
|
||
response: files.FilesTransferResult,
|
||
}),
|
||
organize: ep({
|
||
method: "post",
|
||
path: "/files/organize",
|
||
scope: "organize.execute",
|
||
summary: "按作品分组整理(每组走 planImport,与 /organization-records 同实现)",
|
||
body: files.FilesOrganizeRequest,
|
||
response: files.FilesOrganizeResult,
|
||
}),
|
||
delete: ep({
|
||
method: "post",
|
||
path: "/files/delete",
|
||
scope: "organize.execute",
|
||
summary: "删除(默认进回收站;引用检查:做种/资产/绑定;permanent 仅 admin 会话)",
|
||
body: files.FilesDeleteRequest,
|
||
response: files.FilesDeleteResult,
|
||
}),
|
||
trash: ep({
|
||
method: "get",
|
||
path: "/files/trash",
|
||
scope: "organize.read",
|
||
summary: "回收站列表",
|
||
query: files.FilesTrashQuery,
|
||
response: files.FilesTrashList,
|
||
}),
|
||
trashRestore: ep({
|
||
method: "post",
|
||
path: "/files/trash/restore",
|
||
scope: "organize.execute",
|
||
summary: "从回收站还原(原位被占的项失败 conflict)",
|
||
body: files.FilesTrashRestoreRequest,
|
||
response: files.FilesTrashRestoreResult,
|
||
}),
|
||
trashPurge: ep({
|
||
method: "post",
|
||
path: "/files/trash/purge",
|
||
scope: "admin",
|
||
auth: "session",
|
||
summary: "清空回收站(缺省只清过期项;仅管理员控制台会话,API Key 与 DEV key 一律 403)",
|
||
body: files.FilesTrashPurgeRequest,
|
||
response: files.FilesTrashPurgeResult,
|
||
}),
|
||
identify: ep({
|
||
method: "post",
|
||
path: "/files/identify",
|
||
scope: "organize.read",
|
||
summary: "识别(只读):解析 + 本地/TMDB 召回前 5 候选,不落库",
|
||
body: files.FilesIdentifyRequest,
|
||
response: files.FilesIdentifyResult,
|
||
}),
|
||
scrape: ep({
|
||
method: "post",
|
||
path: "/files/scrape",
|
||
scope: "organize.execute",
|
||
summary:
|
||
"刮削:对目录或视频文件就地写 NFO 与图片(按路径识别作品,不要求已登记;认不准时返回候选,选定后再提交)",
|
||
body: files.FilesScrapeRequest,
|
||
response: files.FilesScrapeResult,
|
||
}),
|
||
scrapeRecords: ep({
|
||
method: "get",
|
||
path: "/files/scrape-records",
|
||
scope: "organize.read",
|
||
summary: "按 id 查刮削记录(目录、识别结果、逐文件结果)",
|
||
query: files.FilesScrapeRecordsQuery,
|
||
response: files.FilesScrapeRecordsResult,
|
||
}),
|
||
probe: ep({
|
||
method: "post",
|
||
path: "/files/probe",
|
||
scope: "organize.execute",
|
||
summary: "ffprobe 摘要(≤20 项;已登记资产写回 media_info,所以要 organize.execute)",
|
||
body: files.FilesProbeRequest,
|
||
response: files.FilesProbeResult,
|
||
}),
|
||
download: ep({
|
||
method: "get",
|
||
path: "/files/download",
|
||
scope: "admin",
|
||
auth: "session",
|
||
summary: "下载小文件(nfo/字幕/图片/文本/.torrent,≤20 MiB;流式;仅管理员控制台会话;审计)",
|
||
query: files.FilesDownloadQuery,
|
||
response: files.FilesDownloadResponse,
|
||
}),
|
||
preview: ep({
|
||
method: "get",
|
||
path: "/files/preview",
|
||
scope: "admin",
|
||
auth: "session",
|
||
summary:
|
||
"结构化预览(文本/NFO 前 1 MiB 带编码探测、字幕前 200 条、.torrent 文件列表、图片尺寸;仅管理员控制台会话)",
|
||
query: files.FilesPreviewQuery,
|
||
response: files.FilePreview,
|
||
}),
|
||
records: ep({
|
||
method: "get",
|
||
path: "/files/records",
|
||
scope: "organize.read",
|
||
summary: "按整理记录 id 或 planId 回查整理记录(整理页预填)",
|
||
query: files.FilesRecordsQuery,
|
||
response: files.FilesRecordsResult,
|
||
}),
|
||
// W9-H 文件管理 v2 后端缺口
|
||
entityFromCandidate: ep({
|
||
method: "post",
|
||
path: "/files/entity-from-candidate",
|
||
scope: "organize.execute",
|
||
summary: "按 TMDB 候选就地建本地作品 + 外部映射(accepted/manual)+ 季集单元;幂等",
|
||
body: files.FilesEntityFromCandidateRequest,
|
||
response: files.FilesEntityFromCandidateResult,
|
||
}),
|
||
scrapeBatch: ep({
|
||
method: "post",
|
||
path: "/files/scrape-batch",
|
||
scope: "organize.execute",
|
||
summary: "批量刷新元数据:按作品去重入队 refreshMetadata(锁定字段不变)",
|
||
body: files.FilesScrapeBatchRequest,
|
||
response: files.FilesScrapeBatchResult,
|
||
}),
|
||
tree: ep({
|
||
method: "get",
|
||
path: "/files/tree",
|
||
scope: "organize.read",
|
||
summary: "目录摘要:上一级、子目录(是否有下级)、视频/已登记计数(树与上一级用)",
|
||
query: files.FilesTreeQuery,
|
||
response: files.FilesTreeResult,
|
||
}),
|
||
recordSources: ep({
|
||
method: "get",
|
||
path: "/files/record-sources",
|
||
scope: "organize.read",
|
||
summary: "整理记录的源路径换算成登记目录 FileRef(重新整理用)",
|
||
query: files.FilesRecordSourcesQuery,
|
||
response: files.FilesRecordSourcesResult,
|
||
}),
|
||
suggestName: ep({
|
||
method: "post",
|
||
path: "/files/suggest-name",
|
||
scope: "organize.read",
|
||
summary: "按命名模板(作品 > 库根 > 全局)生成推荐名;识别不可信时 name=null + 原因",
|
||
body: files.FilesSuggestNameRequest,
|
||
response: files.FilesSuggestNameResult,
|
||
}),
|
||
},
|
||
} as const;
|