Hilfetexte ergänzt
This commit is contained in:
157
app.py
157
app.py
@@ -1,8 +1,17 @@
|
||||
#!/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 welcome page functions
|
||||
import generate_welcome_page
|
||||
import generate_welcome_page # Logo-Suche und Willkommensseite-Generierung
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
@@ -23,8 +32,8 @@ 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")
|
||||
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"}
|
||||
@@ -33,28 +42,35 @@ app = Flask(__name__)
|
||||
app.secret_key = "CHANGE_THIS_SECRET!!!"
|
||||
|
||||
login_manager = LoginManager(app)
|
||||
login_manager.login_view = "login"
|
||||
login_manager.login_view = "login" # Nicht eingeloggte User werden zum Login umgeleitet
|
||||
|
||||
|
||||
# -------------------------------------------------
|
||||
# Config Helpers
|
||||
# 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", {})
|
||||
@@ -65,12 +81,15 @@ def get_screen_config(cfg, site, 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):
|
||||
@@ -79,6 +98,7 @@ def normalize_url(item):
|
||||
|
||||
|
||||
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):
|
||||
@@ -87,6 +107,8 @@ def playlist_item_name(item):
|
||||
|
||||
|
||||
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):
|
||||
@@ -95,9 +117,12 @@ def playlist_item_enabled(item):
|
||||
|
||||
|
||||
# -------------------------------------------------
|
||||
# Priority-Helper
|
||||
# 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)
|
||||
@@ -105,6 +130,7 @@ def load_priority_files():
|
||||
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):
|
||||
@@ -134,6 +160,7 @@ def load_priority_files():
|
||||
"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("._"):
|
||||
@@ -154,26 +181,30 @@ def load_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
|
||||
# -------------------------------------------------
|
||||
# Auth / Benutzer
|
||||
# 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
|
||||
|
||||
@@ -188,26 +219,33 @@ def login():
|
||||
|
||||
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")
|
||||
|
||||
|
||||
# -------------------------------------------------
|
||||
# Customer / Willkommensseite
|
||||
# Willkommensseite (Kunden-Logos + Generierung)
|
||||
# -------------------------------------------------
|
||||
@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"""
|
||||
"""
|
||||
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 ""
|
||||
|
||||
# Default to first site if not specified
|
||||
# Fallback auf ersten Standort, wenn keiner angegeben
|
||||
if not site:
|
||||
config = load_config()
|
||||
sites = get_site_list(config)
|
||||
@@ -277,10 +315,14 @@ def add_customer():
|
||||
hostname=os.uname().nodename
|
||||
)
|
||||
|
||||
|
||||
@app.route("/api/customer", methods=["POST"])
|
||||
# @login_required
|
||||
def api_add_customer():
|
||||
"""API endpoint for customer creation"""
|
||||
"""
|
||||
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()
|
||||
@@ -289,7 +331,7 @@ def api_add_customer():
|
||||
if not customer_name:
|
||||
return jsonify({"error": "Customer name required"}), 400
|
||||
|
||||
# Default to first site
|
||||
# Default auf ersten Standort
|
||||
if not site:
|
||||
config = load_config()
|
||||
sites = get_site_list(config)
|
||||
@@ -324,25 +366,34 @@ def api_add_customer():
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
|
||||
# -------------------------------------------------
|
||||
# Medien ausliefern
|
||||
# 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)
|
||||
@@ -363,6 +414,7 @@ def player(site, screen):
|
||||
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()
|
||||
@@ -374,12 +426,10 @@ def player(site, screen):
|
||||
return True
|
||||
return False
|
||||
|
||||
# ------------------------------
|
||||
# Normale Screen-Playlist
|
||||
# ------------------------------
|
||||
# ─── Normale Screen-Playlist ───
|
||||
normal_files = []
|
||||
|
||||
# 1. Playlist-Reihenfolge
|
||||
# 1. Playlist-Reihenfolge aus config.json
|
||||
for item in playlist:
|
||||
if not playlist_item_enabled(item):
|
||||
continue
|
||||
@@ -397,7 +447,7 @@ def player(site, screen):
|
||||
item["name"] for item in normal_files if item["kind"] == "file"
|
||||
}
|
||||
|
||||
# 2. Neue Dateien hintendran
|
||||
# 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("._"):
|
||||
@@ -405,9 +455,7 @@ def player(site, screen):
|
||||
if allowed_file(f):
|
||||
normal_files.append({"kind": "file", "name": f})
|
||||
|
||||
# ------------------------------
|
||||
# Priority-Playlist (global)
|
||||
# ------------------------------
|
||||
# ─── Priority-Playlist (global) ───
|
||||
priority_cfg = config.get("priority", {})
|
||||
|
||||
if priority_cfg.get("enabled", False):
|
||||
@@ -446,9 +494,14 @@ def player(site, screen):
|
||||
|
||||
# -------------------------------------------------
|
||||
# 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 = {
|
||||
@@ -463,13 +516,13 @@ def playlist_hash(site, screen):
|
||||
|
||||
|
||||
# -------------------------------------------------
|
||||
# Admin Dashboard
|
||||
# Admin
|
||||
# -------------------------------------------------
|
||||
|
||||
@app.route("/admin")
|
||||
@login_required
|
||||
def admin_redirect():
|
||||
"""Redirect to first available site"""
|
||||
"""Leitet zum ersten konfigurierten Standort weiter."""
|
||||
cfg = load_config()
|
||||
sites = get_site_list(cfg)
|
||||
if sites:
|
||||
@@ -480,6 +533,7 @@ def admin_redirect():
|
||||
@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")
|
||||
@@ -495,6 +549,7 @@ def add_site():
|
||||
@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)
|
||||
@@ -511,13 +566,16 @@ def delete_site(site):
|
||||
@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)
|
||||
|
||||
# Validate site exists
|
||||
# Prüfen, ob der Standort existiert
|
||||
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)
|
||||
@@ -532,19 +590,19 @@ def admin(site):
|
||||
|
||||
site_media_base = os.path.join(MEDIA_DIR, current_site)
|
||||
|
||||
# Discover screens: from config + filesystem
|
||||
# Screens erkennen: aus Config + Dateisystem
|
||||
known_screens = set()
|
||||
site_cfg = cfg.setdefault("sites", {}).setdefault(current_site, {"screens": {}})
|
||||
config_screens = site_cfg.setdefault("screens", {})
|
||||
|
||||
# Merge with filesystem directories
|
||||
# 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)
|
||||
|
||||
# Add screens from config that might not have a directory yet
|
||||
# Screens aus Config ergänzen (auch ohne Ordner)
|
||||
for s in config_screens:
|
||||
known_screens.add(s)
|
||||
|
||||
@@ -558,7 +616,7 @@ def admin(site):
|
||||
screen_cfg["newsticker_text"] = screen_cfg.get("newsticker_text", "")
|
||||
screen_cfg["newsticker_enabled"] = screen_cfg.get("newsticker_enabled", False)
|
||||
|
||||
# 1 Playlist-Reihenfolge
|
||||
# 1. Playlist-Einträge in Config-Reihenfolge
|
||||
for item in playlist:
|
||||
item_name = playlist_item_name(item)
|
||||
|
||||
@@ -595,7 +653,7 @@ def admin(site):
|
||||
"enabled": playlist_item_enabled(item)
|
||||
})
|
||||
|
||||
# 2 Neue Dateien anhängen
|
||||
# 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("._"):
|
||||
@@ -631,11 +689,12 @@ def admin(site):
|
||||
|
||||
|
||||
# -------------------------------------------------
|
||||
# Admin: Priority-Playlist (separate Seite)
|
||||
# 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()
|
||||
@@ -658,6 +717,7 @@ def admin_priority(site):
|
||||
@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)
|
||||
|
||||
@@ -681,14 +741,13 @@ def update_screen(site, screen):
|
||||
@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()
|
||||
|
||||
# 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
|
||||
@@ -703,6 +762,7 @@ def delete_screen(site, screen):
|
||||
@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}")
|
||||
@@ -720,11 +780,12 @@ def add_screen(site):
|
||||
|
||||
|
||||
# -------------------------------------------------
|
||||
# Admin: Upload
|
||||
# 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}")
|
||||
@@ -744,11 +805,12 @@ def upload(site, screen):
|
||||
|
||||
|
||||
# -------------------------------------------------
|
||||
# Admin: URL hinzufügen
|
||||
# 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()
|
||||
|
||||
@@ -776,12 +838,12 @@ def add_url(site, screen):
|
||||
|
||||
|
||||
# -------------------------------------------------
|
||||
# Admin: Datei löschen
|
||||
# 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":
|
||||
@@ -808,12 +870,15 @@ def delete_file(site, screen, filename):
|
||||
|
||||
|
||||
# -------------------------------------------------
|
||||
# Admin: Playlist speichern
|
||||
# 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"]
|
||||
@@ -825,10 +890,12 @@ def save_playlist(site, screen):
|
||||
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:
|
||||
|
||||
Reference in New Issue
Block a user