Kolejne zmiany,

This commit is contained in:
dm
2025-12-15 11:28:53 +01:00
parent c0b9d5a584
commit 6b5a913666
48 changed files with 1630 additions and 868 deletions

View File

@@ -0,0 +1,210 @@
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 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() {
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 }
);
} finally {
try { db.close(); } catch {}
}
}