From fceeac55f8b2f8924e4bad6fbec2162c0c259557 Mon Sep 17 00:00:00 2001 From: david Date: Tue, 18 Aug 2026 11:43:09 -0400 Subject: [PATCH] feat: Add real-time live terminal execution console to Stash Transcode module --- qbittorrent_performer_sorter.html | 145 ++++++++++++++++++++++++++++-- 1 file changed, 140 insertions(+), 5 deletions(-) diff --git a/qbittorrent_performer_sorter.html b/qbittorrent_performer_sorter.html index c4d33d5..e137a8f 100644 --- a/qbittorrent_performer_sorter.html +++ b/qbittorrent_performer_sorter.html @@ -2246,6 +2246,32 @@

Click "Scan & Correlate Transcodes" to index all generated Stash transcodes and match them to original video files.

+ + +
+
+
+ + ๐Ÿ“Ÿ Transcode Operation Live Terminal + + IDLE +
+
+ + + +
+
+
+ [System] Transcode Live Execution Console ready. Start a replacement or batch scan to stream operations here... +
+
@@ -7789,6 +7815,11 @@ Return ONLY a valid JSON object with an array named "results": try { if (btn) { btn.disabled = true; btn.textContent = 'Correlating Transcodes...'; } + logTranscodeTerm(`โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”`, 'info'); + logTranscodeTerm(`๐Ÿ” Starting Stash Transcode Correlator scan...`, 'progress'); + logTranscodeTerm(`Transcode Dir: ${transcodeDir}`, 'info'); + logTranscodeTerm(`Stash DB Prefix: ${stashPrefix} โž” Local Mount Prefix: ${localPrefix}`, 'info'); + if (container) { container.innerHTML = `
@@ -7817,13 +7848,17 @@ Return ONLY a valid JSON object with an array named "results": if (statMatches) statMatches.textContent = `${transcodeItems.length} matched (${data.total_transcodes_on_disk || 0} on disk)`; const statSavings = document.getElementById('stat-transcode-savings'); + const gb = (data.total_potential_savings_bytes || 0) / (1024**3); if (statSavings) { - const gb = (data.total_potential_savings_bytes || 0) / (1024**3); statSavings.textContent = `${gb.toFixed(2)} GB`; } + logTranscodeTerm(`โœ“ Scan Completed: Indexed ${data.total_transcodes_on_disk || 0} files on disk, matched ${transcodeItems.length} scenes.`, 'success'); + logTranscodeTerm(`Total Potential Storage Savings: ${gb.toFixed(2)} GB across ${transcodeItems.filter(x => x.saved_bytes > 0).length} positive saving scenes.`, 'info'); + renderStashTranscodes(); } catch (err) { + logTranscodeTerm(`Scan failed: ${err.message}`, 'error'); showToast(`Transcode scan error: ${err.message}`, 'error'); if (container) { container.innerHTML = `
Failed to correlate transcodes: ${err.message}
`; @@ -7989,13 +8024,90 @@ Return ONLY a valid JSON object with an array named "results": if (countEl) countEl.textContent = selectedTranscodeHashes.size; } + // ==================== Real-Time Transcode Terminal Logger ==================== + function logTranscodeTerm(message, level = 'info') { + const body = document.getElementById('transcode-terminal-body'); + const statusBadge = document.getElementById('transcode-term-status'); + const autoscroll = document.getElementById('transcode-term-autoscroll')?.checked; + if (!body) return; + + const now = new Date(); + const timeStr = now.toTimeString().split(' ')[0] + '.' + String(now.getMilliseconds()).padStart(3, '0'); + + let color = '#94a3b8'; + let icon = 'โ„น๏ธ'; + if (level === 'success') { color = '#34d399'; icon = 'โœ…'; } + else if (level === 'warn') { color = '#fbbf24'; icon = 'โš ๏ธ'; } + else if (level === 'error') { color = '#f87171'; icon = 'โŒ'; } + else if (level === 'progress') { color = '#38bdf8'; icon = 'โณ'; } + else if (level === 'stage') { color = '#c084fc'; icon = 'โš™๏ธ'; } + + const line = document.createElement('div'); + line.style.cssText = `color: ${color}; margin-bottom: 2px; font-family: var(--font-mono); font-size: 0.73rem;`; + line.innerHTML = `[${timeStr}] ${icon} ${escapeHtml(message)}`; + body.appendChild(line); + + if (statusBadge) { + if (level === 'error') { + statusBadge.textContent = 'ERROR'; + statusBadge.style.color = 'var(--accent-rose)'; + statusBadge.style.background = 'rgba(244,63,94,0.15)'; + } else if (level === 'progress' || level === 'stage') { + statusBadge.textContent = 'RUNNING'; + statusBadge.style.color = 'var(--accent-cyan)'; + statusBadge.style.background = 'rgba(6,182,212,0.15)'; + } else if (level === 'success') { + statusBadge.textContent = 'SUCCESS'; + statusBadge.style.color = 'var(--accent-green)'; + statusBadge.style.background = 'rgba(16,185,129,0.15)'; + } + } + + if (autoscroll) { + body.scrollTop = body.scrollHeight; + } + } + + const btnClearTranscodeTerm = document.getElementById('btn-clear-transcode-term'); + if (btnClearTranscodeTerm) { + btnClearTranscodeTerm.addEventListener('click', () => { + const body = document.getElementById('transcode-terminal-body'); + const statusBadge = document.getElementById('transcode-term-status'); + if (body) body.innerHTML = '[System] Console cleared. Waiting for operations...'; + if (statusBadge) { + statusBadge.textContent = 'IDLE'; + statusBadge.style.color = 'var(--text-muted)'; + statusBadge.style.background = 'rgba(255,255,255,0.06)'; + } + }); + } + + const btnCopyTranscodeTerm = document.getElementById('btn-copy-transcode-term'); + if (btnCopyTranscodeTerm) { + btnCopyTranscodeTerm.addEventListener('click', () => { + const body = document.getElementById('transcode-terminal-body'); + if (body) { + navigator.clipboard.writeText(body.innerText || ''); + showToast('Terminal output copied to clipboard!', 'success'); + } + }); + } + async function executeSingleTranscodeReplace(item, btnEl) { const deleteCache = document.getElementById('transcode-opt-delete-cache')?.checked; const triggerRescan = document.getElementById('transcode-opt-trigger-rescan')?.checked; + logTranscodeTerm(`โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”`, 'info'); + logTranscodeTerm(`Initiating single replacement for: "${item.basename}"`, 'progress'); + logTranscodeTerm(`Original Path: ${item.orig_local_path} (${(item.orig_size/(1024*1024)).toFixed(1)} MB)`, 'info'); + logTranscodeTerm(`Transcode Path: ${item.transcode_path} (${(item.transcode_size/(1024*1024)).toFixed(1)} MB)`, 'info'); + logTranscodeTerm(`Expected Space Saving: ${(item.saved_bytes/(1024*1024)).toFixed(1)} MB`, 'info'); + try { btnEl.disabled = true; btnEl.textContent = 'Replacing...'; + logTranscodeTerm(`Stage 1/4: Staging transcode to "${item.orig_local_path}.transcode.tmp"...`, 'stage'); + const res = await fetch('/api/stash/transcodes/replace', { method: 'POST', headers: { 'Content-Type': 'application/json' }, @@ -8008,7 +8120,14 @@ Return ONLY a valid JSON object with an array named "results": }); const data = await res.json(); if (data.success) { - showToast(`โœ“ Replaced "${item.basename}" (Saved ${(data.saved_bytes/(1024*1024)).toFixed(1)} MB)!`, 'success'); + logTranscodeTerm(`Stage 2/4: Atomic replace verified. Original video overwritten successfully.`, 'stage'); + if (deleteCache) logTranscodeTerm(`Stage 3/4: Deleted transcode cache at ${item.transcode_path}`, 'stage'); + if (triggerRescan) logTranscodeTerm(`Stage 4/4: Triggered background Stash rescan for parent directory.`, 'stage'); + + const savedMb = (data.saved_bytes/(1024*1024)).toFixed(1); + logTranscodeTerm(`โœ“ COMPLETED: "${item.basename}" replaced (Saved ${savedMb} MB)!`, 'success'); + showToast(`โœ“ Replaced "${item.basename}" (Saved ${savedMb} MB)!`, 'success'); + const row = document.getElementById(`transcode-row-${item.oshash}`); if (row) { row.style.opacity = '0.4'; @@ -8016,18 +8135,20 @@ Return ONLY a valid JSON object with an array named "results": btnEl.textContent = 'โœ… Replaced'; } } else { + logTranscodeTerm(`FAILED: ${data.error || 'Unknown error'}`, 'error'); showToast(`Replacement failed: ${data.error}`, 'error'); btnEl.disabled = false; btnEl.textContent = 'โšก Replace'; } } catch (err) { + logTranscodeTerm(`EXCEPTION: ${err.message}`, 'error'); showToast(`Replace error: ${err.message}`, 'error'); btnEl.disabled = false; btnEl.textContent = 'โšก Replace'; } } - // Batch Replace Action with Live Per-Item Progress + // Batch Replace Action with Live Per-Item Progress & Terminal Logging const btnBatchReplaceTranscodes = document.getElementById('btn-batch-replace-transcodes'); if (btnBatchReplaceTranscodes) { btnBatchReplaceTranscodes.addEventListener('click', async () => { @@ -8046,13 +8167,19 @@ Return ONLY a valid JSON object with an array named "results": const deleteCache = document.getElementById('transcode-opt-delete-cache')?.checked; const triggerRescan = document.getElementById('transcode-opt-trigger-rescan')?.checked; + logTranscodeTerm(`โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•`, 'info'); + logTranscodeTerm(`๐Ÿš€ Starting Batch Replacement: ${selectedItems.length} items (Total Est. Savings: ${totalSavedMb} MB)`, 'progress'); + let successCount = 0; let totalSavedBytes = 0; btnBatchReplaceTranscodes.disabled = true; for (let i = 0; i < selectedItems.length; i++) { const item = selectedItems[i]; - btnBatchReplaceTranscodes.textContent = `Replacing ${i + 1} / ${selectedItems.length}...`; + const currentIdx = i + 1; + btnBatchReplaceTranscodes.textContent = `Replacing ${currentIdx} / ${selectedItems.length}...`; + + logTranscodeTerm(`[${currentIdx}/${selectedItems.length}] Processing "${item.basename}"...`, 'progress'); const row = document.getElementById(`transcode-row-${item.oshash}`); const replaceBtn = row?.querySelector('.btn-transcode-replace-single'); @@ -8062,6 +8189,7 @@ Return ONLY a valid JSON object with an array named "results": } try { + const isLast = (i === selectedItems.length - 1); const res = await fetch('/api/stash/transcodes/replace', { method: 'POST', headers: { 'Content-Type': 'application/json' }, @@ -8069,19 +8197,22 @@ Return ONLY a valid JSON object with an array named "results": orig_local_path: item.orig_local_path, transcode_path: item.transcode_path, delete_transcode: deleteCache, - trigger_rescan: (i === selectedItems.length - 1) ? triggerRescan : false + trigger_rescan: isLast ? triggerRescan : false }) }); const data = await res.json(); if (data.success) { successCount++; + const savedMb = ((data.saved_bytes || 0) / (1024*1024)).toFixed(1); totalSavedBytes += (data.saved_bytes || 0); + logTranscodeTerm(`[${currentIdx}/${selectedItems.length}] โœ“ Replaced "${item.basename}" (+${savedMb} MB saved)`, 'success'); if (row) { row.style.opacity = '0.4'; row.style.background = 'rgba(16, 185, 129, 0.1)'; if (replaceBtn) replaceBtn.textContent = 'โœ… Replaced'; } } else { + logTranscodeTerm(`[${currentIdx}/${selectedItems.length}] โŒ Failed: "${item.basename}" -> ${data.error}`, 'error'); if (replaceBtn) { replaceBtn.disabled = false; replaceBtn.textContent = 'โŒ Failed'; @@ -8089,6 +8220,7 @@ Return ONLY a valid JSON object with an array named "results": } } } catch (err) { + logTranscodeTerm(`[${currentIdx}/${selectedItems.length}] โŒ Error: "${item.basename}" -> ${err.message}`, 'error'); if (replaceBtn) { replaceBtn.disabled = false; replaceBtn.textContent = 'โŒ Error'; @@ -8098,6 +8230,8 @@ Return ONLY a valid JSON object with an array named "results": } const savedMb = (totalSavedBytes / (1024 * 1024)).toFixed(1); + logTranscodeTerm(`โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•`, 'info'); + logTranscodeTerm(`๐ŸŽ‰ Batch Complete! Successfully replaced ${successCount}/${selectedItems.length} files. Total space saved: ${savedMb} MB.`, 'success'); showToast(`โœ“ Batch completed: Replaced ${successCount} / ${selectedItems.length} files (Saved ${savedMb} MB)!`, 'success'); btnBatchReplaceTranscodes.disabled = false; btnBatchReplaceTranscodes.textContent = `โšก Batch Replace Selected (${selectedTranscodeHashes.size})`; @@ -8113,6 +8247,7 @@ Return ONLY a valid JSON object with an array named "results": const btnScanStashTranscodes = document.getElementById('btn-scan-stash-transcodes'); if (btnScanStashTranscodes) btnScanStashTranscodes.addEventListener('click', scanStashTranscodes); + if (btnScanStashTranscodes) btnScanStashTranscodes.addEventListener('click', scanStashTranscodes); // ==================== POST-MOVE PIPELINE AUTOMATION ==================== async function triggerPostMoveAutomation(targetDir) {