generated from Dicken/dickendock
round24: Favicon-Groesse und Wort-Erkennung verbessert, ausgeblendet-Badge entfernt, Proxmox-Port-Bug gefixt, Suche um Geraete und Spaeter-lesen erweitert
This commit is contained in:
@@ -81,19 +81,35 @@ export async function searchIconDb(query: string, limit = 24): Promise<IconSearc
|
||||
}
|
||||
|
||||
/**
|
||||
* Generische Infrastruktur-/Container-Begriffe, die beim Zerlegen eines
|
||||
* Dienstnamens in Wörter ignoriert werden - "LXC Adguard" soll als "Adguard"
|
||||
* gesucht werden, nicht als "LXC" (das nie ein sinnvoller Icon-Treffer wäre).
|
||||
* Begriffe, die beim Zerlegen eines Dienstnamens in Wörter komplett
|
||||
* ignoriert werden - reine Infrastruktur-/Container-Präfixe, nie ein
|
||||
* sinnvoller Icon-Treffer ("LXC Adguard" soll nach "Adguard" suchen, nicht
|
||||
* nach "LXC").
|
||||
*/
|
||||
const STOPWORDS = new Set([
|
||||
"lxc", "vm", "docker", "container", "server", "service", "app", "box",
|
||||
"host", "node", "instance", "srv", "ct", "the", "der", "die", "das",
|
||||
"lxc", "vm", "docker", "container", "srv", "ct", "instance",
|
||||
"the", "der", "die", "das",
|
||||
]);
|
||||
|
||||
/**
|
||||
* Generische Kategorie-Wörter - beschreiben WAS etwas ist, nicht WELCHES
|
||||
* Produkt es ist ("Cam Reolink Keller" soll primär nach "Reolink" suchen,
|
||||
* nicht nach "Cam"). Werden nicht komplett verworfen (manchmal ist die
|
||||
* Kategorie das einzige brauchbare Wort), aber erst als letzter Ausweg
|
||||
* versucht, nachdem alle spezifischeren Wörter erfolglos blieben.
|
||||
*/
|
||||
const GENERIC_WORDS = new Set([
|
||||
"cam", "camera", "media", "print", "printer", "bridge", "hub", "sync",
|
||||
"box", "sensor", "plug", "light", "dock", "home", "server", "service",
|
||||
"app", "web", "tv", "cast", "stream", "monitor", "gateway", "node",
|
||||
"backup", "storage", "network", "device", "system",
|
||||
]);
|
||||
|
||||
function normalize(s: string): string {
|
||||
return s.toLowerCase().trim().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
||||
}
|
||||
|
||||
/** Wörter in ursprünglicher Reihenfolge (für Bigramme wie "philips-hue" wichtig). */
|
||||
function significantWords(name: string): string[] {
|
||||
return name
|
||||
.split(/[\s_-]+/)
|
||||
@@ -101,33 +117,58 @@ function significantWords(name: string): string[] {
|
||||
.filter((w) => w.length >= 3 && !STOPWORDS.has(w));
|
||||
}
|
||||
|
||||
async function exactMatch(candidate: string, names: string[]): Promise<string | null> {
|
||||
const nameSet = new Set(names);
|
||||
if (nameSet.has(candidate)) return candidate;
|
||||
/** Aufeinanderfolgende Wortpaare in Original-Reihenfolge, z. B. "philips hue bridge" -> ["philips-hue", "hue-bridge"]. */
|
||||
function bigrams(words: string[]): string[] {
|
||||
const out: string[] = [];
|
||||
for (let i = 0; i < words.length - 1; i++) out.push(`${words[i]}-${words[i + 1]}`);
|
||||
return out;
|
||||
}
|
||||
|
||||
/** Spezifische Wörter zuerst (längste zuerst - meist Markennamen), generische zuletzt. */
|
||||
function byPriority(words: string[]): string[] {
|
||||
const specific = words.filter((w) => !GENERIC_WORDS.has(w)).sort((a, b) => b.length - a.length);
|
||||
const generic = words.filter((w) => GENERIC_WORDS.has(w)).sort((a, b) => b.length - a.length);
|
||||
return [...specific, ...generic];
|
||||
}
|
||||
|
||||
function exactMatch(candidate: string, names: string[]): string | null {
|
||||
if (names.includes(candidate)) return candidate;
|
||||
const collapsed = candidate.replace(/-/g, "");
|
||||
const found = names.find((n) => n.replace(/-/g, "") === collapsed);
|
||||
return found ?? null;
|
||||
return names.find((n) => n.replace(/-/g, "") === collapsed) ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sucht den besten Treffer in der Icon-Datenbank für einen Dienst-/
|
||||
* Software-Namen ("Plex", "LXC Adguard", "Pi-hole") - für den Fall, dass der
|
||||
* Scanner selbst kein Favicon gefunden hat (siehe networkScanner.ts). Läuft
|
||||
* unbeaufsichtigt während eines Scans, liefert deshalb nur EINEN Treffer
|
||||
* (den ersten gefundenen: erst der ganze Name, dann Wort für Wort, generische
|
||||
* Begriffe wie "LXC"/"Docker" werden dabei übersprungen) statt einer Auswahl.
|
||||
* Software-Namen ("Plex", "LXC Adguard", "Philips Hue Bridge") - für den
|
||||
* Fall, dass der Scanner selbst kein Favicon gefunden hat (siehe
|
||||
* networkScanner.ts). Läuft unbeaufsichtigt während eines Scans, liefert
|
||||
* deshalb nur EINEN Treffer und ausschließlich über exakte (bzw. nach
|
||||
* Entfernen von Bindestrichen exakte) Übereinstimmungen - keine
|
||||
* Teilstring-Suche, die wäre für eine automatische, unbeaufsichtigte
|
||||
* Zuordnung zu unzuverlässig.
|
||||
*
|
||||
* Reihenfolge: ganzer Name -> Wortpaare in Originalreihenfolge (erkennt
|
||||
* "Philips Hue" als Einheit) -> einzelne Wörter, spezifische (längere,
|
||||
* nicht-generische) zuerst.
|
||||
*/
|
||||
export async function findBestIconMatch(name: string): Promise<string | null> {
|
||||
const names = await getIconNames();
|
||||
|
||||
const wholeName = normalize(name);
|
||||
if (wholeName.length >= 2) {
|
||||
const match = await exactMatch(wholeName, names);
|
||||
const match = exactMatch(wholeName, names);
|
||||
if (match) return `${CDN_BASE}/${match}.png`;
|
||||
}
|
||||
|
||||
for (const word of significantWords(name)) {
|
||||
const match = await exactMatch(word, names);
|
||||
const words = significantWords(name);
|
||||
|
||||
for (const pair of bigrams(words)) {
|
||||
const match = exactMatch(pair, names);
|
||||
if (match) return `${CDN_BASE}/${match}.png`;
|
||||
}
|
||||
|
||||
for (const word of byPriority(words)) {
|
||||
const match = exactMatch(word, names);
|
||||
if (match) return `${CDN_BASE}/${match}.png`;
|
||||
}
|
||||
|
||||
@@ -135,11 +176,17 @@ export async function findBestIconMatch(name: string): Promise<string | null> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Wie findBestIconMatch, liefert aber ALLE plausiblen Kandidaten (ganzer
|
||||
* Name + jedes einzelne aussagekräftige Wort, exakte Treffer UND
|
||||
* Teilstring-Treffer) statt nur den ersten - für die interaktive
|
||||
* "Fehlende Favicons ergänzen"-Übersicht, wo der Mensch selbst auswählt,
|
||||
* welcher Treffer stimmt, statt dass automatisch geraten wird.
|
||||
* Wie findBestIconMatch, liefert aber ALLE plausiblen Kandidaten statt nur
|
||||
* den ersten - für die interaktive "Fehlende Favicons ergänzen"-Übersicht,
|
||||
* wo der Mensch selbst auswählt, welcher Treffer stimmt.
|
||||
*
|
||||
* Reihenfolge: erst alle exakten Treffer (ganzer Name, Wortpaare, einzelne
|
||||
* Wörter nach Priorität), DANACH erst - falls noch Platz bis zum Limit ist -
|
||||
* Teilstring-Treffer, wieder erst über spezifische Wörter. Generische
|
||||
* Kategorie-Wörter (siehe GENERIC_WORDS) werden für die Teilstring-Suche nur
|
||||
* herangezogen, wenn die spezifischen Wörter GAR NICHTS geliefert haben -
|
||||
* genau das war das gemeldete Problem ("Cam Blink Sync" fand nur
|
||||
* Kamera-Zubehör über "Cam" statt "Blink").
|
||||
*/
|
||||
export async function suggestIconsForServiceName(name: string, limit = 8): Promise<IconSearchResult[]> {
|
||||
const names = await getIconNames();
|
||||
@@ -147,20 +194,31 @@ export async function suggestIconsForServiceName(name: string, limit = 8): Promi
|
||||
const results: IconSearchResult[] = [];
|
||||
|
||||
function addIfNew(iconName: string) {
|
||||
if (seen.has(iconName)) return;
|
||||
if (seen.has(iconName) || results.length >= limit) return;
|
||||
seen.add(iconName);
|
||||
results.push({ name: iconName, url: `${CDN_BASE}/${iconName}.png` });
|
||||
}
|
||||
|
||||
const words = significantWords(name);
|
||||
const specificWords = byPriority(words).filter((w) => !GENERIC_WORDS.has(w));
|
||||
const genericWords = byPriority(words).filter((w) => GENERIC_WORDS.has(w));
|
||||
|
||||
const wholeName = normalize(name);
|
||||
if (wholeName.length >= 2) {
|
||||
const exact = await exactMatch(wholeName, names);
|
||||
const exact = exactMatch(wholeName, names);
|
||||
if (exact) addIfNew(exact);
|
||||
}
|
||||
for (const pair of bigrams(words)) {
|
||||
const exact = exactMatch(pair, names);
|
||||
if (exact) addIfNew(exact);
|
||||
}
|
||||
for (const word of specificWords) {
|
||||
const exact = exactMatch(word, names);
|
||||
if (exact) addIfNew(exact);
|
||||
}
|
||||
|
||||
for (const word of significantWords(name)) {
|
||||
const exact = await exactMatch(word, names);
|
||||
if (exact) addIfNew(exact);
|
||||
// Teilstring-Suche nur über spezifische Wörter, solange noch Platz ist.
|
||||
for (const word of specificWords) {
|
||||
if (results.length >= limit) break;
|
||||
for (const n of names) {
|
||||
if (results.length >= limit) break;
|
||||
@@ -168,5 +226,21 @@ export async function suggestIconsForServiceName(name: string, limit = 8): Promi
|
||||
}
|
||||
}
|
||||
|
||||
return results.slice(0, limit);
|
||||
// Generische Wörter (z. B. "cam", "media") nur als allerletzter Ausweg,
|
||||
// wenn spezifische Wörter überhaupt nichts geliefert haben.
|
||||
if (results.length === 0) {
|
||||
for (const word of genericWords) {
|
||||
const exact = exactMatch(word, names);
|
||||
if (exact) addIfNew(exact);
|
||||
}
|
||||
for (const word of genericWords) {
|
||||
if (results.length >= limit) break;
|
||||
for (const n of names) {
|
||||
if (results.length >= limit) break;
|
||||
if (n.includes(word)) addIfNew(n);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user