Files
UHD_Media_Tool/apps/web/app/components/downloads/manual-download-model.ts
T
rootandClaude Opus 5.5 6e645bce8f feat(下载对话框): 学 MoviePilot 重做手动下载对话框
- 标题区:海报、作品名 (年份)、站点;资源行显示原始标题、促销、H&R、做种 / 下载、大小、发布时间
- 下载器与保存目录并排下拉,缺省「自动」,下方写明「将保存到」的实际路径;保留「自定义路径」
- 作品身份折叠区:类型 + TMDB 编号(可搜索作品),未识别时默认展开,指定后随下载绑定
- 已有版本先显示一行摘要,选一次性洗版才展开勾选列表
- 搜索、工作台、原站视图三处调用方补传资源字段

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Cxwgnn2pbzQ7rXv3rq7fei
2026-10-06 12:10:03 +08:00

129 lines
5.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type {
AssetVersion,
DownloadSavePaths,
DownloadSaveTarget,
LibraryVersions,
ManualAcquisitionRequest,
VersionSummary,
} from "@umt/contracts";
import { formatBytes } from "~/lib/utils.ts";
// 手动下载对话框的纯逻辑:保存目录解析、已有版本摘要、提交参数。界面在 manual-download-dialog.tsx。
export type ManualDownloadOptions = Omit<ManualAcquisitionRequest, "sourceReleaseId">;
export type TmdbType = "movie" | "tv";
/** 保存目录下拉的取值:"" = 自动,CUSTOM_DIR = 自定义路径,其余是目录本身 */
export const CUSTOM_DIR = "__custom__";
/** 选中的下载器;没选时用「自动」会用的那个 */
export function targetOf(paths: DownloadSavePaths | undefined, instanceId: string): DownloadSaveTarget | null {
if (!paths) return null;
const id = instanceId || paths.autoInstanceId;
return paths.instances.find((t) => t.instanceId === id) ?? null;
}
/** 「将保存到」显示的路径:自动 = 下载器默认保存目录;自定义取输入框;没有可用下载器为 null */
export function resolveSavePath(
target: DownloadSaveTarget | null,
dir: string,
custom: string,
): { path: string; hostPath: string | null } | null {
if (dir === CUSTOM_DIR) {
const p = custom.trim();
return p ? { path: p, hostPath: null } : null;
}
if (!target) return null;
const hit = dir ? target.dirs.find((d) => d.path === dir) : target.dirs[0];
return hit ? { path: hit.path, hostPath: hit.hostPath } : null;
}
/** 本机路径与下载器路径不同时括注本机路径,方便对照文件管理 */
export function savePathText(r: { path: string; hostPath: string | null }): string {
return r.hostPath && r.hostPath !== r.path ? `${r.path}(本机 ${r.hostPath})` : r.path;
}
/** 已有版本去重展开(电影 / 每集 / 未对上集的) */
export function existingVersions(inv: LibraryVersions | undefined): AssetVersion[] {
const out = new Map<string, AssetVersion>();
for (const v of inv?.movie?.versions ?? []) out.set(v.assetId, v);
for (const season of inv?.seasons ?? [])
for (const episode of season.episodes) for (const v of episode.versions) out.set(v.assetId, v);
for (const v of inv?.unplaced ?? []) out.set(v.assetId, v);
return [...out.values()];
}
/** 一行摘要:「媒体库已有:1080p WEB-DL · 8.9 GB」;多种规格只列前两种 */
export function versionSummaryText(s: VersionSummary | undefined): string | null {
if (!s?.versionCount) return null;
const specs = s.specs.map((g) => [g.resolution, g.source, ...g.hdr].filter(Boolean).join(" ")).filter(Boolean);
const head = specs.slice(0, 2).join("、") + (specs.length > 2 ? ` 等 ${specs.length} 种` : "");
const parts = [
head,
s.versionCount > 1 ? `${s.versionCount} 个版本` : "",
s.sizeBytes ? formatBytes(s.sizeBytes, 1) : "",
];
return `媒体库已有:${parts.filter(Boolean).join(" · ")}`;
}
/** 下载前指定的作品:本地作品,或按类型 + TMDB 编号 */
export type IdentityChoice =
| { kind: "entity"; id: string; title: string }
| { kind: "tmdb"; type: TmdbType; id: string };
/** 作品当前的 TMDB 身份;和它相同就不算改动 */
export function identityChanged(
choice: IdentityChoice | null,
current: { entityId?: string | undefined; tmdb?: { type: TmdbType; id: string } | null },
): boolean {
if (!choice) return false;
if (choice.kind === "entity") return choice.id !== current.entityId;
if (!/^\d+$/.test(choice.id)) return false;
return !(current.tmdb && current.tmdb.type === choice.type && current.tmdb.id === choice.id);
}
export function buildManualOptions(s: {
entityId?: string | undefined;
currentTmdb?: { type: TmdbType; id: string } | null;
identity: IdentityChoice | null;
instanceId: string;
target: DownloadSaveTarget | null;
dir: string;
custom: string;
allowUnrecognized: boolean;
mode: "normal" | "replace";
selected: string[];
}): ManualDownloadOptions {
const changed = identityChanged(s.identity, { entityId: s.entityId, tmdb: s.currentTmdb ?? null });
const identity: ManualDownloadOptions["identity"] =
changed && s.identity
? s.identity.kind === "entity"
? { mediaEntityId: s.identity.id }
: { external: { provider: "tmdb", entityType: s.identity.type, externalId: s.identity.id } }
: undefined;
// 改了作品时已有版本属于旧作品,只能正常入库
const mode = identity ? "normal" : s.mode;
const savePath = s.dir === CUSTOM_DIR ? s.custom.trim() : s.dir;
// 自动下载器 + 指定目录:钉住解析出的那个下载器,目录才对得上
const instanceId = s.instanceId || (savePath && s.dir !== CUSTOM_DIR ? (s.target?.instanceId ?? "") : "");
return {
...(identity ? { identity } : s.entityId ? { entityId: s.entityId } : {}),
...(instanceId ? { downloaderInstanceId: instanceId } : {}),
...(savePath ? { savePath } : {}),
...(s.allowUnrecognized && !identity ? { allowUnrecognized: true } : {}),
importMode: mode,
replacementAssetIds: mode === "replace" ? s.selected : [],
};
}
/** 从标题解析结果猜类型:有季或集就是剧集 */
export function kindHintOf(parsed: unknown): TmdbType {
const p = (parsed ?? {}) as { season?: unknown; episodes?: unknown };
return (p.season !== undefined && p.season !== null) || (p.episodes !== undefined && p.episodes !== null)
? "tv"
: "movie";
}
/** 作品类型 → TMDB 类型 */
export const tmdbTypeOf = (kind: string | undefined): TmdbType => (kind === "movie" ? "movie" : "tv");