Initial commit
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
/**
|
||||
* 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(/<!\[CDATA\[([\s\S]*?)]]>/g, "$1")
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
|
||||
export function parseNfo(xml: string): NfoMetadata | null {
|
||||
if (!/<movie\b|<id\b|<title\b/i.test(xml)) return null;
|
||||
|
||||
const runtimeRaw = firstMatch(xml, "runtime");
|
||||
const runtime = runtimeRaw ? parseInt(runtimeRaw, 10) : NaN;
|
||||
|
||||
return strip({
|
||||
title: firstMatch(xml, "title") ?? firstMatch(xml, "originaltitle"),
|
||||
code: firstMatch(xml, "id") ?? firstMatch(xml, "num"),
|
||||
releaseDate: firstMatch(xml, "premiered") ?? firstMatch(xml, "releasedate") ?? firstMatch(xml, "year"),
|
||||
runtimeMin: Number.isFinite(runtime) ? runtime : undefined,
|
||||
director: firstMatch(xml, "director"),
|
||||
studio: firstMatch(xml, "studio") ?? firstMatch(xml, "maker"),
|
||||
series: firstMatch(xml, "set") ?? firstMatch(xml, "series"),
|
||||
actresses: allMatches(xml, "actor").map((block) => {
|
||||
const name = block.match(/<name>([\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<string, unknown>)[k] = v;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
Reference in New Issue
Block a user