import path from "node:path"; import { XMLParser } from "fast-xml-parser"; import Database from "better-sqlite3"; const FEEDS = [ { url: "https://www.jambox.pl/xml/listakanalow-smart.xml", name: "Smart" }, { url: "https://www.jambox.pl/xml/listakanalow-optimum.xml", name: "Optimum" }, { url: "https://www.jambox.pl/xml/listakanalow-platinum.xml", name: "Platinum" }, { url: "https://www.jambox.pl/xml/listakanalow-pluspodstawowy.xml", name: "Podstawowy" }, { url: "https://www.jambox.pl/xml/listakanalow-pluskorzystny.xml", name: "Korzystny" }, { url: "https://www.jambox.pl/xml/listakanalow-plusbogaty.xml", name: "Bogaty" }, ]; const DB_PATH = process.env.FUZ_DB_PATH || path.join(process.cwd(), "src", "data", "ServicesRange.db"); function isAuthorized(request) { const expected = import.meta.env.JAMBOX_ADMIN_TOKEN; if (!expected) return false; const token = request.headers.get("x-admin-token"); return token === expected; } function getDb() { const db = new Database(DB_PATH); db.pragma("journal_mode = WAL"); return db; } async function fetchXml(url) { const res = await fetch(url, { headers: { accept: "application/xml,text/xml,*/*" } }); if (!res.ok) throw new Error(`XML HTTP ${res.status} for ${url}`); return await res.text(); } function parseNodes(xmlText) { const parser = new XMLParser({ ignoreAttributes: false, attributeNamePrefix: "@_", }); const json = parser.parse(xmlText); const nodes = json?.xml?.node ?? json?.node ?? []; return Array.isArray(nodes) ? nodes : [nodes]; } function decodeEntities(input) { if (!input) return ""; let s = String(input) .replace(/ /g, " ") .replace(/–/g, "–") .replace(/"/g, '"') .replace(/'/g, "'") .replace(/'/g, "'") .replace(/&/g, "&") .replace(/</g, "<") .replace(/>/g, ">"); s = s.replace(/&#(\d+);/g, (_, d) => String.fromCodePoint(Number(d))); s = s.replace(/&#x([0-9a-fA-F]+);/g, (_, h) => String.fromCodePoint(parseInt(h, 16))); return s; } function htmlToMarkdown(input) { if (!input) return ""; let html = ""; if (typeof input === "string") html = input; else if (input?.p) { if (typeof input.p === "string") html = `

${input.p}

`; else if (Array.isArray(input.p)) html = input.p.map((p) => `

${p}

`).join(""); } else html = String(input); let s = decodeEntities(html); s = s .replace(/<\s*(ul|ol)[^>]*>/gi, "\n__LIST_START__\n") .replace(/<\/\s*(ul|ol)\s*>/gi, "\n__LIST_END__\n") .replace(/<\s*li[^>]*>/gi, "__LI__") .replace(/<\/\s*li\s*>/gi, "\n") .replace(/<\s*br\s*\/?\s*>/gi, "\n") .replace(/<\/\s*(p|div)\s*>/gi, "\n") .replace(/<\s*(p|div)[^>]*>/gi, "") .replace(/<[^>]+>/g, "") .replace(/\r/g, "") .replace(/[ \t]+\n/g, "\n") .replace(/\n{3,}/g, "\n\n") .trim(); const lines = s.split("\n").map((x) => x.trim()); const out = []; let inList = false; for (const line of lines) { if (!line) { if (!inList) out.push(""); continue; } if (line === "__LIST_START__") { inList = true; continue; } if (line === "__LIST_END__") { inList = false; out.push(""); continue; } if (inList && line.startsWith("__LI__")) { out.push(`- ${line.replace("__LI__", "").trim()}`); continue; } out.push(line); } return out.join("\n").trim(); } function extractLogoUrl(node) { const logo = node?.field_logo_fid; if (!logo) return null; if (typeof logo === "string") { const m = logo.match(/src="([^"]+)"/); return m?.[1] ?? null; } if (logo?.img?.["@_src"]) return logo.img["@_src"]; return null; } async function downloadLogoAsBase64(url) { try { const res = await fetch(url); if (!res.ok) return null; const ct = res.headers.get("content-type") || "image/png"; const buf = Buffer.from(await res.arrayBuffer()); if (!buf.length) return null; return `data:${ct};base64,${buf.toString("base64")}`; } catch { return null; } } export async function POST({ request }) { if (!isAuthorized(request)) { return new Response(JSON.stringify({ ok: false, error: "Unauthorized" }), { status: 401, headers: { "content-type": "application/json; charset=utf-8" }, }); } const db = getDb(); const upsert = db.prepare(` INSERT INTO jambox_channels (nazwa, pckg_name, image, opis) VALUES (@nazwa, @pckg_name, @image, @opis) ON CONFLICT(nazwa, pckg_name) DO UPDATE SET image = COALESCE(excluded.image, jambox_channels.image), opis = COALESCE(excluded.opis, jambox_channels.opis) `); const logoCache = new Map(); const rows = []; try { for (const feed of FEEDS) { const xml = await fetchXml(feed.url); const nodes = parseNodes(xml); for (const node of nodes) { const name = (node?.nazwa_kanalu ?? node?.nazwa ?? "").trim(); if (!name) continue; const opis = htmlToMarkdown(node?.opis) || null; const key = name.toLowerCase(); let img = logoCache.get(key); if (img === undefined) { const logoUrl = extractLogoUrl(node); img = logoUrl ? await downloadLogoAsBase64(logoUrl) : null; logoCache.set(key, img); } rows.push({ nazwa: name, pckg_name: feed.name, image: img, opis, }); } } const trx = db.transaction((data) => { for (const r of data) upsert.run(r); }); trx(rows); return new Response(JSON.stringify({ ok: true, rows: rows.length, db: DB_PATH }), { headers: { "content-type": "application/json; charset=utf-8" }, }); } catch (e) { console.error("import jambox_channels:", e); return new Response(JSON.stringify({ ok: false, error: String(e?.message || e) }), { status: 500, headers: { "content-type": "application/json; charset=utf-8" }, }); } finally { try { db.close(); } catch {} } }