Usuniecie skryptów i zabezpieczenie tokenem api do aktualizacji danych z jambox

This commit is contained in:
dm
2025-12-16 08:44:16 +01:00
parent 116a915cac
commit 142d289be9
11 changed files with 256 additions and 821 deletions

Binary file not shown.

Binary file not shown.

View File

@@ -12,8 +12,14 @@ const FEEDS = [
]; ];
const DB_PATH = const DB_PATH =
process.env.FUZ_DB_PATH || process.env.FUZ_DB_PATH || path.join(process.cwd(), "src", "data", "ServicesRange.db");
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() { function getDb() {
const db = new Database(DB_PATH); const db = new Database(DB_PATH);
@@ -22,9 +28,7 @@ function getDb() {
} }
async function fetchXml(url) { async function fetchXml(url) {
const res = await fetch(url, { const res = await fetch(url, { headers: { accept: "application/xml,text/xml,*/*" } });
headers: { accept: "application/xml,text/xml,*/*" },
});
if (!res.ok) throw new Error(`XML HTTP ${res.status} for ${url}`); if (!res.ok) throw new Error(`XML HTTP ${res.status} for ${url}`);
return await res.text(); return await res.text();
} }
@@ -50,14 +54,8 @@ function decodeEntities(input) {
.replace(/&/g, "&") .replace(/&/g, "&")
.replace(/&lt;/g, "<") .replace(/&lt;/g, "<")
.replace(/&gt;/g, ">"); .replace(/&gt;/g, ">");
s = s.replace(/&#(\d+);/g, (_, d) => String.fromCodePoint(Number(d)));
s = s.replace(/&#(\d+);/g, (_, d) => s = s.replace(/&#x([0-9a-fA-F]+);/g, (_, h) => String.fromCodePoint(parseInt(h, 16)));
String.fromCodePoint(Number(d))
);
s = s.replace(/&#x([0-9a-fA-F]+);/g, (_, h) =>
String.fromCodePoint(parseInt(h, 16))
);
return s; return s;
} }
@@ -68,8 +66,7 @@ function htmlToMarkdown(input) {
if (typeof input === "string") html = input; if (typeof input === "string") html = input;
else if (input?.p) { else if (input?.p) {
if (typeof input.p === "string") html = `<p>${input.p}</p>`; if (typeof input.p === "string") html = `<p>${input.p}</p>`;
else if (Array.isArray(input.p)) else if (Array.isArray(input.p)) html = input.p.map((p) => `<p>${p}</p>`).join("");
html = input.p.map((p) => `<p>${p}</p>`).join("");
} else html = String(input); } else html = String(input);
let s = decodeEntities(html); let s = decodeEntities(html);
@@ -131,18 +128,23 @@ async function downloadLogoAsBase64(url) {
try { try {
const res = await fetch(url); const res = await fetch(url);
if (!res.ok) return null; if (!res.ok) return null;
const ct = res.headers.get("content-type") || "image/png"; const ct = res.headers.get("content-type") || "image/png";
const buf = Buffer.from(await res.arrayBuffer()); const buf = Buffer.from(await res.arrayBuffer());
if (!buf.length) return null; if (!buf.length) return null;
return `data:${ct};base64,${buf.toString("base64")}`; return `data:${ct};base64,${buf.toString("base64")}`;
} catch { } catch {
return null; return null;
} }
} }
export async function POST() { 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 db = getDb();
const upsert = db.prepare(` const upsert = db.prepare(`
@@ -153,7 +155,7 @@ export async function POST() {
opis = COALESCE(excluded.opis, jambox_channels.opis) opis = COALESCE(excluded.opis, jambox_channels.opis)
`); `);
const logoCache = new Map(); const logoCache = new Map();
const rows = []; const rows = [];
try { try {
@@ -190,20 +192,15 @@ export async function POST() {
trx(rows); trx(rows);
return new Response( return new Response(JSON.stringify({ ok: true, rows: rows.length, db: DB_PATH }), {
JSON.stringify({ headers: { "content-type": "application/json; charset=utf-8" },
ok: true, });
rows: rows.length,
db: DB_PATH,
}),
{ headers: { "content-type": "application/json; charset=utf-8" } }
);
} catch (e) { } catch (e) {
console.error("import jambox_channels:", e); console.error("import jambox_channels:", e);
return new Response( return new Response(JSON.stringify({ ok: false, error: String(e?.message || e) }), {
JSON.stringify({ ok: false, error: String(e.message || e) }), status: 500,
{ status: 500 } headers: { "content-type": "application/json; charset=utf-8" },
); });
} finally { } finally {
try { db.close(); } catch {} try { db.close(); } catch {}
} }

View File

@@ -4,6 +4,13 @@ import fs from "node:fs/promises";
const URL = "https://www.jambox.pl/xml/mozliwosci.xml"; const URL = "https://www.jambox.pl/xml/mozliwosci.xml";
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 toArray(v) { function toArray(v) {
if (!v) return []; if (!v) return [];
return Array.isArray(v) ? v : [v]; return Array.isArray(v) ? v : [v];
@@ -22,7 +29,7 @@ function decodeEntities(input) {
s = s.replace(/&#(\d+);/g, (_, d) => String.fromCodePoint(Number(d))); s = s.replace(/&#(\d+);/g, (_, d) => String.fromCodePoint(Number(d)));
s = s.replace(/&#x([0-9a-fA-F]+);/g, (_, h) => s = s.replace(/&#x([0-9a-fA-F]+);/g, (_, h) =>
String.fromCodePoint(parseInt(h, 16)) String.fromCodePoint(parseInt(h, 16)),
); );
return s; return s;
@@ -171,13 +178,23 @@ function toYaml(sections) {
return out.join("\n").trimEnd() + "\n"; return out.join("\n").trimEnd() + "\n";
} }
export async function POST() { 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 res = await fetch(URL, { const res = await fetch(URL, {
headers: { accept: "application/xml,text/xml,*/*" }, headers: { accept: "application/xml,text/xml,*/*" },
}); });
if (!res.ok) { if (!res.ok) {
return new Response(`JAMBOX XML: HTTP ${res.status}`, { status: 502 }); return new Response(JSON.stringify({ ok: false, error: `JAMBOX XML: HTTP ${res.status}` }), {
status: 502,
headers: { "content-type": "application/json; charset=utf-8" },
});
} }
const xml = await res.text(); const xml = await res.text();
@@ -195,10 +212,7 @@ export async function POST() {
if (!title) return null; if (!title) return null;
const blocks = [ const blocks = [...parseHtmlContent(n?.teaser), ...parseHtmlContent(n?.description)];
...parseHtmlContent(n?.teaser),
...parseHtmlContent(n?.description),
];
const content = blocksToMarkdown(blocks); const content = blocksToMarkdown(blocks);
if (!content) return null; if (!content) return null;
@@ -216,7 +230,7 @@ export async function POST() {
await fs.mkdir(outDir, { recursive: true }); await fs.mkdir(outDir, { recursive: true });
await fs.writeFile(outFile, toYaml(sections), "utf8"); await fs.writeFile(outFile, toYaml(sections), "utf8");
return new Response(JSON.stringify({ ok: true, count: sections.length }), { return new Response(JSON.stringify({ ok: true, count: sections.length, file: outFile }), {
headers: { "content-type": "application/json" }, headers: { "content-type": "application/json; charset=utf-8" },
}); });
} }

View File

@@ -0,0 +1,205 @@
import path from "node:path";
import fs from "node:fs/promises";
import yaml from "js-yaml";
import { XMLParser } from "fast-xml-parser";
const JAMBOX_NUMBERS_URL = "https://www.jambox.pl/xml/jamboxwliczbach.xml";
/**
* Proste auth, żeby nikt z internetu nie mógł Ci pisać po YAML.
* .env: JAMBOX_ADMIN_TOKEN="..."
*/
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;
}
async function fetchXml(url) {
const res = await fetch(url);
if (!res.ok) {
throw new Error(`Błąd pobierania XML: ${res.status} ${res.statusText}`);
}
return await res.text();
}
function parseCountsNode(xmlText) {
const parser = new XMLParser({
ignoreAttributes: false,
attributeNamePrefix: "@_",
});
const json = parser.parse(xmlText);
const node = json.xml?.node ?? json.node ?? null;
if (!node) {
throw new Error("Nie znaleziono <node> w jamboxwliczbach.xml");
}
return node;
}
function getInt(node, key) {
const raw = node?.[key];
const n = Number(raw);
return Number.isFinite(n) ? n : null;
}
/**
* Mapowanie kluczy XML na source+slug (tak jak w Twoim starym skrypcie)
*/
function xmlKeyFor(source, slug, isHd) {
const s = String(source || "").toUpperCase();
const sl = String(slug || "").toLowerCase();
// EVIO: ilosc_kanalow_eviooptimum / ilosc_kanalow_hd_eviooptimum
if (s === "EVIO") {
return isHd ? `ilosc_kanalow_hd_evio${sl}` : `ilosc_kanalow_evio${sl}`;
}
// PLUS: ilosc_kanalow_korzystny / ilosc_kanalow_hd_korzystny
if (s === "PLUS") {
return isHd ? `ilosc_kanalow_hd_${sl}` : `ilosc_kanalow_${sl}`;
}
return null;
}
function findParamIndex(params, key) {
return params.findIndex(
(p) => String(p?.klucz ?? "").toLowerCase() === key.toLowerCase(),
);
}
export async function POST({ request }) {
try {
if (!isAuthorized(request)) {
return new Response(JSON.stringify({ ok: false, error: "Unauthorized" }), {
status: 401,
headers: { "Content-Type": "application/json" },
});
}
const body = await request.json().catch(() => ({}));
const dryRun = body?.dryRun === true;
const YAML_PATH =
import.meta.env.JAMBOX_TV_YAML_PATH ||
path.join(process.cwd(), "src", "content", "internet-telewizja", "cards.yaml");
// 1) XML
const xml = await fetchXml(JAMBOX_NUMBERS_URL);
const node = parseCountsNode(xml);
// 2) YAML
const rawYaml = await fs.readFile(YAML_PATH, "utf8");
const doc = yaml.load(rawYaml);
if (!doc || !Array.isArray(doc.cards)) {
return new Response(JSON.stringify({ ok: false, error: "YAML: brak doc.cards" }), {
status: 400,
headers: { "Content-Type": "application/json" },
});
}
const changedItems = [];
const unchangedItems = [];
const skippedItems = [];
for (const card of doc.cards) {
const id = card?.id ?? null;
const source = String(card?.source ?? "");
const slug = String(card?.slug ?? "");
if (!source || !slug) {
skippedItems.push({ id, reason: "brak source/slug" });
continue;
}
const params = Array.isArray(card?.parametry) ? card.parametry : [];
const iCanals = findParamIndex(params, "canals");
const iHd = findParamIndex(params, "canalshd");
// nie dopisujemy na siłę, tylko aktualizujemy to co istnieje
if (iCanals === -1 && iHd === -1) {
skippedItems.push({ id, source, slug, reason: "brak canals/canalshd w parametry" });
continue;
}
const keyCanals = xmlKeyFor(source, slug, false);
const keyHd = xmlKeyFor(source, slug, true);
const xmlCanals = keyCanals ? getInt(node, keyCanals) : null;
const xmlHd = keyHd ? getInt(node, keyHd) : null;
if (xmlCanals == null && xmlHd == null) {
skippedItems.push({
id,
source,
slug,
reason: "brak wartości w XML",
keys: { keyCanals, keyHd },
});
continue;
}
const before = {
canals: iCanals !== -1 ? Number(params[iCanals]?.value) : null,
canalshd: iHd !== -1 ? Number(params[iHd]?.value) : null,
};
const after = {
canals: xmlCanals ?? before.canals,
canalshd: xmlHd ?? before.canalshd,
};
const willChange =
(after.canals != null && before.canals !== after.canals) ||
(after.canalshd != null && before.canalshd !== after.canalshd);
if (!willChange) {
unchangedItems.push({ id, source, slug, before });
continue;
}
if (!dryRun) {
if (iCanals !== -1 && after.canals != null) params[iCanals].value = after.canals;
if (iHd !== -1 && after.canalshd != null) params[iHd].value = after.canalshd;
card.parametry = params;
}
changedItems.push({ id, source, slug, before, after, keys: { keyCanals, keyHd } });
}
if (!dryRun && changedItems.length > 0) {
const newYaml = yaml.dump(doc, {
lineWidth: -1,
noRefs: true,
sortKeys: false,
});
await fs.writeFile(YAML_PATH, newYaml, "utf8");
}
return new Response(
JSON.stringify({
ok: true,
dryRun,
yamlPath: YAML_PATH,
changed: changedItems.length,
unchanged: unchangedItems.length,
skipped: skippedItems.length,
changedItems,
skippedItems,
}),
{ status: 200, headers: { "Content-Type": "application/json" } },
);
} catch (err) {
console.error("update-channels error:", err);
return new Response(JSON.stringify({ ok: false, error: String(err?.message ?? err) }), {
status: 500,
headers: { "Content-Type": "application/json" },
});
}
}

View File

@@ -27,7 +27,6 @@ export async function GET() {
return url; return url;
}); });
// Budowa XML
const body = `<?xml version="1.0" encoding="UTF-8"?> const body = `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"> <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
${urls ${urls

View File

@@ -1,280 +0,0 @@
// scripts/importJamboxChannelLists.js
import Database from "better-sqlite3";
import { XMLParser } from "fast-xml-parser";
const DB_PATH =
process.env.FUZ_DB_PATH || "./src/data/ServicesRange.db";
// źródła: URL + docelowy pakiet (source + slug)
const FEEDS = [
{
url: "https://www.jambox.pl/xml/listakanalow-smart.xml",
source: "EVIO",
slug: "smart",
},
{
url: "https://www.jambox.pl/xml/listakanalow-optimum.xml",
source: "EVIO",
slug: "optimum",
},
{
url: "https://www.jambox.pl/xml/listakanalow-platinum.xml",
source: "EVIO",
slug: "platinum",
},
{
url: "https://www.jambox.pl/xml/listakanalow-pluspodstawowy.xml",
source: "PLUS",
slug: "podstawowy",
},
{
url: "https://www.jambox.pl/xml/listakanalow-pluskorzystny.xml",
source: "PLUS",
slug: "korzystny",
},
{
url: "https://www.jambox.pl/xml/listakanalow-plusbogaty.xml",
source: "PLUS",
slug: "bogaty",
},
];
/**
* Helper: pobranie XML-a
*/
async function fetchXml(url) {
const res = await fetch(url);
if (!res.ok) {
throw new Error(
`Błąd pobierania XML z ${url}: ${res.status} ${res.statusText}`
);
}
return await res.text();
}
/**
* Parser XML -> tablica node'ów z <xml><node>...</node></xml>
*/
function parseNodes(xmlText) {
const parser = new XMLParser({
ignoreAttributes: false,
attributeNamePrefix: "@_",
});
const json = parser.parse(xmlText);
let nodes = json.xml?.node ?? json.node ?? [];
if (!nodes) return [];
if (!Array.isArray(nodes)) nodes = [nodes];
return nodes;
}
/**
* Prosta dekodacja encji HTML (&ndash; &nbsp; etc.)
* nie bawimy się w pełen parser HTML, tylko najczęstsze kody
*/
function decodeHtmlEntities(str) {
if (!str) return "";
return String(str)
.replace(/&ndash;/g, "")
.replace(/&nbsp;/g, " ")
.replace(/&quot;/g, '"')
.replace(/&apos;/g, "'")
.replace(/&amp;/g, "&")
.replace(/&lt;/g, "<")
.replace(/&gt;/g, ">");
}
/**
* Z node'a zrobimy obiekt kanału
*/
function mapChannelNode(node) {
const number = Number(node.nr);
const name = node.nazwa_kanalu ?? node.nazwa ?? null;
if (!number || !name) {
console.warn("⚠ Pomijam node bez numeru / nazwy:", node);
return null;
}
// opis chcemy HTML, ale z poprawionymi encjami
let descriptionHtml = "";
if (typeof node.opis === "string") {
descriptionHtml = node.opis;
} else if (node.opis && node.opis.p) {
// <opis><p>...</p></opis>
if (typeof node.opis.p === "string") {
descriptionHtml = `<p>${node.opis.p}</p>`;
} else if (Array.isArray(node.opis.p)) {
// kilka <p>...</p>
descriptionHtml = node.opis.p
.map((p) =>
typeof p === "string" ? `<p>${p}</p>` : ""
)
.join("");
}
}
descriptionHtml = decodeHtmlEntities(descriptionHtml.trim());
// logo interesuje nas przede wszystkim src
let logoUrl = null;
const logoNode = node.field_logo_fid;
if (typeof logoNode === "string") {
// próbujemy wyciągnąć src="..."
const m = logoNode.match(/src="([^"]+)"/);
if (m) logoUrl = m[1];
} else if (logoNode && logoNode.img) {
// fast-xml-parser: <field_logo_fid><img ... /></field_logo_fid>
const img = logoNode.img;
if (img["@_src"]) {
logoUrl = img["@_src"];
}
}
// gwarantowany
const guaranteed = String(node.gwarantowany || "")
.toLowerCase()
.includes("gwarantowany")
? 1
: 0;
return {
number,
name: String(name).trim(),
description: descriptionHtml || null,
logo_url: logoUrl || null,
guaranteed,
};
}
/**
* Upewniamy się, że tabela docelowa istnieje
*/
function ensureSchema(db) {
db.exec(`
CREATE TABLE IF NOT EXISTS jambox_package_channels (
id INTEGER PRIMARY KEY AUTOINCREMENT,
package_id INTEGER NOT NULL,
number INTEGER NOT NULL,
name TEXT NOT NULL,
description TEXT,
logo_url TEXT,
guaranteed INTEGER NOT NULL DEFAULT 0,
updated_at TEXT NOT NULL,
UNIQUE (package_id, number),
FOREIGN KEY (package_id)
REFERENCES jambox_base_packages (id) ON DELETE CASCADE
);
`);
}
/**
* Znajdowanie id pakietu po (source, slug)
*/
function getPackageId(db, source, slug) {
const row = db
.prepare(
`
SELECT id
FROM jambox_base_packages
WHERE source = ?
AND slug = ?;
`
)
.get(source, slug);
return row?.id ?? null;
}
/**
* Upsert kanałów dla danego pakietu
*/
function upsertChannelsForPackage(db, packageId, channels) {
const now = new Date().toISOString();
const stmt = db.prepare(`
INSERT INTO jambox_package_channels
(package_id, number, name, description, logo_url, guaranteed, updated_at)
VALUES
(@package_id, @number, @name, @description, @logo_url, @guaranteed, @updated_at)
ON CONFLICT(package_id, number) DO UPDATE SET
name = excluded.name,
description = excluded.description,
logo_url = excluded.logo_url,
guaranteed = excluded.guaranteed,
updated_at = excluded.updated_at;
`);
const tx = db.transaction((rows) => {
let count = 0;
for (const ch of rows) {
stmt.run({
package_id: packageId,
number: ch.number,
name: ch.name,
description: ch.description,
logo_url: ch.logo_url,
guaranteed: ch.guaranteed,
updated_at: now,
});
count++;
}
console.log(` → zapisano/zmieniono ${count} kanałów dla package_id=${packageId}`);
});
tx(channels);
}
async function main() {
console.log(`Używam bazy: ${DB_PATH}`);
const db = new Database(DB_PATH);
ensureSchema(db);
for (const feed of FEEDS) {
console.log(
`\n=== Przetwarzam ${feed.url} (source=${feed.source}, slug=${feed.slug}) ===`
);
const packageId = getPackageId(db, feed.source, feed.slug);
if (!packageId) {
console.warn(
`⚠ Brak pakietu w jambox_base_packages dla source=${feed.source}, slug="${feed.slug}". Pomijam ten feed.`
);
continue;
}
try {
const xml = await fetchXml(feed.url);
const nodes = parseNodes(xml);
console.log(`📦 Znaleziono ${nodes.length} node'ów (kanałów) w XML-u.`);
const channels = [];
for (const node of nodes) {
const mapped = mapChannelNode(node);
if (!mapped) continue;
channels.push(mapped);
}
console.log(
`🧮 Po zmapowaniu pozostaje ${channels.length} poprawnych kanałów.`
);
upsertChannelsForPackage(db, packageId, channels);
} catch (err) {
console.error(`❌ Błąd przy przetwarzaniu ${feed.url}:`, err);
}
}
db.close();
console.log("\n🎉 Import list kanałów JAMBOX zakończony.");
}
main().catch((err) => {
console.error("❌ Krytyczny błąd:", err);
process.exit(1);
});

View File

@@ -1,127 +0,0 @@
import Database from "better-sqlite3";
import { XMLParser } from "fast-xml-parser";
const DB_PATH = process.env.FUZ_DB_PATH || "./src/data/ServicesRange.db";
const SOURCES = [
{
kind: "premium",
url: "https://www.jambox.pl/xml/slownik-pakietypremium.xml",
},
{
kind: "tematyczne_plus",
url: "https://www.jambox.pl/xml/slownik-pakietytematyczneplus.xml",
},
];
async function fetchXml(url) {
const res = await fetch(url);
if (!res.ok) {
throw new Error(
`Błąd pobierania XML z ${url}: ${res.status} ${res.statusText}`
);
}
return await res.text();
}
function parseNodesFromXml(xmlText) {
const parser = new XMLParser({
ignoreAttributes: false,
attributeNamePrefix: "@_",
});
const json = parser.parse(xmlText);
let nodes = json.xml?.node ?? json.node ?? [];
if (!nodes) return [];
if (!Array.isArray(nodes)) nodes = [nodes];
return nodes;
}
function ensureSchema(db) {
db.exec(`
CREATE TABLE IF NOT EXISTS jambox_tv_addons (
tid INTEGER PRIMARY KEY,
name TEXT NOT NULL,
kind TEXT NOT NULL,
is_active INTEGER NOT NULL DEFAULT 1,
updated_at TEXT NOT NULL
);
`);
}
function upsertTvAddons(db, kind, nodes) {
const now = new Date().toISOString();
const upsert = db.prepare(`
INSERT INTO jambox_tv_addons (tid, name, kind, is_active, updated_at)
VALUES (@tid, @name, @kind, 1, @updated_at)
ON CONFLICT(tid) DO UPDATE SET
name = excluded.name,
kind = excluded.kind,
is_active = excluded.is_active,
updated_at = excluded.updated_at;
`);
const tx = db.transaction((rows) => {
let ok = 0;
for (const node of rows) {
const name = node.name ?? node.nazwa ?? node["#text"] ?? "";
const tidRaw = node.tid ?? node.id ?? null;
if (!tidRaw || !name) {
console.warn(`⚠ Pomijam node bez tid/name (kind=${kind}):`, node);
continue;
}
const tid = Number(tidRaw);
if (!Number.isFinite(tid) || tid <= 0) {
console.warn(`⚠ Pomijam node z niepoprawnym tid (kind=${kind}):`, node);
continue;
}
upsert.run({
tid,
name: String(name).trim(),
kind,
updated_at: now,
});
ok++;
}
console.log(`${kind}: zapisano/zmieniono ${ok} rekordów.`);
});
tx(nodes);
}
async function main() {
console.log(`Używam bazy: ${DB_PATH}`);
const db = new Database(DB_PATH);
ensureSchema(db);
for (const { kind, url } of SOURCES) {
console.log(`\n=== Przetwarzam ${kind} (${url}) ===`);
try {
const xml = await fetchXml(url);
const nodes = parseNodesFromXml(xml);
console.log(`📦 Znaleziono ${nodes.length} node'ów (kind=${kind})`);
upsertTvAddons(db, kind, nodes);
} catch (err) {
console.error(`❌ Błąd przy imporcie ${kind}:`, err);
}
}
db.close();
console.log("\n🎉 Import słowników pakietów dodatkowych zakończony.");
}
main().catch((err) => {
console.error("❌ Krytyczny błąd:", err);
process.exit(1);
});

View File

@@ -1,134 +0,0 @@
import Database from "better-sqlite3";
import { XMLParser } from "fast-xml-parser";
const DB_PATH = "./src/data/ServicesRange.db";
const SOURCES = [
{
source: "EVIO",
url: "https://www.jambox.pl/xml/slownik-pakietypodstawoweevio.xml",
},
{
source: "PLUS",
url: "https://www.jambox.pl/xml/slownik-pakietypodstawoweplus.xml",
},
];
function slugify(str) {
if (!str) return null;
return str
.toString()
.normalize("NFD")
.replace(/[\u0300-\u036f]/g, "")
.toLowerCase()
.trim()
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-+|-+$/g, "");
}
async function fetchXml(url) {
const res = await fetch(url);
if (!res.ok) {
throw new Error(`Błąd pobierania XML z ${url}: ${res.status} ${res.statusText}`);
}
return await res.text();
}
function parseNodesFromXml(xmlText) {
const parser = new XMLParser({
ignoreAttributes: false,
attributeNamePrefix: "@_",
});
const json = parser.parse(xmlText);
const nodes = json.xml?.node ?? json.node ?? [];
if (Array.isArray(nodes)) return nodes;
if (!nodes) return [];
return [nodes];
}
function ensureSchema(db) {
db.exec(`
CREATE TABLE IF NOT EXISTS jambox_base_packages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
source TEXT NOT NULL,
tid INTEGER NOT NULL,
name TEXT NOT NULL,
slug TEXT,
sort_order INTEGER,
updated_at TEXT NOT NULL,
UNIQUE(source, tid)
);
`);
}
function upsertBasePackages(db, source, nodes) {
const now = new Date().toISOString();
const upsert = db.prepare(`
INSERT INTO jambox_base_packages (source, tid, name, slug, sort_order, updated_at)
VALUES (@source, @tid, @name, @slug, @sort_order, @updated_at)
ON CONFLICT(source, tid) DO UPDATE SET
name = excluded.name,
slug = excluded.slug,
sort_order = excluded.sort_order,
updated_at = excluded.updated_at;
`);
const tx = db.transaction((rows) => {
rows.forEach((node, index) => {
const name = node.name ?? node["#text"] ?? "";
const tidRaw = node.tid ?? node.id ?? null;
if (!tidRaw || !name) {
console.warn(`⚠ Pomijam node bez tid/name (source=${source}):`, node);
return;
}
const tid = Number(tidRaw);
const record = {
source,
tid,
name: String(name).trim(),
slug: slugify(String(name)),
sort_order: index + 1,
updated_at: now,
};
upsert.run(record);
});
});
tx(nodes);
console.log(`${source}: zapisano/zmieniono ${nodes.length} pakietów.`);
}
async function main() {
console.log(`Używam bazy: ${DB_PATH}`);
const db = new Database(DB_PATH);
ensureSchema(db);
for (const { source, url } of SOURCES) {
console.log(`\n=== Przetwarzam ${source} (${url}) ===`);
try {
const xml = await fetchXml(url);
const nodes = parseNodesFromXml(xml);
console.log(`📦 Znaleziono ${nodes.length} node'ów dla ${source}`);
upsertBasePackages(db, source, nodes);
} catch (err) {
console.error(`❌ Błąd przy źródle ${source}:`, err);
}
}
db.close();
console.log("\n🎉 Import pakietów podstawowych zakończony.");
}
main().catch((err) => {
console.error("❌ Krytyczny błąd:", err);
process.exit(1);
});

View File

@@ -1,239 +0,0 @@
// scripts/updateJamboxChannels.js
import Database from "better-sqlite3";
import { XMLParser } from "fast-xml-parser";
const DB_PATH =
process.env.FUZ_DB_PATH || "./src/data/ServicesRange.db";
const JAMBOX_NUMBERS_URL = "https://www.jambox.pl/xml/jamboxwliczbach.xml";
/**
* Prosty helper do pobrania XML-a
*/
async function fetchXml(url) {
const res = await fetch(url);
if (!res.ok) {
throw new Error(
`Błąd pobierania XML z ${url}: ${res.status} ${res.statusText}`
);
}
return await res.text();
}
/**
* Parsowanie XML -> pojedynczy node z licznikami
*/
function parseCountsNode(xmlText) {
const parser = new XMLParser({
ignoreAttributes: false,
attributeNamePrefix: "@_",
});
const json = parser.parse(xmlText);
// dokładnie jak w podglądzie: <xml><node>...</node></xml>
const node = json.xml?.node ?? json.node ?? null;
if (!node) {
throw new Error("Nie znaleziono <node> w jamboxwliczbach.xml");
}
return node;
}
function getInt(node, key) {
const raw = node[key];
const n = Number(raw);
if (!Number.isFinite(n)) {
console.warn(`⚠ Pole ${key} ma dziwną wartość:`, raw);
return 0;
}
return n;
}
/**
* Pobierz ID cech "Liczba Kanałów" i "Liczba Kanałów HD"
* żeby nie polegać na tym, że to zawsze 7 i 8.
*/
function getChannelFeatureIds(db) {
const rows = db
.prepare(
`
SELECT id, label
FROM internet_features
WHERE label IN ('Liczba Kanałów', 'Liczba Kanałów HD');
`
)
.all();
let channelsId = null;
let channelsHdId = null;
for (const row of rows) {
if (row.label === "Liczba Kanałów") channelsId = row.id;
if (row.label === "Liczba Kanałów HD") channelsHdId = row.id;
}
if (!channelsId || !channelsHdId) {
throw new Error(
`Brak wymaganych cech w internet_features (Liczba Kanałów / Liczba Kanałów HD).`
);
}
console.log(
` Feature IDs: "Liczba Kanałów" = ${channelsId}, "Liczba Kanałów HD" = ${channelsHdId}`
);
return { channelsId, channelsHdId };
}
/**
* Zwraca mapę (source, slug) -> package_id z jambox_base_packages
*/
function getPackageIdLookup(db) {
const rows = db
.prepare(
`
SELECT id, source, slug
FROM jambox_base_packages;
`
)
.all();
const map = new Map();
for (const row of rows) {
if (!row.source || !row.slug) continue;
const key = `${row.source}::${row.slug}`;
map.set(key, row.id);
}
return map;
}
/**
* Aktualizacja rekordów w jambox_package_feature_values
* dla "Liczba Kanałów" i "Liczba Kanałów HD"
*/
function updateChannelFeatureValues(db, packages, featureIds, packageIdMap) {
const { channelsId, channelsHdId } = featureIds;
const upsertStmt = db.prepare(`
INSERT INTO jambox_package_feature_values (package_id, feature_id, value)
VALUES (@package_id, @feature_id, @value)
ON CONFLICT(package_id, feature_id) DO UPDATE SET
value = excluded.value;
`);
const tx = db.transaction((rows) => {
let total = 0;
for (const pkg of rows) {
const key = `${pkg.source}::${pkg.slug}`;
const packageId = packageIdMap.get(key);
if (!packageId) {
console.warn(
`⚠ Brak pakietu w jambox_base_packages dla: source=${pkg.source}, slug="${pkg.slug}"`
);
continue;
}
// Liczba Kanałów
upsertStmt.run({
package_id: packageId,
feature_id: channelsId,
value: String(pkg.canals),
});
// Liczba Kanałów HD
upsertStmt.run({
package_id: packageId,
feature_id: channelsHdId,
value: String(pkg.canalshd),
});
total += 2;
}
console.log(
`✅ Zaktualizowano / wstawiono ${total} wartości cech w jambox_package_feature_values.`
);
});
tx(packages);
}
async function main() {
console.log(`Używam bazy: ${DB_PATH}`);
const db = new Database(DB_PATH); // tu normalne R/W połączenie
console.log(`\n=== Pobieram JAMBOX "w liczbach" ===`);
const xml = await fetchXml(JAMBOX_NUMBERS_URL);
const node = parseCountsNode(xml);
// 🔗 Ręczne mapowanie z ich XML-a na nasze pakiety (source + slug)
const packages = [
// EVIO
{
source: "EVIO",
slug: "smart",
canals: getInt(node, "ilosc_kanalow_eviosmart"),
canalshd: getInt(node, "ilosc_kanalow_hd_eviosmart"),
},
{
source: "EVIO",
slug: "optimum",
canals: getInt(node, "ilosc_kanalow_eviooptimum"),
canalshd: getInt(node, "ilosc_kanalow_hd_eviooptimum"),
},
{
source: "EVIO",
slug: "platinum",
canals: getInt(node, "ilosc_kanalow_evioplatinum"),
canalshd: getInt(node, "ilosc_kanalow_hd_evioplatinum"),
},
// PLUS
{
source: "PLUS",
slug: "podstawowy",
canals: getInt(node, "ilosc_kanalow_podstawowy"),
canalshd: getInt(node, "ilosc_kanalow_hd_podstawowy"),
},
{
source: "PLUS",
slug: "korzystny",
canals: getInt(node, "ilosc_kanalow_korzystny"),
canalshd: getInt(node, "ilosc_kanalow_hd_korzystny"),
},
{
source: "PLUS",
slug: "bogaty",
canals: getInt(node, "ilosc_kanalow_bogaty"),
canalshd: getInt(node, "ilosc_kanalow_hd_bogaty"),
},
];
console.log("🧮 Pakiety do aktualizacji:");
packages.forEach((p) => {
console.log(
` - ${p.source} / ${p.slug}: ${p.canals} kanałów, ${p.canalshd} HD`
);
});
// 🔹 Pobierz ID cech i mapę pakietów
const featureIds = getChannelFeatureIds(db);
const packageIdMap = getPackageIdLookup(db);
// 🔹 Aktualizacja tabeli jambox_package_feature_values
updateChannelFeatureValues(db, packages, featureIds, packageIdMap);
db.close();
console.log("\n🎉 Aktualizacja liczby kanałów zakończona.");
}
main().catch((err) => {
console.error("❌ Krytyczny błąd:", err);
process.exit(1);
});