generated from Dicken/dickendock
111 lines
3.9 KiB
TypeScript
111 lines
3.9 KiB
TypeScript
import { request } from "node:https";
|
|
|
|
const TREE_URL = "https://raw.githubusercontent.com/homarr-labs/dashboard-icons/main/tree.json";
|
|
const CDN_BASE = "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/png";
|
|
const CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 1 Tag
|
|
|
|
let cachedNames: string[] | null = null;
|
|
let cachedAt = 0;
|
|
let inFlight: Promise<string[]> | null = null;
|
|
|
|
function fetchJson(url: string): Promise<unknown> {
|
|
return new Promise((resolve, reject) => {
|
|
const req = request(url, { timeout: 8000 }, (res) => {
|
|
if (res.statusCode && res.statusCode >= 400) {
|
|
reject(new Error(`HTTP ${res.statusCode}`));
|
|
res.resume();
|
|
return;
|
|
}
|
|
const chunks: Buffer[] = [];
|
|
res.on("data", (c) => chunks.push(c));
|
|
res.on("end", () => {
|
|
try {
|
|
resolve(JSON.parse(Buffer.concat(chunks).toString("utf-8")));
|
|
} catch (e) {
|
|
reject(e);
|
|
}
|
|
});
|
|
});
|
|
req.on("timeout", () => req.destroy(new Error("Timeout")));
|
|
req.on("error", reject);
|
|
req.end();
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Lädt (und cacht 24h im Speicher) die Liste aller Icon-Namen aus dem
|
|
* homarr-labs/dashboard-icons-Projekt (freie Icon-Sammlung speziell für
|
|
* Homelab-/Selfhosted-Dashboards, via jsDelivr-CDN ausgeliefert).
|
|
*
|
|
* Bewusst als einzige Stelle im Projekt, die tatsächlich einen externen
|
|
* Dienst kontaktiert (abgesehen vom eigentlichen Aufruf der Dienste selbst)
|
|
* - das Icon-Datenbank-Feature wurde ausdrücklich so gewünscht. Schlägt der
|
|
* Abruf fehl (kein Internetzugang, Dienst nicht erreichbar), liefert die
|
|
* Suche einfach keine Treffer statt einen Fehler zu werfen.
|
|
*/
|
|
async function getIconNames(): Promise<string[]> {
|
|
if (cachedNames && Date.now() - cachedAt < CACHE_TTL_MS) return cachedNames;
|
|
if (inFlight) return inFlight;
|
|
|
|
inFlight = (async () => {
|
|
try {
|
|
const tree = (await fetchJson(TREE_URL)) as { png?: string[] };
|
|
const names = (tree.png ?? []).map((f) => f.replace(/\.png$/, ""));
|
|
cachedNames = names;
|
|
cachedAt = Date.now();
|
|
return names;
|
|
} catch {
|
|
// Kein Absturz, nur keine Treffer - die restliche Favicon-Auswahl
|
|
// (bereits im System verwendete Favicons) funktioniert unabhängig davon weiter.
|
|
return cachedNames ?? [];
|
|
} finally {
|
|
inFlight = null;
|
|
}
|
|
})();
|
|
|
|
return inFlight;
|
|
}
|
|
|
|
export interface IconSearchResult {
|
|
name: string;
|
|
url: string;
|
|
}
|
|
|
|
export async function searchIconDb(query: string, limit = 24): Promise<IconSearchResult[]> {
|
|
const q = query.trim().toLowerCase();
|
|
if (q.length < 2) return [];
|
|
|
|
const names = await getIconNames();
|
|
const matches = names.filter((n) => n.includes(q)).slice(0, limit);
|
|
return matches.map((name) => ({ name, url: `${CDN_BASE}/${name}.png` }));
|
|
}
|
|
|
|
/**
|
|
* Sucht den besten Treffer in der Icon-Datenbank für einen Dienst-/
|
|
* Software-Namen ("Plex", "Pi-hole", "Adguard Home") - für den Fall, dass
|
|
* der Scanner selbst kein Favicon gefunden hat (siehe networkScanner.ts).
|
|
* Bewusst konservativ: nur ein Treffer, wenn der normalisierte Name exakt
|
|
* (oder nach Entfernen von Bindestrichen) übereinstimmt - eine reine
|
|
* Teilstring-Suche würde bei kurzen/generischen Namen zu viele falsche
|
|
* Zuordnungen liefern (z. B. "nas" -> "nas4free" wäre noch plausibel, aber
|
|
* "web" könnte alles Mögliche treffen).
|
|
*/
|
|
export async function findBestIconMatch(name: string): Promise<string | null> {
|
|
const normalized = name
|
|
.toLowerCase()
|
|
.trim()
|
|
.replace(/[^a-z0-9]+/g, "-")
|
|
.replace(/^-+|-+$/g, "");
|
|
if (normalized.length < 2) return null;
|
|
|
|
const names = await getIconNames();
|
|
const nameSet = new Set(names);
|
|
if (nameSet.has(normalized)) return `${CDN_BASE}/${normalized}.png`;
|
|
|
|
const collapsed = normalized.replace(/-/g, "");
|
|
const exactCollapsed = names.find((n) => n.replace(/-/g, "") === collapsed);
|
|
if (exactCollapsed) return `${CDN_BASE}/${exactCollapsed}.png`;
|
|
|
|
return null;
|
|
}
|