Compare commits
3 Commits
2c72f48e28
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f754c7f0a3 | ||
|
|
efcbaac5a4 | ||
|
|
96355f6897 |
@@ -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
39
app.py
@@ -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 = []
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user