docker.videokonverter/app/static/js/websocket.js
data ea5a81cd17 v2.4.0: Video-Player, Import-Zuordnung, Loeschen, Audio-Fix
- Video-Player mit ffmpeg-Transcoding (EAC3/DTS/AC3 -> AAC)
- Play-Buttons in allen Ansichten (Serien, Filme, Ordner)
- Delete-Buttons fuer einzelne Videos (DB + Datei)
- Import: Nicht-erkannte Dateien per Modal zuordnen/ueberspringen
- Import: Start blockiert wenn ungeloeste Items vorhanden
- Audio channelmap Fix: 5.1(side) -> 5.1 fuer libopus
- ENV-Variablen: VK_* Prefix (VK_DB_HOST, VK_MODE etc.)
- WebSocket: Server-Log Push statt HTTP-Polling
- Ordner-Loeschen Fix im Filebrowser
- Import: Duplikat-Erkennung bei erneutem Scan

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 17:35:37 +01:00

186 lines
7.1 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);
}
}
} 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} &rarr; ${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();