Compare commits

...

3 Commits

Author SHA1 Message Date
Erik Thiele
f754c7f0a3 docs: Medien-Cache in AGENTS.md beschreiben
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 12:45:23 +02:00
Erik Thiele
efcbaac5a4 chore: Version auf 6.2.12 heben
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 12:45:16 +02:00
Erik Thiele
96355f6897 feat: Medien im Player mit ?v=<mtime> und langem Cache ausliefern
Bisher lieferte Flask Medien mit no-cache aus; der Player musste jede Datei
bei jeder Anzeige neu anfragen. Versionierte URLs duerfen jetzt ein Jahr
ohne Rueckfrage aus dem Cache kommen, unversionierte bleiben unveraendert.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 12:45:16 +02:00
3 changed files with 37 additions and 7 deletions

View File

@@ -154,6 +154,8 @@ Standorte (sites) sind die oberste Organisationsebene und gruppieren Screens.
- User-Dropdown im Header: zeigt E-Mail + Role-Badge, Menü mit Passwort ändern, MFA einrichten (mit "Aktiv"-Badge wenn MFA eingeschaltet), Userverwaltung (Admin), Abmelden. - User-Dropdown im Header: zeigt E-Mail + Role-Badge, Menü mit Passwort ändern, MFA einrichten (mit "Aktiv"-Badge wenn MFA eingeschaltet), Userverwaltung (Admin), Abmelden.
- Info-Tab in Screen-Cards: zeigt Client-Info-Tabelle (IP, Browser, Auflösung, zuletzt gesehen) aus dem Heartbeat-JSON für den jeweiligen Screen - Info-Tab in Screen-Cards: zeigt Client-Info-Tabelle (IP, Browser, Auflösung, zuletzt gesehen) aus dem Heartbeat-JSON für den jeweiligen Screen
- **Medien-Cache**: `player()` gibt jedem Datei-Eintrag `v` (mtime) mit, der Player haengt `?v=<mtime>` an. `send_media()` setzt dann `max-age` 1 Jahr + `immutable`; ohne `?v` bleibt Flasks `no-cache`. Eine ersetzte Datei bekommt so automatisch eine neue URL. Grosse Videos cacht der Browser trotzdem oft nicht (eigener Media-Cache) - dafuer braeuchte es einen Service Worker.
## Betrieb / Diagnose ## Betrieb / Diagnose
- `docker compose up -d` startet nur neu und **baut nicht**. Nach Code-Aenderungen `--build` verwenden, sonst laeuft weiter das alte Image - das hat bei der Fehlersuche mehrfach zu falschen Schluessen gefuehrt. - `docker compose up -d` startet nur neu und **baut nicht**. Nach Code-Aenderungen `--build` verwenden, sonst laeuft weiter das alte Image - das hat bei der Fehlersuche mehrfach zu falschen Schluessen gefuehrt.

39
app.py
View File

@@ -53,7 +53,7 @@ HISTORY_FILE = os.path.join(BASE_DIR, "history.json") # Aktivitätsverlauf
HISTORY_MAX = 1000 # so viele Einträge werden aufbewahrt HISTORY_MAX = 1000 # so viele Einträge werden aufbewahrt
HISTORY_PER_PAGE = 50 # Einträge je Seite im Dashboard-Verlauf HISTORY_PER_PAGE = 50 # Einträge je Seite im Dashboard-Verlauf
APP_VERSION = "6.2.11" APP_VERSION = "6.2.12"
UPLOAD_EXTENSIONS = {".jpg", ".jpeg", ".png", ".mp4"} UPLOAD_EXTENSIONS = {".jpg", ".jpeg", ".png", ".mp4"}
app = Flask(__name__) app = Flask(__name__)
@@ -1208,17 +1208,41 @@ def api_add_customer():
# ------------------------------------------------- # -------------------------------------------------
# Medien ausliefern (statische Dateien) # Medien ausliefern (statische Dateien)
# ------------------------------------------------- # -------------------------------------------------
# Traegt die URL ein ?v=<mtime>, darf der Player die Datei ein Jahr lang ohne
# Rueckfrage aus dem Cache nehmen: eine geaenderte Datei bekommt eine neue URL.
# Ohne ?v bleibt es beim Flask-Standard (no-cache + ETag), sonst saehe ein
# unversionierter Aufruf nach dem Ersetzen einer Datei weiter die alte Fassung.
MEDIA_CACHE_MAX_AGE = 365 * 24 * 3600
def media_version(path):
"""Liefert die mtime einer Datei als Versionskennung fuer ?v=, 0 wenn sie fehlt."""
try:
return int(os.path.getmtime(path))
except OSError:
return 0
def send_media(directory, filename):
"""send_from_directory mit langem Cache, sofern die URL versioniert ist."""
if not request.args.get("v"):
return send_from_directory(directory, filename)
resp = send_from_directory(directory, filename, max_age=MEDIA_CACHE_MAX_AGE)
resp.cache_control.immutable = True
return resp
@app.route("/media/<site>/priority/<path:filename>") @app.route("/media/<site>/priority/<path:filename>")
def media_priority(site, filename): def media_priority(site, filename):
"""Liefert die Priority-Medien eines Standorts aus media/<site>/priority/ aus.""" """Liefert die Priority-Medien eines Standorts aus media/<site>/priority/ aus."""
return send_from_directory(priority_dir(site), filename) return send_media(priority_dir(site), filename)
@app.route("/media/<site>/<screen>/<path:filename>") @app.route("/media/<site>/<screen>/<path:filename>")
def media(site, screen, filename): def media(site, screen, filename):
"""Liefert Medien-Dateien für einen bestimmten Screen aus.""" """Liefert Medien-Dateien für einen bestimmten Screen aus."""
path = os.path.join(MEDIA_DIR, site, screen) path = os.path.join(MEDIA_DIR, site, screen)
return send_from_directory(path, filename) return send_media(path, filename)
# ------------------------------------------------- # -------------------------------------------------
@@ -1279,7 +1303,8 @@ def player(site, screen):
continue continue
if item_name and os.path.exists(os.path.join(folder, item_name)) and allowed_file(item_name): 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}) normal_files.append({"kind": "file", "name": item_name, "enabled": True,
"v": media_version(os.path.join(folder, item_name))})
existing_file_names = { existing_file_names = {
item["name"] for item in normal_files if item["kind"] == "file" item["name"] for item in normal_files if item["kind"] == "file"
@@ -1291,7 +1316,8 @@ def player(site, screen):
if f in existing_file_names or f.startswith("._"): if f in existing_file_names or f.startswith("._"):
continue continue
if allowed_file(f): if allowed_file(f):
normal_files.append({"kind": "file", "name": f}) normal_files.append({"kind": "file", "name": f,
"v": media_version(os.path.join(folder, f))})
# ─── Priority-Playlist (global) ─── # ─── Priority-Playlist (global) ───
priority_cfg = get_priority_config(config, site) priority_cfg = get_priority_config(config, site)
@@ -1311,7 +1337,8 @@ def player(site, screen):
file_path = os.path.join(priority_dir(site), 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,
"v": media_version(file_path)})
else: else:
prio_files = [] prio_files = []

View File

@@ -472,7 +472,8 @@ function playNext() {
} }
// Datei aus dem Medienverzeichnis // Datei aus dem Medienverzeichnis
const src = `/media/${site}/${basePath}/${item.name}`; // ?v=<mtime> erlaubt dem Browser, die Datei ohne Rueckfrage aus dem Cache zu nehmen
const src = `/media/${site}/${basePath}/${item.name}` + (item.v ? `?v=${item.v}` : "");
if (isVideo(item)) { if (isVideo(item)) {
vid.style.display = "block"; vid.style.display = "block";
vid.src = src; vid.src = src;