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; 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(); 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");