/** * Minimal Kodi/Jellyfin-style .nfo parser. Operates on a string that contains * an XML document and extracts the fields we map onto cover metadata. * * No XML library dependency — small, regex-based, deliberately lenient. * Returns null if the buffer doesn't look like a movie .nfo. */ export interface NfoMetadata { title?: string; code?: string; releaseDate?: string; runtimeMin?: number; director?: string; studio?: string; series?: string; actresses?: string[]; genres?: string[]; notes?: string; } const TAG_RE = (tag: string) => new RegExp(`<${tag}\\b[^>]*>([\\s\\S]*?)<\\/${tag}>`, "gi"); function firstMatch(xml: string, tag: string): string | undefined { const re = TAG_RE(tag); const m = re.exec(xml); if (!m) return undefined; return decodeEntities(m[1].trim()); } function allMatches(xml: string, tag: string): string[] { const re = TAG_RE(tag); const out: string[] = []; let m: RegExpExecArray | null; while ((m = re.exec(xml)) !== null) { const text = decodeEntities(m[1].trim()); if (text) out.push(text); } return out; } function decodeEntities(s: string): string { return s .replace(//g, "$1") .replace(/&/g, "&") .replace(/</g, "<") .replace(/>/g, ">") .replace(/"/g, '"') .replace(/'/g, "'"); } export function parseNfo(xml: string): NfoMetadata | null { if (!/ { const name = block.match(/([\s\S]*?)<\/name>/i); return decodeEntities(name ? name[1].trim() : block.trim()); }).filter(Boolean), genres: [...allMatches(xml, "genre"), ...allMatches(xml, "tag")], notes: firstMatch(xml, "plot") ?? firstMatch(xml, "outline"), }); } 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; }