fix: Priority-Playlist je Standort statt global

Die Priority-Playlist lag unter config["priority"] und wirkte damit auf
*alle* Standorte. In der Praxis lief so ein Stuttgart-spezifischer
Eintrag (wbxroompresence...find=Stuttgart) auch auf den Karlsruher
Screens.

Sie liegt jetzt unter sites.<site>.priority, die Medien unter
media/<site>/priority/:
- get_priority_config(cfg, site) liefert die Konfiguration des
  Standorts. Ist dort nichts hinterlegt, wird ein globaler Altbestand
  einmalig uebernommen - per deepcopy, damit spaetere Aenderungen nicht
  zwischen Standorten durchschlagen.
- load_priority_files(site), priority_dir(site) sowie Player, Hash,
  Upload, Loeschen, URL-Anlegen, Umsortieren und Einzeleintrag-
  Bearbeitung arbeiten durchgaengig standortbezogen.
- Route /media/priority/<datei> -> /media/<site>/priority/<datei>,
  Templates entsprechend.
- Der Ordnername "priority" ist wie "logos" in der Screen-Erkennung
  ausgenommen. media/<site>/priority/ waere sonst als Screen in der
  Konfiguration gelandet; "priority" ist in den Routen ohnehin als
  Kennzeichen belegt, ein Screen dieses Namens war nie moeglich.

Migration der vorhandenen Konfiguration: der globale Block wurde in alle
drei Standorte uebernommen und der globale Schluessel entfernt. Der
leere globale Ordner media/priority/ ist entfallen.

README, AGENTS.md und Hilfeseite nachgezogen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Erik Thiele
2026-09-04 10:32:27 +02:00
parent a9cbe42df9
commit d2938f87f3
6 changed files with 92 additions and 43 deletions

View File

@@ -29,7 +29,7 @@ Standorte (sites) sind die oberste Organisationsebene und gruppieren Screens.
| `GET /admin/<site>` | Admin-Dashboard für einen Standort (`@site_access_required`) | | `GET /admin/<site>` | Admin-Dashboard für einen Standort (`@site_access_required`) |
| `GET /admin/<site>/priority` | Priority-Playlist als separate Seite | | `GET /admin/<site>/priority` | Priority-Playlist als separate Seite |
| `GET /media/<site>/<screen>/<file>` | Medien-Datei ausliefern | | `GET /media/<site>/<screen>/<file>` | Medien-Datei ausliefern |
| `GET /media/priority/<file>` | Priority-Medien (global) | | `GET /media/<site>/priority/<file>` | Priority-Medien des Standorts |
| `GET /media/<site>/background/<filename>` | Hintergrundbild der Willkommensseite | | `GET /media/<site>/background/<filename>` | Hintergrundbild der Willkommensseite |
| `GET /playlist/<site>/<screen>/hash` | Playlist-Checksumme für Auto-Reload + zeichnet Heartbeat auf (Online-Status) | | `GET /playlist/<site>/<screen>/hash` | Playlist-Checksumme für Auto-Reload + zeichnet Heartbeat auf (Online-Status) |
| `GET /willkommen?site=<site>` | Willkommensseite für Standort generieren | | `GET /willkommen?site=<site>` | Willkommensseite für Standort generieren |
@@ -109,10 +109,11 @@ Standorte (sites) sind die oberste Organisationsebene und gruppieren Screens.
- `GET /admin` requires login; redirects to first accessible site for the user. - `GET /admin` requires login; redirects to first accessible site for the user.
- URL playlist items are stored as dicts like `{"url": "https://...", "zoom": 0.8}` and the zoom value must survive save/reorder flows. File items may also be dicts (`{"name": "bild.jpg", ...}`); `save_playlist` copies existing dicts, so extra keys like `schedule` survive reordering. - URL playlist items are stored as dicts like `{"url": "https://...", "zoom": 0.8}` and the zoom value must survive save/reorder flows. File items may also be dicts (`{"name": "bild.jpg", ...}`); `save_playlist` copies existing dicts, so extra keys like `schedule` survive reordering.
- `.html` items in `media/` are rendered inline as content, not in an iframe. - `.html` items in `media/` are rendered inline as content, not in an iframe.
- `config.priority.enabled` makes the priority playlist show on every screen. - `sites.<site>.priority.enabled` makes the priority playlist show on every screen **of that site**.
- `POST /api/customer` generates `welcome.html` and inserts it at the front of the playlist of every target screen for the specified site. Target screens are those with `welcome_enabled`; if no screen of the site has the key at all, the old behaviour applies (`lobby` only). Optional `valid_until` (`"YYYY-MM-DD"`) attaches a schedule so the page disappears on its own. - `POST /api/customer` generates `welcome.html` and inserts it at the front of the playlist of every target screen for the specified site. Target screens are those with `welcome_enabled`; if no screen of the site has the key at all, the old behaviour applies (`lobby` only). Optional `valid_until` (`"YYYY-MM-DD"`) attaches a schedule so the page disappears on its own.
- New standorte can be added by creating `media/<neuer-standort>/<screen>/` directories and optionally adding config to `config.json["sites"][<neuer-standort>]`. - New standorte can be added by creating `media/<neuer-standort>/<screen>/` directories and optionally adding config to `config.json["sites"][<neuer-standort>]`.
- Priority playlist (`config.priority`) is global and affects all sites/screens. - **Die Priority-Playlist gilt je Standort** (`sites.<site>.priority`), nicht global. Frueher lag sie unter `config.priority` und wirkte auf alle Standorte - dadurch lief Stuttgarter Inhalt auch in Karlsruhe. `get_priority_config(cfg, site)` liefert die Konfiguration des Standorts und uebernimmt einen globalen Altbestand beim ersten Zugriff einmalig (mit `deepcopy`, damit Aenderungen nicht zwischen Standorten durchschlagen).
- Priority-Medien liegen unter `media/<site>/priority/`, ausgeliefert ueber `/media/<site>/priority/<datei>`. Die Ordnernamen `priority` und `logos` sind in der Screen-Erkennung ausgenommen - sonst wuerden sie als Screens in der Konfiguration landen.
- Willkommensseite (`customer.html`) accepts up to 3 customer names; logos are fetched via OpenAI→Brandfetch and displayed in a flex row (portrait: column). - Willkommensseite (`customer.html`) accepts up to 3 customer names; logos are fetched via OpenAI→Brandfetch and displayed in a flex row (portrait: column).
- `generate_welcome_html(customer_names, logo_urls, contacts=None, site="stuttgart", background_url=None)` takes lists for up to 3 customers; logos have equal width (280px) with fixed height 120px, text left-aligned below each logo. - `generate_welcome_html(customer_names, logo_urls, contacts=None, site="stuttgart", background_url=None)` takes lists for up to 3 customers; logos have equal width (280px) with fixed height 120px, text left-aligned below each logo.
- Contact info supports Markdown: `**bold**`, `*italic*`, `` `code` ``, `- list items`, and line breaks (preserved as `<br>`). - Contact info supports Markdown: `**bold**`, `*italic*`, `` `code` ``, `- list items`, and line breaks (preserved as `<br>`).
@@ -166,7 +167,7 @@ Standorte (sites) sind die oberste Organisationsebene und gruppieren Screens.
- `users.json` is NOT gitignored (tracked for initial admin setup, contains no secrets by default). - `users.json` is NOT gitignored (tracked for initial admin setup, contains no secrets by default).
- The app has no configured tests, lint, typecheck, formatter, or CI. - The app has no configured tests, lint, typecheck, formatter, or CI.
- Hardcoded secrets exist in tracked files; do not commit new secrets or reshuffle them casually. - Hardcoded secrets exist in tracked files; do not commit new secrets or reshuffle them casually.
- Wichtige Helper-Funktionen in `app.py`: `load_config()`, `save_config()`, `get_site_list()`, `get_screen_config()`, `is_url()`, `normalize_url()`, `playlist_item_name()`, `is_item_active()`, `_schedule_time()`, `_window_start_date()`, `_in_validity_range()`, `load_priority_files()`, `prio_redirect()`, `get_background_url()`, `load_users()`, `save_users()`, `get_user()`, `init_user_db()`, `get_accessible_sites()`, `record_heartbeat()`, `screen_is_active()`, `get_client_info()`, `add_history_entry()`. - Wichtige Helper-Funktionen in `app.py`: `load_config()`, `save_config()`, `get_site_list()`, `get_screen_config()`, `is_url()`, `normalize_url()`, `playlist_item_name()`, `is_item_active()`, `_schedule_time()`, `_window_start_date()`, `_in_validity_range()`, `load_priority_files(site)`, `get_priority_config(cfg, site)`, `priority_dir(site)`, `prio_redirect()`, `get_background_url()`, `load_users()`, `save_users()`, `get_user()`, `init_user_db()`, `get_accessible_sites()`, `record_heartbeat()`, `screen_is_active()`, `get_client_info()`, `add_history_entry()`.
- Zugriffs-Dekoratoren in `app.py`: `admin_required`, `site_access_required`. - Zugriffs-Dekoratoren in `app.py`: `admin_required`, `site_access_required`.
- Hintergrundbild der Willkommensseite wird pro Standort unter `media/<site>/background.*` gespeichert; Fallback auf `static/wallpaper.png` wenn keine Datei existiert. - Hintergrundbild der Willkommensseite wird pro Standort unter `media/<site>/background.*` gespeichert; Fallback auf `static/wallpaper.png` wenn keine Datei existiert.
- `get_background_url(site)` prüft auf benutzerdefiniertes Hintergrundbild für einen Standort. - `get_background_url(site)` prüft auf benutzerdefiniertes Hintergrundbild für einen Standort.

View File

@@ -9,7 +9,7 @@ Browserbasiertes Digital-Signage-System für interne Info-Screens.
- Browserbasierter Player (Kiosk-Modus, Chromium, Firefox) - Browserbasierter Player (Kiosk-Modus, Chromium, Firefox)
- Bilder, Videos (MP4/H.264), HTML-Seiten, URL-Playlist-Einträge - Bilder, Videos (MP4/H.264), HTML-Seiten, URL-Playlist-Einträge
- **Multi-Standort**: Standorte (sites) gruppieren Screens - **Multi-Standort**: Standorte (sites) gruppieren Screens
- **Priority-Playlist**: globale Inhalte wirken auf alle Player - **Priority-Playlist**: pro Standort; die Inhalte laufen auf allen Screens **dieses** Standorts, nicht standortübergreifend
- **Willkommensseite**: bis zu 3 Kundenlogos via OpenAI + Brandfetch oder **manueller Upload mit automatischem Freistellen** (transparenter Hintergrund), pro Standort konfigurierbares Hintergrundbild; erscheint auf allen Screens mit `welcome_enabled` (mehrere möglich), optional mit Ablaufdatum - **Willkommensseite**: bis zu 3 Kundenlogos via OpenAI + Brandfetch oder **manueller Upload mit automatischem Freistellen** (transparenter Hintergrund), pro Standort konfigurierbares Hintergrundbild; erscheint auf allen Screens mit `welcome_enabled` (mehrere möglich), optional mit Ablaufdatum
- Pro Screen eigene Playlist mit Drag-&-Drop-Reihenfolge - Pro Screen eigene Playlist mit Drag-&-Drop-Reihenfolge
- Auto-Reload bei Playlist-Änderungen - Auto-Reload bei Playlist-Änderungen
@@ -53,7 +53,7 @@ Flask App (Server)
│ │ │ ├── bild.jpg │ │ │ ├── bild.jpg
│ │ │ ├── video.mp4 │ │ │ ├── video.mp4
│ │ │ └── welcome.html │ │ │ └── welcome.html
│ └── priority/ │ │ └── priority/
└── generate_welcome_page.py └── generate_welcome_page.py
``` ```
@@ -79,9 +79,9 @@ signage/
│ │ ├── casino/ │ │ ├── casino/
│ │ ├── videosysteme/ │ │ ├── videosysteme/
│ │ ├── background.jpg # Hintergrundbild der Willkommensseite (optional) │ │ ├── background.jpg # Hintergrundbild der Willkommensseite (optional)
│ │ ├── priority/ # Priority-Medien dieses Standorts
│ │ ├── logo_1.png # hochgeladene Kundenlogos (Slot 1-3) │ │ ├── logo_1.png # hochgeladene Kundenlogos (Slot 1-3)
│ │ └── welcome.html # Standort-Kopie der Willkommensseite │ │ └── welcome.html # Standort-Kopie der Willkommensseite
│ └── priority/
├── templates/ ├── templates/
│ ├── admin.html # Admin-Dashboard (Übersicht) │ ├── admin.html # Admin-Dashboard (Übersicht)
│ ├── admin_dashboard.html # Admin Dashboard (Statistiken + Verlauf) │ ├── admin_dashboard.html # Admin Dashboard (Statistiken + Verlauf)
@@ -242,7 +242,7 @@ Beides fehlte anfangs und machte die Fehlersuche unnötig schwer.
| `POST /admin/users/reset-password/<email>` | Passwort-Reset mit temporärem Passwort (Admin) | | `POST /admin/users/reset-password/<email>` | Passwort-Reset mit temporärem Passwort (Admin) |
| `POST /admin/users/edit/<email>` | User bearbeiten speichern (Admin) | | `POST /admin/users/edit/<email>` | User bearbeiten speichern (Admin) |
| `GET /media/<site>/<screen>/<file>` | Medien-Datei ausliefern | | `GET /media/<site>/<screen>/<file>` | Medien-Datei ausliefern |
| `GET /media/priority/<file>` | Priority-Medien (global) | | `GET /media/<site>/priority/<file>` | Priority-Medien des Standorts |
| `GET /media/<site>/logos/<file>` | Manuell hochgeladenes Kundenlogo | | `GET /media/<site>/logos/<file>` | Manuell hochgeladenes Kundenlogo |
| `GET /admin/help` | Hilfeseite zur Bedienung | | `GET /admin/help` | Hilfeseite zur Bedienung |
| `GET /customer` | Alias für `/willkommen` | | `GET /customer` | Alias für `/willkommen` |
@@ -427,10 +427,6 @@ OPENAI_API_KEY=sk-...
"background_url": null "background_url": null
} }
} }
},
"priority": {
"enabled": true,
"playlist": [...]
} }
} }
``` ```
@@ -460,6 +456,30 @@ OPENAI_API_KEY=sk-...
| `voice_agent_target` | string | `"overlay"` (iframe) oder `"redirect"` | | `voice_agent_target` | string | `"overlay"` (iframe) oder `"redirect"` |
| `voice_agent_position` | string | Position des Buttons: `top-left`, `top-center`, `top-right`, `middle-left`, `middle-center`, `middle-right`, `bottom-left`, `bottom-center`, `bottom-right` | | `voice_agent_position` | string | Position des Buttons: `top-left`, `top-center`, `top-right`, `middle-left`, `middle-center`, `middle-right`, `bottom-left`, `bottom-center`, `bottom-right` |
## Priority-Playlist (pro Standort)
Liegt in `config.json` unter `sites.<site>.priority`, die Medien unter `media/<site>/priority/`:
```jsonc
"sites": {
"stuttgart": {
"priority": {
"enabled": true, // schaltet die Priority-Playlist dieses Standorts
"playlist": [ ... ] // gleiches Eintragsformat wie eine Screen-Playlist
},
"screens": { ... }
}
}
```
Die Einträge laufen auf allen Screens **dieses** Standorts, eingestreut zwischen den normalen Playlist-Einträgen. Ein anderer Standort ist davon nicht betroffen.
> Früher lag die Priority-Playlist global unter `config.priority` und wirkte auf *alle* Standorte. `get_priority_config()` übernimmt einen solchen Altbestand beim ersten Zugriff einmalig in den jeweiligen Standort.
Die Ordnernamen `priority` und `logos` sind unter `media/<site>/` **reserviert** und werden bei der Screen-Erkennung übersprungen – sonst würden sie als Screens in der Oberfläche auftauchen.
---
<a id="playlist-eintraege"></a> <a id="playlist-eintraege"></a>
## Playlist-Einträge ## Playlist-Einträge
@@ -531,7 +551,9 @@ Optional pro Eintrag. Ohne `schedule` läuft der Eintrag immer (sofern `enabled`
- `_window_start_date(now, start, end)` – Kalendertag, an dem das laufende Zeitfenster begann (`None` = ausserhalb) - `_window_start_date(now, start, end)` – Kalendertag, an dem das laufende Zeitfenster begann (`None` = ausserhalb)
- `_in_validity_range(schedule, day)` – optionalen Gültigkeitszeitraum prüfen - `_in_validity_range(schedule, day)` – optionalen Gültigkeitszeitraum prüfen
- `schedule_text(schedule)` – Jinja-Filter: Klartext einer Zeitsteuerung für Tooltips - `schedule_text(schedule)` – Jinja-Filter: Klartext einer Zeitsteuerung für Tooltips
- `load_priority_files()` – Priority-Playlist + Dateien laden - `load_priority_files(site)` – Priority-Playlist eines Standorts + Dateien laden
- `get_priority_config(cfg, site)` – Priority-Konfiguration des Standorts (übernimmt einmalig einen globalen Altbestand)
- `priority_dir(site)` – Pfad zu `media/<site>/priority/`
- `welcome_target_screens(config, site)` – Zielscreens der Willkommensseite (Fallback `lobby`) - `welcome_target_screens(config, site)` – Zielscreens der Willkommensseite (Fallback `lobby`)
- `save_uploaded_logo(file, site, slot, freistellen=True)` – hochgeladenes Logo freistellen und als PNG ablegen - `save_uploaded_logo(file, site, slot, freistellen=True)` – hochgeladenes Logo freistellen und als PNG ablegen
- `remove_logo_background(image)` – Hintergrund per Flood Fill entfernen, gibt `(bild, hinweis)` zurück - `remove_logo_background(image)` – Hintergrund per Flood Fill entfernen, gibt `(bild, hinweis)` zurück

75
app.py
View File

@@ -10,6 +10,7 @@ zugeschaltet werden, die auf allen Playern abwechselnd eingeblendet wird.
import os import os
import json import json
import copy
import hashlib import hashlib
import re import re
import time import time
@@ -380,16 +381,38 @@ def is_item_active(item):
# ------------------------------------------------- # -------------------------------------------------
# Priority-Playlist-Helfer # Priority-Playlist-Helfer
# ------------------------------------------------- # -------------------------------------------------
def load_priority_files(): PRIORITY_DIRNAME = "priority"
"""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.
def priority_dir(site):
"""Verzeichnis der Priority-Medien eines Standorts: media/<site>/priority/."""
return os.path.join(MEDIA_DIR, site, PRIORITY_DIRNAME)
def get_priority_config(cfg, site):
"""Priority-Konfiguration eines Standorts (erzeugt fehlende Ebenen).
Die Priority-Playlist gilt ausschliesslich fuer den eigenen Standort.
Aeltere Konfigurationen hatten sie global unter cfg["priority"]; ist beim
Standort noch nichts hinterlegt, wird der Altbestand einmalig uebernommen."""
site_cfg = cfg.setdefault("sites", {}).setdefault(site, {})
if "priority" not in site_cfg:
alt = cfg.get("priority")
site_cfg["priority"] = copy.deepcopy(alt) if isinstance(alt, dict) else {}
return site_cfg["priority"]
def load_priority_files(site):
"""Lädt die Priority-Playlist eines Standorts aus config.json plus die
dazugehörigen Dateien aus media/<site>/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.""" Neue Dateien im Ordner, die noch nicht in der Playlist sind, werden angehängt."""
cfg = load_config() cfg = load_config()
prio_dir = os.path.join(MEDIA_DIR, "priority") prio_dir = priority_dir(site)
os.makedirs(prio_dir, exist_ok=True) os.makedirs(prio_dir, exist_ok=True)
priority_files = [] priority_files = []
prio_playlist = cfg.get("priority", {}).get("playlist", []) prio_playlist = get_priority_config(cfg, site).get("playlist", [])
# 1. Einträge aus der config.json-Playlist # 1. Einträge aus der config.json-Playlist
for item in prio_playlist: for item in prio_playlist:
@@ -1143,11 +1166,10 @@ def api_add_customer():
# ------------------------------------------------- # -------------------------------------------------
# Medien ausliefern (statische Dateien) # Medien ausliefern (statische Dateien)
# ------------------------------------------------- # -------------------------------------------------
@app.route("/media/priority/<path:filename>") @app.route("/media/<site>/priority/<path:filename>")
def media_priority(filename): def media_priority(site, filename):
"""Liefert globale Priority-Medien aus media/priority/ aus.""" """Liefert die Priority-Medien eines Standorts aus media/<site>/priority/ aus."""
path = os.path.join(MEDIA_DIR, "priority") return send_from_directory(priority_dir(site), filename)
return send_from_directory(path, filename)
@app.route("/media/<site>/<screen>/<path:filename>") @app.route("/media/<site>/<screen>/<path:filename>")
@@ -1229,7 +1251,7 @@ def player(site, screen):
normal_files.append({"kind": "file", "name": f}) normal_files.append({"kind": "file", "name": f})
# ─── Priority-Playlist (global) ─── # ─── Priority-Playlist (global) ───
priority_cfg = config.get("priority", {}) priority_cfg = get_priority_config(config, site)
if priority_cfg.get("enabled", False): if priority_cfg.get("enabled", False):
prio_files = [] prio_files = []
@@ -1243,7 +1265,7 @@ def player(site, screen):
prio_files.append({"kind": "url", "url": url_data["url"], "zoom": url_data.get("zoom", 1.0), "enabled": is_item_active(item), "passthrough": item.get("passthrough_params", False) if isinstance(item, dict) else False, "schedule": item.get("schedule") if isinstance(item, dict) else None}) prio_files.append({"kind": "url", "url": url_data["url"], "zoom": url_data.get("zoom", 1.0), "enabled": is_item_active(item), "passthrough": item.get("passthrough_params", False) if isinstance(item, dict) else False, "schedule": item.get("schedule") if isinstance(item, dict) else None})
continue continue
file_path = os.path.join(MEDIA_DIR, "priority", item_name) file_path = os.path.join(priority_dir(site), item_name)
if os.path.exists(file_path): if os.path.exists(file_path):
prio_files.append({"kind": "file", "name": item_name, "enabled": True}) prio_files.append({"kind": "file", "name": item_name, "enabled": True})
else: else:
@@ -1296,7 +1318,7 @@ def playlist_hash(site, screen):
screen_playlist = ( screen_playlist = (
config.get("sites", {}).get(site, {}).get("screens", {}).get(screen, {}).get("playlist", []) config.get("sites", {}).get(site, {}).get("screens", {}).get(screen, {}).get("playlist", [])
) )
prio_cfg = config.get("priority", {}) prio_cfg = get_priority_config(config, site)
prio_playlist = prio_cfg.get("playlist", []) if prio_cfg.get("enabled", False) else [] prio_playlist = prio_cfg.get("playlist", []) if prio_cfg.get("enabled", False) else []
relevant = { relevant = {
@@ -1409,15 +1431,17 @@ def admin(site):
config_screens = site_cfg.setdefault("screens", {}) config_screens = site_cfg.setdefault("screens", {})
# Ordner im Medienverzeichnis = Screens. # Ordner im Medienverzeichnis = Screens.
# "logos" ist reserviert: aeltere Versionen legten dort hochgeladene # Reservierte Namen: "logos" (hochgeladene Kundenlogos) und "priority"
# Kundenlogos ab, und ein solcher Ordner wuerde sonst dauerhaft als Screen # (Priority-Medien des Standorts). Solche Ordner wuerden sonst dauerhaft als
# in der Konfiguration landen. # Screen in der Konfiguration landen; "priority" ist ausserdem in den Routen
# bereits als Kennzeichen fuer die Priority-Playlist belegt.
reserviert = {generate_welcome_page.LOGO_URL_PREFIX, PRIORITY_DIRNAME}
if os.path.isdir(site_media_base): if os.path.isdir(site_media_base):
for entry in os.listdir(site_media_base): for entry in os.listdir(site_media_base):
entry_path = os.path.join(site_media_base, entry) entry_path = os.path.join(site_media_base, entry)
if (os.path.isdir(entry_path) if (os.path.isdir(entry_path)
and not entry.startswith("._") and not entry.startswith("._")
and entry != generate_welcome_page.LOGO_URL_PREFIX): and entry not in reserviert):
known_screens.add(entry) known_screens.add(entry)
# Screens aus Config ergänzen (auch ohne Ordner) # Screens aus Config ergänzen (auch ohne Ordner)
@@ -1523,7 +1547,7 @@ def admin_priority(site):
"""Zeigt die Priority-Playlist-Seite mit Tabs (Playlist + Medien).""" """Zeigt die Priority-Playlist-Seite mit Tabs (Playlist + Medien)."""
cfg = load_config() cfg = load_config()
site_list = get_accessible_sites(cfg, current_user) site_list = get_accessible_sites(cfg, current_user)
priority_files = load_priority_files() priority_files = load_priority_files(site)
return render_template( return render_template(
"priority.html", "priority.html",
@@ -1719,7 +1743,8 @@ def upload(site, screen):
filename = secure_filename(file.filename) filename = secure_filename(file.filename)
if screen == "priority": if screen == "priority":
file.save(os.path.join(MEDIA_DIR, "priority", filename)) os.makedirs(priority_dir(site), exist_ok=True)
file.save(os.path.join(priority_dir(site), filename))
else: else:
file.save(os.path.join(MEDIA_DIR, site, screen, filename)) file.save(os.path.join(MEDIA_DIR, site, screen, filename))
@@ -1790,7 +1815,7 @@ def add_url(site, screen):
config = load_config() config = load_config()
if screen == "priority": if screen == "priority":
config.setdefault("priority", {}).setdefault("playlist", []).append(url_data) get_priority_config(config, site).setdefault("playlist", []).append(url_data)
else: else:
cfg = get_screen_config(config, site, screen) cfg = get_screen_config(config, site, screen)
cfg.setdefault("playlist", []).append(url_data) cfg.setdefault("playlist", []).append(url_data)
@@ -1809,8 +1834,8 @@ def delete_file(site, screen, filename):
cfg = load_config() cfg = load_config()
if screen == "priority": if screen == "priority":
path = os.path.join(MEDIA_DIR, "priority", filename) path = os.path.join(priority_dir(site), filename)
playlist = cfg.get("priority", {}).get("playlist", []) playlist = get_priority_config(cfg, site).get("playlist", [])
playlist[:] = [item for item in playlist if not ( playlist[:] = [item for item in playlist if not (
(isinstance(item, str) and item == filename) or (isinstance(item, str) and item == filename) or
(isinstance(item, dict) and item.get("url") == filename) (isinstance(item, dict) and item.get("url") == filename)
@@ -1847,7 +1872,7 @@ def save_playlist(site, screen):
enabled_map = data.get("enabled", {}) enabled_map = data.get("enabled", {})
if screen == "priority": if screen == "priority":
old_playlist = cfg.get("priority", {}).get("playlist", []) old_playlist = get_priority_config(cfg, site).get("playlist", [])
else: else:
screens_cfg = cfg.get("sites", {}).get(site, {}).get("screens", {}) screens_cfg = cfg.get("sites", {}).get(site, {}).get("screens", {})
old_playlist = screens_cfg.get(screen, {}).get("playlist", []) old_playlist = screens_cfg.get(screen, {}).get("playlist", [])
@@ -1868,7 +1893,7 @@ def save_playlist(site, screen):
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)}) 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": if screen == "priority":
cfg.setdefault("priority", {})["playlist"] = new_playlist get_priority_config(cfg, site)["playlist"] = new_playlist
else: else:
cfg.setdefault("sites", {}).setdefault(site, {}).setdefault("screens", {}).setdefault(screen, {})["playlist"] = new_playlist cfg.setdefault("sites", {}).setdefault(site, {}).setdefault("screens", {}).setdefault(screen, {})["playlist"] = new_playlist
@@ -1907,7 +1932,7 @@ def update_playlist_item(site, screen):
config = load_config() config = load_config()
if screen == "priority": if screen == "priority":
playlist = config.setdefault("priority", {}).setdefault("playlist", []) playlist = get_priority_config(config, site).setdefault("playlist", [])
else: else:
playlist = config["sites"][site]["screens"][screen].setdefault("playlist", []) playlist = config["sites"][site]["screens"][screen].setdefault("playlist", [])

View File

@@ -170,12 +170,13 @@
<div class="card mb-3"> <div class="card mb-3">
<div class="card-header"><h3 class="card-title" id="help-priority"><i class="ti ti-player-play me-1"></i>Priority-Playlist</h3></div> <div class="card-header"><h3 class="card-title" id="help-priority"><i class="ti ti-player-play me-1"></i>Priority-Playlist</h3></div>
<div class="card-body"> <div class="card-body">
<p>Die Priority-Playlist ist <strong>global</strong> und wird auf <em>allen</em> Playern eines Standorts eingeblendet – zwischen den normalen Playlist-Einträgen.</p> <p>Die Priority-Playlist gilt <strong>pro Standort</strong>. Ihre Inhalte werden auf <em>allen</em> Screens dieses Standorts eingeblendet – zwischen den normalen Playlist-Einträgen.</p>
<ul> <ul>
<li>Aufruf über <code>/admin/&lt;standort&gt;/priority</code></li> <li>Aufruf über <code>/admin/&lt;standort&gt;/priority</code></li>
<li>Globale Medien und URLs verwalten, ebenfalls per Drag & Drop</li> <li>Medien und URLs dieses Standorts verwalten, ebenfalls per Drag & Drop</li>
<li>Priority muss in der Konfiguration aktiviert sein (<code>config.json → priority.enabled</code>)</li> <li>Priority muss für den Standort aktiviert sein (<code>config.json → sites.&lt;standort&gt;.priority.enabled</code>)</li>
<li>Die <a href="#help-zeitsteuerung">Zeitsteuerung</a> steht hier genauso zur Verfügung wie in den Screen-Playlists</li> <li>Die <a href="#help-zeitsteuerung">Zeitsteuerung</a> steht hier genauso zur Verfügung wie in den Screen-Playlists</li>
<li>Jeder Standort hat seine <strong>eigene</strong> Priority-Playlist – Inhalte eines Standorts erscheinen nie auf den Screens eines anderen.</li>
</ul> </ul>
</div> </div>
</div> </div>

View File

@@ -472,7 +472,7 @@ function playNext() {
} }
// Datei aus dem Medienverzeichnis // Datei aus dem Medienverzeichnis
const src = entry.isPrio ? `/media/priority/${item.name}` : `/media/${site}/${basePath}/${item.name}`; const src = `/media/${site}/${basePath}/${item.name}`;
if (isVideo(item)) { if (isVideo(item)) {
vid.style.display = "block"; vid.style.display = "block";
vid.src = src; vid.src = src;

View File

@@ -147,7 +147,7 @@
{% if file.type == "image" %} {% if file.type == "image" %}
data-bs-toggle="tooltip" data-bs-toggle="tooltip"
data-bs-html="true" data-bs-html="true"
title="<img src='/media/priority/{{ file.name }}' style='max-width: 200px; max-height: 150px; border: none; background: white; padding: 2px;' alt='Vorschau'>" title="<img src='/media/{{ current_site }}/priority/{{ file.name }}' style='max-width: 200px; max-height: 150px; border: none; background: white; padding: 2px;' alt='Vorschau'>"
{% elif file.type == "url" %} {% elif file.type == "url" %}
data-bs-toggle="tooltip" data-bs-toggle="tooltip"
title="URL: {{ file.name }}{% if file.zoom %} (Zoom: {{ file.zoom }}x){% endif %}" title="URL: {{ file.name }}{% if file.zoom %} (Zoom: {{ file.zoom }}x){% endif %}"