Files
silo-server/web/src/lib/mediaFormat.ts
T
95779c47a9 fix(web): update metadata for selected version (#414)
* fix(web): update metadata for selected version

* refactor(web): simplify selected duration lookup

* refactor(web): share the hero runtime formatter via mediaFormat

MovieContent and EpisodeContent carried identical local formatDuration
helpers; move the minutes-based badge formatter into lib/mediaFormat as
formatRuntimeMinutes alongside the other canonical media-spec helpers.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Quick104 <31828688+Quick104@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 15:08:16 -04:00

119 lines
4.3 KiB
TypeScript

// Canonical media-spec formatting helpers shared by item detail views, the
// player stats overlay, poster overlays, and admin views. Display policy
// differences between surfaces (empty-string vs "—" fallback, GB vs GiB
// labels) are expressed via parameters here instead of per-surface copies.
export const CODEC_LABELS: Record<string, string> = {
aac: "AAC",
ac3: "AC3",
av1: "AV1",
dts: "DTS",
dtshd: "DTS-HD",
eac3: "EAC3",
flac: "FLAC",
h264: "H.264",
hevc: "HEVC",
mp3: "MP3",
opus: "Opus",
truehd: "TrueHD",
};
/** Exact-match codec display label; unknown codecs are uppercased. */
export function formatCodecLabel(codec?: string, fallback = "—"): string {
const normalized = codec?.trim();
if (!normalized) {
return fallback;
}
return CODEC_LABELS[normalized.toLowerCase()] ?? normalized.toUpperCase();
}
/** Fuzzy audio-family label for quality badges ("truehd atmos" → "Atmos"). */
export function mapAudioLabel(codec: string): string {
const lower = codec.toLowerCase();
if (lower.includes("atmos")) return "Atmos";
if (lower.includes("truehd")) return "TrueHD";
if (lower.includes("dts-hd") || lower.includes("dts:x")) return "DTS-HD";
if (lower.includes("dts")) return "DTS";
if (lower.includes("eac3") || lower.includes("e-ac-3")) return "EAC3";
if (lower.includes("aac")) return "AAC";
if (lower.includes("flac")) return "FLAC";
return codec.toUpperCase();
}
interface FormatFileSizeOptions {
fallback?: string;
/** Use IEC unit labels (GiB/MiB/KiB) instead of GB/MB/KB. Math is 1024-based either way. */
iecUnits?: boolean;
}
export function formatFileSize(bytes?: number, options: FormatFileSizeOptions = {}): string {
const { fallback = "", iecUnits = false } = options;
if (!isPositive(bytes)) return fallback;
const [giga, mega, kilo] = iecUnits ? ["GiB", "MiB", "KiB"] : ["GB", "MB", "KB"];
if (bytes >= 1024 ** 3) return `${(bytes / 1024 ** 3).toFixed(1)} ${giga}`;
if (bytes >= 1024 ** 2) return `${(bytes / 1024 ** 2).toFixed(1)} ${mega}`;
if (bytes >= 1024) return `${(bytes / 1024).toFixed(1)} ${kilo}`;
return `${bytes} B`;
}
export function formatBitrate(kbps?: number, fallback = ""): string {
if (!isPositive(kbps)) return fallback;
return `${Math.round(kbps).toLocaleString()} kbps`;
}
export function formatMbpsFromKbps(kbps?: number, fallback = "—"): string {
if (!isPositive(kbps)) return fallback;
return `${(kbps / 1000).toFixed(1)} Mbps`;
}
export function formatSampleRate(sampleRate?: number, fallback = ""): string {
if (!isPositive(sampleRate)) return fallback;
return `${sampleRate.toLocaleString()} Hz`;
}
export function formatChannels(channels?: number): string {
if (!channels || channels <= 0) return "";
if (channels === 8) return "7.1";
if (channels === 6) return "5.1";
if (channels === 2) return "stereo";
return `${channels} ch`;
}
/** Ensures a raw Dolby Vision label ("Profile 8.1") carries the "Dolby Vision" prefix. */
export function dolbyVisionLabel(raw: string): string {
return raw.toLowerCase().startsWith("dolby vision") ? raw : `Dolby Vision ${raw}`;
}
// Kometa-style pretty resolution label: "2160p" → "4K", others keep their
// lowercase-p form ("1080p", "720p"). Unknown values are uppercased so they
// at least look intentional.
export function prettyResolution(value: string | undefined): string | null {
if (!value) return null;
const v = value.toLowerCase().trim();
if (v === "") return null;
if (v === "2160p" || v === "4k" || v === "uhd") return "4K";
if (v === "4320p" || v === "8k") return "8K";
if (/^\d+p$/.test(v)) return v;
return value.toUpperCase();
}
// Compact HDR suffix used for combined Resolution+HDR badges. Any Dolby
// Vision variant collapses to "DV"; any other HDR variant to "HDR".
export function compactHdrSuffix(value: string | undefined): string | null {
if (!value) return null;
if (value.includes("DV")) return "DV";
return "HDR";
}
function isPositive(value?: number): value is number {
return Number.isFinite(value) && value != null && value > 0;
}
/** Hero runtime badge label ("2h 43m", "43m"); "" for zero/unknown. */
export function formatRuntimeMinutes(minutes: number): string {
if (minutes <= 0) return "";
const h = Math.floor(minutes / 60);
const m = minutes % 60;
return h > 0 ? `${h}h ${m}m` : `${m}m`;
}