Version 3 mit URL und Newsticker

Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
Erik Thiele
2026-04-25 19:26:35 +02:00
parent 5ac9dee8fc
commit 742f993a73
5 changed files with 373 additions and 74 deletions

123
app.py
View File

@@ -23,7 +23,7 @@ BASE_DIR = os.path.dirname(os.path.abspath(__file__))
MEDIA_DIR = os.path.join(BASE_DIR, "media") MEDIA_DIR = os.path.join(BASE_DIR, "media")
CONFIG_FILE = os.path.join(BASE_DIR, "config.json") CONFIG_FILE = os.path.join(BASE_DIR, "config.json")
APP_VERSION = "2.1.0" APP_VERSION = "3.1.0"
UPLOAD_EXTENSIONS = {".jpg", ".jpeg", ".png", ".mp4"} UPLOAD_EXTENSIONS = {".jpg", ".jpeg", ".png", ".mp4"}
app = Flask(__name__) app = Flask(__name__)
@@ -48,6 +48,14 @@ def save_config(cfg):
os.fsync(f.fileno()) os.fsync(f.fileno())
print("✅ config.json gespeichert") print("✅ config.json gespeichert")
def is_url(value):
return (
isinstance(value, str)
and value.lower().startswith(("http://", "https://"))
)
# ------------------------------------------------- # -------------------------------------------------
# Auth / Benutzer # Auth / Benutzer
# ------------------------------------------------- # -------------------------------------------------
@@ -64,6 +72,7 @@ def load_user(user_id):
@app.route("/login", methods=["GET", "POST"]) @app.route("/login", methods=["GET", "POST"])
def login(): def login():
config = load_config() config = load_config()
error = None
if request.method == "POST": if request.method == "POST":
if ( if (
@@ -72,8 +81,9 @@ def login():
): ):
login_user(Admin()) login_user(Admin())
return redirect("/admin") return redirect("/admin")
error = "Ungültige Zugangsdaten"
return render_template("login.html") return render_template("login.html", error=error)
@app.route("/logout") @app.route("/logout")
def logout(): def logout():
@@ -124,13 +134,23 @@ def player(screen):
# 1. Playlist-Reihenfolge # 1. Playlist-Reihenfolge
for name in playlist: for name in playlist:
if is_url(name):
normal_files.append({"kind": "url", "url": name})
continue
if os.path.exists(os.path.join(folder, name)) and allowed_file(name): if os.path.exists(os.path.join(folder, name)) and allowed_file(name):
normal_files.append(name) normal_files.append({"kind": "file", "name": name})
existing_file_names = {
item["name"] for item in normal_files if item["kind"] == "file"
}
# 2. Neue Dateien hintendran # 2. Neue Dateien hintendran
for f in sorted(os.listdir(folder)): for f in sorted(os.listdir(folder)):
if f not in normal_files and allowed_file(f): if f in existing_file_names or f.startswith("._"):
normal_files.append(f) continue
if allowed_file(f):
normal_files.append({"kind": "file", "name": f})
# ------------------------------ # ------------------------------
# Priority-Playlist (global) # Priority-Playlist (global)
@@ -139,7 +159,15 @@ def player(screen):
priority_cfg = config.get("priority", {}) priority_cfg = config.get("priority", {})
if priority_cfg.get("enabled", False): if priority_cfg.get("enabled", False):
prio_files = priority_cfg.get("playlist", []) prio_files = []
for name in priority_cfg.get("playlist", []):
if is_url(name):
prio_files.append({"kind": "url", "url": name})
continue
file_path = os.path.join(MEDIA_DIR, "priority", name)
if os.path.exists(file_path):
prio_files.append({"kind": "file", "name": name})
else: else:
prio_files = [] prio_files = []
@@ -148,7 +176,9 @@ def player(screen):
screen=screen, screen=screen,
normal_files=normal_files, # ✅ explizit normal_files=normal_files, # ✅ explizit
prio_files=prio_files, # ✅ IMMER Liste prio_files=prio_files, # ✅ IMMER Liste
interval=screen_cfg.get("interval", 10) interval=screen_cfg.get("interval", 10),
newsticker_text=screen_cfg.get("newsticker_text", ""),
newsticker_enabled=screen_cfg.get("newsticker_enabled", False)
) )
@@ -193,10 +223,26 @@ def admin():
continue continue
files = [] files = []
playlist = cfg["screens"][screen].get("playlist", []) screen_cfg = cfg.setdefault("screens", {}).setdefault(screen, {
"interval": 10,
"show_images": True,
"show_videos": True,
"playlist": []
})
playlist = screen_cfg.get("playlist", [])
screen_cfg["newsticker_text"] = screen_cfg.get("newsticker_text", "")
screen_cfg["newsticker_enabled"] = screen_cfg.get("newsticker_enabled", False)
# 1⃣ Playlist-Reihenfolge # 1⃣ Playlist-Reihenfolge
for name in playlist: for name in playlist:
if is_url(name):
files.append({
"name": name,
"type": "url",
"size": "URL"
})
continue
file_path = os.path.join(path, name) file_path = os.path.join(path, name)
if not os.path.exists(file_path): if not os.path.exists(file_path):
continue continue
@@ -227,7 +273,7 @@ def admin():
"size": size "size": size
}) })
screens[screen] = cfg["screens"][screen] screens[screen] = screen_cfg
media_files[screen] = files media_files[screen] = files
screen_status[screen] = "active" if files else "empty" screen_status[screen] = "active" if files else "empty"
@@ -241,7 +287,15 @@ def admin():
# 1⃣ Playlist-Reihenfolge # 1⃣ Playlist-Reihenfolge
for name in prio_playlist: for name in prio_playlist:
file_path = os.path.join(MEDIA_DIR, "priority") if is_url(name):
priority_files.append({
"name": name,
"type": "url",
"size": "URL"
})
continue
file_path = os.path.join(prio_dir, name)
if not os.path.exists(file_path): if not os.path.exists(file_path):
continue continue
@@ -291,11 +345,18 @@ def admin():
@login_required @login_required
def update_screen(screen): def update_screen(screen):
config = load_config() config = load_config()
cfg = config["screens"][screen] cfg = config.setdefault("screens", {}).setdefault(screen, {
"interval": 10,
"show_images": True,
"show_videos": True,
"playlist": []
})
cfg["interval"] = int(request.form.get("interval", 10)) cfg["interval"] = int(request.form.get("interval", 10))
cfg["show_images"] = "show_images" in request.form cfg["show_images"] = "show_images" in request.form
cfg["show_videos"] = "show_videos" in request.form cfg["show_videos"] = "show_videos" in request.form
cfg["newsticker_text"] = request.form.get("newsticker_text", "")
cfg["newsticker_enabled"] = "newsticker_enabled" in request.form
save_config(config) save_config(config)
return redirect("/admin") return redirect("/admin")
@@ -319,11 +380,38 @@ def upload(screen):
return redirect("/admin") return redirect("/admin")
# -------------------------------------------------
# Admin: URL hinzufügen
# -------------------------------------------------
@app.route("/admin/add-url/<screen>", methods=["POST"])
@login_required
def add_url(screen):
url = request.form.get("url", "").strip()
if not is_url(url):
return redirect("/admin")
config = load_config()
if screen == "priority":
config.setdefault("priority", {}).setdefault("playlist", []).append(url)
else:
screen_cfg = config.setdefault("screens", {}).setdefault(screen, {
"interval": 10,
"show_images": True,
"show_videos": True,
"playlist": []
})
screen_cfg.setdefault("playlist", []).append(url)
save_config(config)
return redirect("/admin")
# ------------------------------------------------- # -------------------------------------------------
# Admin: Datei löschen # Admin: Datei löschen
# ------------------------------------------------- # -------------------------------------------------
@app.route("/admin/delete/<screen>/<filename>", methods=["POST"]) @app.route("/admin/delete/<screen>/<path:filename>", methods=["POST"])
@login_required @login_required
def delete_file(screen, filename): def delete_file(screen, filename):
cfg = load_config() cfg = load_config()
@@ -358,9 +446,18 @@ def save_playlist(screen):
data = request.get_json() data = request.get_json()
if screen == "priority": if screen == "priority":
cfg.setdefault("priority", {}).setdefault("playlist", [])
cfg["priority"]["playlist"] = data["playlist"] cfg["priority"]["playlist"] = data["playlist"]
else: else:
cfg["screens"][screen]["playlist"] = data["playlist"] screen_cfg = cfg.setdefault("screens", {}).setdefault(screen, {
"interval": 10,
"show_images": True,
"show_videos": True,
"playlist": [],
"newsticker_text": "",
"newsticker_enabled": False
})
screen_cfg["playlist"] = data["playlist"]
save_config(cfg) save_config(cfg)
return "", 204 return "", 204

View File

@@ -1,27 +1,33 @@
{ {
"priority": { "priority": {
"enabled": true, "enabled": true,
"playlist": [] "playlist": [
"https://www.meteoblue.com/en/meteotv/d7b0fd"
]
}, },
"screens": { "screens": {
"lobby": { "lobby": {
"interval": 10, "interval": 15,
"show_images": true, "show_images": true,
"show_videos": true, "show_videos": false,
"playlist": [ "playlist": [
"HilfeKI.jpg", "https://wbxroompresence.cancom.io/standort?find=Stuttgart",
"QRCode.png", "Hilfe_KI.jpg",
"Will_AI_Replace_Developers_1.jpg", "e00a687f-d82a-446d-b8f3-07895fbc7309.png",
"Video CANCOM Jahresr\u00fcckblick 2025.MP4" "Video_CANCOM_LIVE_2025_Stuttgart.MP4"
] ],
"newsticker_text": "Hallo dies ist der Newsticker",
"newsticker_enabled": false
}, },
"casino": { "casino": {
"interval": 10, "interval": 10,
"show_images": true, "show_images": true,
"show_videos": true, "show_videos": true,
"playlist": [ "playlist": [
"JET-Design.JPG" "https://wbxroompresence.cancom.io/standort?find=Stuttgart"
] ],
"newsticker_text": "Hallo dies ist der Newsticker",
"newsticker_enabled": false
} }
}, },
"admin": { "admin": {

View File

@@ -38,6 +38,7 @@
background-color: #eeeeee; background-color: #eeeeee;
border-left: 6px solid #DA002D; border-left: 6px solid #DA002D;
padding-left: 1rem; padding-left: 1rem;
font-size: 1.1rem; /* leicht größer */
font-weight: 600; font-weight: 600;
} }
@@ -93,6 +94,22 @@
.drag-handle:active { .drag-handle:active {
cursor: grabbing; cursor: grabbing;
} }
/* CANCOM CI Tooltip Styling */
.tooltip {
--bs-tooltip-border-color: #DA002D;
}
.tooltip .tooltip-inner {
border: 1px solid #DA002D;
background-color: #ffffff;
color: #212121;
box-shadow: none;
}
.tooltip .tooltip-arrow::before {
border-top-color: #DA002D;
border-bottom-color: #DA002D;
background-color: #ffffff;
}
</style> </style>
</head> </head>
@@ -116,7 +133,7 @@
Admin Dashboard Admin Dashboard
<h2 class="page-title text-white">Simple Signage</h2> <h2 class="page-title text-white">Simple Signage</h2>
</span> </span>
</a>s </a>
<div class="d-none d-sm-inline"> <div class="d-none d-sm-inline">
<a href="/logout" class="btn">Logout</a> <a href="/logout" class="btn">Logout</a>
@@ -139,7 +156,7 @@
⚠ PRIORITY (global) ⚠ PRIORITY (global)
</strong> </strong>
</div> </div>
<span class="badge bg-success">wirkt auf alle Player</span> <span class="badge bg-green text-green-fg">wirkt auf alle Player</span>
</div> </div>
<div class="card-body"> <div class="card-body">
@@ -161,6 +178,14 @@
</div> </div>
</form> </form>
<form action="/admin/add-url/priority" method="post" class="mb-3">
<div class="input-group">
<input type="url" name="url" class="form-control"
placeholder="https://example.com" required>
<button class="btn btn-secondary" type="submit">URL hinzufügen</button>
</div>
</form>
<!-- Playlist --> <!-- Playlist -->
<h4>Playlist Reihenfolge</h4> <h4>Playlist Reihenfolge</h4>
@@ -169,21 +194,37 @@
<li class="list-group-item playlist-row" <li class="list-group-item playlist-row"
data-file="{{ file.name }}"> data-file="{{ file.name }}">
<span class="playlist-name">{{ file.name }}</span> <span class="playlist-name"
{% if file.type == "image" %}
data-bs-toggle="tooltip"
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'>"
{% elif file.type == "url" %}
data-bs-toggle="tooltip"
title="URL: {{ file.name }}"
{% endif %}>
{{ file.name }}
</span>
{% if file.type == "image" %} {% if file.type == "image" %}
<span class="badge bg-blue-lt">Bild</span> <span class="badge bg-blue-lt">Bild</span>
{% elif file.type == "url" %}
<span class="badge bg-info">URL</span>
{% else %} {% else %}
<span class="badge bg-orange-lt">Video</span> <span class="badge bg-orange-lt">Video</span>
{% endif %} {% endif %}
<span class="playlist-size">{{ file.size }} KB</span> <span class="playlist-size">
{% if file.type == "url" %}
{{ file.size }}
{% else %}
{{ file.size }} KB
{% endif %}
</span>
<!-- Delete --> <!-- Delete -->
<form action="{{url_for('delete_file', <form action="{{ url_for('delete_file', screen='priority', filename=file.name) }}"
screen='priority',
filename=file.name) }}"
method="post" method="post"
class="d-inline" class="d-inline"
onmousedown="event.stopPropagation()"> onmousedown="event.stopPropagation()">
@@ -215,9 +256,9 @@
<!-- Card Header --> <!-- Card Header -->
<div class="card-header d-flex justify-content-between align-items-center"> <div class="card-header d-flex justify-content-between align-items-center">
<span>Screen: {{ screen }}</span> <span>Screen: https://signage.ccmake.de/player/{{ screen }}</span>
{% if screen_status[screen] == "active" %} {% if screen_status[screen] == "active" %}
<span class="badge bg-success">Aktiv</span> <span class="badge bg-green text-green-fg">Aktiv</span>
{% else %} {% else %}
<span class="badge bg-warning text-dark">Leer</span> <span class="badge bg-warning text-dark">Leer</span>
{% endif %} {% endif %}
@@ -226,13 +267,26 @@
<div class="card-body"> <div class="card-body">
<!-- Einstellungen --> <!-- Einstellungen -->
<h2>Einstellungen</h2>
<form action="/admin/update/{{ screen }}" method="post"> <form action="/admin/update/{{ screen }}" method="post">
<div class="mb-3"> <div class="mb-2">
<label class="form-label">Intervall (Sekunden)</label> <label class="form-label">Intervall (Sekunden)</label>
<input type="number" class="form-control" <input type="number" class="form-control"
name="interval" min="1" value="{{ cfg.interval }}"> name="interval" min="1" value="{{ cfg.interval }}">
</div> </div>
<div class="mb-4">
<label class="form-label">Newsticker-Text</label>
<input type="text" class="form-control" name="newsticker_text" value="{{ cfg.newsticker_text|default('') }}" maxlength="200">
</div>
<div class="form-check form-switch mb-2">
<input class="form-check-input" type="checkbox"
name="newsticker_enabled"
{% if cfg.newsticker_enabled %}checked{% endif %}>
<label class="form-check-label">Newsticker anzeigen</label>
</div>
<div class="form-check form-switch mb-2"> <div class="form-check form-switch mb-2">
<input class="form-check-input" type="checkbox" <input class="form-check-input" type="checkbox"
name="show_images" name="show_images"
@@ -240,7 +294,7 @@
<label class="form-check-label">Bilder anzeigen</label> <label class="form-check-label">Bilder anzeigen</label>
</div> </div>
<div class="form-check form-switch mb-3"> <div class="form-check form-switch mb-4">
<input class="form-check-input" type="checkbox" <input class="form-check-input" type="checkbox"
name="show_videos" name="show_videos"
{% if cfg.show_videos %}checked{% endif %}> {% if cfg.show_videos %}checked{% endif %}>
@@ -255,7 +309,7 @@
<hr> <hr>
<!-- Upload --> <!-- Upload -->
<h4>Medien hochladen</h4> <h2>Medien hochladen / URL hinzufügen</h2>
<form action="/admin/upload/{{ screen }}" <form action="/admin/upload/{{ screen }}"
method="post" enctype="multipart/form-data"> method="post" enctype="multipart/form-data">
<div class="input-group mb-3"> <div class="input-group mb-3">
@@ -264,26 +318,54 @@
</div> </div>
</form> </form>
<form action="/admin/add-url/{{ screen }}" method="post" class="mb-4">
<div class="input-group">
<input type="url" name="url" class="form-control"
placeholder="https://example.com" required>
<button class="btn btn-secondary" type="submit">URL hinzufügen</button>
</div>
</form>
<hr>
<!-- Playlist --> <!-- Playlist -->
<h4>Playlist Reihenfolge</h4> <h2>Playlist Reihenfolge</h2>
<ul class="list-group mb-3" id="playlist-{{ screen }}"> <ul class="list-group mb-3" id="playlist-{{ screen }}">
{% for file in media_files[screen] %} {% for file in media_files[screen] %}
<li class="list-group-item playlist-row" <li class="list-group-item playlist-row"
data-file="{{ file.name }}"> data-file="{{ file.name }}">
<span class="playlist-name">{{ file.name }}</span> <span class="playlist-name"
{% if file.type == "image" %}
data-bs-toggle="tooltip"
data-bs-html="true"
title="<img src='/media/{{ screen }}/{{ file.name }}' style='max-width: 200px; max-height: 150px; border: none; background: white; padding: 2px;' alt='Vorschau'>"
{% elif file.type == "url" %}
data-bs-toggle="tooltip"
title="URL: {{ file.name }}"
{% endif %}>
{{ file.name }}
</span>
{% if file.type == "image" %} {% if file.type == "image" %}
<span class="badge bg-blue-lt">Bild</span> <span class="badge bg-purple text-purple-fg">Bild</span>
{% elif file.type == "url" %}
<span class="badge bg-blue text-blue-fg">URL</span>
{% else %} {% else %}
<span class="badge bg-orange-lt">Video</span> <span class="badge bg-orange text-orange-fg">Video</span>
{% endif %} {% endif %}
<span class="playlist-size">{{ file.size }} KB</span> <span class="playlist-size">
{% if file.type == "url" %}
{{ file.size }}
{% else %}
{{ file.size }} KB
{% endif %}
</span>
<!-- Delete --> <!-- Delete -->
<form action="/admin/delete/{{ screen }}/{{ file.name }}" <form action="{{ url_for('delete_file', screen=screen, filename=file.name) }}"
method="post"> method="post">
<button type="submit" <button type="submit"
class="btn btn-outline-danger btn-sm" class="btn btn-outline-danger btn-sm"
@@ -360,9 +442,11 @@ document.addEventListener("DOMContentLoaded", function () {
window.savePlaylist = savePlaylist; window.savePlaylist = savePlaylist;
{% for screen in screens.keys() %} // Initialize tooltips
initSortable("{{ screen }}"); var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
{% endfor %} var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
return new bootstrap.Tooltip(tooltipTriggerEl);
});
}); });
</script> </script>

View File

@@ -2,14 +2,36 @@
<html lang="de"> <html lang="de">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<title>Signage Login</title> <title>CANCOM Simple Signage Admin</title>
<link href="https://unpkg.com/@tabler/core@latest/dist/css/tabler.min.css" rel="stylesheet"> <link rel="icon" href="{{ url_for('static', filename='favicon.ico') }}">
<link href="https://unpkg.com/@tabler/core@1.0.0-beta20/dist/css/tabler.min.css" rel="stylesheet">
<style>
body {
background-color: #F4F6F8;
color: #212121;
}
.btn-primary {
background-color: #DA002D;
border-color: #DA002D;
}
.btn-primary:hover {
background-color: #B00024;
border-color: #B00024;
}
</style>
</head> </head>
<body class="d-flex align-items-center justify-content-center bg-dark" style="height:100vh;"> <body class="d-flex align-items-center justify-content-center" style="height:100vh;">
<div class="card card-md"> <div class="card card-md">
<div class="card-body"> <div class="card-body">
<h2 class="text-center mb-4">Signage Admin</h2> <h2 class="text-center mb-4">Signage Admin</h2>
{% if error %}
<div class="alert alert-danger text-center" role="alert">
{{ error }}
</div>
{% endif %}
<form method="post"> <form method="post">
<div class="mb-3"> <div class="mb-3">
<label class="form-label">Benutzername</label> <label class="form-label">Benutzername</label>

View File

@@ -7,6 +7,7 @@
<style> <style>
html, body { html, body {
font-family: system-ui, sans-serif;
margin: 0; margin: 0;
padding: 0; padding: 0;
width: 100%; width: 100%;
@@ -15,22 +16,86 @@
overflow: hidden; overflow: hidden;
} }
img, video { img, video, iframe {
width: 100vw; width: 100vw;
height: 100vh; height: 100vh;
object-fit: contain;
background: black; background: black;
display: none; display: none;
position: fixed;
top: 0;
left: 0;
z-index: 1;
} }
img, iframe {
object-fit: contain;
border: 0;
}
video {
object-fit: contain;
}
.newsticker-text {
flex: 1;
overflow: hidden;
position: relative;
height: 100%;
}
.newsticker-track {
display: inline-block;
white-space: nowrap;
position: absolute;
left: 100%;
top: 0;
height: 40px;
line-height: 40px;
animation: ticker-scroll 40s linear infinite;
}
.newsticker-track span {
padding-right: 4rem;
}
@keyframes ticker-scroll {
from { transform: translateX(0); }
to { transform: translateX(-100vw); }
}
</style> </style>
</head> </head>
<body> <body>
<img id="image"> <img id="image">
<video id="video" muted autoplay playsinline></video> <video id="video" muted autoplay playsinline></video>
<iframe id="iframe"></iframe>
{% if newsticker_enabled %}
<div id="newsticker-bar" style="position:fixed;left:0;right:0;bottom:0;height:40px;z-index:10000;background:#DA002D;color:#fff;display:flex;align-items:center;overflow:hidden;">
<div id="newsticker-text" class="newsticker-text">
<div class="newsticker-track">
<span>{{ newsticker_text }}</span>
</div>
</div>
<div id="newsticker-clock" style="padding:0 18px 0 24px;font-size:1.1em;font-family:monospace;min-width:90px;text-align:right;"></div>
</div>
{% endif %}
<script> <script>
// Newsticker Uhrzeit
function updateClock() {
const el = document.getElementById('newsticker-clock');
if (!el) return;
const now = new Date();
el.textContent = now.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit', second: '2-digit' });
}
setInterval(updateClock, 1000);
updateClock();
/* ----------------------------- /* -----------------------------
Daten vom Server Daten vom Server
----------------------------- */ ----------------------------- */
@@ -44,85 +109,110 @@ const screen = "{{ screen }}";
----------------------------- */ ----------------------------- */
let normalIndex = 0; let normalIndex = 0;
let prioIndex = 0; let prioIndex = 0;
let playPrioNext = false;
let mode = "normal"; // "normal" oder "prio" let mode = "normal"; // "normal" oder "prio"
const img = document.getElementById("image"); const img = document.getElementById("image");
const vid = document.getElementById("video"); const vid = document.getElementById("video");
const iframe = document.getElementById("iframe");
/* ----------------------------- /* -----------------------------
Hilfsfunktionen Hilfsfunktionen
----------------------------- */ ----------------------------- */
function isVideo(file) { function normalizeItem(item) {
return file.toLowerCase().endsWith(".mp4"); if (typeof item === "string") {
const lower = item.toLowerCase();
if (lower.startsWith("http://") || lower.startsWith("https://")) {
return { kind: "url", url: item };
}
return { kind: "file", name: item };
}
return item;
} }
function isVideo(item) {
return item.kind === "file" && item.name.toLowerCase().endsWith(".mp4");
}
function isImage(item) {
return item.kind === "file" && /\.(jpg|jpeg|png)$/i.test(item.name);
}
function getNextItem() { function getNextItem() {
const normalizedNormal = normalFiles.map(normalizeItem);
const normalizedPrio = prioFiles.map(normalizeItem);
// ✅ Sonderfall: nur Priority vorhanden // ✅ Sonderfall: nur Priority vorhanden
if (normalFiles.length === 0 && prioFiles.length > 0) { if (normalizedNormal.length === 0 && normalizedPrio.length > 0) {
return { return {
file: prioFiles[prioIndex++ % prioFiles.length], item: normalizedPrio[prioIndex++ % normalizedPrio.length],
isPrio: true isPrio: true
}; };
} }
// ✅ Sonderfall: nur normale Playlist vorhanden // ✅ Sonderfall: nur normale Playlist vorhanden
if (prioFiles.length === 0 && normalFiles.length > 0) { if (normalizedPrio.length === 0 && normalizedNormal.length > 0) {
return { return {
file: normalFiles[normalIndex++ % normalFiles.length], item: normalizedNormal[normalIndex++ % normalizedNormal.length],
isPrio: false isPrio: false
}; };
} }
// ✅ Normal-Phase // ✅ Normal-Phase
if (mode === "normal") { if (mode === "normal") {
const file = normalFiles[normalIndex++]; const item = normalizedNormal[normalIndex++];
if (normalIndex >= normalFiles.length) { if (normalIndex >= normalizedNormal.length) {
normalIndex = 0; normalIndex = 0;
if (prioFiles.length > 0) { if (normalizedPrio.length > 0) {
mode = "prio"; // ➜ nach kompletter Normal-Playlist wechseln mode = "prio"; // ➜ nach kompletter Normal-Playlist wechseln
} }
} }
return { file, isPrio: false }; return { item, isPrio: false };
} }
// ✅ Priority-Phase // ✅ Priority-Phase
if (mode === "prio") { if (mode === "prio") {
const file = prioFiles[prioIndex++]; const item = normalizedPrio[prioIndex++];
if (prioIndex >= prioFiles.length) { if (prioIndex >= normalizedPrio.length) {
prioIndex = 0; prioIndex = 0;
mode = "normal"; // ➜ nach kompletter Priority zurück mode = "normal"; // ➜ nach kompletter Priority zurück
} }
return { file, isPrio: true }; return { item, isPrio: true };
} }
return null; return null;
} }
/* ----------------------------- /* -----------------------------
Medien abspielen Medien abspielen
----------------------------- */ ----------------------------- */
function playNext() { function playNext() {
const item = getNextItem(); const entry = getNextItem();
if (!item) return; if (!entry) return;
const basePath = item.isPrio ? "priority" : screen; const item = entry.item;
const src = `/media/${basePath}/${item.file}`; const basePath = entry.isPrio ? "priority" : screen;
if (isVideo(item.file)) { img.style.display = "none";
img.style.display = "none"; vid.style.display = "none";
iframe.style.display = "none";
vid.pause();
vid.src = "";
iframe.src = "";
if (item.kind === "url") {
iframe.style.display = "block";
iframe.src = item.url;
setTimeout(playNext, interval);
return;
}
const src = `/media/${basePath}/${item.name}`;
if (isVideo(item)) {
vid.style.display = "block"; vid.style.display = "block";
vid.src = src; vid.src = src;
vid.onended = playNext; vid.onended = playNext;
vid.play(); vid.play();
} else { } else {
vid.pause();
vid.style.display = "none";
img.style.display = "block"; img.style.display = "block";
img.src = src; img.src = src;
setTimeout(playNext, interval); setTimeout(playNext, interval);