Projekt aus Docker-Image videoconverter:2.9 extrahiert. Enthält zweiphasigen Import-Workflow mit Serien-Zuordnung. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
200 lines
7.8 KiB
JavaScript
200 lines
7.8 KiB
JavaScript
/**
|
|
* WebSocket-Client fuer Echtzeit-Updates
|
|
* Verbindet sich mit dem Server und aktualisiert Dashboard dynamisch
|
|
*/
|
|
|
|
let ws = null;
|
|
let videoActive = {};
|
|
let videoQueue = {};
|
|
let reconnectTimer = null;
|
|
|
|
// WebSocket verbinden
|
|
function connectWebSocket() {
|
|
if (!window.WS_URL) return;
|
|
|
|
ws = new WebSocket(WS_URL);
|
|
|
|
ws.onopen = function () {
|
|
console.log("WebSocket verbunden:", WS_URL);
|
|
if (reconnectTimer) {
|
|
clearTimeout(reconnectTimer);
|
|
reconnectTimer = null;
|
|
}
|
|
};
|
|
|
|
ws.onmessage = function (event) {
|
|
try {
|
|
const packet = JSON.parse(event.data);
|
|
|
|
if (packet.data_flow !== undefined) {
|
|
updateProgress(packet.data_flow);
|
|
} else if (packet.data_convert !== undefined) {
|
|
updateActiveConversions(packet.data_convert);
|
|
} else if (packet.data_queue !== undefined) {
|
|
updateQueue(packet.data_queue);
|
|
} else if (packet.data_log !== undefined) {
|
|
// Log-Nachrichten ans Benachrichtigungs-System weiterleiten
|
|
if (typeof addNotification === "function") {
|
|
addNotification(packet.data_log.message, packet.data_log.level);
|
|
}
|
|
} else if (packet.data_import !== undefined) {
|
|
// Import-Fortschritt an Library weiterleiten
|
|
if (typeof handleImportWS === "function") {
|
|
handleImportWS(packet.data_import);
|
|
}
|
|
} else if (packet.data_library_scan !== undefined) {
|
|
// Scan-Fortschritt an Library weiterleiten
|
|
if (typeof handleScanWS === "function") {
|
|
handleScanWS(packet.data_library_scan);
|
|
}
|
|
}
|
|
// Globaler Progress-Balken aktualisieren
|
|
if (typeof _updateGlobalProgress === "function") {
|
|
_updateGlobalProgress(packet);
|
|
}
|
|
} catch (e) {
|
|
console.error("WebSocket Nachricht parsen fehlgeschlagen:", e);
|
|
}
|
|
};
|
|
|
|
ws.onclose = function () {
|
|
console.log("WebSocket getrennt, Reconnect in 3s...");
|
|
reconnectTimer = setTimeout(connectWebSocket, 3000);
|
|
};
|
|
|
|
ws.onerror = function (err) {
|
|
console.error("WebSocket Fehler:", err);
|
|
};
|
|
}
|
|
|
|
// === Aktive Konvertierungen ===
|
|
|
|
function updateActiveConversions(data) {
|
|
const container = document.getElementById("active-conversions");
|
|
if (!container) return;
|
|
|
|
// Entfernte Jobs loeschen
|
|
for (const key in videoActive) {
|
|
if (!(key in data)) {
|
|
const elem = document.getElementById("convert_" + key);
|
|
if (elem) elem.remove();
|
|
delete videoActive[key];
|
|
}
|
|
}
|
|
|
|
// Neue Jobs hinzufuegen
|
|
for (const [key, video] of Object.entries(data)) {
|
|
if (!videoActive[key]) {
|
|
const card = document.createElement("div");
|
|
card.className = "video-card";
|
|
card.id = "convert_" + key;
|
|
card.innerHTML = `
|
|
<h3 title="${video.source_path}">${video.source_file_name} → ${video.target_file_name}</h3>
|
|
<div class="progress-container">
|
|
<div class="progress-bar"></div>
|
|
</div>
|
|
<div class="progress-text">
|
|
<span class="loading-pct">0</span>%
|
|
</div>
|
|
<div class="video-card-values">
|
|
<div class="video-card-values-items"><b>Frames</b><br><span class="frames">0</span></div>
|
|
<div class="video-card-values-items"><b>FPS</b><br><span class="fps">0</span></div>
|
|
<div class="video-card-values-items"><b>Speed</b><br><span class="speed">0</span>x</div>
|
|
<div class="video-card-values-items"><b>Groesse</b><br><span class="size">0</span> <span class="size_unit">KiB</span></div>
|
|
<div class="video-card-values-items"><b>Bitrate</b><br><span class="bitrate">0</span> <span class="bitrate_unit">kbits/s</span></div>
|
|
<div class="video-card-values-items"><b>Zeit</b><br><span class="time">0 Min</span></div>
|
|
<div class="video-card-values-items"><b>Verbleibend</b><br><span class="eta">-</span></div>
|
|
<div class="video-card-values-items">
|
|
<button class="btn-danger" onclick="sendCommand('cancel', ${key})">Abbrechen</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
container.appendChild(card);
|
|
videoActive[key] = video;
|
|
}
|
|
}
|
|
}
|
|
|
|
function updateProgress(flow) {
|
|
const container = document.getElementById("convert_" + flow.id);
|
|
if (!container) return;
|
|
|
|
container.querySelector(".frames").textContent = flow.frames || 0;
|
|
container.querySelector(".fps").textContent = flow.fps || 0;
|
|
container.querySelector(".speed").textContent = flow.speed || 0;
|
|
container.querySelector(".size").textContent = flow.size ? flow.size[0] : 0;
|
|
container.querySelector(".size_unit").textContent = flow.size ? flow.size[1] : "KiB";
|
|
container.querySelector(".bitrate").textContent = flow.bitrate ? flow.bitrate[0] : 0;
|
|
container.querySelector(".bitrate_unit").textContent = flow.bitrate ? flow.bitrate[1] : "kbits/s";
|
|
container.querySelector(".time").textContent = flow.time || "0 Min";
|
|
container.querySelector(".eta").textContent = flow.time_remaining || "-";
|
|
container.querySelector(".loading-pct").textContent = (flow.loading || 0).toFixed(1);
|
|
|
|
const bar = container.querySelector(".progress-bar");
|
|
bar.style.width = (flow.loading || 0) + "%";
|
|
}
|
|
|
|
// === Warteschlange ===
|
|
|
|
function updateQueue(data) {
|
|
const container = document.getElementById("queue");
|
|
if (!container) return;
|
|
|
|
// Entfernte/geaenderte Jobs loeschen
|
|
for (const key in videoQueue) {
|
|
if (!(key in data) || videoQueue[key]?.status !== data[key]?.status) {
|
|
const elem = document.getElementById("queue_" + key);
|
|
if (elem) elem.remove();
|
|
delete videoQueue[key];
|
|
}
|
|
}
|
|
|
|
// Neue Jobs hinzufuegen
|
|
for (const [key, video] of Object.entries(data)) {
|
|
if (!videoQueue[key]) {
|
|
const card = document.createElement("div");
|
|
card.className = "queue-card";
|
|
card.id = "queue_" + key;
|
|
|
|
let statusHtml;
|
|
if (video.status === 1) {
|
|
statusHtml = '<span class="status-badge active">Aktiv</span>';
|
|
} else if (video.status === 3) {
|
|
statusHtml = '<span class="status-badge error">Fehler</span>';
|
|
} else if (video.status === 4) {
|
|
statusHtml = '<span class="status-badge warn">Abgebrochen</span>';
|
|
} else {
|
|
statusHtml = '<span class="status-badge queued">Wartend</span>';
|
|
}
|
|
|
|
card.innerHTML = `
|
|
<h4 title="${video.source_path}">${video.source_file_name}</h4>
|
|
<div class="queue-card-footer">
|
|
${statusHtml}
|
|
<div>
|
|
${video.status === 3 || video.status === 4 ?
|
|
`<button class="btn-secondary btn-small" onclick="sendCommand('retry', ${key})">Wiederholen</button>` : ""}
|
|
<button class="btn-danger btn-small" onclick="sendCommand('delete', ${key})">Loeschen</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
container.appendChild(card);
|
|
videoQueue[key] = video;
|
|
}
|
|
}
|
|
}
|
|
|
|
// === Befehle senden ===
|
|
|
|
function sendCommand(command, id) {
|
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
ws.send(JSON.stringify({
|
|
data_command: { cmd: command, id: id }
|
|
}));
|
|
} else {
|
|
console.warn("WebSocket nicht verbunden");
|
|
}
|
|
}
|
|
|
|
// Verbindung herstellen
|
|
connectWebSocket();
|