208 lines
5.9 KiB
JavaScript
208 lines
5.9 KiB
JavaScript
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 = `<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 {}
|
||
}
|
||
}
|