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 '
- : `TVDB zuordnen `;
+ : `TVDB zuordnen `;
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()}
- ▶
+ ▶
Conv
- ✕
+ ✕
`;
}
@@ -464,7 +464,7 @@ function renderSeriesGrid(series) {
const genres = s.genres ? `${escapeHtml(s.genres)}
` : "";
const tvdbBtn = s.tvdb_id
? `TVDB `
- : `TVDB zuordnen `;
+ : `TVDB zuordnen `;
html += `';
return html;
@@ -662,9 +662,9 @@ function renderEpisodesTab(series) {
${fileExt}
${audioInfo || "-"}
- ▶
+ ▶
Conv
- ✕
+ ✕
`;
}
@@ -915,9 +915,9 @@ function openMovieDetail(movieId) {
${formatSize(v.file_size || 0)}
${formatDuration(v.duration_sec || 0)}
- ▶
+ ▶
Conv
- ✕
+ ✕
`;
}
@@ -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
+
+ ⏸ Pause
+
+ Pausiert
+