Files
fuz-site/src/pages/api/jambox/import-jambox-channels.js

208 lines
5.9 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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(/&lt;/g, "<")
.replace(/&gt;/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 = `<p>${input.p}</p>`;
else if (Array.isArray(input.p)) html = input.p.map((p) => `<p>${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 {}
}
}