Initial commit

This commit is contained in:
admin
2026-05-26 22:46:00 +02:00
commit 7e2c2ff89c
256 changed files with 51523 additions and 0 deletions
+85
View File
@@ -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(/&amp;/g, "&")
.replace(/&lt;/g, "<")
.replace(/&gt;/g, ">")
.replace(/&quot;/g, '"')
.replace(/&apos;/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;
}