import { parseNfo, type NfoMetadata } from "./nfoParser"; /** * Lenient JSON metadata parser. Accepts a wide range of common keys produced by * scrapers (Kodi/Jellyfin/Stash/JavSP/etc.) and normalizes them into the same * shape as `NfoMetadata` so the cover editor can pre-fill from a file or a * pasted blob. */ export function parseMetaJson(raw: string): NfoMetadata | null { let obj: unknown; try { obj = JSON.parse(raw); } catch { return null; } if (!obj || typeof obj !== "object") return null; // Some scrapers wrap in {"data": {...}} or arrays. const root = (obj as Record); const data = (root.data && typeof root.data === "object" ? root.data : root) as Record; const pick = (...keys: string[]): string | undefined => { for (const k of keys) { const v = data[k]; if (typeof v === "string" && v.trim()) return v.trim(); if (typeof v === "number" && Number.isFinite(v)) return String(v); } return undefined; }; const pickArray = (...keys: string[]): string[] => { for (const k of keys) { const v = data[k]; if (Array.isArray(v)) { return v .map((x) => { if (typeof x === "string") return x.trim(); if (x && typeof x === "object") { const name = (x as Record).name ?? (x as Record).Name; return typeof name === "string" ? name.trim() : ""; } return ""; }) .filter(Boolean); } if (typeof v === "string" && v.trim()) { return v.split(/[,、,]/).map((s) => s.trim()).filter(Boolean); } } return []; }; const runtimeRaw = pick("runtime", "runtimeMin", "runtime_min", "duration"); const runtimeMin = runtimeRaw ? parseInt(runtimeRaw, 10) : NaN; const out: NfoMetadata = { title: pick("title", "originaltitle", "originalTitle", "name"), code: pick("code", "id", "num", "number", "javId", "jav_id"), releaseDate: pick("releaseDate", "release_date", "premiered", "releasedate", "date", "year"), runtimeMin: Number.isFinite(runtimeMin) ? runtimeMin : undefined, director: pick("director"), studio: pick("studio", "maker", "manufacturer"), series: pick("series", "set"), actresses: pickArray("actresses", "actors", "actor", "performers", "cast", "stars"), genres: pickArray("genres", "genre", "tags", "categories"), notes: pick("plot", "outline", "summary", "description", "notes"), }; return strip(out); } /** * Try JSON first, fall back to XML. Returns null if neither matches. */ export function parseMetaAny(raw: string): NfoMetadata | null { const trimmed = raw.trim(); if (!trimmed) return null; if (trimmed.startsWith("{") || trimmed.startsWith("[")) { return parseMetaJson(trimmed); } return parseNfo(trimmed); } function strip(o: NfoMetadata): NfoMetadata { const out: NfoMetadata = {}; for (const [k, v] of Object.entries(o)) { if (v == null) continue; if (Array.isArray(v) && v.length === 0) continue; if (typeof v === "string" && v === "") continue; (out as Record)[k] = v; } return out; }