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:
rootandClaude Opus 5.5 committed 2026-10-06 17:54:28 +08:00
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!);
+2 -2
View File
@@ -211,8 +211,8 @@ export const DOWNLOAD_STATUS_LABEL: Record<DownloadStatus, string> = {
paused: "已暂停",
stalled: "无进展",
completed: "下载完成",
importing: "入库中",
imported: "已入库",
importing: "整理中",
imported: "已整理",
failed: "失败",
removed: "已移除",
missing: "下载器中缺失",
+1 -1
View File
@@ -182,7 +182,7 @@ export const TRIGGER_LABEL: Record<OrganizationTrigger, string> = { auto: "自
/** 整理方式 / 文件操作(记录的 action) */
export const ACTION_LABEL = {
copy: "复制",
in_place: "原地入库",
in_place: "原地整理",
move: "移动",
rename: "重命名",
relocate: "目录调整",
@@ -25,12 +25,12 @@ describe("下载面板展示换算(DL-2)", () => {
expect(formatEta(3 * 86400 + 4 * 3600)).toBe("3 天 4 小时");
});
it("最近一次整理结果:已入库 / 整理中 / 待整理 / 入库失败 / 部分入库;有问题的链到整理记录就地处理", () => {
it("最近一次整理结果:已整理 / 整理中 / 待整理 / 整理失败 / 部分整理;有问题的链到整理记录就地处理", () => {
expect(organizeState({ status: "completed", organize: null })).toMatchObject({ label: "待整理", to: null });
expect(organizeState({ status: "importing", organize: null })?.label).toBe("整理中");
expect(organizeState({ status: "imported", organize: null })?.label).toBe("已入库");
expect(organizeState({ status: "imported", organize: null })?.label).toBe("已整理");
expect(organizeState({ status: "completed", organize: rec("succeeded") })).toMatchObject({
label: "已入库",
label: "已整理",
tone: "success",
to: `/console/organize/${RID}`,
});
@@ -44,7 +44,7 @@ describe("下载面板展示换算(DL-2)", () => {
});
// 原因码转成人话,不直接给人看
expect(failed).toMatchObject({
label: "入库失败",
label: "整理失败",
tone: "destructive",
hint: "没识别出作品。指定作品后重新整理",
});
@@ -52,7 +52,7 @@ describe("下载面板展示换算(DL-2)", () => {
expect(organizeState({ status: "completed", organize: rec("failed", { reason: "磁盘已满" }) })?.hint).toBe(
"磁盘已满",
);
expect(organizeState({ status: "completed", organize: rec("partial") })?.label).toBe("部分入库");
expect(organizeState({ status: "completed", organize: rec("partial") })?.label).toBe("部分整理");
expect(organizeState({ status: "completed", organize: rec("failed", { reason: "transfer_failed" }) })?.hint).toBe(
"文件没有整理成功。在整理记录里查看失败的文件",
);
@@ -72,11 +72,11 @@ describe("下载面板展示换算(DL-2)", () => {
it("外部任务:没整理过显示 —(null),整理过的照常显示结果", () => {
expect(organizeState({ status: "completed", organize: null, managed: false })).toBeNull();
expect(organizeState({ status: "completed", organize: null, managed: true })?.label).toBe("待整理");
expect(organizeState({ status: "completed", organize: rec("succeeded"), managed: false })?.label).toBe("已入库");
expect(organizeState({ status: "completed", organize: rec("succeeded"), managed: false })?.label).toBe("已整理");
expect(
organizeState({ status: "completed", organize: rec("failed", { reason: "transfer_failed" }), managed: false })
?.label,
).toBe("入库失败");
).toBe("整理失败");
});
it("连接数:全 0 且总数未知时不显示", () => {
@@ -40,7 +40,7 @@ export function reasonHint(raw: string | null | undefined): string | null {
}
/**
* 已完成任务的最近一次整理结果:已入库 / 整理中 / 待整理 / 入库失败 / 部分入库。
* 已完成任务的最近一次整理结果:已整理 / 整理中 / 待整理 / 整理失败 / 部分整理。
* 外部任务(managed=false)没整理过时不算"待整理",返回 null(界面显示 —);整理过的照常显示结果。
*/
export function organizeState(
@@ -51,9 +51,9 @@ export function organizeState(
// 有问题的整理在整理记录详情里就地处理(待处理中心已取消)
const to = rec ? consolePath("organize", rec.recordId) : null;
if (rec?.status === "succeeded" || (t.status === "imported" && rec?.status !== "failed"))
return { label: "已入库", tone: "success", to, hint: null };
if (rec?.status === "failed") return { label: "入库失败", tone: "destructive", to, hint: reasonHint(rec.reason) };
if (rec?.status === "partial") return { label: "部分入库", tone: "warning", to, hint: reasonHint(rec.reason) };
return { label: "已整理", tone: "success", to, hint: null };
if (rec?.status === "failed") return { label: "整理失败", tone: "destructive", to, hint: reasonHint(rec.reason) };
if (rec?.status === "partial") return { label: "部分整理", tone: "warning", to, hint: reasonHint(rec.reason) };
if (rec?.status === "queued" || rec?.status === "running" || t.status === "importing")
return { label: "整理中", tone: "neutral", to, hint: null };
return { label: "待整理", tone: "warning", to: rec?.decision ? to : null, hint: null };
@@ -119,7 +119,7 @@ export function CacheVolumes() {
</Button>
}
title="按规则清理下载缓存?"
description="只清理已入库、且满足保留期与 H&R 的本工具任务,其余不动。"
description="只清理已整理、且满足保留期与 H&R 的本工具任务,其余不动。"
confirmLabel="清理"
onConfirm={() =>
cleanup.mutateAsync(undefined).then(
@@ -96,7 +96,7 @@ export function DownloadDetail({ task, live }: { task: DownloadTask; live?: Down
const trace = useDownloadTrace(task, plan.data);
const [organizing, setOrganizing] = useState(false);
const canOrganize = ["completed", "importing", "imported"].includes(task.status);
// 下载完成但最近一次整理失败:状态行只写"下载完成"会误导,单独说明入库失败
// 下载完成但最近一次整理失败:状态行只写"下载完成"会误导,单独说明整理失败
const lastRecord = [...(trace.records.data?.items ?? [])].sort((a, b) =>
(b.startedAt ?? b.createdAt).localeCompare(a.startedAt ?? a.createdAt),
)[0];
@@ -143,7 +143,7 @@ export function DownloadDetail({ task, live }: { task: DownloadTask; live?: Down
) : null}
{importFailed ? (
<p className="rounded-lg border border-destructive/40 bg-destructive/10 px-3 py-2 text-caption text-destructive">
入库失败:{reasonLine(importFailed.reason)}。
整理失败:{reasonLine(importFailed.reason)}。
<Link to={`/console/organize/${importFailed.id}`} className="ml-1 underline underline-offset-4">
去整理记录处理
</Link>
@@ -610,10 +610,10 @@ export function ExistingVersionsSection({
<div className="flex min-w-0 flex-wrap items-center justify-between gap-2">
<p className="min-w-0 text-caption text-foreground">{summary ?? `媒体库已有 ${versions.length} 个版本`}</p>
<FilterSelect
label="入库方式"
label="已有版本"
value={mode === "normal" ? "" : mode}
onChange={(v) => onModeChange(v === "replace" ? "replace" : "normal")}
allLabel="正常入库,保留已有版本"
allLabel="保留"
options={[{ value: "replace", label: "一次性洗版" }]}
/>
</div>
@@ -640,7 +640,7 @@ export function ExistingVersionsSection({
))}
</fieldset>
<p className="text-meta text-muted-foreground">
本次新文件成功入库后替换所选旧文件。剧集只替换成功对应的季集,其余保留。
本次新文件整理成功后替换所选旧文件。剧集只替换成功对应的季集,其余保留。
</p>
</>
) : null}
@@ -45,7 +45,7 @@ export const VALIDATION: Record<EpisodeValidationState, { label: string; tone: S
pass: { label: "对得上", tone: "success" },
unverified: { label: "未核对", tone: "warning" },
fail: { label: "对不上", tone: "destructive" },
special: { label: "不按集入库", tone: "neutral" },
special: { label: "不按集整理", tone: "neutral" },
skipped: { label: "不适用", tone: "neutral" },
};
+6 -6
View File
@@ -450,7 +450,7 @@ export function RenameDialog({
<Notice tone="neutral">同步 {pv.assetLocations} 条文件位置,写一条整理记录。</Notice>
) : null}
{pv.bindings.length > 0 ? (
<Notice tone="warning">这是作品或季的绑定目录。改名后旧绑定标为已替代,之后入库用新目录。</Notice>
<Notice tone="warning">这是作品或季的绑定目录。改名后旧绑定标为已替代,之后整理用新目录。</Notice>
) : null}
</>
) : null}
@@ -629,7 +629,7 @@ export function TransferDialog({
<StatusBadge tone="warning">冲突 {conflicts.length}(跳过,不覆盖)</StatusBadge>
) : null}
{plan.pairs.some((p) => p.registered) ? (
<StatusBadge>含已入库文件,执行后同步位置</StatusBadge>
<StatusBadge>含已整理文件,执行后同步位置</StatusBadge>
) : null}
</div>
{hnrBlocked ? (
@@ -790,7 +790,7 @@ export function DeleteDialog({
<DialogContent className="max-h-[92dvh] grid-rows-[auto_minmax(0,1fr)_auto] sm:max-w-2xl">
<DialogHeader>
<DialogTitle>{single ? `删除 ${single}?` : `删除 ${items.length} 项?`}</DialogTitle>
<DialogDescription>已检查做种任务、已入库文件和路径绑定。下载器里的任务不受影响。</DialogDescription>
<DialogDescription>已检查做种任务、已整理文件和路径绑定。下载器里的任务不受影响。</DialogDescription>
</DialogHeader>
<div className="min-h-0 space-y-3 overflow-y-auto">
{check.isPending ? (
@@ -837,7 +837,7 @@ export function DeleteDialog({
</p>
))}
{c.assets.length ? (
<p>已入库文件 {c.assets.length} 个(删除后从媒体库移除,已满足的收录版本退回待处理)</p>
<p>已整理文件 {c.assets.length} 个(删除后从媒体库移除,已满足的收录版本退回待处理)</p>
) : null}
{c.bindings.map((bd) => (
<p key={bd.bindingId}>
@@ -869,7 +869,7 @@ export function DeleteDialog({
<span>
解除路径绑定
<span className="block text-meta text-muted-foreground">
绑定标为已替代,该作品下次入库重新选目录。
绑定标为已替代,该作品下次整理重新选目录。
</span>
</span>
</label>
@@ -1139,7 +1139,7 @@ export function ScrapeDialog({ items, root, onClose }: { items: FileEntry[]; roo
<DialogHeader>
<DialogTitle>刮削{single ? ` · ${single.name}` : ` · ${items.length} 项`}</DialogTitle>
<DialogDescription>
按路径识别作品,在所在目录写 NFO 和图片,Emby、Jellyfin、Kodi 直接读取。没入库的也能刮削。
按路径识别作品,在所在目录写 NFO 和图片,Emby、Jellyfin、Kodi 直接读取。没整理的也能刮削。
</DialogDescription>
</DialogHeader>
{!started ? (
@@ -110,7 +110,7 @@ export function FileDetail({
value:
e.type === "dir"
? e.dirSummary
? `${e.dirSummary.videoCount}${e.dirSummary.partial ? "+" : ""} 个视频 · ${e.dirSummary.registeredCount} 个已入库`
? `${e.dirSummary.videoCount}${e.dirSummary.partial ? "+" : ""} 个视频 · ${e.dirSummary.registeredCount} 个已整理`
: "—"
: formatBytes(e.size, 2),
},
@@ -135,7 +135,7 @@ export function FileDetail({
</DetailSection>
{e.asset ? (
<DetailSection title="已入库文件">
<DetailSection title="已整理文件">
<KvList
items={[
{
+3 -3
View File
@@ -94,7 +94,7 @@ const base = {
describe("状态与禁用原因", () => {
it("状态徽标带文字,H&R 未满足为 warning,逃逸为 destructive", () => {
expect(entryStatusView({ ...base, status: "registered" })?.label).toBe("已入库");
expect(entryStatusView({ ...base, status: "registered" })?.label).toBe("已整理");
expect(
entryStatusView({
...base,
@@ -125,7 +125,7 @@ describe("状态与禁用原因", () => {
expect(opDisabledReason("move", seeding, lib)).toMatch(/H&R/);
expect(opDisabledReason("move", base, { kind: "library", writable: false })).toMatch(/只读/);
expect(opDisabledReason("rename", base, { kind: "download", writable: false })).toBeNull();
expect(opDisabledReason("scrape", base, lib)).toMatch(/未登记/);
expect(opDisabledReason("scrape", base, lib)).toMatch(/整理后才能/);
expect(opDisabledReason("writeNfo", base, lib)).toBeNull();
expect(opDisabledReason("writeNfo", base, { kind: "library", writable: false })).toMatch(/只读/);
expect(opDisabledReason("copyPath", { ...base, escape: true }, lib)).toBeNull();
@@ -272,7 +272,7 @@ describe("v2 菜单与副行", () => {
type: "dir" as const,
dirSummary: { videoCount: 13, registeredCount: 0, partial: false },
})?.text,
).toBe("13 个视频 · 未入库");
).toBe("13 个视频 · 未整理");
// 没有 parsedHint(服务端解析不出标题)时不显示副行,前端不自己解析
expect(entrySubline({ ...video, parsedHint: null })).toBeNull();
});
+6 -6
View File
@@ -26,7 +26,7 @@ export function entryStatusView(e: Pick<FileEntry, "status" | "download" | "esca
tone: "neutral",
};
case "registered":
return { label: "已入库", tone: "success" };
return { label: "已整理", tone: "success" };
case "seeding":
return e.download?.hnr === "unsatisfied"
? { label: "做种中·H&R 未满足", tone: "warning", hint: "H&R 满足前不能改名、删除或移动" }
@@ -42,7 +42,7 @@ export const FILTER_CHIPS = [
{ value: "", label: "全部" },
{ value: "video", label: "视频" },
{ value: "unidentified", label: "未识别" },
{ value: "registered", label: "已入库" },
{ value: "registered", label: "已整理" },
{ value: "downloading", label: "下载中" },
{ value: "temp", label: "未完成下载" },
] as const;
@@ -98,7 +98,7 @@ export function opDisabledReason(op: FileOp, e: FileEntry, root: Pick<FileRoot,
if (e.type === "file" && e.fileKind !== "video") return "只有视频或目录能整理";
return null;
case "scrape":
return e.asset ? null : "未登记,先整理入库";
return e.asset ? null : "整理后才能刷新元数据";
case "writeNfo":
if (root.kind === "library" && !root.writable) return "只读目录,不写 NFO";
if (e.status === "temp") return "未完成的文件不能刮削";
@@ -314,7 +314,7 @@ export function deleteBlockText(b: string | null): { text: string; soft: boolean
case "is_root":
return { text: "不能删除登记目录本身", soft: false };
case "needs_retire":
return { text: "含已入库文件,勾选「同时从媒体库移除」后才能删除", soft: true };
return { text: "含已整理文件,勾选「同时从媒体库移除」后才能删除", soft: true };
case "needs_unbind":
return { text: "是作品或季的绑定目录,勾选「解除路径绑定」后才能删除", soft: true };
case "root_unavailable":
@@ -431,7 +431,7 @@ export function menuModel(list: FileEntry[], root: Pick<FileRoot, "kind" | "writ
/**
* 行内 hover / 键盘聚焦时浮出的常用操作(桌面,§5.1;照 MP 的悬停图标,但只露 2–3 个,其余留在 ⋯ 菜单):
* 已入库 → 打开作品、刷新元数据、重命名;未入库视频 / 目录 → 整理、识别、刮削;
* 已整理 → 打开作品、刷新元数据、重命名;未整理视频 / 目录 → 整理、识别、刮削;
* 其余能识别的(临时文件里的视频等)→ 识别、重命名、移动到;别的文件 → 重命名、移动到。
*/
export function hoverOps(e: FileEntry): MenuKey[] {
@@ -472,7 +472,7 @@ export function entrySubline(e: FileEntry): { text: string; tone: "link" | "hint
const s = e.dirSummary;
if (!s.videoCount && !s.registeredCount) return null;
return {
text: `${s.videoCount}${s.partial ? "+" : ""} 个视频${s.registeredCount ? ` · ${s.registeredCount} 个已入库` : s.videoCount ? " · 未入库" : ""}`,
text: `${s.videoCount}${s.partial ? "+" : ""} 个视频${s.registeredCount ? ` · ${s.registeredCount} 个已整理` : s.videoCount ? " · 未整理" : ""}`,
tone: "meta",
};
}
@@ -45,9 +45,9 @@ export function seasonLine(s: Pick<EpisodeStructureSeason, "airDate" | "episodeC
return [s.airDate ? s.airDate.slice(0, 4) : null, `${s.episodeCount} 集`].filter(Boolean).join(" · ");
}
/** 已入库 x/y;一集都没有时返回 null(只列非零项) */
/** 已整理 x/y;一集都没有时返回 null(只列非零项) */
export function inLibraryText(s: Pick<EpisodeStructureSeason, "inLibrary" | "episodeCount">): string | null {
return s.inLibrary > 0 ? `已入库 ${s.inLibrary}/${s.episodeCount}` : null;
return s.inLibrary > 0 ? `已整理 ${s.inLibrary}/${s.episodeCount}` : null;
}
const pad = (n: number) => String(n).padStart(2, "0");
@@ -63,5 +63,5 @@ export function originText(
/** 切换确认的说明:改的是作品设置,影响季表、集数映射、订阅与刮削 */
export function switchDescription(target: string): string {
return `作品改按「${target}」分季:季与集按 TMDB 原季集迁到新位置,已入库文件跟着走;集数映射、订阅和刮削都按新季集,已绑定的资源会重新换算。之后可以再切回来。`;
return `作品改按「${target}」分季:季与集按 TMDB 原季集迁到新位置,已整理文件跟着走;集数映射、订阅和刮削都按新季集,已绑定的资源会重新换算。之后可以再切回来。`;
}
@@ -110,7 +110,7 @@ describe("季集结构:纯函数", () => {
it("季卡片文案与原季集对照", () => {
expect(seasonLine({ airDate: "2024-07-07", episodeCount: 13 })).toBe("2024 · 13 集");
expect(inLibraryText({ inLibrary: 0, episodeCount: 13 })).toBeNull();
expect(inLibraryText({ inLibrary: 3, episodeCount: 13 })).toBe("已入库 3/13");
expect(inLibraryText({ inLibrary: 3, episodeCount: 13 })).toBe("已整理 3/13");
expect(originText(2, { episodeNumber: 2, tmdbSeason: 1, tmdbEpisode: 14 })).toBe("原 S01E14");
expect(originText(1, { episodeNumber: 2, tmdbSeason: 1, tmdbEpisode: 2 })).toBeNull();
});
@@ -131,14 +131,14 @@ describe("季集结构:组件", () => {
expect(html).toContain('aria-label="当前"');
});
it("季卡片:季名、首播年 · 集数、已入库 x/y,选中的高亮", () => {
it("季卡片:季名、首播年 · 集数、已整理 x/y,选中的高亮", () => {
const html = renderToStaticMarkup(
createElement(SeasonCardGrid, { seasons: structure.seasons, selected: 2, onPick: () => {} }),
);
expect(html).toContain("第一部");
expect(html).toContain("2024 · 12 集");
expect(html).toContain("已入库 1/13");
expect(html).not.toContain("已入库 0/12");
expect(html).toContain("已整理 1/13");
expect(html).not.toContain("已整理 0/12");
expect(html).toMatch(/aria-pressed="true"[^>]*class="[^"]*pill-selected/);
});
@@ -141,7 +141,7 @@ export function StructurePreview({ seasons }: { seasons: EpisodeStructureSeason[
overview: null,
summary: (
<StatusBadge tone={s.episodeCount && s.inLibrary >= s.episodeCount ? "success" : "neutral"}>
已入库 {s.inLibrary}/{s.episodeCount}
已整理 {s.inLibrary}/{s.episodeCount}
</StatusBadge>
),
}}
@@ -171,7 +171,7 @@ export function StructurePreview({ seasons }: { seasons: EpisodeStructureSeason[
runtime: null,
stillPath: null,
voteAverage: null,
status: e.inLibrary ? { label: "已入库", tone: "success" } : { label: "未入库", tone: "neutral" },
status: e.inLibrary ? { label: "已整理", tone: "success" } : { label: "未整理", tone: "neutral" },
extra: originText(s.seasonNumber, e) ? (
<p className="mt-0.5 font-mono text-meta text-muted-foreground">
{originText(s.seasonNumber, e)}
@@ -228,7 +228,7 @@ export function EpisodeStructureSwitcher({ entityId, children }: { entityId: str
<div className="space-y-3">
<div className="flex flex-wrap items-center justify-between gap-2 rounded-xl border border-dashed border-border px-3 py-2">
<p className="text-caption text-muted-foreground">
预览「{target?.label ?? DEFAULT_STRUCTURE_LABEL}」的分季,已入库按 TMDB 原季集对照。
预览「{target?.label ?? DEFAULT_STRUCTURE_LABEL}」的分季,已整理按 TMDB 原季集对照。
</p>
<Button size="sm" disabled={setGroup.isPending} onClick={() => setConfirm(true)}>
改用这套结构
@@ -240,7 +240,7 @@ export function ExternalTab() {
empty={{
icon: Fingerprint,
title: "没有外部身份",
description: "没有外部编号的作品也能入库。",
description: "没有外部编号的作品也能整理。",
}}
hasMore={list.hasNextPage}
onLoadMore={() => list.fetchNextPage()}
+5 -5
View File
@@ -106,8 +106,8 @@ export const DOWNLOAD_STATUS: Record<DownloadStatus, ToneLabel> = {
paused: { label: "已暂停", tone: "warning" },
stalled: { label: "无进展", tone: "warning" },
completed: { label: "下载完成", tone: "neutral" },
importing: { label: "入库中", tone: "neutral" },
imported: { label: "已入库", tone: "success" },
importing: { label: "整理中", tone: "neutral" },
imported: { label: "已整理", tone: "success" },
failed: { label: "失败", tone: "destructive" },
removed: { label: "已移除", tone: "neutral" },
missing: { label: "下载器中缺失", tone: "warning" },
@@ -334,10 +334,10 @@ export const opts = <K extends string>(rec: Record<K, string | { label: string }
/** 收录版本 / 候选的原因串(如 `blocked:no_main_video`、`deferred:slot_busy;inflight:<id>`)→ 中文;不认识的原样显示 */
const SLOT_REASON: Record<string, string> = {
"blocked:no_main_video": "没有可入库的主视频",
"blocked:no_main_video": "没有可整理的主视频",
"deferred:slot_busy": "已有计划在途,等它结束再评估",
verified: "已核验入库",
verified_partial: "部分核验入库",
verified: "已核验整理",
verified_partial: "部分核验整理",
hard_conditions_met: "硬条件全部满足",
};
export function slotReasonText(raw: string): string {
@@ -41,7 +41,7 @@ const ROLE: Record<string, string> = {
episode_thumb: "剧照",
};
const SOURCE: Record<string, string> = { library: "已入库的作品", auto: "按路径识别", chosen: "手动指定" };
const SOURCE: Record<string, string> = { library: "已整理的作品", auto: "按路径识别", chosen: "手动指定" };
export function scrapeCounts(s: Pick<ScrapeRecord, "written" | "skipped" | "failed">): string {
return [
@@ -153,7 +153,7 @@ export function ScrapePanel({ detail }: { detail: LibraryDetail }) {
title="刮削"
description={
s?.finishedAt
? `上次刮削 ${formatDateTime(s.finishedAt)} · ${s.trigger === "auto" ? "入库后自动" : "手动"}`
? `上次刮削 ${formatDateTime(s.finishedAt)} · ${s.trigger === "auto" ? "整理后自动" : "手动"}`
: "NFO 和图片写进作品目录,Emby、Jellyfin、Kodi 直接读取。"
}
actions={
+3 -3
View File
@@ -109,7 +109,7 @@ function VersionDetail({ v, match }: { v: AssetVersion; match?: UnitMatch | unde
{ label: "字幕", value: v.subtitleLanguages.map(langLabel).join("、") || null },
{ label: "时长", value: duration(v.durationSec) },
{ label: "色深", value: v.bitDepth ? `${v.bitDepth} bit` : null },
{ label: "入库时间", value: formatDateTime(v.organizedAt) },
{ label: "整理时间", value: formatDateTime(v.organizedAt) },
{
label: "下载任务",
value: v.downloadTaskId ? (
@@ -192,7 +192,7 @@ export function VersionList({
<PaginatedList
rows={versions}
as="ul"
label="入库文件版本"
label="已整理文件版本"
resetKey={match?.assetId ?? "all"}
compact={compact}
className="space-y-0.5"
@@ -296,7 +296,7 @@ export function MovieVersionsPanel({ entityId }: { entityId: string }) {
);
const state = match?.state === "satisfied" ? `符合「${target?.name}」` : gapLine(match);
return (
<Panel title="入库版本" description={[summaryText(data.summary), state].filter(Boolean).join(" · ")}>
<Panel title="已整理版本" description={[summaryText(data.summary), state].filter(Boolean).join(" · ")}>
<div className="-mx-2">
<VersionList versions={versions} match={match} />
</div>
@@ -431,7 +431,7 @@ export const EXCLUDE_REASON: Record<string, string> = {
below_min_size: "低于最小体积",
sample: "样片",
too_small: "体积过小",
not_imported_kind: "不入库的文件类型",
not_imported_kind: "不整理的文件类型",
orphan_sidecar: "孤立附属文件",
no_episode: "解析不出集号",
special_extra: "特典/花絮",
@@ -488,7 +488,7 @@ export function OrganizeWizard({ source, onClose }: { source: OrganizeSource; on
<p className="text-meta text-muted-foreground">{source.scanSummary}</p>
) : null}
<FormSelect
label={inPlace ? "入库位置" : "来源配置"}
label={inPlace ? "整理位置" : "来源配置"}
value={inPlace ? "none" : (profile?.id ?? (ambiguous ? "choose" : "none"))}
onChange={pickProfile}
choices={[
@@ -631,9 +631,9 @@ export function OrganizeWizard({ source, onClose }: { source: OrganizeSource; on
})
}
choices={[
{ value: "in_place", label: "原地入库 · 保留位置与名称", disabled: !canInPlace },
{ value: "in_place", label: "原地整理 · 保留位置与名称", disabled: !canInPlace },
{ value: "copy", label: "复制 · 保留源文件" },
{ value: "move", label: "移动 · 入库后删除源文件", disabled: hnrCount > 0 },
{ value: "move", label: "移动 · 整理后删除源文件", disabled: hnrCount > 0 },
]}
/>
{!inPlace ? (
@@ -109,7 +109,7 @@ export function StepOptions({
onSelect={() => set({ action: "move" })}
disabled={hnrCount ? `${hnrCount} 项做种中且 H&R 未满足,不能移动` : null}
title="移动"
hint="入库后删源;跨卷时按复制 → 核验 → 删源"
hint="整理后删源;跨卷时按复制 → 核验 → 删源"
/>
</div>
</Section>
@@ -207,7 +207,7 @@ export function StepOptions({
</div>
</Section>
<Section title="入库后">
<Section title="整理后">
<div className="flex items-start justify-between gap-3 rounded-xl border border-border px-3 py-2">
<span className="text-body">
刷新元数据
@@ -216,7 +216,7 @@ export function StepOptions({
<Switch
checked={options.refreshMetadata}
onCheckedChange={(v) => set({ refreshMetadata: v })}
aria-label="入库后刷新元数据"
aria-label="整理后刷新元数据"
/>
</div>
<p className="text-meta text-muted-foreground">
@@ -112,7 +112,7 @@ function FileLine({ f, roots, nested }: { f: GroupFile; roots: FileRoot[]; neste
) : f.target === f.source ? (
<StatusBadge>原地登记</StatusBadge>
) : !f.target ? (
<StatusBadge>不入库</StatusBadge>
<StatusBadge>不整理</StatusBadge>
) : null;
return (
<li className={cn("min-w-0 py-1", nested && "pl-5")}>
@@ -138,7 +138,7 @@ function Summary({
{files.length ? (
<p>
{files.join("、")}({formatBytes(plan.bytes, 1)})会移入回收站,可以在回收站还原。
{plan.assets ? `其中 ${plan.assets} 个是已入库文件,会从媒体库移除。` : ""}
{plan.assets ? `其中 ${plan.assets} 个是已整理文件,会从媒体库移除。` : ""}
</p>
) : (
<p>{withFiles ? "没有找到还在的文件,只删记录。" : "只删记录,文件不动。"}</p>
@@ -26,7 +26,7 @@ const decision = (over: Partial<NonNullable<RecordAttention["decision"]>> = {})
{ key: "candidate_0", label: "Foo (2019)", input: { candidate: { mediaEntityId: R } } },
{ key: "select_entity", label: "选择作品(本地)", input: { requires: "mediaEntityId" } },
{ key: "select_tmdb", label: "选择作品(TMDB)", input: { requires: "tmdb" } },
{ key: "provisional", label: "作为临时作品入库", input: {} },
{ key: "provisional", label: "作为临时作品整理", input: {} },
{ key: "dismiss", label: "忽略" },
],
agentRunId: null,
@@ -54,7 +54,7 @@ describe("整理记录行内处理", () => {
for (const t of ["指定作品", "让智能体识别", "重新整理", "忽略", "Foo (2019)"]) expect(out).toContain(t);
// 「选择作品(本地 / TMDB)」并进「指定作品」,不单列;临时作品不在行内
expect(out).not.toContain("选择作品(本地)");
expect(out).not.toContain("作为临时作品入库");
expect(out).not.toContain("作为临时作品整理");
});
it("智能体识别中:显示进度与会话入口,不再给指定 / 识别按钮", () => {
@@ -89,7 +89,7 @@ describe("整理记录列表:整理方式、分类、分组", () => {
"移动",
"复制",
"硬链接",
"原地入库",
"原地整理",
]);
});
@@ -88,7 +88,7 @@ describe("库内状态文案", () => {
it("未收录 / 已建档 / 已收录 / 正在下载;收录版本满足与缺失", () => {
expect(inventoryBadge(inv({})).label).toBe("不在媒体库");
expect(inventoryBadge(inv({ state: "tracked" })).label).toBe("在媒体库 · 未下载");
expect(inventoryBadge(inv({ state: "owned", versionCount: 2 })).label).toBe("已入库 · 2 个版本");
expect(inventoryBadge(inv({ state: "owned", versionCount: 2 })).label).toBe("已整理 · 2 个版本");
expect(inventoryBadge(inv({ state: "downloading", downloadingCount: 1 })).label).toBe("正在下载 1 个");
expect(
slotLine(inv({ state: "owned", slotTotal: 2, slotSatisfied: 1, missing: [{ slotId: ID, name: "2160p" }] })),
+1 -1
View File
@@ -37,7 +37,7 @@ export function inventoryBadge(inv: WorkInventory): { label: string; tone: Statu
case "downloading":
return { label: `正在下载 ${inv.downloadingCount} 个`, tone: "warning" };
case "owned":
return { label: `已入库 · ${inv.versionCount} 个版本`, tone: "success" };
return { label: `已整理 · ${inv.versionCount} 个版本`, tone: "success" };
default:
return { label: "在媒体库 · 未下载", tone: "neutral" };
}
@@ -186,7 +186,7 @@ export function SubscriptionMenu({
open={confirmReset}
onOpenChange={setConfirmReset}
title="重置这个订阅的进度?"
description="取消还没提交、或下载任务已不在的计划,清掉延后记录,然后重新评估。仍在下载的任务、已入库文件和总集数保留。"
description="取消还没提交、或下载任务已不在的计划,清掉延后记录,然后重新评估。仍在下载的任务、已整理文件和总集数保留。"
confirmLabel="重置"
onConfirm={() =>
ops.reset(sub.id).catch((e) => {
@@ -216,7 +216,7 @@ export function DeleteSubscriptionsDialog({
open={open}
onOpenChange={onOpenChange}
title={ids.length > 1 ? `删除 ${ids.length} 个订阅?` : "删除这个订阅?"}
description="不再搜索和追更。已入库文件、收录版本和下载记录保留,可在订阅历史里重新订阅。"
description="不再搜索和追更。已整理文件、收录版本和下载记录保留,可在订阅历史里重新订阅。"
confirmLabel="删除"
destructive
onConfirm={() =>
@@ -65,7 +65,7 @@ export function slotBadge(s: SubscriptionSlotSummary): ToneLabel {
if (s.inflight > 0) return { label: `在途 ${s.inflight}`, tone: "neutral" };
if (s.missing > 0) return { label: `缺 ${s.missing}`, tone: "destructive" };
if (s.total !== null && s.total > 0 && s.have >= s.total) return { label: "已齐", tone: "success" };
return { label: s.have > 0 ? `已入库 ${s.have}` : "未开始", tone: "neutral" };
return { label: s.have > 0 ? `已整理 ${s.have}` : "未开始", tone: "neutral" };
}
/** 名称列表折叠:≤max 个名 + "+N";空 = emptyLabel */
@@ -121,7 +121,7 @@ export function cellKind(c: CoverageCell): CellKind {
}
export const CELL_LABEL: Record<CellKind, string> = {
have: "已入库",
have: "已整理",
upgradable: "可升级",
inflight: "在途",
deferred: "延后",
@@ -429,9 +429,9 @@ export function draftToParams(
// ── 日历(设计 §4)──
export const CALENDAR_STATE_LABEL: Record<CalendarEntryState, string> = {
have: "已入库",
partial: "部分入库",
missing: "未入库",
have: "已整理",
partial: "部分整理",
missing: "未整理",
upcoming: "未播出",
};
@@ -57,7 +57,7 @@ export function ProgressBlock({ x, className }: { x: Pick<Item, "progress" | "mo
"bg-[repeating-linear-gradient(90deg,var(--color-foreground)_0_4px,transparent_4px_8px)] opacity-20",
)}
role="img"
aria-label={`已入库 ${x.progress.have},在途 ${x.progress.inflight},总 ${x.progress.total ?? "未知"}`}
aria-label={`已整理 ${x.progress.have},在途 ${x.progress.inflight},总 ${x.progress.total ?? "未知"}`}
>
{unknown ? null : (
<>
@@ -110,7 +110,7 @@ export function SlotPanel({
disabled={setOrder.isPending}
renderExtra={(r, i) => (
<span className="flex shrink-0 flex-wrap items-center gap-x-3 gap-y-1 text-meta text-muted-foreground tabular-nums max-sm:w-full max-sm:pl-9">
<span>已入库 {r.summary.have}</span>
<span>已整理 {r.summary.have}</span>
<span className={r.summary.missing ? "text-destructive" : undefined}>缺 {r.summary.missing}</span>
<span>在途 {r.summary.inflight}</span>
{i === 0 ? null : <span>{r.autoFill ? "自动补全" : "只登记缺失"}</span>}
@@ -694,7 +694,7 @@ export function SubscriptionFormDialog({
<FormField
label="洗版"
htmlFor={f("upgrade")}
hint="已入库的低于规则组最高档时继续找更好的,拿到最高档即完成"
hint="已整理的低于规则组最高档时继续找更好的,拿到最高档即完成"
>
<Pick
id={f("upgrade")}
@@ -167,7 +167,7 @@ export function SubtitleDialog({
) : targets.error ? (
<ErrorState error={targets.error} onRetry={() => void targets.refetch()} />
) : !list.length ? (
<EmptyState title="媒体库里还没有这部作品的视频" description="整理入库后才能补字幕。" />
<EmptyState title="媒体库里还没有这部作品的视频" description="整理后才能补字幕。" />
) : (
<div className="min-w-0 space-y-4">
<Tabs value={tab} onValueChange={(v) => setTab(v as "upload" | "online")}>
+2 -2
View File
@@ -30,8 +30,8 @@ const DOWNLOAD: Record<string, Labeled> = {
paused: { label: "已暂停", tone: "warning" },
stalled: { label: "无进展", tone: "warning" },
completed: { label: "下载完成", tone: "success" },
importing: { label: "入库中", tone: "neutral" },
imported: { label: "已入库", tone: "success" },
importing: { label: "整理中", tone: "neutral" },
imported: { label: "已整理", tone: "success" },
failed: { label: "失败", tone: "destructive" },
removed: { label: "已移除", tone: "neutral" },
missing: { label: "丢失", tone: "destructive" },
+1 -1
View File
@@ -717,7 +717,7 @@ export default function FilesPage() {
key: "scrape",
label: "刷新元数据",
icon: RefreshCw,
disabled: selected.every((e) => e.asset) ? null : "只对已入库的文件",
disabled: selected.every((e) => e.asset) ? null : "只对已整理的文件",
onSelect: () => act("scrape", selected),
});
items.push({
+6 -6
View File
@@ -141,7 +141,7 @@ function PathsPanel({ detail }: { detail: LibraryDetail }) {
<EmptyState
plain
icon={FolderTree}
title="还没有入库文件"
title="还没有整理过的文件"
description="收录版本满足后,文件位置显示在这里。"
className="py-8"
/>
@@ -608,7 +608,7 @@ function UnitsPanel({ entity, detail }: { entity: ReturnType<typeof useEntityDet
hideBelow: "sm",
cell: (u) =>
covered.has(u.id) ? (
<StatusBadge tone="success">已入库</StatusBadge>
<StatusBadge tone="success">已整理</StatusBadge>
) : (
<span className="text-muted-foreground">—</span>
),
@@ -711,10 +711,10 @@ function EpisodesPanel({ detail, units }: { detail: LibraryDetail; units: Conten
if (hit) return hit;
const season = seasonUnitOf(u.seasonNumber!);
if (covered.has(u.id) || (season && covered.has(season.id)))
return { label: "已入库", tone: "success", have: true };
return { label: "已整理", tone: "success", have: true };
const air = finalOf(u, "airDate")?.value ?? u.airDate;
if (typeof air === "string" && air > today()) return { label: "未播出", tone: "neutral", have: false };
return { label: sub ? "缺失" : "未入库", tone: sub ? "destructive" : "neutral", have: false };
return { label: sub ? "缺失" : "未整理", tone: sub ? "destructive" : "neutral", have: false };
};
const haveIn = (n: number) => episodes.filter((u) => u.seasonNumber === n && statusOf(u)?.have).length;
const initial =
@@ -747,7 +747,7 @@ function EpisodesPanel({ detail, units }: { detail: LibraryDetail; units: Conten
description={
sub
? "库内状态按订阅的覆盖矩阵;标题与简介按语言回退链取值"
: "没有订阅时只看是否已入库;标题与简介按语言回退链取值"
: "没有订阅时只看是否已整理;标题与简介按语言回退链取值"
}
actions={
<Button variant="outline" size="sm" disabled={refresh.isPending} onClick={refreshFull}>
@@ -817,7 +817,7 @@ function EpisodesPanel({ detail, units }: { detail: LibraryDetail; units: Conten
})(),
summary: (
<StatusBadge tone={total && have >= total ? "success" : "neutral"}>
已入库 {have}/{total ?? "?"}
已整理 {have}/{total ?? "?"}
</StatusBadge>
),
}}
+1 -1
View File
@@ -166,7 +166,7 @@ function WorksView() {
},
{
key: "org",
header: "最近入库",
header: "最近整理",
hideBelow: "lg",
cell: (r) => <span className="text-meta text-muted-foreground">{relTime(r.lastOrganizedAt)}</span>,
},
@@ -208,7 +208,7 @@ function InventoryPanel({ d }: { d: TmdbMediaDetail }) {
) : (
<>
<p>
{inv.versionCount} 个版本已入库
{inv.versionCount} 个版本已整理
{inv.downloadingCount ? ` · ${inv.downloadingCount} 个正在下载` : ""}
{inv.subscribed ? " · 已订阅" : ""}
</p>
@@ -284,7 +284,7 @@ function SeasonsPanel({ d }: { d: TmdbMediaDetail }) {
});
const seasons = [...d.seasons].sort((a, b) => (a.seasonNumber || 999) - (b.seasonNumber || 999));
return (
<Panel title="季与集" description={d.localEntityId ? "本地一列是已入库集数" : "不在媒体库"}>
<Panel title="季与集" description={d.localEntityId ? "本地一列是已整理集数" : "不在媒体库"}>
<PaginatedList rows={seasons} label="季列表" resetKey={d.externalId} className="space-y-2">
{(s) => (
<SeasonSection
@@ -299,7 +299,7 @@ function SeasonsPanel({ d }: { d: TmdbMediaDetail }) {
summary:
d.localEntityId && s.localHave !== null ? (
<StatusBadge tone={s.localHave >= s.episodeCount && s.episodeCount > 0 ? "success" : "neutral"}>
已入库 {s.localHave}/{s.episodeCount}
已整理 {s.localHave}/{s.episodeCount}
</StatusBadge>
) : undefined,
}}
@@ -48,7 +48,7 @@ const TOTAL_SOURCE: Record<string, string> = {
tmdb: "TMDB",
injected: "注入",
manual: "手动",
covered_floor: "按已入库",
covered_floor: "按已整理",
previous: "沿用",
};
+1 -1
View File
@@ -253,7 +253,7 @@ export default function LoginPage() {
<section className="hidden lg:block">
<p className="text-label font-semibold tracking-[0.18em] text-muted-foreground uppercase">媒体自动化</p>
<h1 className="mt-3 max-w-xl font-display text-display-sm leading-tight font-semibold text-foreground">
订阅、下载、整理入库
订阅、下载、整理
</h1>
<p className="mt-4 max-w-lg text-lead text-muted-foreground">
每一步都有记录。拿不准的事项进待处理,等你决定。