Files
signage/app.py
2026-06-05 12:55:01 +02:00

823 lines
26 KiB
Python
Executable File

import os
import json
import hashlib
# Import welcome page functions
import generate_welcome_page
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")
CONFIG_FILE = os.path.join(BASE_DIR, "config.json")
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"
# -------------------------------------------------
# Config Helpers
# -------------------------------------------------
def load_config():
if not os.path.exists(CONFIG_FILE):
return {"admin": {}, "sites": {}}
with open(CONFIG_FILE) as f:
return json.load(f)
def save_config(cfg):
with open(CONFIG_FILE, "w") as f:
json.dump(cfg, f, indent=2)
f.flush()
def get_site_list(cfg):
return sorted(cfg.get("sites", {}).keys())
def get_screen_config(cfg, site, screen):
return (
cfg
.setdefault("sites", {})
.setdefault(site, {})
.setdefault("screens", {})
.setdefault(screen, {})
)
def is_url(item):
if isinstance(item, str):
return item.startswith("http://") or item.startswith("https://")
return isinstance(item, dict) and "url" in item
def normalize_url(item):
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):
if isinstance(item, str):
return item
if isinstance(item, dict):
return item.get("url") or item.get("name", "")
return ""
def playlist_item_enabled(item):
if isinstance(item, (str,)):
return True
if isinstance(item, dict):
return item.get("enabled", True)
return True
# -------------------------------------------------
# Priority-Helper
# -------------------------------------------------
def load_priority_files():
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", [])
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)
})
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):
return f"/admin/{site}/priority"
# -------------------------------------------------
# User & Login
# -------------------------------------------------
# Auth / Benutzer
# -------------------------------------------------
class Admin(UserMixin):
id = 1
@login_manager.user_loader
def load_user(user_id):
return Admin()
# -------------------------------------------------
# Login / Logout
# -------------------------------------------------
@app.route("/login", methods=["GET", "POST"])
def login():
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():
logout_user()
return redirect("/login")
# -------------------------------------------------
# Customer / Willkommensseite
# -------------------------------------------------
@app.route("/willkommen", methods=["GET", "POST"])
@app.route("/customer", methods=["GET", "POST"])
# @login_required
def add_customer():
"""Add new customer with logo search and welcome page"""
error = None
success = None
logo_urls = [None, None, None]
customer_names = ["", "", ""]
site = request.args.get("site") or request.form.get("site") or ""
# Default to first site if not specified
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"])
# @login_required
def api_add_customer():
"""API endpoint for customer creation"""
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 to first site
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
# -------------------------------------------------
@app.route("/media/priority/<path:filename>")
def media_priority(filename):
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):
path = os.path.join(MEDIA_DIR, site, screen)
return send_from_directory(path, filename)
# -------------------------------------------------
# Player
# -------------------------------------------------
@app.route("/player/<site>/<screen>")
def player(site, screen):
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):
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
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 hintendran
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
# -------------------------------------------------
@app.route("/playlist/<site>/<screen>/hash")
def playlist_hash(site, screen):
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 Dashboard
# -------------------------------------------------
@app.route("/admin")
@login_required
def admin_redirect():
"""Redirect to first available site"""
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("/admin/<site>")
@login_required
def admin(site):
cfg = load_config()
site_list = get_site_list(cfg)
# Validate site exists
if site not in site_list:
# Create default site config if filesystem dir exists but no config
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)
# Discover screens: from config + filesystem
known_screens = set()
site_cfg = cfg.setdefault("sites", {}).setdefault(current_site, {"screens": {}})
config_screens = site_cfg.setdefault("screens", {})
# Merge with filesystem directories
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)
# Add screens from config that might not have a directory yet
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-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 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
)
# -------------------------------------------------
# Admin: Priority-Playlist (separate Seite)
# -------------------------------------------------
@app.route("/admin/<site>/priority")
@login_required
def admin_priority(site):
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,
version=APP_VERSION,
year=datetime.now().year,
hostname=os.uname().nodename,
priority_files=priority_files
)
# -------------------------------------------------
# Admin: Screen-Einstellungen speichern
# -------------------------------------------------
@app.route("/admin/<site>/update/<screen>", methods=["POST"])
@login_required
def update_screen(site, screen):
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):
config = load_config()
# Aus config.json entfernen
screens_cfg = config.get("sites", {}).get(site, {}).get("screens", {})
screens_cfg.pop(screen, None)
save_config(config)
# Medien-Verzeichnis löschen
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):
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: Upload
# -------------------------------------------------
@app.route("/admin/<site>/upload/<screen>", methods=["POST"])
@login_required
def upload(site, screen):
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 hinzufügen
# -------------------------------------------------
@app.route("/admin/<site>/add-url/<screen>", methods=["POST"])
@login_required
def add_url(site, screen):
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 löschen
# -------------------------------------------------
@app.route("/admin/<site>/delete/<screen>/<path:filename>", methods=["POST"])
@login_required
def delete_file(site, screen, filename):
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 speichern
# -------------------------------------------------
@app.route("/admin/<site>/playlist/<screen>", methods=["POST"])
@login_required
def save_playlist(site, screen):
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", [])
item_map = {}
for item in old_playlist:
item_map[playlist_item_name(item)] = item
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)