#!/usr/bin/env python3 """ LayerNX © – LNX Search: SearXNG beim Image-Build auf LayerNX umbauen. python3 lnx_brand.py * Theme: lnx-search.css wird an das SearXNG-Theme angehängt (Wiki-Stil, Schriften, Farben) * Startseite: Logo „LayerNX Search“, angemeldet stattdessen eine Begrüßung („Guten Abend, Moritz“) + Dienste * Logo: eigenes SVG-Logo (lnx-logo.html / lnx-mark.html) statt SearXNG-Logo * Kopfzeile: Konto-Link (accounts.layernx.de), Cookie-Hinweis, Footer mit LayerNX-Domains * Dateien: Schriften, Win7-Symbole, Skripte nach static/themes/simple/lnx/ * Favicons/App-Icons: LayerNX * vorkomprimierte .gz-Dateien werden neu erzeugt, veraltete .br entfernt „Powered by SearXNG“ und der Link zum Original-Quellcode bleiben (AGPL); zusätzlich verlinkt der Footer den Quellcode dieser Version (rechtliches.layernx.de/credits#lnx-search). Findet das Skript eine Stelle in einer neueren SearXNG-Version nicht, gibt es eine Warnung aus. """ from __future__ import annotations import base64 import gzip import re import shutil import sys from pathlib import Path warnings = 0 def warn(msg: str) -> None: global warnings warnings += 1 print(f"[lnx-brand] WARNUNG: {msg}", file=sys.stderr) def recompress(path: Path) -> None: """Whitenoise liefert vorkomprimierte Varianten aus – die müssen zur Datei passen.""" br = path.with_name(path.name + ".br") if br.exists(): br.unlink() gz = path.with_name(path.name + ".gz") if path.suffix in (".css", ".js", ".svg", ".html", ".json"): gz.write_bytes(gzip.compress(path.read_bytes(), 9, mtime=0)) elif gz.exists(): gz.unlink() def replace_in(path: Path, old: str, new: str, count: int = 1, regex: bool = False) -> None: text = path.read_text(encoding="utf-8") if regex: out, n = re.subn(old, new, text, count=count, flags=re.S) if not n: warn(f"Muster '{old[:50]}' nicht gefunden in {path.name}") return else: if old not in text: warn(f"'{old[:50]}' nicht gefunden in {path.name}") return out = text.replace(old, new, count) path.write_text(out, encoding="utf-8") def copy_tree(src: Path, dst: Path) -> int: n = 0 if not src.is_dir(): warn(f"{src} fehlt") return 0 for f in src.rglob("*"): if f.is_file(): t = dst / f.relative_to(src) t.parent.mkdir(parents=True, exist_ok=True) shutil.copyfile(f, t) recompress(t) n += 1 return n FOOTER = """""" HEAD_SCRIPTS = """ """ ACCOUNT_LINK = """ """ def main(searx: Path, lnx: Path) -> int: static = searx / "static" / "themes" / "simple" img = static / "img" tpl = searx / "templates" / "simple" brand = lnx / "brand" out = static / "lnx" # 1. Theme-CSS anhängen css = (lnx / "lnx-search.css").read_text(encoding="utf-8") for name in ("sxng-ltr.min.css", "sxng-rtl.min.css"): p = static / name if p.exists(): p.write_text(p.read_text(encoding="utf-8") + "\n" + css, encoding="utf-8") recompress(p) else: warn(f"{name} fehlt") # 2. Eigene Dateien: Skripte, Kritzeleien, Schriften, Symbole, Cookie-Hinweis n = copy_tree(lnx / "static", out) n += copy_tree(lnx / "fonts", out / "fonts") n += copy_tree(lnx / "icons", out / "icons") n += copy_tree(lnx / "consent" / "js", out / "js") n += copy_tree(lnx / "consent" / "css", out / "css") print(f"[lnx-brand] {n} Dateien nach static/themes/simple/lnx kopiert") # 3. Favicons & App-Icons fav192 = (brand / "favicon-192.png").read_bytes() fav_svg = ('' f'') for name, data in {"favicon.png": fav192, "favicon.svg": fav_svg.encode(), "192.png": fav192, "512.png": (brand / "nx.png").read_bytes(), "searxng.png": (brand / "layernx-wordmark.png").read_bytes(), "searxng.svg": (brand / "layernx-logo.svg").read_bytes()}.items(): p = img / name p.write_bytes(data) recompress(p) # 4. Templates: Startseite, Logos for name in ("index.html", "lnx-logo.html", "lnx-mark.html"): shutil.copyfile(lnx / "templates" / name, tpl / name) # Logo neben dem Suchfeld (wird „without context“ eingebunden -> reines SVG) shutil.copyfile(lnx / "templates" / "lnx-mark.html", tpl / "searxng-wordmark.min.svg") replace_in(tpl / "search.html", "", "") replace_in(tpl / "page_with_header.html", r']*>', "{% include 'simple/lnx-logo.html' %}", regex=True) # 5. base.html: Meta, Skripte, Konto-Link, Footer base = tpl / "base.html" replace_in(base, r'', '', regex=True) replace_in(base, r"\{% endblock %\}\{\{ instance_name \}\}", "{% endblock %}LayerNX Search", regex=True) replace_in(base, r'', '', regex=True) replace_in(base, r"()", r"\1" + HEAD_SCRIPTS.replace("\\", "\\\\"), regex=True) replace_in(base, " \n {% block header %}", " " + ACCOUNT_LINK + "\n {% block header %}") replace_in(base, r"", FOOTER.replace("\\", "\\\\"), regex=True) print(f"[lnx-brand] LayerNX-Theme angewendet ({warnings} Warnung(en))") return 0 if __name__ == "__main__": sys.exit(main(Path(sys.argv[1]), Path(sys.argv[2])))