From 0ebe6002152d3d1df2927585b1818a86ae9b014c Mon Sep 17 00:00:00 2001 From: data Date: Sat, 28 Feb 2026 08:06:17 +0100 Subject: [PATCH] feat: VideoKonverter v3.0 - Bugfixes, Queue-Pause, Button-Audit - fix: escapeAttr() ueberall mit Anfuehrungszeichen versehen (14 Stellen) Ohne Quotes brachen onclick-Handler bei Titeln mit Leerzeichen -> TVDB-Zuordnung, Play, Delete, Import-Browser betroffen - fix: escapeHtml() in onclick durch escapeAttr() ersetzt (4 Stellen) escapeHtml erzeugt & statt \' -> JS-Syntaxfehler in Handlern - fix: Import-Modal schliesst nach Start statt Ladebalken anzuzeigen Globaler Progress-Balken in base.html uebernimmt den Fortschritt - feat: Queue Pause/Resume - laufende Konvertierung laeuft fertig, keine neuen Jobs gestartet bis Weiter geklickt wird API: POST /api/queue/pause, /api/queue/resume, GET /api/queue/status Button im Dashboard neben Warteschlange-Header Co-Authored-By: Claude Opus 4.6 --- video-konverter/app/routes/api.py | 23 ++++++++++ video-konverter/app/services/queue.py | 28 +++++++++++- video-konverter/app/static/js/library.js | 47 +++++++------------- video-konverter/app/static/js/websocket.js | 27 +++++++++++ video-konverter/app/templates/dashboard.html | 8 +++- 5 files changed, 100 insertions(+), 33 deletions(-) diff --git a/video-konverter/app/routes/api.py b/video-konverter/app/routes/api.py index 2e3e468..0ee8b91 100644 --- a/video-konverter/app/routes/api.py +++ b/video-konverter/app/routes/api.py @@ -79,6 +79,26 @@ def setup_api_routes(app: web.Application, config: Config, return web.json_response({"message": "Job wiederholt"}) return web.json_response({"error": "Job nicht fehlgeschlagen"}, status=400) + # --- Queue Pause/Resume --- + + async def post_queue_pause(request: web.Request) -> web.Response: + """POST /api/queue/pause - Queue pausieren""" + success = await queue_service.pause_queue() + if success: + return web.json_response({"message": "Queue pausiert", "paused": True}) + return web.json_response({"message": "Queue bereits pausiert", "paused": True}) + + async def post_queue_resume(request: web.Request) -> web.Response: + """POST /api/queue/resume - Queue fortsetzen""" + success = await queue_service.resume_queue() + if success: + return web.json_response({"message": "Queue fortgesetzt", "paused": False}) + return web.json_response({"message": "Queue laeuft bereits", "paused": False}) + + async def get_queue_status(request: web.Request) -> web.Response: + """GET /api/queue/status - Pause-Status abfragen""" + return web.json_response({"paused": queue_service.is_paused}) + # --- Settings --- async def get_settings(request: web.Request) -> web.Response: @@ -372,6 +392,9 @@ def setup_api_routes(app: web.Application, config: Config, app.router.add_delete("/api/jobs/{job_id}", delete_job) app.router.add_post("/api/jobs/{job_id}/cancel", post_cancel) app.router.add_post("/api/jobs/{job_id}/retry", post_retry) + app.router.add_post("/api/queue/pause", post_queue_pause) + app.router.add_post("/api/queue/resume", post_queue_resume) + app.router.add_get("/api/queue/status", get_queue_status) app.router.add_get("/api/settings", get_settings) app.router.add_put("/api/settings", put_settings) app.router.add_get("/api/presets", get_presets) diff --git a/video-konverter/app/services/queue.py b/video-konverter/app/services/queue.py index bd7d050..14725c3 100644 --- a/video-konverter/app/services/queue.py +++ b/video-konverter/app/services/queue.py @@ -33,6 +33,7 @@ class QueueService: self.jobs: OrderedDict[int, ConversionJob] = OrderedDict() self._active_count: int = 0 self._running: bool = False + self._paused: bool = False self._queue_task: Optional[asyncio.Task] = None self._queue_file = str(config.data_path / "queue.json") self._db_pool: Optional[aiomysql.Pool] = None @@ -172,6 +173,28 @@ class QueueService: logging.info(f"Job abgebrochen: {job.media.source_filename}") return True + async def pause_queue(self) -> bool: + """Pausiert die Queue - laufende Jobs werden fertig, keine neuen gestartet""" + if self._paused: + return False + self._paused = True + logging.info("Queue pausiert - keine neuen Jobs werden gestartet") + await self.ws_manager.broadcast_queue_update() + return True + + async def resume_queue(self) -> bool: + """Setzt die Queue fort""" + if not self._paused: + return False + self._paused = False + logging.info("Queue fortgesetzt") + await self.ws_manager.broadcast_queue_update() + return True + + @property + def is_paused(self) -> bool: + return self._paused + async def retry_job(self, job_id: int) -> bool: """Setzt fehlgeschlagenen Job zurueck auf QUEUED""" job = self.jobs.get(job_id) @@ -203,7 +226,7 @@ class QueueService: if job.status in (JobStatus.QUEUED, JobStatus.ACTIVE, JobStatus.FAILED, JobStatus.CANCELLED): queue[job_id] = job.to_dict_queue() - return {"data_queue": queue} + return {"data_queue": queue, "queue_paused": self._paused} def get_active_jobs(self) -> dict: """Aktive Jobs fuer WebSocket""" @@ -226,7 +249,8 @@ class QueueService: """Hauptschleife: Startet neue Jobs wenn Kapazitaet frei""" while self._running: try: - if self._active_count < self.config.max_parallel_jobs: + if (not self._paused and + self._active_count < self.config.max_parallel_jobs): next_job = self._get_next_queued() if next_job: asyncio.create_task(self._execute_job(next_job)) diff --git a/video-konverter/app/static/js/library.js b/video-konverter/app/static/js/library.js index 135d6f0..79d3f2d 100644 --- a/video-konverter/app/static/js/library.js +++ b/video-konverter/app/static/js/library.js @@ -302,7 +302,7 @@ function renderMovieGrid(movies) { const size = m.total_size ? formatSize(m.total_size) : ""; const tvdbBtn = m.tvdb_id ? 'TVDB' - : ``; + : ``; const overview = m.overview ? `

${escapeHtml(m.overview.substring(0, 120))}${m.overview.length > 120 ? '...' : ''}

` : ""; @@ -403,9 +403,9 @@ function renderVideoTable(items) { ${formatDuration(v.duration_sec || 0)} ${(v.container || "-").toUpperCase()} - + - + `; } @@ -464,7 +464,7 @@ function renderSeriesGrid(series) { const genres = s.genres ? `
${escapeHtml(s.genres)}
` : ""; const tvdbBtn = s.tvdb_id ? `TVDB` - : ``; + : ``; html += `
${poster} @@ -492,7 +492,7 @@ function renderBreadcrumb(crumbs, pathId) { html += `Basis`; for (const c of crumbs) { html += ' / '; - html += `${escapeHtml(c.name)}`; + html += `${escapeHtml(c.name)}`; } html += '
'; return html; @@ -662,9 +662,9 @@ function renderEpisodesTab(series) { ${fileExt} ${audioInfo || "-"} - + - + `; } @@ -915,9 +915,9 @@ function openMovieDetail(movieId) { ${formatSize(v.file_size || 0)} ${formatDuration(v.duration_sec || 0)} - + - + `; } @@ -1516,7 +1516,7 @@ function renderTvdbReviewList() { const poster = s.poster ? `` : '
?
'; - html += `
`; + html += `
`; html += poster; html += `
`; html += `${escapeHtml(s.name)}`; @@ -1615,7 +1615,7 @@ function executeManualReviewSearch(index) { return; } results.innerHTML = list.map(r => ` -
+
${r.poster ? `` : '
?
'}
${escapeHtml(r.name)} @@ -2255,7 +2255,7 @@ function importBrowse(path) { for (const c of (data.breadcrumb || [])) { if (c.path === "/mnt" || c.path.length < 5) continue; html += ` / `; - html += `${escapeHtml(c.name)}`; + html += `${escapeHtml(c.name)}`; } html += '
'; @@ -2263,7 +2263,7 @@ function importBrowse(path) { const parts = data.current_path.split("/"); if (parts.length > 2) { const parentPath = parts.slice(0, -1).join("/") || "/mnt"; - html += `
+ html += `
🔙 ..
`; @@ -2272,7 +2272,7 @@ function importBrowse(path) { // Unterordner: Einfachklick = auswaehlen, Doppelklick = navigieren for (const f of (data.folders || [])) { const meta = f.video_count > 0 ? `${f.video_count} Videos` : ""; - html += `
+ html += `
📁 ${escapeHtml(f.name)} ${meta} @@ -2831,25 +2831,12 @@ let _importWsActive = false; // WebSocket liefert Updates? async function executeImport() { if (!currentImportJobId) return; - document.getElementById("import-preview").style.display = "none"; - document.getElementById("import-progress").style.display = ""; - document.getElementById("import-status-text").textContent = "Importiere..."; - document.getElementById("import-bar").style.width = "0%"; - _importWsActive = false; + // Modal schliessen - Fortschritt laeuft ueber globalen Progress-Balken + closeImportModal(); + resetImport(); // Starte Import (non-blocking - Server antwortet sofort) fetch(`/api/library/import/${currentImportJobId}/execute`, {method: "POST"}); - - // Fallback-Polling nur starten falls WS nicht verbunden - if (!ws || ws.readyState !== WebSocket.OPEN) { - startImportPolling(); - } else { - // WS verbunden - trotzdem langsames Fallback-Polling - // falls WS-Updates ausbleiben - setTimeout(() => { - if (!_importWsActive) startImportPolling(); - }, 3000); - } } // WebSocket-Handler fuer Import-Fortschritt diff --git a/video-konverter/app/static/js/websocket.js b/video-konverter/app/static/js/websocket.js index 021f810..6c04bc2 100644 --- a/video-konverter/app/static/js/websocket.js +++ b/video-konverter/app/static/js/websocket.js @@ -7,6 +7,7 @@ let ws = null; let videoActive = {}; let videoQueue = {}; let reconnectTimer = null; +let queuePaused = false; // WebSocket verbinden function connectWebSocket() { @@ -32,6 +33,9 @@ function connectWebSocket() { updateActiveConversions(packet.data_convert); } else if (packet.data_queue !== undefined) { updateQueue(packet.data_queue); + if (packet.queue_paused !== undefined) { + updatePauseState(packet.queue_paused); + } } else if (packet.data_log !== undefined) { // Log-Nachrichten ans Benachrichtigungs-System weiterleiten if (typeof addNotification === "function") { @@ -196,5 +200,28 @@ function sendCommand(command, id) { } } +// === Queue Pause/Resume === + +function updatePauseState(paused) { + queuePaused = paused; + const btn = document.getElementById("btn-queue-pause"); + const hint = document.getElementById("queue-paused-hint"); + if (btn) { + btn.innerHTML = paused ? "▶ Weiter" : "⏸ Pause"; + btn.className = paused ? "btn-primary btn-small" : "btn-secondary btn-small"; + } + if (hint) { + hint.style.display = paused ? "" : "none"; + } +} + +function toggleQueuePause() { + const url = queuePaused ? "/api/queue/resume" : "/api/queue/pause"; + fetch(url, {method: "POST"}) + .then(r => r.json()) + .then(data => updatePauseState(data.paused)) + .catch(e => console.error("Pause/Resume fehlgeschlagen:", e)); +} + // Verbindung herstellen connectWebSocket(); diff --git a/video-konverter/app/templates/dashboard.html b/video-konverter/app/templates/dashboard.html index b0fc951..c4b85ad 100644 --- a/video-konverter/app/templates/dashboard.html +++ b/video-konverter/app/templates/dashboard.html @@ -21,7 +21,13 @@
-

Warteschlange

+
+

Warteschlange

+ + +