feat(copy): 处理状态的「入库」改为「整理」,术语表与 check:copy 同步
真正入库取决于媒体服务器有没有扫到,本工具只负责整理(元数据、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
This commit is contained in:
1 parent
79c3b8c9b9
commit
550ac285f4
67 files changed
+171
-143
No files matched your search
@@ -133,7 +133,7 @@ export function createAcquisitionOrganizeModule(overrides: AoOverrides = {}): Se
|
||||
key: "cache.threshold",
|
||||
defaults: {
|
||||
name: "下载盘检查",
|
||||
description: "下载盘超过暂停线时清理已入库的下载",
|
||||
description: "下载盘超过暂停线时清理已整理的下载",
|
||||
group: "maintain",
|
||||
schedule: every(planMs),
|
||||
},
|
||||
@@ -155,8 +155,8 @@ export function createAcquisitionOrganizeModule(overrides: AoOverrides = {}): Se
|
||||
registerScheduledJob(ctx, {
|
||||
key: "downloads.orchestrate",
|
||||
defaults: {
|
||||
name: "补提交与补入库",
|
||||
description: "补交没提交成功的下载,补整理下载完但没入库的任务,重新排队卡住的整理",
|
||||
name: "补提交与补整理",
|
||||
description: "补交没提交成功的下载,补整理下载完但没整理的任务,重新排队卡住的整理",
|
||||
group: "download",
|
||||
schedule: every(planMs),
|
||||
},
|
||||
@@ -165,7 +165,7 @@ export function createAcquisitionOrganizeModule(overrides: AoOverrides = {}): Se
|
||||
const imports = await sweepPendingImports(d);
|
||||
const rec = await recoverStalledRecords(d);
|
||||
return (
|
||||
`补提交 ${submitted},补入库 ${imports}` +
|
||||
`补提交 ${submitted},补整理 ${imports}` +
|
||||
(rec.requeued ? `,重排整理 ${rec.requeued}` : "") +
|
||||
(rec.failed ? `,整理停止 ${rec.failed}` : "") +
|
||||
(rec.removed ? `,移除下载任务 ${rec.removed}` : "")
|
||||
@@ -278,7 +278,7 @@ async function sweepPendingImports(d: AoDeps): Promise<number> {
|
||||
);
|
||||
if (q) n++;
|
||||
} catch (err) {
|
||||
d.ctx.logger.warn({ downloadTaskId: r.id, err: { message: errMessage(err) } }, "补入库入队失败");
|
||||
d.ctx.logger.warn({ downloadTaskId: r.id, err: { message: errMessage(err) } }, "补整理入队失败");
|
||||
}
|
||||
}
|
||||
return n;
|
||||
|
||||
@@ -158,7 +158,7 @@ export function registerDownloadRoutes(app: ModuleApp, d: AoDeps) {
|
||||
method: "post",
|
||||
path: "/download-cache/cleanup",
|
||||
scope: "admin",
|
||||
summary: "入队一轮缓存清理(只清已验证入库且满足保留/H&R 的纳管任务);返回任务 id,进度见任务页",
|
||||
summary: "入队一轮缓存清理(只清已核验整理且满足保留/H&R 的纳管任务);返回任务 id,进度见任务页",
|
||||
body: z.object({ downloaderInstanceId: z.uuid().optional() }),
|
||||
response: accepted,
|
||||
},
|
||||
|
||||
@@ -14,7 +14,7 @@ export function registerLibraryVersionRoutes(app: ModuleApp, d: AoDeps) {
|
||||
method: "get",
|
||||
path: "/library/{id}/versions",
|
||||
scope: "read",
|
||||
summary: "作品的入库版本:每季每集的本地文件版本与规格、季 / 作品汇总、与版本目标的比对",
|
||||
summary: "作品的已整理版本:每季每集的本地文件版本与规格、季 / 作品汇总、与版本目标的比对",
|
||||
params: IdParam,
|
||||
query: LibraryVersionsQuery,
|
||||
response: LibraryVersions,
|
||||
|
||||
@@ -433,7 +433,7 @@ export function registerLibraryRoutes(app: ModuleApp, d: AoDeps) {
|
||||
mode: req.mode,
|
||||
actor: { type: actor?.type === "api_key" ? "api_key" : "user", id: actor?.id ?? null },
|
||||
});
|
||||
if (!recs.length) throw new ApiException("precondition_failed", "作品没有已入库文件。到文件管理里对目录刮削");
|
||||
if (!recs.length) throw new ApiException("precondition_failed", "作品没有已整理文件。到文件管理里对目录刮削");
|
||||
return {
|
||||
mediaEntityId: params.id,
|
||||
taskId: recs[0]?.taskId ?? null,
|
||||
|
||||
@@ -63,7 +63,7 @@ export async function planManualAcquisition(
|
||||
if (!topo?.savePath) throw new ApiException("precondition_failed", "没有可用的下载器或保存目录");
|
||||
const replacementIds = body.importMode === "replace" ? [...new Set(body.replacementAssetIds)] : [];
|
||||
if (body.importMode === "replace" && (!entityId || replacementIds.length === 0))
|
||||
throw new ApiException("bad_request", "洗版入库需要明确作品及要替换的已有版本");
|
||||
throw new ApiException("bad_request", "洗版需要明确作品及要替换的已有版本");
|
||||
if (replacementIds.length) {
|
||||
const assets = await db
|
||||
.selectFrom("media_asset")
|
||||
|
||||
@@ -420,7 +420,7 @@ export async function executeRecord(
|
||||
if (outcome.replaced?.disposition === "left") {
|
||||
await upsertDecision(d, db, {
|
||||
kind: "other",
|
||||
title: "新版本已入库,旧文件未能处置",
|
||||
title: "新版本已整理,旧文件未能处置",
|
||||
summary: `${pf.dst} 已替换为新版本;旧文件留在 ${outcome.replaced.path}`,
|
||||
subjectType: "organization_record",
|
||||
subjectId: recordId,
|
||||
@@ -604,7 +604,7 @@ export async function verifyInPlace(
|
||||
): Promise<FileOutcome> {
|
||||
const observed = await fsr.proxy.statSafe(path, { root: frozen.rootPath, sentinel: frozen.sentinel });
|
||||
if (observed.kind !== "ok" || !observed.facts.isFile || observed.facts.isSymbolicLink) {
|
||||
return { status: "failed", error: { code: "SOURCE_MISSING", message: "原地入库的文件不存在或不可访问" } };
|
||||
return { status: "failed", error: { code: "SOURCE_MISSING", message: "原地整理的文件不存在或不可访问" } };
|
||||
}
|
||||
const planned = frozen.sourceFacts?.[path];
|
||||
if (observed.facts.size !== size || (planned && observed.facts.mtimeMs !== planned.mtimeMs)) {
|
||||
|
||||
@@ -437,7 +437,7 @@ export async function finalizeRecord(d: AoDeps, recordId: string): Promise<Execu
|
||||
kind: "other",
|
||||
severity: "info",
|
||||
title: "旧版本待处置",
|
||||
summary: "新版本已核验入库,旧文件保留,按保留策略处理",
|
||||
summary: "新版本已核验整理,旧文件保留,按保留策略处理",
|
||||
subjectType: "media_asset",
|
||||
subjectId: old,
|
||||
dedupeKey: `old_version:${old}`,
|
||||
@@ -583,7 +583,7 @@ export async function closeOrganizeDecisions(d: AoDeps, trx: Exec, downloadTaskI
|
||||
key: rec.trigger === "auto" ? "organize_retry" : "manual_organize",
|
||||
optionKey: null,
|
||||
by,
|
||||
note: "整理已成功入库",
|
||||
note: "整理成功",
|
||||
organizationRecordId: rec.id,
|
||||
}),
|
||||
})
|
||||
|
||||
@@ -36,7 +36,7 @@ export async function freezeReplacement(
|
||||
ids: string[],
|
||||
): Promise<FrozenReplacement> {
|
||||
const selected = [...new Set(ids)];
|
||||
if (!selected.length) throw new Error("洗版入库必须选择旧版本");
|
||||
if (!selected.length) throw new Error("洗版必须选择旧版本");
|
||||
const assets = await d.ctx.db.selectFrom("media_asset").selectAll().where("id", "in", selected).execute();
|
||||
if (
|
||||
assets.length !== selected.length ||
|
||||
@@ -592,7 +592,7 @@ export async function quarantineNotVideo(d: AoDeps, frozen: FrozenPlan, recordId
|
||||
const names = [...rejectedMains].map((p) => basename(p));
|
||||
await upsertDecision(d, db, {
|
||||
kind: "other",
|
||||
title: `${rejectedMains.size} 个文件不是视频,没有入库`,
|
||||
title: `${rejectedMains.size} 个文件不是视频,没有整理`,
|
||||
summary: `${names.slice(0, 5).join("、")}${names.length > 5 ? " 等" : ""} 没有视频流(伪装或损坏的文件)。${
|
||||
moved.length ? "已拷进媒体库的文件移入回收站" : ""
|
||||
}${left.length ? `${moved.length ? "," : ""}${left.length} 个文件留在媒体库里待核对` : ""}${
|
||||
|
||||
@@ -565,7 +565,7 @@ export async function scrapeAfterImport(
|
||||
});
|
||||
} catch (err) {
|
||||
// 新记录入队失败时 queueScrape 已把它标成 task_enqueue_failed(整理详情可见);这里再兜一条,防止没留下记录
|
||||
d.ctx.logger.warn({ entityId, recordId, dir, err: { message: errMessage(err) } }, "入库后自动刮削没排上");
|
||||
d.ctx.logger.warn({ entityId, recordId, dir, err: { message: errMessage(err) } }, "整理后自动刮削没排上");
|
||||
const marked = await d.ctx.db
|
||||
.selectFrom("scrape_record")
|
||||
.select("id")
|
||||
@@ -577,7 +577,7 @@ export async function scrapeAfterImport(
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
d.ctx.logger.warn({ entityId, recordId, err: { message: errMessage(err) } }, "入库后自动刮削没排上");
|
||||
d.ctx.logger.warn({ entityId, recordId, err: { message: errMessage(err) } }, "整理后自动刮削没排上");
|
||||
await recordScrapeSkipped(d, recordId, "scrape_internal_error", { root, match });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -798,7 +798,7 @@ export async function afterSlotVerified(
|
||||
kind: "other",
|
||||
severity: "info",
|
||||
title: "疑似缺集",
|
||||
summary: `已入库最高集之前缺:${desc}。总集数未知,订阅继续追踪`.slice(0, 500),
|
||||
summary: `已整理最高集之前缺:${desc}。总集数未知,订阅继续追踪`.slice(0, 500),
|
||||
subjectType: "acquisition_slot",
|
||||
subjectId: slot.id,
|
||||
dedupeKey: `suspected_missing:${slot.id}`,
|
||||
|
||||
@@ -100,7 +100,7 @@ const OUTCOME_TEXT: Record<NameIdentifyResult["outcome"], string> = {
|
||||
export function takeoverSummary(t: TakeoverIdentify | null): string {
|
||||
if (!t) return "下载内容未关联作品。识别或指定作品后再整理";
|
||||
const what = [t.parsed.title, t.parsed.year].filter(Boolean).join(" ");
|
||||
return `按文件名识别${what ? `「${what}」` : ""}:${OUTCOME_TEXT[t.result.outcome]}。选择作品、作为临时作品入库,或忽略`;
|
||||
return `按文件名识别${what ? `「${what}」` : ""}:${OUTCOME_TEXT[t.result.outcome]}。选择作品、作为临时作品整理,或忽略`;
|
||||
}
|
||||
|
||||
/** 接管识别失败 / 歧义的事项选项:候选(最多 3 个)、选择作品(本地 / TMDB)、作为临时作品入库、忽略 */
|
||||
@@ -140,7 +140,7 @@ export function identityOptions(t: TakeoverIdentify | null, fallbackTitle: strin
|
||||
t?.parsed.mediaTypeHint === "tv" || t?.parsed.season !== undefined || t?.parsed.episodes !== undefined;
|
||||
out.push({
|
||||
key: "provisional",
|
||||
label: "作为临时作品入库",
|
||||
label: "作为临时作品整理",
|
||||
description: "按文件名建本地临时作品后整理,之后可合并或补外部身份",
|
||||
input: { provisional: { kind: tvLike ? "series" : "movie", title, year: t?.parsed.year ?? null } },
|
||||
});
|
||||
@@ -153,7 +153,7 @@ export function identityOptions(t: TakeoverIdentify | null, fallbackTitle: strin
|
||||
export const CONFLICT_OPTIONS: DecisionOptionInput[] = [
|
||||
{ key: "replace", label: "替换", description: "新文件核验通过后替换已有文件,旧文件进回收站" },
|
||||
{ key: "keep_both", label: "保留两者", description: "新文件改名(加序号)后放在旁边,已有文件不动" },
|
||||
{ key: "skip", label: "跳过", description: "已存在的文件不整理,其余文件照常入库" },
|
||||
{ key: "skip", label: "跳过", description: "已存在的文件不整理,其余文件照常整理" },
|
||||
];
|
||||
|
||||
// ── 保留两者:把目标已存在的主视频(连同附属文件)改名为 "<名称> (n)" ──
|
||||
|
||||
@@ -68,7 +68,7 @@ export async function renameEntry(
|
||||
if (hits.length > 0)
|
||||
throw new FilesError(
|
||||
"download_active",
|
||||
`该路径属于下载任务 ${hits.map((h) => h.taskId).join(", ")},重命名会让下载器找不到文件。先整理入库再改`,
|
||||
`该路径属于下载任务 ${hits.map((h) => h.taskId).join(", ")},重命名会让下载器找不到文件。先整理再改`,
|
||||
);
|
||||
|
||||
const dirAbs = dirname(r.abs);
|
||||
|
||||
@@ -306,7 +306,7 @@ export async function organizeFiles(
|
||||
if (action === "in_place") {
|
||||
// 本次保留实际库根与路径,不使用来源配置的搬运目标。
|
||||
if (!r.def.libraryRootId)
|
||||
throw new FilesError("bad_request", "原地入库需要文件位于已登记的媒体库目录;请先在设置中登记该目录");
|
||||
throw new FilesError("bad_request", "原地整理需要文件位于已登记的媒体库目录;请先在设置中登记该目录");
|
||||
targetLib = r.def.libraryRootId;
|
||||
}
|
||||
if (action !== "in_place" && it.targetDir) {
|
||||
|
||||
@@ -230,7 +230,7 @@ export async function transfer(
|
||||
if (hnrBlocks.length > 0)
|
||||
throw new FilesError(
|
||||
"download_active",
|
||||
`源属于下载任务 ${[...new Set(hnrBlocks.map((h) => h.taskId))].join(", ")},直接移动会让下载器丢失文件。改用整理入库,它会按做种义务决定能否移动`,
|
||||
`源属于下载任务 ${[...new Set(hnrBlocks.map((h) => h.taskId))].join(", ")},直接移动会让下载器丢失文件。改用整理,它会按做种义务决定能否移动`,
|
||||
);
|
||||
if (!isAdmin(principal)) {
|
||||
// 每个源按其(目录则为其下)已登记资产求作品集合,逐一核对授权;任一不覆盖 → 整体拒绝并列出未授权项
|
||||
|
||||
@@ -157,7 +157,7 @@ const BLOCK_ERR: Record<
|
||||
> = {
|
||||
download_active: ["download_active", "属于未完成或做种义务未满足的下载任务"],
|
||||
is_root: ["bad_request", "不能删除根目录或只读根下的内容"],
|
||||
needs_retire: ["needs_retire", "含已入库文件,需勾选「同时从媒体库移除」"],
|
||||
needs_retire: ["needs_retire", "含已整理文件,需勾选「同时从媒体库移除」"],
|
||||
needs_unbind: ["needs_unbind", "是作品/季的绑定目录,需勾选「解除绑定」"],
|
||||
root_unavailable: ["root_unavailable", "根不可用"],
|
||||
not_found: ["not_found", "不存在"],
|
||||
|
||||
@@ -76,7 +76,7 @@ describe.skipIf(!hasPg)("整理:不是视频的文件不留在媒体库", () =>
|
||||
.select(["title", "status"])
|
||||
.where("dedupe_key", "=", `organize_not_video:${plan.recordId}`)
|
||||
.execute();
|
||||
expect(decisions).toEqual([{ title: "1 个文件不是视频,没有入库", status: "open" }]);
|
||||
expect(decisions).toEqual([{ title: "1 个文件不是视频,没有整理", status: "open" }]);
|
||||
|
||||
// 重放:已结束记录不重复移动
|
||||
await executeRecord(h.d, plan.recordId!);
|
||||
|
||||
Reference in new issue
Block a user