#!/usr/bin/env python3
"""
Willkommensseite-Generierung für CANCOM Simple Signage.
───────────────────────────────────────────────────────
Diese Hilfsdatei sucht anhand von Kundennamen Logos (via OpenAI + Brandfetch),
generiert daraus eine HTML-Willkommensseite und fügt sie in die Playlists aller
Zielscreens ein (Screens mit "welcome_enabled", Fallback "lobby").
"""
import os
import json
from openai import OpenAI
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
MEDIA_DIR = os.path.join(BASE_DIR, "media")
WELCOME_FILENAME = "welcome.html"
# -------------------------------------------------
# OpenAI-Client
# -------------------------------------------------
def get_openai_client():
api_key = os.environ.get("OPENAI_API_KEY")
if not api_key:
raise ValueError("OPENAI_API_KEY environment variable not set")
return OpenAI(api_key=api_key)
# -------------------------------------------------
# Brandfetch-Logo-Suche
# -------------------------------------------------
def get_brandfetch_logo(domain):
"""Versucht, ein Firmenlogo über Brandfetch CDN abzurufen.
Baut eine direkte CDN-URL: https://cdn.brandfetch.io/{domain}/logo"""
print(f"🔍 Generate - Versuche Brandfetch für Domain: {domain}")
logo_url = f"https://cdn.brandfetch.io/{domain}/logo?c=1idyd4Tpb2nKaXIIc8T"
print(f"✅ Generate - Brandfetch Logo gefunden: {logo_url}")
return logo_url
# -------------------------------------------------
# Logo-Ermittlung via OpenAI
# -------------------------------------------------
def search_customer_logo(customer_name):
"""
Sucht das Logo eines Kunden über einen zweistufigen Prozess:
1. OpenAI GPT-4 ermittelt die Domain des Unternehmens
2. Brandfetch CDN liefert das Logo zur Domain
Gibt die Logo-URL zurück oder None, wenn nichts gefunden wurde.
"""
print(f"✅ Generate - Suche nach Kundenlogo für: {customer_name}")
try:
client = get_openai_client()
response = client.chat.completions.create(
model="gpt-4",
messages=[
{
"role": "system",
"content": """Du bist ein Experte für die Suche nach Domain Namen. Du findest anhand des Firmennamens die richtige Domain dazu
Gib nur den Domain Namen zurück, keine weiteren Informationen.
FALLBACK: Wenn du kein direkt Aufrufbare Domain findest, antworte mit "FALLBACK"."""
},
{
"role": "user",
"content": f"Finde die Domain für dasUnternehmen: {customer_name}"
}
],
temperature=0.1,
max_tokens=100
)
logo_url = response.choices[0].message.content.strip()
print(f"🔍 OpenAI Logo-URL: {logo_url}")
if logo_url.upper() == "FALLBACK":
logo_url = None
if logo_url:
brandfetch_url = get_brandfetch_logo(logo_url)
if brandfetch_url:
return brandfetch_url
return None
except Exception as e:
print(f"❌ Logo search error: {e}")
return None
# -------------------------------------------------
# HTML-Generierung
# -------------------------------------------------
def md_to_html(text):
"""Wandelt einfaches Markdown in HTML um (fett, kursiv, code, zeilenumbrüche, aufzählungen)."""
import re
text = re.sub(r'\*\*(.+?)\*\*', r'\1', text)
text = re.sub(r'\*(.+?)\*', r'\1', text)
text = re.sub(r'`(.+?)`', r'\1', text)
lines = text.split('\n')
parts = []
in_list = False
for line in lines:
stripped = line.strip()
is_li = stripped.startswith('- ') or stripped.startswith('* ')
if is_li:
if not in_list:
parts.append('
')
in_list = True
parts.append(f'
{stripped[2:]}
')
else:
if in_list:
parts.append('
')
in_list = False
if stripped:
parts.append(stripped)
parts.append(' ')
if in_list:
parts.append('')
if parts and parts[-1] == ' ':
parts.pop()
return ''.join(parts)
def generate_welcome_html(customer_names, logo_urls, contacts=None, site="stuttgart", background_url=None):
"""
Generiert den HTML-Code für die Willkommensseite.
- Links oben: CANCOM-Logo + "meets" + Kundenlogos
- Unter jedem Logo: Ansprechpartner-Kontaktdaten (Landscape)
- Portrait: Kontaktdaten links neben den Logos
customer_names: Liste der Kundennamen (max. 3)
logo_urls: Liste der Logo-URLs (gleiche Reihenfolge)
contacts: Liste der Ansprechpartner-Texte (Markdown, optional)
site: Standort-Name für die Anzeige
background_url: URLs zum Hintergrundbild (None → Standard-Wallpaper)
"""
site_display = site.capitalize()
names_str = ", ".join(customer_names) if customer_names else "Unbekannt"
print(f"✅ Generate - Willkommensseite wird generiert für: {names_str} (site: {site})")
site_display = site.capitalize()
if contacts is None:
contacts = [""] * len(customer_names)
bg_image = background_url if background_url else "/static/wallpaper.png"
cancom_svg = ''''''
logo_html = ""
for i, (name, url) in enumerate(zip(customer_names, logo_urls)):
if url:
contact_html = ""
if i < len(contacts) and contacts[i]:
contact_html = f'''
{md_to_html(contacts[i])}
'''
logo_html += f'''
{contact_html}
'''
html_content = f"""
Willkommen bei CANCOM
{cancom_svg}
meets
{logo_html}
Herzlich Willkommen in unserer
Niederlassung {site_display}!
"""
return html_content
# -------------------------------------------------
# Speichern + Playlist-Einfügung
# -------------------------------------------------
def welcome_target_screens(config, site):
"""Ermittelt die Screens eines Standorts, auf denen die Willkommensseite laufen soll.
Ein Screen ist Ziel, wenn "welcome_enabled" gesetzt ist. Ist der Schluessel bei
keinem Screen des Standorts vorhanden, greift das alte Verhalten: nur "lobby".
Damit bleiben bestehende Standorte ohne Konfigurationsaufwand unveraendert."""
screens = config.get("sites", {}).get(site, {}).get("screens", {})
configured = [name for name, cfg in screens.items()
if isinstance(cfg, dict) and cfg.get("welcome_enabled")]
if configured:
return sorted(configured)
# Kein Screen markiert: nur dann auf "lobby" zurückfallen, wenn noch nirgends
# eine bewusste Entscheidung getroffen wurde. Wurde das Häkchen überall
# entfernt, ist "keine Screens" das gewollte Ergebnis.
entschieden = any(isinstance(cfg, dict) and "welcome_enabled" in cfg
for cfg in screens.values())
if entschieden:
return []
return ["lobby"] if "lobby" in screens else []
def save_welcome_page(customer_names, logo_urls, contacts=None, site="stuttgart",
background_url=None, screens=None):
"""Speichert die generierte Willkommensseite als welcome.html in den Zielscreens.
screens: Liste der Screen-Namen; None → Zielscreens aus der Konfiguration.
contacts: Liste der Ansprechpartner-Texte (Markdown, optional)
background_url: Benutzerdefiniertes Hintergrundbild oder None (Standard).
Gibt den Dateinamen zurück oder None bei Fehler.
Die Seite referenziert alle Bilder absolut (/static/..., /media//...),
ist also ortsunabhängig - eine Kopie je Screen rendert identisch."""
try:
from app import load_config
if screens is None:
screens = welcome_target_screens(load_config(), site)
if not screens:
print(f"⚠️ Generate - Kein Zielscreen für die Willkommensseite (site: {site})")
return None
html_content = generate_welcome_html(customer_names, logo_urls, contacts=contacts,
site=site, background_url=background_url)
for screen in screens:
welcome_dir = os.path.join(MEDIA_DIR, site, screen)
os.makedirs(welcome_dir, exist_ok=True)
filepath = os.path.join(os.path.abspath(welcome_dir), WELCOME_FILENAME)
with open(filepath, "w", encoding="utf-8") as f:
f.write(html_content)
if os.path.getsize(filepath) == 0:
raise IOError("File not written")
print(f"✅ Generate - Willkommensseite gespeichert in: {filepath}")
names_str = ", ".join(customer_names)
print(f"✅ Generate - Willkommensseite generiert für: {names_str} "
f"(site: {site}, screens: {', '.join(screens)})")
return WELCOME_FILENAME
except Exception as e:
print(f"❌ Error saving welcome page: {e}")
return None
def _strip_welcome(playlist, filename):
"""Entfernt alle welcome.html-Einträge aus einer Playlist (String- und Dict-Form)."""
playlist[:] = [
item for item in playlist
if not (
(isinstance(item, str) and item == filename) or
(isinstance(item, dict) and item.get("name") == filename)
)
]
def add_customer_to_playlists(html_filename, site="stuttgart", screens=None, valid_until=None):
"""Setzt die Willkommensseite an Position 0 der Playlist jedes Zielscreens.
screens: Liste der Zielscreens; None → aus der Konfiguration ermitteln.
valid_until: optionales Datum "YYYY-MM-DD". Ist es gesetzt, bekommt der
Eintrag eine Zeitsteuerung und verschwindet danach von selbst.
Aus allen übrigen Screens des Standorts wird die Seite entfernt, damit ein
abgewählter Screen nicht mit einer alten Begrüßung stehen bleibt.
Importiert load_config/save_config aus app.py (kein Zirkelimport)."""
try:
from app import load_config, save_config
config = load_config()
site_cfg = config.setdefault("sites", {}).setdefault(site, {"screens": {}})
all_screens = site_cfg.setdefault("screens", {})
if screens is None:
screens = welcome_target_screens(config, site)
# Nur Screens, die es wirklich gibt - kein stillschweigendes Anlegen
targets = [name for name in screens if name in all_screens]
entry = {"name": html_filename, "enabled": True}
if valid_until:
# Ganztaegig bis einschliesslich valid_until
entry["schedule"] = {
"mode": "daily",
"start": "00:00",
"end": "23:59",
"valid_until": valid_until
}
for name, screen_cfg in all_screens.items():
if not isinstance(screen_cfg, dict):
continue
playlist = screen_cfg.setdefault("playlist", [])
_strip_welcome(playlist, html_filename)
if name in targets:
playlist.insert(0, dict(entry))
save_config(config)
if targets:
print(f"✅ Generate - Willkommensseite platziert (site: {site}, "
f"screens: {', '.join(targets)})")
else:
print(f"⚠️ Generate - Kein Zielscreen vorhanden (site: {site})")
return bool(targets)
except Exception as e:
print(f"❌ Generate - Error adding to playlist: {e}")
return False