import type { LocalMediaHit, MediaKind, SearchInventoryState, SearchRelease, SearchReleaseParsed, SearchRuleVerdict, SearchSiteStatus, } from "@umt/contracts"; import { Compass, type LucideIcon, Zap } from "lucide-react"; import type { StatusTone } from "~/components/console/status-badge.tsx"; import { MEDIA_KIND } from "~/components/library/labels.ts"; // 搜索结果的展示口径 export const mediaKindLabel = (k: MediaKind) => MEDIA_KIND[k] ?? k; /** 本地作品的一行摘要:订阅 / 版本 / 下载中 / 缺口 */ export function slotSummary(h: LocalMediaHit): string { const c = h.slotCounts; const parts: string[] = []; if (h.subscribed) parts.push("已订阅"); if (h.versionCount) parts.push(`${h.versionCount} 个版本`); if (h.downloadingCount) parts.push(`${h.downloadingCount} 个下载中`); const gaps = c.empty + c.attention + c.upgrade_pending; if (gaps) parts.push(`${gaps} 个收录版本未满足`); return parts.join(" · ") || "未订阅"; } export const actionIcon = (kind: "page" | "action"): LucideIcon => (kind === "page" ? Compass : Zap); export const INVENTORY_STATE: Record = { downloading: { label: "下载中", tone: "warning" }, gap: { label: "有缺口", tone: "warning" }, owned: { label: "已有版本", tone: "success" }, subscribed: { label: "已订阅", tone: "neutral" }, none: { label: "本地无版本", tone: "neutral" }, }; export const SITE_STATUS: Record = { ok: "正常", failed: "失败", timeout: "超时", skipped: "跳过", }; export const SORT_OPTIONS = [ { value: "default", label: "默认" }, { value: "seeders", label: "做种数" }, { value: "size", label: "大小" }, { value: "uploadedAt", label: "发布时间" }, { value: "site", label: "站点" }, ] as const; const PROMO: Record = { free: "免费", "2xfree": "2x 免费", "50%": "50%", "2x50%": "2x 50%", "30%": "30%", "2x": "2x 上传", }; /** 促销文案;none/unknown 返回 null */ export function promoLabel(type: string): string | null { if (!type || type === "none" || type === "unknown") return null; return PROMO[type] ?? type; } const GB = 1024 ** 3; export const gbToBytes = (s: string): number | undefined => { const n = Number(s); return s.trim() && Number.isFinite(n) && n >= 0 ? Math.round(n * GB) : undefined; }; // ── 资源行(SEARCH-5:学 MP 的规格标签、促销角标、按作品分组;加上本工具的规则判定)── export type SpecDim = "episode" | "resolution" | "source" | "codec" | "hdr" | "audio" | "lang" | "group"; export interface ReleaseSpecTag { key: string; dim: SpecDim; label: string; /** 悬停提示:「分辨率:1080p」 */ title: string; } export const SPEC_DIM_LABEL: Record = { episode: "季集", resolution: "分辨率", source: "来源", codec: "编码", hdr: "HDR", audio: "音频", lang: "语言", group: "制作组", }; const SUB_LABEL: Record = { zh: "中字", "zh-Hans": "简中", "zh-Hant": "繁中", en: "英字", ja: "日字", ko: "韩字", }; const DUB_LABEL: Record = { cmn: "国语", yue: "粤语", en: "英语配音" }; const pad2 = (n: number) => String(n).padStart(2, "0"); /** * 规格标签,固定顺序:季集 → 分辨率 → 来源 → 编码 → HDR → 音频 → 配音 / 字幕语言 → 制作组。 * 都来自标题解析;同名去重,不猜。 */ export function releaseSpecTags(p: SearchReleaseParsed): ReleaseSpecTag[] { const out: ReleaseSpecTag[] = []; const seen = new Set(); const add = (dim: SpecDim, label: string | null | undefined) => { const l = label?.trim(); if (!l || seen.has(`${dim}:${l.toLowerCase()}`)) return; seen.add(`${dim}:${l.toLowerCase()}`); out.push({ key: `${dim}:${l}`, dim, label: l, title: `${SPEC_DIM_LABEL[dim]}:${l}` }); }; if (p.season !== null || p.episodes) { const s = p.season !== null ? `S${pad2(p.season)}${p.seasonEnd && p.seasonEnd !== p.season ? `-S${pad2(p.seasonEnd)}` : ""}` : ""; const e = p.episodes ? `E${pad2(p.episodes.start)}${p.episodes.end !== p.episodes.start ? `-E${pad2(p.episodes.end)}` : ""}` : ""; add("episode", `${s}${e}`); } add("resolution", p.resolution); add("source", p.source); add("codec", p.videoCodec); for (const h of p.hdr) add("hdr", h); for (const a of p.audio ?? []) add("audio", a); for (const l of p.audioLangs ?? []) add("lang", DUB_LABEL[l] ?? l); for (const l of p.subtitles ?? []) add("lang", SUB_LABEL[l] ?? l); add("group", p.releaseGroup); return out; } /** 促销标签:免费(含 2x 免费)属状态,用 success;其余折扣中性 */ export function promoBadge( r: Pick, ): { label: string; tone: "success" | "neutral" } | null { const label = promoLabel(r.promotion.type); if (!label) return null; return { label, tone: r.free ? "success" : "neutral" }; } /** H&R:明确有 → warning;未知 → 中性「H&R 未知」;明确没有 → 不显示 */ export function hrBadge(r: Pick): { label: string; tone: "warning" | "neutral" } | null { if (r.hr === true) return { label: "H&R", tone: "warning" }; if (r.hr === "unknown") return { label: "H&R 未知", tone: "neutral" }; return null; } export interface VerdictView { label: string; tone: "success" | "neutral"; /** 悬停提示:档位名、不满足的逐条说明 */ title: string; } /** 规则判定 → 文案:「命中第 1 档」「满足收录条件」「不满足:分辨率、来源」「无法判断:分辨率」;多个收录版本时带名字 */ export function verdictView(v: SearchRuleVerdict): VerdictView { const dims = (xs: string[]) => (xs.length > 3 ? `${xs.slice(0, 3).join("、")} 等 ${xs.length} 项` : xs.join("、")); const text = v.outcome === "hit" ? v.tier !== null ? `命中第 ${v.tier} 档` : "满足收录条件" : v.outcome === "miss" ? `不满足:${dims(v.failed) || "收录条件"}` : `无法判断:${dims(v.failed) || "缺少字段"}`; const label = v.slotCount > 1 ? `「${v.slotName}」${text}` : text; const title = [v.slotCount > 1 ? `收录版本「${v.slotName}」` : null, v.tierName, ...v.details] .filter(Boolean) .join("\n"); return { label, tone: v.outcome === "hit" ? "success" : "neutral", title }; } /** 与库里版本比较:「比库里版本高 1 档」「比库里版本好」「与库里版本同档」「不如库里版本」 */ export function vsLibraryLabel(v: SearchRuleVerdict): string | null { const c = v.vsLibrary; if (!c) return null; if (c.cmp === "higher") return c.steps ? `比库里版本高 ${c.steps} 档` : "比库里版本好"; if (c.cmp === "same") return "与库里版本同档"; return "不如库里版本"; } export interface WorkGroup { /** 作品 ID;未关联作品的一组为 null */ key: string | null; entity: SearchRelease["entity"]; entityMatch: SearchRelease["entityMatch"]; inventory: SearchRelease["inventory"]; items: SearchRelease[]; } /** * 按作品分组(保持结果顺序,组按首次出现排;未关联作品的放最后)。 * 只涉及一部作品(或全都未关联)时 grouped = false:作品名只在列表上方出现一次,行里不重复。 */ export function groupByWork(rows: readonly SearchRelease[]): { grouped: boolean; groups: WorkGroup[] } { const map = new Map(); let loose: WorkGroup | null = null; for (const r of rows) { if (!r.entity) { loose ??= { key: null, entity: null, entityMatch: null, inventory: null, items: [] }; loose.items.push(r); continue; } let g = map.get(r.entity.id); if (!g) { g = { key: r.entity.id, entity: r.entity, entityMatch: r.entityMatch, inventory: r.inventory, items: [] }; map.set(r.entity.id, g); } // 同一作品里有已识别的,组头就不标「未确认」 if (r.entityMatch === "resolved") g.entityMatch = "resolved"; g.inventory ??= r.inventory; g.items.push(r); } const groups = [...map.values(), ...(loose ? [loose] : [])]; return { grouped: groups.length > 1, groups }; }