feat(下载对话框): 学 MoviePilot 重做手动下载对话框
- 标题区:海报、作品名 (年份)、站点;资源行显示原始标题、促销、H&R、做种 / 下载、大小、发布时间 - 下载器与保存目录并排下拉,缺省「自动」,下方写明「将保存到」的实际路径;保留「自定义路径」 - 作品身份折叠区:类型 + TMDB 编号(可搜索作品),未识别时默认展开,指定后随下载绑定 - 已有版本先显示一行摘要,选一次性洗版才展开勾选列表 - 搜索、工作台、原站视图三处调用方补传资源字段 Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Cxwgnn2pbzQ7rXv3rq7fei
This commit is contained in:
1 parent
4bb19433ad
commit
6e645bce8f
7 files changed
+1012
-117
No files matched your search
@@ -0,0 +1,274 @@
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import type { AssetVersion, DownloadSavePaths, DownloadSaveTarget } from "@umt/contracts";
|
||||
import { createElement, type ReactNode } from "react";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { ExistingVersionsSection, IdentitySection, SaveTargetFields } from "./manual-download-dialog.tsx";
|
||||
import {
|
||||
buildManualOptions,
|
||||
CUSTOM_DIR,
|
||||
kindHintOf,
|
||||
resolveSavePath,
|
||||
savePathText,
|
||||
targetOf,
|
||||
versionSummaryText,
|
||||
} from "./manual-download-model.ts";
|
||||
|
||||
// 手动下载对话框:保存目录解析与显示、未识别时身份区展开并随提交带上身份、已有版本摘要与洗版展开。
|
||||
|
||||
const A = "11111111-1111-4111-8111-111111111111";
|
||||
const B = "22222222-2222-4222-8222-222222222222";
|
||||
const E = "33333333-3333-4333-8333-333333333333";
|
||||
|
||||
const paths: DownloadSavePaths = {
|
||||
autoInstanceId: B,
|
||||
instances: [
|
||||
{
|
||||
instanceId: A,
|
||||
name: "qb-a",
|
||||
isDefault: false,
|
||||
dirs: [{ path: "/downloads", hostPath: "/downloads", isDefault: true }],
|
||||
},
|
||||
{
|
||||
instanceId: B,
|
||||
name: "qb-main",
|
||||
isDefault: true,
|
||||
dirs: [
|
||||
{ path: "/data/downloads", hostPath: "/mnt/dl", isDefault: true },
|
||||
{ path: "/data/tv", hostPath: "/mnt/tv", isDefault: false },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const render = (node: ReactNode) =>
|
||||
renderToStaticMarkup(createElement(QueryClientProvider, { client: new QueryClient() }, node));
|
||||
|
||||
const base = {
|
||||
entityId: undefined,
|
||||
currentTmdb: null,
|
||||
identity: null,
|
||||
instanceId: "",
|
||||
target: targetOf(paths, ""),
|
||||
dir: "",
|
||||
custom: "",
|
||||
allowUnrecognized: false,
|
||||
mode: "normal" as const,
|
||||
selected: [],
|
||||
};
|
||||
|
||||
describe("保存目录", () => {
|
||||
it("自动:解析出自动下载器的默认保存目录;本机路径不同时括注", () => {
|
||||
const t = targetOf(paths, "");
|
||||
expect(t?.instanceId).toBe(B);
|
||||
const r = resolveSavePath(t, "", "");
|
||||
expect(r).toEqual({ path: "/data/downloads", hostPath: "/mnt/dl" });
|
||||
expect(savePathText(r!)).toBe("/data/downloads(本机 /mnt/dl)");
|
||||
expect(resolveSavePath(targetOf(paths, A), "", "")).toEqual({ path: "/downloads", hostPath: "/downloads" });
|
||||
expect(savePathText({ path: "/downloads", hostPath: "/downloads" })).toBe("/downloads");
|
||||
});
|
||||
|
||||
it("选目录 / 自定义路径;自动下载器 + 指定目录时钉住解析出的下载器", () => {
|
||||
const t = targetOf(paths, "");
|
||||
expect(resolveSavePath(t, "/data/tv", "")).toEqual({ path: "/data/tv", hostPath: "/mnt/tv" });
|
||||
expect(resolveSavePath(t, CUSTOM_DIR, " ")).toBeNull();
|
||||
expect(resolveSavePath(t, CUSTOM_DIR, " /x/y ")).toEqual({ path: "/x/y", hostPath: null });
|
||||
expect(buildManualOptions({ ...base, dir: "/data/tv" })).toMatchObject({
|
||||
downloaderInstanceId: B,
|
||||
savePath: "/data/tv",
|
||||
});
|
||||
const auto = buildManualOptions(base);
|
||||
expect(auto).not.toHaveProperty("savePath");
|
||||
expect(auto).not.toHaveProperty("downloaderInstanceId");
|
||||
const custom = buildManualOptions({ ...base, dir: CUSTOM_DIR, custom: " /x/y " });
|
||||
expect(custom).toMatchObject({ savePath: "/x/y" });
|
||||
expect(custom).not.toHaveProperty("downloaderInstanceId");
|
||||
});
|
||||
|
||||
it("界面:并排两个下拉,下方写明将保存到哪;自定义时展开输入框", () => {
|
||||
const props = {
|
||||
formId: "f",
|
||||
instances: paths.instances,
|
||||
autoInstanceId: B,
|
||||
instanceId: "",
|
||||
onInstanceChange: () => {},
|
||||
target: targetOf(paths, "") as DownloadSaveTarget,
|
||||
dir: "",
|
||||
onDirChange: () => {},
|
||||
custom: "",
|
||||
onCustomChange: () => {},
|
||||
};
|
||||
const html = render(createElement(SaveTargetFields, props));
|
||||
expect(html).toContain("下载器");
|
||||
expect(html).toContain("自动:qb-main");
|
||||
expect(html).toContain("将保存到:/data/downloads(本机 /mnt/dl)");
|
||||
expect(html).not.toContain("自定义路径</label>");
|
||||
const custom = render(createElement(SaveTargetFields, { ...props, dir: CUSTOM_DIR, custom: "/x" }));
|
||||
expect(custom).toContain("将保存到:/x");
|
||||
expect(custom).toContain('id="f-custom"');
|
||||
const none = render(
|
||||
createElement(SaveTargetFields, { ...props, instances: [], autoInstanceId: null, target: null }),
|
||||
);
|
||||
expect(none).toContain("没有可用的下载器");
|
||||
});
|
||||
});
|
||||
|
||||
describe("作品身份", () => {
|
||||
const identityProps = {
|
||||
formId: "f",
|
||||
onToggle: () => {},
|
||||
type: "movie" as const,
|
||||
onTypeChange: () => {},
|
||||
tmdbId: "",
|
||||
onTmdbIdChange: () => {},
|
||||
picked: null,
|
||||
onPick: () => {},
|
||||
onClearPick: () => {},
|
||||
changed: false,
|
||||
subject: "Spider-Man.2026.1080p",
|
||||
};
|
||||
|
||||
it("未识别:默认展开,显示类型与 TMDB 编号输入和提示", () => {
|
||||
const html = render(
|
||||
createElement(IdentitySection, { ...identityProps, open: true, current: null, unrecognized: true }),
|
||||
);
|
||||
expect(html).toContain('aria-expanded="true"');
|
||||
expect(html).toContain("未识别");
|
||||
expect(html).toContain("TMDB 编号");
|
||||
expect(html).toContain('id="f-tmdb"');
|
||||
expect(html).toContain("暂未识别到作品");
|
||||
});
|
||||
|
||||
it("已识别:折叠只显示当前作品", () => {
|
||||
const html = render(
|
||||
createElement(IdentitySection, {
|
||||
...identityProps,
|
||||
open: false,
|
||||
current: { title: "蜘蛛侠:崭新之日", year: 2026, kind: "movie", tmdbId: "969681" },
|
||||
unrecognized: false,
|
||||
}),
|
||||
);
|
||||
expect(html).toContain('aria-expanded="false"');
|
||||
expect(html).toContain("蜘蛛侠:崭新之日 (2026) · 电影 · TMDB 969681");
|
||||
expect(html).not.toContain('id="f-tmdb"');
|
||||
});
|
||||
|
||||
it("提交:未识别时指定 TMDB 身份就带上 identity,不再要求确认;本地作品给 mediaEntityId", () => {
|
||||
const tmdb = buildManualOptions({
|
||||
...base,
|
||||
allowUnrecognized: true,
|
||||
identity: { kind: "tmdb", type: "tv", id: "1399" },
|
||||
});
|
||||
expect(tmdb).toMatchObject({ identity: { external: { provider: "tmdb", entityType: "tv", externalId: "1399" } } });
|
||||
expect(tmdb).not.toHaveProperty("allowUnrecognized");
|
||||
expect(tmdb).not.toHaveProperty("entityId");
|
||||
const local = buildManualOptions({ ...base, identity: { kind: "entity", id: E, title: "x" } });
|
||||
expect(local.identity).toEqual({ mediaEntityId: E });
|
||||
// 没指定或编号不完整:照旧确认后下载
|
||||
expect(buildManualOptions({ ...base, allowUnrecognized: true })).toMatchObject({ allowUnrecognized: true });
|
||||
expect(buildManualOptions({ ...base, identity: { kind: "tmdb", type: "movie", id: "12a" } })).not.toHaveProperty(
|
||||
"identity",
|
||||
);
|
||||
});
|
||||
|
||||
it("已识别且身份没改:只带 entityId;改了身份就换成 identity 并退回正常入库", () => {
|
||||
const same = buildManualOptions({
|
||||
...base,
|
||||
entityId: E,
|
||||
currentTmdb: { type: "movie", id: "969681" },
|
||||
identity: { kind: "tmdb", type: "movie", id: "969681" },
|
||||
mode: "replace",
|
||||
selected: [A],
|
||||
});
|
||||
expect(same).toMatchObject({ entityId: E, importMode: "replace", replacementAssetIds: [A] });
|
||||
expect(same).not.toHaveProperty("identity");
|
||||
const other = buildManualOptions({
|
||||
...base,
|
||||
entityId: E,
|
||||
currentTmdb: { type: "movie", id: "969681" },
|
||||
identity: { kind: "tmdb", type: "movie", id: "1" },
|
||||
mode: "replace",
|
||||
selected: [A],
|
||||
});
|
||||
expect(other).toMatchObject({ importMode: "normal", replacementAssetIds: [] });
|
||||
expect(other).not.toHaveProperty("entityId");
|
||||
});
|
||||
|
||||
it("类型缺省按标题解析出的季集猜", () => {
|
||||
expect(kindHintOf({ season: 1 })).toBe("tv");
|
||||
expect(kindHintOf({ season: null, episodes: { start: 1, end: 2 } })).toBe("tv");
|
||||
expect(kindHintOf({ season: null, episodes: null })).toBe("movie");
|
||||
expect(kindHintOf(undefined)).toBe("movie");
|
||||
});
|
||||
});
|
||||
|
||||
describe("已有版本", () => {
|
||||
const version = (id: string, p: Partial<AssetVersion> = {}): AssetVersion =>
|
||||
({
|
||||
assetId: id,
|
||||
status: "active",
|
||||
resolution: "1080p",
|
||||
source: "WEB-DL",
|
||||
isEncode: true,
|
||||
videoCodec: "H.264",
|
||||
bitDepth: null,
|
||||
hdr: [],
|
||||
audio: [],
|
||||
subtitleLanguages: [],
|
||||
releaseGroup: null,
|
||||
edition: null,
|
||||
sizeBytes: 9_556_302_233,
|
||||
durationSec: null,
|
||||
fileName: `${id}.mkv`,
|
||||
relativePath: null,
|
||||
absolutePath: null,
|
||||
libraryRootId: null,
|
||||
locationStatus: null,
|
||||
organizedAt: "2026-10-01T00:00:00.000Z",
|
||||
downloadTaskId: null,
|
||||
...p,
|
||||
}) as AssetVersion;
|
||||
|
||||
it("摘要一行:规格 + 大小;多种规格只列前两种", () => {
|
||||
expect(
|
||||
versionSummaryText({
|
||||
versionCount: 1,
|
||||
sizeBytes: 9_556_302_233,
|
||||
specs: [{ resolution: "1080p", source: "WEB-DL", hdr: [], count: 1 }],
|
||||
}),
|
||||
).toBe("媒体库已有:1080p WEB-DL · 8.9 GB");
|
||||
expect(
|
||||
versionSummaryText({
|
||||
versionCount: 3,
|
||||
sizeBytes: 0,
|
||||
specs: [
|
||||
{ resolution: "2160p", source: "WEB-DL", hdr: ["DV"], count: 1 },
|
||||
{ resolution: "1080p", source: "BluRay", hdr: [], count: 1 },
|
||||
{ resolution: "720p", source: "HDTV", hdr: [], count: 1 },
|
||||
],
|
||||
}),
|
||||
).toBe("媒体库已有:2160p WEB-DL DV、1080p BluRay 等 3 种 · 3 个版本");
|
||||
expect(versionSummaryText({ versionCount: 0, sizeBytes: 0, specs: [] })).toBeNull();
|
||||
});
|
||||
|
||||
it("正常入库只显示摘要;选一次性洗版才展开勾选列表", () => {
|
||||
const props = {
|
||||
formId: "f",
|
||||
loading: false,
|
||||
error: false,
|
||||
summary: "媒体库已有:1080p WEB-DL · 8.9 GB",
|
||||
versions: [version(A), version(B)],
|
||||
onModeChange: () => {},
|
||||
selected: [A],
|
||||
onSelectedChange: () => {},
|
||||
};
|
||||
const normal = render(createElement(ExistingVersionsSection, { ...props, mode: "normal" }));
|
||||
expect(normal).toContain("媒体库已有:1080p WEB-DL · 8.9 GB");
|
||||
expect(normal).not.toContain('type="checkbox"');
|
||||
const replace = render(createElement(ExistingVersionsSection, { ...props, mode: "replace" }));
|
||||
expect(replace).toContain("选择要替换的已有文件");
|
||||
expect(replace.match(/type="checkbox"/g)).toHaveLength(2);
|
||||
expect(replace).toContain(`${A}.mkv`);
|
||||
expect(render(createElement(ExistingVersionsSection, { ...props, versions: [], mode: "normal" }))).toBe("");
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user