Files
signage/app.py
2026-06-07 12:35:10 +02:00

922 lines
32 KiB
Python
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""
CANCOM Simple Signage Flask-App für Digital-Signage-Anzeigen.
──────────────────────────────────────────────────────────────
Diese App verwaltet mehrere Standorte (sites) mit jeweils mehreren
Screens. Jeder Screen hat eine eigene Playlist aus Bildern, Videos,
HTML-Seiten und URLs. Optional kann eine globale Priority-Playlist
zugeschaltet werden, die auf allen Playern abwechselnd eingeblendet wird.
"""
import os
import json
import hashlib
import generate_welcome_page # Logo-Suche und Willkommensseite-Generierung
from datetime import datetime
from flask import (
Flask, jsonify, render_template,
send_from_directory, redirect,
request, abort
)
from flask_login import (
LoginManager, login_user,
login_required, logout_user,
UserMixin
)
from werkzeug.utils import secure_filename
# -------------------------------------------------
# Grundkonfiguration
# -------------------------------------------------
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
MEDIA_DIR = os.path.join(BASE_DIR, "media") # Medien-Dateien je Standort/Screen
CONFIG_FILE = os.path.join(BASE_DIR, "config.json") # Persistente Konfiguration
APP_VERSION = "5.1.0"
UPLOAD_EXTENSIONS = {".jpg", ".jpeg", ".png", ".mp4"}
app = Flask(__name__)
app.secret_key = "CHANGE_THIS_SECRET!!!"
login_manager = LoginManager(app)
login_manager.login_view = "login" # Nicht eingeloggte User werden zum Login umgeleitet
# -------------------------------------------------
# Config-Helfer
# -------------------------------------------------
def load_config():
"""Liest die config.json und gibt das Dict zurück. Legt ein leeres an, falls nicht vorhanden."""
if not os.path.exists(CONFIG_FILE):
return {"admin": {}, "sites": {}}
with open(CONFIG_FILE) as f:
return json.load(f)
def save_config(cfg):
"""Schreibt das Config-Dict zurück in die config.json."""
with open(CONFIG_FILE, "w") as f:
json.dump(cfg, f, indent=2)
f.flush()
def get_site_list(cfg):
"""Gibt eine sortierte Liste aller konfigurierten Standort-Namen zurück."""
return sorted(cfg.get("sites", {}).keys())
def get_screen_config(cfg, site, screen):
"""Holt die Konfiguration für einen bestimmten Screen.
Erzeugt automatisch fehlende Dict-Ebenen (sites → site → screens → screen)."""
return (
cfg
.setdefault("sites", {})
.setdefault(site, {})
.setdefault("screens", {})
.setdefault(screen, {})
)
def is_url(item):
"""Prüft, ob ein Playlist-Eintrag eine URL ist (String mit http/https) oder ein Dict mit "url"-Key."""
if isinstance(item, str):
return item.startswith("http://") or item.startswith("https://")
return isinstance(item, dict) and "url" in item
def normalize_url(item):
"""Normalisiert einen URL-Eintrag zu einem Dict mit "url" und "zoom".
Zoom wird aus Dict übernommen oder defaultmäßig auf 1.0 gesetzt."""
if isinstance(item, str):
return {"url": item, "zoom": 1.0}
if isinstance(item, dict):
return {"url": item["url"], "zoom": item.get("zoom", 1.0)}
return None
def playlist_item_name(item):
"""Ermittelt den Anzeigenamen eines Playlist-Eintrags (String → direkt, Dict → "url" oder "name")."""
if isinstance(item, str):
return item
if isinstance(item, dict):
return item.get("url") or item.get("name", "")
return ""
def playlist_item_enabled(item):
"""Prüft, ob ein Playlist-Eintrag aktiviert ist.
Strings sind immer aktiv, Dicts haben ein optionales "enabled"-Feld (default: True)."""
if isinstance(item, (str,)):
return True
if isinstance(item, dict):
return item.get("enabled", True)
return True
# -------------------------------------------------
# Priority-Playlist-Helfer
# -------------------------------------------------
def load_priority_files():
"""Lädt die Priority-Playlist aus config.json plus die dazugehörigen Dateien
aus media/priority/. Gibt eine Liste von Dicts mit name/type/size/enabled zurück.
Neue Dateien im Ordner, die noch nicht in der Playlist sind, werden angehängt."""
cfg = load_config()
prio_dir = os.path.join(MEDIA_DIR, "priority")
os.makedirs(prio_dir, exist_ok=True)
priority_files = []
prio_playlist = cfg.get("priority", {}).get("playlist", [])
# 1. Einträge aus der config.json-Playlist
for item in prio_playlist:
item_name = playlist_item_name(item)
if is_url(item):
url_data = normalize_url(item)
if url_data:
priority_files.append({
"name": url_data["url"],
"type": "url",
"size": "URL",
"zoom": url_data.get("zoom", 1.0),
"enabled": playlist_item_enabled(item)
})
continue
file_path = os.path.join(prio_dir, item_name)
if not os.path.exists(file_path):
continue
ext = os.path.splitext(item_name)[1].lower()
ftype = "video" if ext == ".mp4" else "image"
size = os.path.getsize(file_path) // 1024
priority_files.append({
"name": item_name,
"type": ftype,
"size": size,
"enabled": playlist_item_enabled(item)
})
# 2. Neue Dateien aus dem Ordner, die noch nicht in der Playlist sind
playlist_names = {playlist_item_name(item) for item in prio_playlist}
for f in sorted(os.listdir(prio_dir)):
if f in playlist_names or f.startswith("._"):
continue
file_path = os.path.join(prio_dir, f)
ext = os.path.splitext(f)[1].lower()
ftype = "video" if ext == ".mp4" else "image"
size = os.path.getsize(file_path) // 1024
priority_files.append({
"name": f,
"type": ftype,
"size": size
})
return priority_files
def prio_redirect(site):
"""Gibt den Redirect-Pfad zurück, der nach Priority-Operationen aufgerufen wird."""
return f"/admin/{site}/priority"
# -------------------------------------------------
# User & Login (Flask-Login)
# -------------------------------------------------
class Admin(UserMixin):
"""Einfacher Admin-User. Es gibt nur einen Admin (keine DB)."""
id = 1
@login_manager.user_loader
def load_user(user_id):
"""Lädt den Admin-User für Flask-Login. user_id wird ignoriert es gibt nur einen User."""
return Admin()
# -------------------------------------------------
# Login / Logout
# -------------------------------------------------
@app.route("/login", methods=["GET", "POST"])
def login():
"""Login-Seite. Bei POST werden die Zugangsdaten aus config.json.admin geprüft."""
config = load_config()
error = None
if request.method == "POST":
if (
request.form.get("username") == config["admin"].get("username")
and request.form.get("password") == config["admin"].get("password")
):
login_user(Admin())
return redirect("/admin")
error = "Ungültige Zugangsdaten"
return render_template("login.html", error=error)
@app.route("/logout")
def logout():
"""Loggt den Admin aus und leitet zur Login-Seite weiter."""
logout_user()
return redirect("/login")
# -------------------------------------------------
# Willkommensseite (Kunden-Logos + Generierung)
# -------------------------------------------------
@app.route("/willkommen", methods=["GET", "POST"])
@app.route("/customer", methods=["GET", "POST"])
def add_customer():
"""
Formular für die Willkommensseite:
- Bis zu 3 Kundennamen eingeben
- Logos per OpenAI → Brandfetch suchen
- welcome.html wird generiert und in der Lobby-Playlist vorne platziert
"""
error = None
success = None
logo_urls = [None, None, None]
customer_names = ["", "", ""]
site = request.args.get("site") or request.form.get("site") or ""
# Fallback auf ersten Standort, wenn keiner angegeben
if not site:
config = load_config()
sites = get_site_list(config)
site = sites[0] if sites else ""
if request.method == "POST":
customer_names = [
request.form.get("customer_name_1", "").strip(),
request.form.get("customer_name_2", "").strip(),
request.form.get("customer_name_3", "").strip(),
]
filled_names = [n for n in customer_names if n]
print(f"🔍 APP.PY - Suche nach Kunden: {filled_names} (site: {site})")
if not filled_names:
error = "Mindestens ein Kundenname erforderlich"
elif not site:
error = "Kein Standort ausgewählt"
else:
try:
found_logos = []
for name in filled_names:
logo = generate_welcome_page.search_customer_logo(name)
found_logos.append(logo)
valid = [(n, l) for n, l in zip(filled_names, found_logos) if l]
if not valid:
error = "Kein Logo konnte gefunden werden"
else:
valid_names = [n for n, l in valid]
valid_urls = [l for n, l in valid]
for i, name in enumerate(customer_names):
for j, (vn, vu) in enumerate(zip(valid_names, valid_urls)):
if vn == name:
logo_urls[i] = vu
break
html_filename = generate_welcome_page.save_welcome_page(valid_names, valid_urls, site=site)
if html_filename:
if generate_welcome_page.add_customer_to_lobby_playlist(html_filename, site=site):
success = f"✅ Willkommensseite mit {len(valid_names)} Kunden erstellt!"
else:
error = "Fehler beim Hinzufügen zur Playliste"
else:
error = "Fehler beim Speichern der Willkommensseite"
except ValueError as e:
error = f"Konfigurationsfehler: {str(e)}"
except Exception as e:
error = f"Fehler: {str(e)}"
config = load_config()
site_list = get_site_list(config)
return render_template(
"customer.html",
error=error,
success=success,
logo_urls=logo_urls,
customer_names=customer_names,
site=site,
site_list=site_list,
current_site=site,
version=APP_VERSION,
year=datetime.now().year,
hostname=os.uname().nodename
)
@app.route("/api/customer", methods=["POST"])
def api_add_customer():
"""
API-Endpunkt für die Willkommensseite (JSON).
Erwartet: {"customer_name": "...", "site": "..."}
Gibt die Logo-URL und den Status zurück.
"""
try:
data = request.get_json()
customer_name = data.get("customer_name", "").strip()
site = data.get("site", "").strip()
if not customer_name:
return jsonify({"error": "Customer name required"}), 400
# Default auf ersten Standort
if not site:
config = load_config()
sites = get_site_list(config)
site = sites[0] if sites else ""
if not site:
return jsonify({"error": "No site configured"}), 400
logo_url = generate_welcome_page.search_customer_logo(customer_name)
if not logo_url:
return jsonify({"error": "Logo not found"}), 400
html_filename = generate_welcome_page.save_welcome_page(customer_name, logo_url, site=site)
if not html_filename:
return jsonify({"error": "Failed to create welcome page"}), 500
if generate_welcome_page.add_customer_to_lobby_playlist(html_filename, site=site):
return jsonify({
"success": True,
"message": f"Customer '{customer_name}' added successfully",
"welcome_page": html_filename,
"logo_url": logo_url,
"site": site
}), 201
else:
return jsonify({"error": "Failed to add to playlist"}), 500
except ValueError as e:
return jsonify({"error": str(e)}), 400
except Exception as e:
return jsonify({"error": str(e)}), 500
# -------------------------------------------------
# Medien ausliefern (statische Dateien)
# -------------------------------------------------
@app.route("/media/priority/<path:filename>")
def media_priority(filename):
"""Liefert globale Priority-Medien aus media/priority/ aus."""
path = os.path.join(MEDIA_DIR, "priority")
return send_from_directory(path, filename)
@app.route("/media/<site>/<screen>/<path:filename>")
def media(site, screen, filename):
"""Liefert Medien-Dateien für einen bestimmten Screen aus."""
path = os.path.join(MEDIA_DIR, site, screen)
return send_from_directory(path, filename)
# -------------------------------------------------
# Player
# -------------------------------------------------
@app.route("/player/<site>/<screen>")
def player(site, screen):
"""
Player-Ansicht für einen Screen.
Baut die Playlist (normal + optional priority) zusammen und rendert player.html.
Der Player zeigt abwechselnd Bilder, Videos, HTML-Seiten und URLs an.
"""
config = load_config()
sites_cfg = config.setdefault("sites", {})
site_cfg = sites_cfg.get(site)
if not site_cfg:
abort(404)
screens_cfg = site_cfg.setdefault("screens", {})
screen_cfg = screens_cfg.get(screen)
if not screen_cfg:
abort(404)
folder = os.path.join(MEDIA_DIR, site, screen)
playlist = screen_cfg.get("playlist", [])
show_images = screen_cfg.get("show_images", True)
show_videos = screen_cfg.get("show_videos", True)
def allowed_file(name):
"""Prüft, ob eine Datei angezeigt werden darf (abhängig von show_images / show_videos)."""
if name.startswith("._"):
return False
ext = name.lower()
if ext.endswith((".jpg", ".jpeg", ".png")):
return show_images
if ext.endswith(".mp4"):
return show_videos
if ext.endswith((".html", ".htm")):
return True
return False
# ─── Normale Screen-Playlist ───
normal_files = []
# 1. Playlist-Reihenfolge aus config.json
for item in playlist:
if not playlist_item_enabled(item):
continue
item_name = playlist_item_name(item)
if is_url(item):
url_data = normalize_url(item)
if url_data:
normal_files.append({"kind": "url", "url": url_data["url"], "zoom": url_data.get("zoom", 1.0), "enabled": True})
continue
if item_name and os.path.exists(os.path.join(folder, item_name)) and allowed_file(item_name):
normal_files.append({"kind": "file", "name": item_name, "enabled": True})
existing_file_names = {
item["name"] for item in normal_files if item["kind"] == "file"
}
# 2. Neue Dateien aus dem Medien-Ordner (ohne Playlist-Eintrag) hinten anhängen
if not playlist:
for f in sorted(os.listdir(folder)):
if f in existing_file_names or f.startswith("._"):
continue
if allowed_file(f):
normal_files.append({"kind": "file", "name": f})
# ─── Priority-Playlist (global) ───
priority_cfg = config.get("priority", {})
if priority_cfg.get("enabled", False):
prio_files = []
for item in priority_cfg.get("playlist", []):
if not playlist_item_enabled(item):
continue
item_name = playlist_item_name(item)
if is_url(item):
url_data = normalize_url(item)
if url_data:
prio_files.append({"kind": "url", "url": url_data["url"], "zoom": url_data.get("zoom", 1.0), "enabled": True})
continue
file_path = os.path.join(MEDIA_DIR, "priority", item_name)
if os.path.exists(file_path):
prio_files.append({"kind": "file", "name": item_name, "enabled": True})
else:
prio_files = []
return render_template(
"player.html",
site=site,
screen=screen,
normal_files=[item for item in normal_files if item.get("enabled", True)],
prio_files=[item for item in prio_files if item.get("enabled", True)],
interval=screen_cfg.get("interval", 10),
newsticker_text=screen_cfg.get("newsticker_text", ""),
newsticker_enabled=screen_cfg.get("newsticker_enabled", False),
custom_url=screen_cfg.get("custom_url", ""),
custom_url_label=screen_cfg.get("custom_url_label", ""),
custom_url_enabled=screen_cfg.get("custom_url_enabled", False),
custom_url_target=screen_cfg.get("custom_url_target", "overlay")
)
# -------------------------------------------------
# Hash für automatisches Player-Reload
# ─────────────────────────────────────────────────
# Der Player pollt alle 5s diese Route.
# Wenn sich der Hash ändert, wird neu geladen.
# -------------------------------------------------
@app.route("/playlist/<site>/<screen>/hash")
def playlist_hash(site, screen):
"""Berechnet einen MD5-Hash über die aktuelle Konfiguration (Screen + Priority).
Ändert sich der Hash, lädt der Player automatisch neu."""
config = load_config()
relevant = {
"sites": {
site: config.get("sites", {}).get(site, {})
},
"priority": config.get("priority", {})
}
blob = json.dumps(relevant, sort_keys=True).encode()
return hashlib.md5(blob).hexdigest()
# -------------------------------------------------
# Admin
# -------------------------------------------------
@app.route("/admin")
@login_required
def admin_redirect():
"""Leitet zum ersten konfigurierten Standort weiter."""
cfg = load_config()
sites = get_site_list(cfg)
if sites:
return redirect(f"/admin/{sites[0]}")
return "<h1>Kein Standort konfiguriert</h1><p>Bitte lege einen Standort im media-Verzeichnis an.</p>", 200
@app.route("/add-site")
@login_required
def add_site():
"""Legt einen neuen Standort an (Verzeichnis + Config-Eintrag)."""
name = request.args.get("name", "").strip().lower()
if not name:
return redirect("/admin")
cfg = load_config()
cfg.setdefault("sites", {}).setdefault(name, {"screens": {}})
os.makedirs(os.path.join(MEDIA_DIR, name), exist_ok=True)
save_config(cfg)
return redirect(f"/admin/{name}")
@app.route("/admin/<site>/delete-site", methods=["POST"])
@login_required
def delete_site(site):
"""Löscht einen Standort inkl. aller Screens und Medien (Config + Verzeichnis)."""
cfg = load_config()
cfg.get("sites", {}).pop(site, None)
save_config(cfg)
site_dir = os.path.join(MEDIA_DIR, site)
if os.path.isdir(site_dir):
import shutil
shutil.rmtree(site_dir)
sites = get_site_list(cfg)
return redirect(f"/admin/{sites[0]}" if sites else "/admin")
@app.route("/admin/<site>")
@login_required
def admin(site):
"""
Admin-Dashboard für einen Standort.
Zeigt alle Screens als Cards mit Tabler-Tabs (Playlist, Einstellungen, Medien).
"""
cfg = load_config()
site_list = get_site_list(cfg)
# Prüfen, ob der Standort existiert
if site not in site_list:
if os.path.isdir(os.path.join(MEDIA_DIR, site)):
cfg.setdefault("sites", {})[site] = {"screens": {}}
save_config(cfg)
site_list = get_site_list(cfg)
else:
abort(404)
current_site = site
screens = {}
media_files = {}
screen_status = {}
site_media_base = os.path.join(MEDIA_DIR, current_site)
# Screens erkennen: aus Config + Dateisystem
known_screens = set()
site_cfg = cfg.setdefault("sites", {}).setdefault(current_site, {"screens": {}})
config_screens = site_cfg.setdefault("screens", {})
# Ordner im Medienverzeichnis = Screens
if os.path.isdir(site_media_base):
for entry in os.listdir(site_media_base):
entry_path = os.path.join(site_media_base, entry)
if os.path.isdir(entry_path) and not entry.startswith("._"):
known_screens.add(entry)
# Screens aus Config ergänzen (auch ohne Ordner)
for s in config_screens:
known_screens.add(s)
for screen_name in sorted(known_screens):
path = os.path.join(site_media_base, screen_name)
os.makedirs(path, exist_ok=True)
files = []
screen_cfg = get_screen_config(cfg, current_site, screen_name)
playlist = screen_cfg.get("playlist", [])
screen_cfg["newsticker_text"] = screen_cfg.get("newsticker_text", "")
screen_cfg["newsticker_enabled"] = screen_cfg.get("newsticker_enabled", False)
# 1. Playlist-Einträge in Config-Reihenfolge
for item in playlist:
item_name = playlist_item_name(item)
if is_url(item):
url_data = normalize_url(item)
if url_data:
files.append({
"name": url_data["url"],
"type": "url",
"size": "URL",
"zoom": url_data.get("zoom", 1.0),
"enabled": playlist_item_enabled(item)
})
continue
file_path = os.path.join(path, item_name)
if not os.path.exists(file_path):
continue
ext = os.path.splitext(item_name)[1].lower()
if ext == ".mp4":
ftype = "video"
if ext in (".jpg", ".jpeg", ".png"):
ftype = "image"
if ext == ".html":
ftype = "html"
size = os.path.getsize(file_path) // 1024
files.append({
"name": item_name,
"type": ftype,
"size": size,
"enabled": playlist_item_enabled(item)
})
# 2. Neue Dateien (ohne Playlist-Eintrag) anhängen
playlist_names = {playlist_item_name(item) for item in playlist}
for f in sorted(os.listdir(path)):
if f in playlist_names or f.startswith("._"):
continue
file_path = os.path.join(path, f)
ext = os.path.splitext(f)[1].lower()
ftype = "video" if ext == ".mp4" else "image"
size = os.path.getsize(file_path) // 1024
files.append({
"name": f,
"type": ftype,
"size": size
})
screens[screen_name] = screen_cfg
media_files[screen_name] = files
screen_status[screen_name] = "active" if files else "empty"
return render_template(
"admin.html",
site_list=site_list,
current_site=current_site,
screens=screens,
media_files=media_files,
screen_status=screen_status,
version=APP_VERSION,
year=datetime.now().year,
hostname=os.uname().nodename,
server_url=cfg.get("server_url", "")
)
# -------------------------------------------------
# Admin: Priority-Playlist (eigene Seite)
# -------------------------------------------------
@app.route("/admin/<site>/priority")
@login_required
def admin_priority(site):
"""Zeigt die Priority-Playlist-Seite mit Tabs (Playlist + Medien)."""
cfg = load_config()
site_list = sorted(cfg.get("sites", {}).keys())
priority_files = load_priority_files()
return render_template(
"priority.html",
site_list=site_list,
current_site=site,
priority_files=priority_files,
version=APP_VERSION,
year=datetime.now().year,
hostname=os.uname().nodename,
server_url=cfg.get("server_url", "")
)
# -------------------------------------------------
# Admin: Screen-Einstellungen speichern
# -------------------------------------------------
@app.route("/admin/<site>/update/<screen>", methods=["POST"])
@login_required
def update_screen(site, screen):
"""Speichert die Einstellungen für einen Screen (Intervall, Newsticker, Show-Bilder/Videos, Custom-URL)."""
config = load_config()
cfg = get_screen_config(config, site, screen)
cfg["interval"] = int(request.form.get("interval") or 10)
cfg["show_images"] = "show_images" in request.form
cfg["show_videos"] = "show_videos" in request.form
cfg["newsticker_text"] = request.form.get("newsticker_text", "")
cfg["newsticker_enabled"] = "newsticker_enabled" in request.form
cfg["custom_url"] = request.form.get("custom_url", "").strip()
cfg["custom_url_label"] = request.form.get("custom_url_label", "").strip()
cfg["custom_url_enabled"] = "custom_url_enabled" in request.form
cfg["custom_url_target"] = request.form.get("custom_url_target", "overlay")
save_config(config)
return redirect(f"/admin/{site}")
# -------------------------------------------------
# Admin: Screen löschen
# -------------------------------------------------
@app.route("/admin/<site>/delete-screen/<screen>", methods=["POST"])
@login_required
def delete_screen(site, screen):
"""Löscht einen Screen (Config-Eintrag + Medien-Verzeichnis)."""
config = load_config()
screens_cfg = config.get("sites", {}).get(site, {}).get("screens", {})
screens_cfg.pop(screen, None)
save_config(config)
screen_dir = os.path.join(MEDIA_DIR, site, screen)
if os.path.isdir(screen_dir):
import shutil
shutil.rmtree(screen_dir)
return redirect(f"/admin/{site}")
# -------------------------------------------------
# Admin: Neuen Screen anlegen
# -------------------------------------------------
@app.route("/admin/<site>/add-screen", methods=["GET"])
@login_required
def add_screen(site):
"""Legt einen neuen Screen an (Verzeichnis + Config-Eintrag)."""
screen_name = request.args.get("name", "").strip().lower()
if not screen_name:
return redirect(f"/admin/{site}")
config = load_config()
os.makedirs(os.path.join(MEDIA_DIR, site, screen_name), exist_ok=True)
screens_cfg = config.setdefault("sites", {}).setdefault(site, {}).setdefault("screens", {})
if screen_name not in screens_cfg:
screens_cfg[screen_name] = {"playlist": [], "interval": 10, "show_images": True, "show_videos": True}
save_config(config)
return redirect(f"/admin/{site}#screen-{screen_name}")
# -------------------------------------------------
# Admin: Medien-Upload
# -------------------------------------------------
@app.route("/admin/<site>/upload/<screen>", methods=["POST"])
@login_required
def upload(site, screen):
"""Lädt eine Datei hoch (Bild oder Video). Bei screen="priority" ins globale Verzeichnis."""
file = request.files.get("file")
if not file:
return redirect(prio_redirect(site) if screen == "priority" else f"/admin/{site}")
ext = os.path.splitext(file.filename)[1].lower()
if ext not in UPLOAD_EXTENSIONS:
return "Ungültiger Dateityp", 400
filename = secure_filename(file.filename)
if screen == "priority":
file.save(os.path.join(MEDIA_DIR, "priority", filename))
else:
file.save(os.path.join(MEDIA_DIR, site, screen, filename))
return redirect(prio_redirect(site) if screen == "priority" else f"/admin/{site}")
# -------------------------------------------------
# Admin: URL zur Playlist hinzufügen
# -------------------------------------------------
@app.route("/admin/<site>/add-url/<screen>", methods=["POST"])
@login_required
def add_url(site, screen):
"""Fügt eine URL (mit Zoom-Faktor) zur Playlist eines Screens oder der Priority-Playlist hinzu."""
url = request.form.get("url", "").strip()
zoom = request.form.get("zoom", "1.0").strip()
if not is_url(url):
return redirect(prio_redirect(site) if screen == "priority" else f"/admin/{site}")
try:
zoom_factor = float(zoom)
if zoom_factor <= 0:
zoom_factor = 1.0
except (ValueError, TypeError):
zoom_factor = 1.0
url_data = {"url": url, "zoom": zoom_factor}
config = load_config()
if screen == "priority":
config.setdefault("priority", {}).setdefault("playlist", []).append(url_data)
else:
cfg = get_screen_config(config, site, screen)
cfg.setdefault("playlist", []).append(url_data)
save_config(config)
return redirect(prio_redirect(site) if screen == "priority" else f"/admin/{site}")
# -------------------------------------------------
# Admin: Datei aus Playlist löschen
# -------------------------------------------------
@app.route("/admin/<site>/delete/<screen>/<path:filename>", methods=["POST"])
@login_required
def delete_file(site, screen, filename):
"""Löscht eine Datei aus der Playlist (Config + Dateisystem)."""
cfg = load_config()
if screen == "priority":
path = os.path.join(MEDIA_DIR, "priority", filename)
playlist = cfg.get("priority", {}).get("playlist", [])
playlist[:] = [item for item in playlist if not (
(isinstance(item, str) and item == filename) or
(isinstance(item, dict) and item.get("url") == filename)
)]
else:
path = os.path.join(MEDIA_DIR, site, screen, filename)
screens_cfg = cfg.get("sites", {}).get(site, {}).get("screens", {})
playlist = screens_cfg.get(screen, {}).get("playlist", [])
playlist[:] = [item for item in playlist if not (
(isinstance(item, str) and item == filename) or
(isinstance(item, dict) and item.get("url") == filename)
)]
if os.path.exists(path):
os.remove(path)
save_config(cfg)
return redirect(prio_redirect(site) if screen == "priority" else f"/admin/{site}")
# -------------------------------------------------
# Admin: Playlist-Reihenfolge speichern (Drag & Drop)
# -------------------------------------------------
@app.route("/admin/<site>/playlist/<screen>", methods=["POST"])
@login_required
def save_playlist(site, screen):
"""
Speichert die über SortableJS per Drag & Drop neu sortierte Playlist.
Der Client schickt ein JSON mit der neuen Reihenfolge und enabled-Status.
"""
cfg = load_config()
data = request.get_json()
new_order = data["playlist"]
enabled_map = data.get("enabled", {})
if screen == "priority":
old_playlist = cfg.get("priority", {}).get("playlist", [])
else:
screens_cfg = cfg.get("sites", {}).get(site, {}).get("screens", {})
old_playlist = screens_cfg.get(screen, {}).get("playlist", [])
# Mapping: Name → Original-Item (für Zoom etc.)
item_map = {}
for item in old_playlist:
item_map[playlist_item_name(item)] = item
# Neue Playlist in der gewünschten Reihenfolge bauen
new_playlist = []
for key in new_order:
if key in item_map:
item = dict(item_map[key]) if isinstance(item_map[key], dict) else {"name": item_map[key]}
item["enabled"] = enabled_map.get(key, True if not isinstance(item_map[key], dict) else item_map[key].get("enabled", True))
new_playlist.append(item)
else:
new_playlist.append({"url": key, "zoom": 1.0, "enabled": enabled_map.get(key, True)} if is_url(key) else {"name": key, "enabled": enabled_map.get(key, True)})
if screen == "priority":
cfg.setdefault("priority", {})["playlist"] = new_playlist
else:
cfg.setdefault("sites", {}).setdefault(site, {}).setdefault("screens", {}).setdefault(screen, {})["playlist"] = new_playlist
save_config(cfg)
return "", 204
# -------------------------------------------------
# Main
# -------------------------------------------------
if __name__ == "__main__":
app.run(debug=True, host="0.0.0.0", port=5005)