GENERATOREN + MEDIEN-VERTEILUNG + AUTOSTART-SETTINGS (33/33 E2E)
1) MADRIX-artige Generator-Layer (15 prozedurale Quellen): - engine.GENERATORS: ball (animiert), gradient, pinwheel (animiert), zone-plate (animiert), checkers, snow (Rauschen animiert), circular, spokes, smpte, gamut, red/green/blue/black/white - alle per gst-launch verifiziert, kombinierbar mit FX1/FX2 (Generator+FX gerendert, 28 Frames im Test) - add_generator + Pattern-Wechsel zur Laufzeit (ball->snow) mit Rebuild, Katalog-Endpoint /api/generators, UI-Dropdown im Layer-Tab - Generatoren in Projekten speicher-/ladbar (gradient nach Load aktiv) - Ungueltige Generator-IDs werden mit 400 abgewiesen 2) Medien-Verteilung ueber mehrere Server: - hms_app/distribution.py: Server-Check (/api/health), Remote-Medien-Liste, Streaming-Import mit Upload-Sicherung - config.servers: Remote-Server registrieren (Name+URL, HTTP-only, Duplikat-Schutz), /api/servers (online/offline + Medien), servers/import: Datei von Remote ins lokale Archiv ziehen - UI: Remote-Server in Einstellungen verwalten, Remote-Medien im Medien-Tab mit 'Ins Archiv ziehen'-Button - run.py --data-root: mehrere Instanzen auf einer Maschine mit getrennten config/media/projects-Ordnern - E2E mit ZWEI echten Servern: A registriert B, sieht B-Medien, zieht clip byte-identisch (SHA verifiziert) und spielt ihn als Layer; Offline-Erkennung nach B-Stop; 404/400-Faelle 3) Autostart in den Einstellungen: - Einstellungen: Autostart-Projekt-Dropdown (aus Projektliste), abwaehlbar; Projekte-Tab weiterhin mit Autostart-Button - FIX Settings-Save-Merge: POST /api/settings merged jetzt auf die AKTUELLEN Settings - Autostart und registrierte Server gehen beim Speichern anderer Einstellungen nicht mehr verloren (E2E: Autostart ueberlebt Settings-Save, FPS uebernommen) E2E: 33/33 PASS (Verteilung/Generatoren) + Regressionen: Projekte 25/25, App 14/14, FX 16/16 - alle gruen
This commit is contained in:
@@ -29,6 +29,7 @@ DEFAULTS: dict = {
|
||||
},
|
||||
"engine": {"max_layers": 8, "loop_default": True,
|
||||
"autostart_project": None},
|
||||
"servers": [],
|
||||
}
|
||||
|
||||
|
||||
@@ -131,6 +132,19 @@ def validate(new: dict) -> tuple[dict | None, str | None]:
|
||||
ap = e.get("autostart_project")
|
||||
if ap is not None and not isinstance(ap, str):
|
||||
errors.append("engine.autostart_project")
|
||||
sv = merged.get("servers")
|
||||
if not isinstance(sv, list):
|
||||
errors.append("servers")
|
||||
else:
|
||||
for item in sv:
|
||||
if (not isinstance(item, dict)
|
||||
or not isinstance(item.get("name"), str)
|
||||
or not item.get("name", "").strip()
|
||||
or not isinstance(item.get("url"), str)
|
||||
or not item.get("url", "").startswith(
|
||||
("http://", "https://"))):
|
||||
errors.append("servers")
|
||||
break
|
||||
if not isinstance(merged["output"].get("fullscreen"), bool):
|
||||
errors.append("output.fullscreen")
|
||||
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
"""Medien-Verteilung: Remote-Server registrieren, Medien abrufen."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
|
||||
TIMEOUT_SHORT = 5
|
||||
TIMEOUT_IMPORT = 180
|
||||
|
||||
|
||||
def check(url: str) -> bool:
|
||||
"""Prueft ob ein Remote-Server ein HMS MediaEngine ist."""
|
||||
try:
|
||||
with urllib.request.urlopen(
|
||||
url.rstrip("/") + "/api/health",
|
||||
timeout=TIMEOUT_SHORT) as r:
|
||||
return json.loads(r.read().decode()).get("status") == "ok"
|
||||
except Exception: # noqa: BLE001
|
||||
return False
|
||||
|
||||
|
||||
def list_media(url: str) -> dict:
|
||||
"""Medienbibliothek eines Remote-Servers abrufen."""
|
||||
with urllib.request.urlopen(
|
||||
url.rstrip("/") + "/api/media",
|
||||
timeout=TIMEOUT_SHORT) as r:
|
||||
return json.loads(r.read().decode())
|
||||
|
||||
|
||||
def import_media(url: str, media_name: str, library) -> tuple[bool, object]:
|
||||
"""Zieht eine Datei per HTTP von einem Remote-Server in die lokale
|
||||
Bibliothek (Streaming, gleiche Upload-Pfad-Sicherung wie Web-Upload)."""
|
||||
q = urllib.parse.quote(media_name)
|
||||
try:
|
||||
with urllib.request.urlopen(
|
||||
url.rstrip("/") + "/media/" + q,
|
||||
timeout=TIMEOUT_IMPORT) as r:
|
||||
length = int(r.headers.get("Content-Length", "0"))
|
||||
saved, err = library.save_stream(media_name, r, length)
|
||||
if err:
|
||||
return False, err
|
||||
return True, saved
|
||||
except urllib.error.HTTPError as e:
|
||||
return False, f"Remote-Fehler HTTP {e.code}"
|
||||
except (urllib.error.URLError, OSError, ValueError) as e:
|
||||
return False, f"Verbindung fehlgeschlagen: {e}"
|
||||
+83
-11
@@ -80,6 +80,26 @@ FX_CATALOG: dict[str, dict] = {
|
||||
"int": True},
|
||||
}
|
||||
|
||||
# Generator-Katalog (prozedurale Quellen, verifiziert per gst-launch):
|
||||
# MADRIX-artige Inhalte: Generator-Layer + Effekte kombinierbar.
|
||||
GENERATORS: dict[str, str] = {
|
||||
"ball": "Springender Ball (animiert)",
|
||||
"gradient": "Farbverlauf",
|
||||
"pinwheel": "Pinwheel (animiert)",
|
||||
"zone-plate": "Zonenplatte (animiert)",
|
||||
"checkers-2": "Schachbrett",
|
||||
"snow": "Rauschen (animiert)",
|
||||
"circular": "Kreismuster",
|
||||
"spokes": "Speichen",
|
||||
"smpte": "SMPTE-Farbbalken",
|
||||
"gamut": "Farbraum-Gamut",
|
||||
"red": "Vollrot",
|
||||
"green": "Vollgrün",
|
||||
"blue": "Vollblau",
|
||||
"black": "Schwarz",
|
||||
"white": "Weiß",
|
||||
}
|
||||
|
||||
|
||||
class Layer:
|
||||
"""Ein aktiver Layer im Compositor."""
|
||||
@@ -153,6 +173,12 @@ class MediaEngine:
|
||||
elem += f" {spec['prop']}={val}"
|
||||
fx_chain += f" ! videoconvert ! {elem}"
|
||||
|
||||
if layer.kind == "generator":
|
||||
caps = (f"video/x-raw,width={layer.width},"
|
||||
f"height={layer.height},framerate=15/1")
|
||||
return (f"videotestsrc is-live=true pattern={layer.name} ! "
|
||||
f"{caps}{fx_chain} ! videoconvert ! mix.sink_{layer.id}")
|
||||
|
||||
if layer.kind == "video":
|
||||
caps = (f"video/x-raw,width={layer.width},"
|
||||
f"height={layer.height},format=BGRA")
|
||||
@@ -298,6 +324,33 @@ class MediaEngine:
|
||||
return None
|
||||
return layer.to_dict()
|
||||
|
||||
def add_generator(self, gen_id: str) -> dict | None:
|
||||
"""Fuegt einen Generator-Layer hinzu (prozedurale Quelle)."""
|
||||
with self._lock:
|
||||
if gen_id not in GENERATORS:
|
||||
self.last_error = f"unbekannter Generator: {gen_id}"
|
||||
return None
|
||||
max_l = int(self.settings["engine"].get("max_layers", 8))
|
||||
if len(self.layers) >= max_l:
|
||||
self.last_error = f"max. {max_l} Layer"
|
||||
return None
|
||||
layer = Layer(self._next_id, gen_id, "generator")
|
||||
self._next_id += 1
|
||||
pv = self.settings["preview"]
|
||||
layer.width = pv["width"]
|
||||
layer.height = pv["height"]
|
||||
layer.z = len(self.layers)
|
||||
self.layers.append(layer)
|
||||
if not self.rebuild():
|
||||
self.layers.remove(layer)
|
||||
return None
|
||||
return layer.to_dict()
|
||||
|
||||
def generators(self) -> list[dict]:
|
||||
"""Generator-Katalog fuer die UI."""
|
||||
return [{"id": g, "label": lbl}
|
||||
for g, lbl in GENERATORS.items()]
|
||||
|
||||
def remove_layer(self, layer_id: int) -> bool:
|
||||
with self._lock:
|
||||
kept = [l for l in self.layers if l.id != layer_id]
|
||||
@@ -358,11 +411,22 @@ class MediaEngine:
|
||||
self.rebuild() # Struktur geaendert -> Pipeline neu
|
||||
return self.get_layer(layer.id)
|
||||
if "name" in patch and patch["name"] != layer.name:
|
||||
f = self.library.file_path(str(patch["name"]))
|
||||
if not f.exists():
|
||||
new_name = str(patch["name"])
|
||||
if layer.kind == "generator":
|
||||
if new_name not in GENERATORS:
|
||||
return None
|
||||
layer.name = new_name
|
||||
else:
|
||||
f = self.library.file_path(new_name)
|
||||
if not f.exists():
|
||||
return None
|
||||
layer.name = new_name
|
||||
layer.kind = (self.library.kind_of(new_name)
|
||||
or layer.kind)
|
||||
# Quell-Wechsel braucht einen Pipeline-Rebuild
|
||||
if not self.rebuild():
|
||||
return None
|
||||
layer.name = str(patch["name"])
|
||||
layer.kind = self.library.kind_of(layer.name) or layer.kind
|
||||
return self.get_layer(layer.id)
|
||||
# Pad-Eigenschaften live setzen (ohne Rebuild)
|
||||
self._apply_pad_props(layer)
|
||||
return layer
|
||||
@@ -539,13 +603,21 @@ class MediaEngine:
|
||||
if not isinstance(raw, dict):
|
||||
continue
|
||||
name = str(raw.get("name", ""))
|
||||
f = self.library.file_path(name)
|
||||
kind = self.library.kind_of(name)
|
||||
if not f.exists() or kind is None:
|
||||
print(f"[Projects] WARNUNG: Medium fehlt, Layer "
|
||||
f"uebersprungen: {name}")
|
||||
continue
|
||||
layer = Layer(self._next_id, name, kind)
|
||||
kind_raw = str(raw.get("kind", ""))
|
||||
if kind_raw == "generator":
|
||||
if name not in GENERATORS:
|
||||
print(f"[Projects] WARNUNG: unbekannter Generator, "
|
||||
f"Layer uebersprungen: {name}")
|
||||
continue
|
||||
layer = Layer(self._next_id, name, "generator")
|
||||
else:
|
||||
f = self.library.file_path(name)
|
||||
kind = self.library.kind_of(name)
|
||||
if not f.exists() or kind is None:
|
||||
print(f"[Projects] WARNUNG: Medium fehlt, Layer "
|
||||
f"uebersprungen: {name}")
|
||||
continue
|
||||
layer = Layer(self._next_id, name, kind)
|
||||
self._next_id += 1
|
||||
layer.alpha = max(0.0, min(1.0,
|
||||
float(raw.get("alpha", 1.0))))
|
||||
|
||||
+85
-1
@@ -2,6 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import json
|
||||
import threading
|
||||
import time
|
||||
@@ -9,6 +10,7 @@ import urllib.parse
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
|
||||
from . import config as cfgmod
|
||||
from . import distribution
|
||||
from .projects import ProjectStore
|
||||
from .ui import HTML_UI
|
||||
|
||||
@@ -102,6 +104,23 @@ class EngineServer:
|
||||
self._json(artnet.monitor())
|
||||
elif path == "/api/fx":
|
||||
self._json(engine.fx_catalog())
|
||||
elif path == "/api/generators":
|
||||
self._json({"items": engine.generators()})
|
||||
elif path == "/api/servers":
|
||||
out = []
|
||||
for s_ in settings.get("servers", []):
|
||||
entry = {"name": s_.get("name"),
|
||||
"url": s_.get("url"),
|
||||
"online": False, "items": []}
|
||||
try:
|
||||
data = distribution.list_media(
|
||||
str(s_.get("url", "")))
|
||||
entry["online"] = True
|
||||
entry["items"] = data.get("items", [])
|
||||
except Exception: # noqa: BLE001
|
||||
pass
|
||||
out.append(entry)
|
||||
self._json({"servers": out})
|
||||
elif path == "/api/projects":
|
||||
self._json({"items": server.projects.list(),
|
||||
"autostart": settings["engine"].get(
|
||||
@@ -134,6 +153,67 @@ class EngineServer:
|
||||
self._send_error_json(400, engine.last_error or "?" )
|
||||
else:
|
||||
self._json({"layer": layer})
|
||||
elif path == "/api/layers/add-generator":
|
||||
body = self._read_json()
|
||||
layer = engine.add_generator(
|
||||
str(body.get("generator", "")))
|
||||
if layer is None:
|
||||
self._send_error_json(
|
||||
400, engine.last_error or "unbekannter Generator")
|
||||
else:
|
||||
self._json({"layer": layer})
|
||||
elif path == "/api/servers/add":
|
||||
body = self._read_json()
|
||||
name = str(body.get("name", "")).strip()
|
||||
url = str(body.get("url", "")).strip()
|
||||
if (not name or not url.startswith(
|
||||
("http://", "https://"))):
|
||||
self._send_error_json(
|
||||
400, "Name und http(s)-URL erforderlich")
|
||||
elif any(s_.get("name") == name
|
||||
for s_ in settings.get("servers", [])):
|
||||
self._send_error_json(400, "Name bereits vergeben")
|
||||
elif not distribution.check(url):
|
||||
self._json({"added": False,
|
||||
"error": "Server nicht erreichbar "
|
||||
"oder kein HMS MediaEngine"})
|
||||
else:
|
||||
settings.setdefault("servers", []).append(
|
||||
{"name": name, "url": url})
|
||||
cfgmod.save(root, settings)
|
||||
self._json({"added": True, "name": name})
|
||||
elif path == "/api/servers/remove":
|
||||
body = self._read_json()
|
||||
name = str(body.get("name", ""))
|
||||
before = len(settings.get("servers", []))
|
||||
settings["servers"] = [
|
||||
s_ for s_ in settings.get("servers", [])
|
||||
if s_.get("name") != name]
|
||||
if len(settings["servers"]) == before:
|
||||
self._send_error_json(404, "Server nicht gefunden")
|
||||
else:
|
||||
cfgmod.save(root, settings)
|
||||
self._json({"removed": True})
|
||||
elif path == "/api/servers/import":
|
||||
body = self._read_json()
|
||||
srv_name = str(body.get("server", ""))
|
||||
media_name = str(body.get("media", ""))
|
||||
url = None
|
||||
for s_ in settings.get("servers", []):
|
||||
if s_.get("name") == srv_name:
|
||||
url = str(s_.get("url", ""))
|
||||
break
|
||||
if url is None:
|
||||
self._send_error_json(404, "Server nicht gefunden")
|
||||
elif not media_name:
|
||||
self._send_error_json(400, "Medienname fehlt")
|
||||
else:
|
||||
ok, result = distribution.import_media(
|
||||
url, media_name, library)
|
||||
if ok:
|
||||
self._json({"saved": result})
|
||||
else:
|
||||
self._send_error_json(400, str(result))
|
||||
elif path == "/api/layers/remove":
|
||||
body = self._read_json()
|
||||
ok = engine.remove_layer(int(body.get("id", -1)))
|
||||
@@ -226,7 +306,11 @@ class EngineServer:
|
||||
|
||||
def _post_settings(self):
|
||||
body = self._read_json()
|
||||
normalized, err = cfgmod.validate(body)
|
||||
# WICHTIG: auf aktuelle Settings mergen, damit nicht
|
||||
# gesendete Felder (autostart_project, servers) erhalten
|
||||
# bleiben und nicht auf Defaults zurueckfallen.
|
||||
merged = cfgmod.deep_merge(copy.deepcopy(settings), body)
|
||||
normalized, err = cfgmod.validate(merged)
|
||||
if err is not None:
|
||||
self._send_error_json(400, err)
|
||||
return
|
||||
|
||||
+44
-4
@@ -105,11 +105,17 @@ footer{background:var(--pan);padding:5px 16px;font-size:11px;color:var(--dim);bo
|
||||
<input type="file" id="file" multiple accept=".mp4,.m4v,.avi,.mkv,.mov,.webm,.jpg,.jpeg,.png,.bmp" style="display:none">
|
||||
<div id="upbar" style="display:none;margin-bottom:10px"><div id="uptext" style="font-size:12px;color:var(--dim)"></div><progress id="uprog" style="width:100%"></progress></div>
|
||||
<div class="grid" id="mgrid"></div>
|
||||
<h2 style="margin-top:20px">Remote-Server</h2>
|
||||
<p style="color:var(--dim);font-size:11px">Medien von anderen HMS-Servern ins lokale Archiv ziehen:</p>
|
||||
<div id="rgrid"></div>
|
||||
</div>
|
||||
|
||||
<div id="page-layers" class="page">
|
||||
<div class="row"><button class="acc" onclick="location.hash='#media';showTab('media')">+ Medien hinzufügen</button>
|
||||
<span style="color:var(--dim);font-size:11px;align-self:center">Aktive Layer bearbeiten:</span></div>
|
||||
<div class="row">
|
||||
<button class="acc" onclick="showTab('media')">+ Medien hinzufügen</button>
|
||||
<select id="gensel" style="width:200px"><option value="">+ Generator…</option></select>
|
||||
<button onclick="addGen()">Hinzufügen</button>
|
||||
</div>
|
||||
<div id="ledit"></div>
|
||||
</div>
|
||||
|
||||
@@ -147,8 +153,13 @@ footer{background:var(--pan);padding:5px 16px;font-size:11px;color:var(--dim);bo
|
||||
<div class="setgrp"><h3>Engine</h3>
|
||||
<div class="frow"><label>Max. Layer</label><input type="number" id="s-engine-max"></div>
|
||||
<div class="frow"><label>Loop als Standard</label><input type="checkbox" id="s-engine-loop"></div>
|
||||
<div class="frow"><label>Autostart-Projekt</label><select id="s-autostart"><option value="">– keins –</option></select><span class="hint">lädt beim Programmstart</span></div>
|
||||
<div class="frow"><label>Fullscreen-Ausgabe</label><input type="checkbox" id="s-output-fs"><span class="hint">Windows: D3D11 · Linux: autovideosink</span></div>
|
||||
</div>
|
||||
<div class="setgrp"><h3>Remote-Server (Medien-Verteilung)</h3>
|
||||
<div class="frow"><label>Neuer Server</label><input type="text" id="srv-name" placeholder="Name" style="width:120px"><input type="text" id="srv-url" placeholder="http://192.168.1.50:8080" style="width:240px"><button onclick="addServer()">+ Hinzufügen</button></div>
|
||||
<div id="srvlist"></div>
|
||||
</div>
|
||||
<div class="setgrp"><h3>Medien</h3>
|
||||
<div class="frow"><label>Ordnername</label><input type="text" id="s-media-dir" style="width:140px"></div>
|
||||
<div class="frow"><label>Max. Upload</label><input type="number" id="s-media-max"> MB</div>
|
||||
@@ -169,7 +180,7 @@ async function api(path,body){try{const r=await fetch('/api/'+path,{method:'POST
|
||||
|
||||
// Tabs
|
||||
document.querySelectorAll('#tabbar button').forEach(b=>b.onclick=()=>showTab(b.dataset.p));
|
||||
function showTab(p){document.querySelectorAll('.page').forEach(x=>x.classList.remove('on'));$('page-'+p).classList.add('on');document.querySelectorAll('#tabbar button').forEach(x=>x.classList.toggle('on',x.dataset.p===p));if(p==='media')loadMedia();if(p==='settings')loadSettings();if(p==='projects')loadProjects();}
|
||||
function showTab(p){document.querySelectorAll('.page').forEach(x=>x.classList.remove('on'));$('page-'+p).classList.add('on');document.querySelectorAll('#tabbar button').forEach(x=>x.classList.toggle('on',x.dataset.p===p));if(p==='media'){loadMedia();loadServers()}if(p==='settings')loadSettings();if(p==='projects')loadProjects();}
|
||||
|
||||
// Live-Steuerung
|
||||
$('master').oninput=e=>{api('master',{intensity:e.target.value/100});$('mv').textContent=e.target.value+'%'};
|
||||
@@ -242,6 +253,8 @@ function upOne(f,done){
|
||||
|
||||
// Settings
|
||||
async function loadSettings(){try{const s=await(await fetch('/api/settings')).json();
|
||||
try{const pr=await(await fetch('/api/projects')).json();const sel=$('s-autostart');sel.innerHTML='<option value="">– keins –</option>'+(pr.items||[]).map(p=>`<option value="${p.name}"${pr.autostart===p.name?' selected':''}>${p.name}</option>`).join('')}catch(e){}
|
||||
loadServers();
|
||||
$('s-web-port').value=s.web.port;$('s-web-bind').value=s.web.bind;
|
||||
$('s-preview-width').value=s.preview.width;$('s-preview-height').value=s.preview.height;$('s-preview-fps').value=s.preview.fps;$('s-preview-q').value=s.preview.jpeg_quality;
|
||||
$('s-artnet-enabled').checked=s.artnet.enabled;$('s-artnet-port').value=s.artnet.listen_port;
|
||||
@@ -260,7 +273,7 @@ async function saveSettings(){const body={
|
||||
master_channel:+$('s-artnet-master').value,layer_start_channel:+$('s-artnet-layer').value,
|
||||
playback_channel:+$('s-artnet-playback').value,retrigger_channel:+$('s-artnet-retrigger').value,blackout_channel:+$('s-artnet-blackout').value,
|
||||
signal_loss_policy:$('s-artnet-policy').value,signal_loss_timeout_s:+$('s-artnet-timeout').value},
|
||||
engine:{max_layers:+$('s-engine-max').value,loop_default:$('s-engine-loop').checked},
|
||||
engine:{max_layers:+$('s-engine-max').value,loop_default:$('s-engine-loop').checked,autostart_project:$('s-autostart').value||null},
|
||||
output:{fullscreen:$('s-output-fs').checked},
|
||||
media:{dir:$('s-media-dir').value,max_upload_mb:+$('s-media-max').value}};
|
||||
const d=await api('settings',body);
|
||||
@@ -277,6 +290,31 @@ async function loadProject(name){const d=await api('projects/load',{name});if(d.
|
||||
async function delProject(name){if(!confirm(name+' löschen?'))return;await api('projects/delete',{name});loadProjects()}
|
||||
async function setAutostart(name){const d=await api('projects/autostart',{name});if(d.autostart){toast('Autostart aktiv: '+d.autostart);loadProjects()}}
|
||||
|
||||
// ---------- Generatoren ----------
|
||||
let GENS=[];
|
||||
async function loadGens(){try{const d=await(await fetch('/api/generators')).json();GENS=d.items||[];$('gensel').innerHTML='<option value="">+ Generator…</option>'+GENS.map(g=>`<option value="${g.id}">${g.label}</option>`).join('')}catch(e){}}
|
||||
async function addGen(){const g=$('gensel').value;if(!g){toast('Generator wählen',true);return}
|
||||
const d=await api('layers/add-generator',{generator:g});if(d.layer){toast('Generator-Layer: '+g);$('gensel').value=''}else if(d.error)toast(d.error,true)}
|
||||
|
||||
// ---------- Remote-Server ----------
|
||||
async function addServer(){const name=$('srv-name').value.trim(),url=$('srv-url').value.trim();
|
||||
if(!name||!url){toast('Name und URL eingeben',true);return}
|
||||
const d=await api('servers/add',{name,url});if(d.added){toast('Server hinzugefügt: '+name);$('srv-name').value='';$('srv-url').value='';loadServers()}
|
||||
else toast(d.error||'Server nicht erreichbar oder kein HMS MediaEngine',true)}
|
||||
async function delServer(name){if(!confirm(name+' entfernen?'))return;await api('servers/remove',{name});loadServers()}
|
||||
async function importRemote(srv,media){toast('Importiere '+media+' von '+srv+'…');
|
||||
const d=await api('servers/import',{server:srv,media});if(d.saved){toast('Importiert: '+d.saved);loadMedia()}else toast(d.error||'Import fehlgeschlagen',true)}
|
||||
|
||||
async function loadServers(){try{const d=await(await fetch('/api/servers')).json();const servers=d.servers||[];
|
||||
// Einstellungen: Server-Liste
|
||||
const sl=$('srvlist');if(sl)sl.innerHTML=servers.map(S=>`<div class="frow"><label>${S.name}<br><small style="color:var(--dim)">${S.url}</small></label><span class="badge${S.online?'':' err'}">${S.online?'online':'offline'}</span><button class="danger" onclick="delServer('${S.name}')">✕</button></div>`).join('')||'<p style="color:var(--dim);font-size:11px">Noch keine Server registriert.</p>';
|
||||
// Medien-Tab: Remote-Medien
|
||||
const rg=$('rgrid');if(rg)rg.innerHTML=servers.map(S=>
|
||||
`<div class="setgrp"><h3>${S.name} <span class="badge${S.online?'':' err'}">${S.online?'online':'offline'}</span></h3>`+
|
||||
(S.online?(S.items||[]).map(m=>`<div class="frow"><label>${m.name} <small style="color:var(--dim)">${fmtSize(m.size)}</small></label><button class="acc" onclick="importRemote('${S.name}','${m.name.replace(/'/g,"\\'")}')">Ins Archiv ziehen</button></div>`).join('')||'<p style="color:var(--dim);font-size:11px">Keine Medien auf dem Server.</p>'):'')+'</div>'
|
||||
).join('')||'<p style="color:var(--dim);font-size:11px">Noch keine Server. In den Einstellungen registrieren.</p>';
|
||||
}catch(e){}}
|
||||
|
||||
let FX=[];
|
||||
async function loadFx(){try{FX=await(await fetch('/api/fx')).json();fillFxSelects()}catch(e){}}
|
||||
function fillFxSelects(){document.querySelectorAll('select[id^="fx1-"],select[id^="fx2-"]').forEach(sel=>{
|
||||
@@ -288,6 +326,8 @@ function fillFxSelects(){document.querySelectorAll('select[id^="fx1-"],select[id
|
||||
const _origRender=renderLayerEdit;
|
||||
renderLayerEdit=function(ls){_origRender(ls);document.querySelectorAll('select[id^="fx1-"]').forEach(s=>s.dataset.lay=s.id.slice(4));document.querySelectorAll('select[id^="fx2-"]').forEach(s=>s.dataset.lay=s.id.slice(4));fillFxSelects()};
|
||||
loadFx();
|
||||
loadGens();
|
||||
loadServers();
|
||||
poll();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
Reference in New Issue
Block a user