diff --git a/app.py b/app.py index 3023244..48a48be 100755 --- a/app.py +++ b/app.py @@ -217,14 +217,52 @@ def playlist_item_name(item): return "" -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 +def is_item_active(item): + """Prüft, ob ein Playlist-Eintrag aktiviert und zeitlich gültig ist. + Berücksichtigt enabled-Status und optionale Zeitsteuerung.""" + now = datetime.now() + + # Standardmäßig aktiv, wenn kein enabled-Status gesetzt ist if isinstance(item, dict): - return item.get("enabled", True) - return True + if not item.get("enabled", True): + return False + + # Zeitsteuerung prüfen + schedule = item.get("schedule") + if schedule: + start_str = schedule.get("start") + end_str = schedule.get("end") + + if not start_str or not end_str: + return item.get("enabled", True) + + try: + start = datetime.fromisoformat(start_str) + end = datetime.fromisoformat(end_str) + + mode = schedule.get("mode", "once") + + if mode == "once": + return start <= now <= end + elif mode == "daily": + current_time = now.time() + start_time = start.time() + end_time = end.time() + return start_time <= current_time <= end_time + elif mode == "weekly": + days = schedule.get("days", []) + current_weekday = now.weekday() # 0=Montag, 6=Sonntag + current_time = now.time() + start_time = start.time() + end_time = end.time() + return (str(current_weekday) in days or + f"{current_weekday}".lower()[:3] in [d.lower() for d in days]) and \ + start_time <= current_time <= end_time + except (ValueError, TypeError): + pass + + # Für Strings oder keine Zeitsteuerung: nur enabled prüfen + return playlist_item_enabled(item) # ------------------------------------------------- @@ -244,18 +282,19 @@ def load_priority_files(): # 1. Einträge aus der config.json-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), - "passthrough": item.get("passthrough_params", False) if isinstance(item, dict) else False - }) - continue + 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": is_item_active(item), + "passthrough": item.get("passthrough_params", False) if isinstance(item, dict) else False, + "schedule": item.get("schedule") + }) + continue file_path = os.path.join(prio_dir, item_name) if not os.path.exists(file_path): @@ -520,11 +559,11 @@ def login(): if trust_token: now = datetime.now() # Abgelaufene Einträge bereinigen - active = [d for d in trusted + active = [d for d in user.trusted_devices if d.get("token") != trust_token or datetime.fromisoformat(d["expires"]) > now] - changed = len(active) != len(trusted) - for d in trusted: + changed = len(active) != len(user.trusted_devices) + for d in user.trusted_devices: if d.get("token") == trust_token: try: expires = datetime.fromisoformat(d["expires"]) @@ -981,7 +1020,7 @@ def player(site, screen): 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, "passthrough": item.get("passthrough_params", False) if isinstance(item, dict) else False}) + normal_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")}) continue if item_name and os.path.exists(os.path.join(folder, item_name)) and allowed_file(item_name): @@ -1011,7 +1050,7 @@ def player(site, screen): 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, "passthrough": item.get("passthrough_params", False) if isinstance(item, dict) else False}) + 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")}) continue file_path = os.path.join(MEDIA_DIR, "priority", item_name) @@ -1020,29 +1059,30 @@ def player(site, screen): 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"), - custom_url_position=screen_cfg.get("custom_url_position", "top-left"), - stay_on_first=screen_cfg.get("stay_on_first", False), - voice_agent_enabled=screen_cfg.get("voice_agent_enabled", False), - voice_agent_show_image=screen_cfg.get("voice_agent_show_image") if screen_cfg.get("voice_agent_show_image") is not None else False, - voice_agent_url=config.get("voice_agent_url", ""), - voice_agent_label=screen_cfg.get("voice_agent_label", "Digitaler Assistent"), - voice_agent_target=screen_cfg.get("voice_agent_target", "overlay"), - voice_agent_position=screen_cfg.get("voice_agent_position", "top-left"), - player_params=dict(request.args) - ) + 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"), + custom_url_position=screen_cfg.get("custom_url_position", "top-left"), + stay_on_first=screen_cfg.get("stay_on_first", False), + voice_agent_enabled=screen_cfg.get("voice_agent_enabled", False), + voice_agent_show_image=screen_cfg.get("voice_agent_show_image") if screen_cfg.get("voice_agent_show_image") is not None else False, + voice_agent_url=config.get("voice_agent_url", ""), + voice_agent_label=screen_cfg.get("voice_agent_label", "Digitaler Assistent"), + voice_agent_target=screen_cfg.get("voice_agent_target", "overlay"), + voice_agent_position=screen_cfg.get("voice_agent_position", "top-left"), + player_params=dict(request.args), + now=datetime.now().isoformat() + ) # ------------------------------------------------- @@ -1201,8 +1241,9 @@ def admin(site): "type": "url", "size": "URL", "zoom": url_data.get("zoom", 1.0), - "enabled": playlist_item_enabled(item), - "passthrough": item.get("passthrough_params", False) if isinstance(item, dict) else False + "enabled": is_item_active(item), + "passthrough": item.get("passthrough_params", False) if isinstance(item, dict) else False, + "schedule": item.get("schedule") }) continue @@ -1224,7 +1265,8 @@ def admin(site): "name": item_name, "type": ftype, "size": size, - "enabled": playlist_item_enabled(item) + "enabled": is_item_active(item), + "schedule": item.get("schedule") }) # 2. Neue Dateien (ohne Playlist-Eintrag) anhängen @@ -1474,7 +1516,7 @@ def upload(site, screen): @app.route("/admin//add-url/", methods=["POST"]) @site_access_required def add_url(site, screen): - """Fügt eine URL (mit Zoom-Faktor) zur Playlist eines Screens oder der Priority-Playlist hinzu.""" + """Fügt eine URL (mit Zoom-Faktor und optionaler Zeitsteuerung) zur Playlist eines Screens oder der Priority-Playlist hinzu.""" url = request.form.get("url", "").strip() zoom = request.form.get("zoom", "1.0").strip() @@ -1488,7 +1530,32 @@ def add_url(site, screen): except (ValueError, TypeError): zoom_factor = 1.0 + # Zeitsteuerung verarbeiten + schedule_enabled = "schedule_enabled" in request.form + schedule = None + + if schedule_enabled: + mode = request.form.get("schedule_mode", "once") + start = request.form.get("schedule_start") + end = request.form.get("schedule_end") + + if not start or not end: + flash("Start- und Endzeit müssen angegeben werden", "error") + return redirect(f"/admin/{site}") + + schedule = { + "mode": mode, + "start": start, + "end": end + } + + if mode == "weekly": + days = request.form.getlist("schedule_days") + schedule["days"] = days + url_data = {"url": url, "zoom": zoom_factor} + if schedule: + url_data["schedule"] = schedule config = load_config() if screen == "priority": @@ -1584,11 +1651,13 @@ def save_playlist(site, screen): @app.route("/admin//update-playlist-item/", methods=["POST"]) @site_access_required def update_playlist_item(site, screen): - """Aktualisiert URL und Zoom eines einzelnen Playlist-Items.""" + """Aktualisiert URL, Zoom und Zeitsteuerung eines einzelnen Playlist-Items.""" data = request.get_json() old_name = data.get("old_name") new_url = data.get("new_url", "").strip() zoom = data.get("zoom", 1.0) + passthrough = data.get("passthrough") + schedule = data.get("schedule") # Kann None, {} oder schedule-Objekt sein if not is_url(new_url): return jsonify({"error": "Ungültige URL"}), 400 @@ -1611,14 +1680,26 @@ def update_playlist_item(site, screen): name = playlist_item_name(item) if name == old_name: updated = {"url": new_url, "zoom": zoom} - passthrough = data.get("passthrough", None) + + # Passthrough beibehalten oder aktualisieren if passthrough is not None: updated["passthrough_params"] = bool(passthrough) else: - # Bestehenden Wert übernehmen falls vorhanden existing = item.get("passthrough_params") if isinstance(item, dict) else None if existing is not None: updated["passthrough_params"] = existing + + # Zeitsteuerung beibehalten, entfernen oder aktualisieren + if schedule is not None: + # Schedule wurde explizit gesendet (kann {} für Entfernen sein) + if schedule: # Nicht leer + updated["schedule"] = schedule + # Wenn schedule leer ist {}, wird es nicht hinzugefügt (entfernt) + else: + # Nicht im Request → bestehenden Wert beibehalten + if isinstance(item, dict) and "schedule" in item: + updated["schedule"] = item["schedule"] + playlist[i] = updated save_config(config) return jsonify({"success": True}) diff --git a/templates/admin.html b/templates/admin.html index 2af334d..24b55b1 100755 --- a/templates/admin.html +++ b/templates/admin.html @@ -240,18 +240,21 @@ {% if file.enabled %}checked{% endif %} title="Element aktivieren"> - - - {{ file.name }}{% if file.type == "url" and file.zoom != 1.0 %} [{{ file.zoom }}x]{% endif %}{% if file.type == "url" %}{% if file.get('passthrough', False) %} {% else %} {% endif %}{% endif %} - + + + {{ file.name }}{% if file.type == "url" and file.zoom != 1.0 %} [{{ file.zoom }}x]{% endif %}{% if file.type == "url" %}{% if file.get('passthrough', False) %} {% else %} {% endif %}{% endif %} + {% if file.schedule %}Zeit{% endif %} + {% if file.type == "image" %} @@ -271,32 +274,33 @@ {% endif %} - -
- -
+ +
+ +
- - {% if file.type == "url" %} - - {% else %} - - {% endif %} + + {% if file.type == "url" %} + + {% else %} + + {% endif %} ⇅ @@ -482,18 +486,87 @@

URL hinzufügen

-
-
- - -
-
- - -
- -
+
+
+ + +
+
+ + +
+ + +
+
+
+ + +
+
+ +
+ + +
@@ -543,20 +616,51 @@ /* ─── Playlist-Item bearbeiten (URL + Zoom) ─── */ var editState = { site: "", screen: "", oldName: "" }; -/* Event-Delegation für Edit-Buttons (global, sofort aktiv) */ -document.addEventListener("click", function (e) { - var btn = e.target.closest(".edit-url-btn"); - if (!btn) return; - e.preventDefault(); - editState.site = btn.dataset.site; - editState.screen = btn.dataset.screen; - editState.oldName = btn.dataset.oldname; - document.getElementById("edit-url-input").value = btn.dataset.oldname; - document.getElementById("edit-zoom-input").value = btn.dataset.zoom; - document.getElementById("edit-passthrough-input").checked = btn.dataset.passthrough === "true"; - document.getElementById("edit-url-error").style.display = "none"; - new bootstrap.Modal(document.getElementById("edit-url-modal")).show(); -}); + /* Event-Delegation für Edit-Buttons (global, sofort aktiv) */ + document.addEventListener("click", function (e) { + var btn = e.target.closest(".edit-url-btn"); + if (!btn) return; + e.preventDefault(); + editState.site = btn.dataset.site; + editState.screen = btn.dataset.screen; + editState.oldName = btn.dataset.oldname; + document.getElementById("edit-url-input").value = btn.dataset.oldname; + document.getElementById("edit-zoom-input").value = btn.dataset.zoom; + document.getElementById("edit-passthrough-input").checked = btn.dataset.passthrough === "true"; + + // Zeitsteuerung aus Daten setzen + const schedule = JSON.parse(btn.dataset.schedule || 'null'); + const enabledEl = document.getElementById("edit-schedule-enabled"); + const optionsEl = document.getElementById("edit-schedule-options"); + + if (schedule) { + enabledEl.checked = true; + optionsEl.style.display = ""; + document.getElementById("edit-schedule-mode").value = schedule.mode || "once"; + document.getElementById("edit-schedule-start").value = schedule.start || ""; + document.getElementById("edit-schedule-end").value = schedule.end || ""; + + // Wochentage setzen + const days = schedule.days || []; + document.getElementById("edit-mon").checked = days.includes("0"); + document.getElementById("edit-tue").checked = days.includes("1"); + document.getElementById("edit-wed").checked = days.includes("2"); + document.getElementById("edit-thu").checked = days.includes("3"); + document.getElementById("edit-fri").checked = days.includes("4"); + document.getElementById("edit-sat").checked = days.includes("5"); + document.getElementById("edit-sun").checked = days.includes("6"); + + // Wochentage-Anzeige entsprechend Modus aktualisieren + updateScheduleDaysVisibility(); + } else { + enabledEl.checked = false; + optionsEl.style.display = "none"; + updateScheduleDaysVisibility(); + } + + document.getElementById("edit-url-error").style.display = "none"; + new bootstrap.Modal(document.getElementById("edit-url-modal")).show(); + }); document.addEventListener("DOMContentLoaded", function () { @@ -610,6 +714,25 @@ document.addEventListener("DOMContentLoaded", function () { var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) { return new bootstrap.Tooltip(tooltipTriggerEl); }); + + /* ─── Zeitsteuerung: Anzeige der Wochentage entsprechend Modus ─── */ + function updateScheduleDaysVisibility() { + const mode = document.getElementById("edit-schedule-mode").value; + const daysEl = document.getElementById("edit-schedule-days"); + daysEl.style.display = mode === "weekly" ? "" : "none"; + } + + /* ─── Zeitsteuerung: Änderungs-Events verarbeiten ─── */ + document.getElementById("edit-schedule-enabled").addEventListener("change", function() { + document.getElementById("edit-schedule-options").style.display = this.checked ? "" : "none"; + }); + + document.getElementById("edit-schedule-mode").addEventListener("change", function() { + updateScheduleDaysVisibility(); + }); + + // Initiale Anzeige + updateScheduleDaysVisibility(); /* ─── Edit-URL-Modal: Speichern-Button ─── */ document.getElementById("edit-url-save").addEventListener("click", function () { @@ -625,6 +748,40 @@ document.addEventListener("DOMContentLoaded", function () { errorEl.style.display = "none"; + // Zeitsteuerung sammeln + const scheduleEnabled = document.getElementById("edit-schedule-enabled").checked; + let schedule = null; + + if (scheduleEnabled) { + const mode = document.getElementById("edit-schedule-mode").value; + const start = document.getElementById("edit-schedule-start").value; + const end = document.getElementById("edit-schedule-end").value; + + if (!start || !end) { + errorEl.textContent = "Start- und Endzeit müssen angegeben werden"; + errorEl.style.display = "block"; + return; + } + + schedule = { + mode: mode, + start: start, + end: end + }; + + if (mode === "weekly") { + const days = []; + if (document.getElementById("edit-mon").checked) days.push("0"); + if (document.getElementById("edit-tue").checked) days.push("1"); + if (document.getElementById("edit-wed").checked) days.push("2"); + if (document.getElementById("edit-thu").checked) days.push("3"); + if (document.getElementById("edit-fri").checked) days.push("4"); + if (document.getElementById("edit-sat").checked) days.push("5"); + if (document.getElementById("edit-sun").checked) days.push("6"); + schedule.days = days; + } + } + fetch("/admin/" + editState.site + "/update-playlist-item/" + editState.screen, { method: "POST", headers: { "Content-Type": "application/json" }, @@ -632,7 +789,8 @@ document.addEventListener("DOMContentLoaded", function () { old_name: editState.oldName, new_url: newUrl, zoom: zoom, - passthrough: document.getElementById("edit-passthrough-input").checked + passthrough: document.getElementById("edit-passthrough-input").checked, + schedule: schedule }) }).then(function (r) { if (r.ok) { @@ -652,7 +810,7 @@ document.addEventListener("DOMContentLoaded", function () { }); - +