feat: Add real-time live terminal execution console to Stash Transcode module
This commit is contained in:
@@ -2246,6 +2246,32 @@
|
||||
<p>Click "Scan & Correlate Transcodes" to index all generated Stash transcodes and match them to original video files.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Real-Time Transcode Execution Console / Terminal -->
|
||||
<div id="transcode-terminal-wrapper" style="margin-top: 1.25rem; border-top: 1px solid var(--border-color); padding-top: 1rem;">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.5rem; flex-wrap: wrap; gap: 0.5rem;">
|
||||
<div style="display: flex; align-items: center; gap: 0.5rem;">
|
||||
<span style="font-size: 0.8rem; font-weight: 700; color: #fff; display: inline-flex; align-items: center; gap: 0.4rem;">
|
||||
📟 Transcode Operation Live Terminal
|
||||
</span>
|
||||
<span id="transcode-term-status" class="badge" style="font-size: 0.68rem; font-family: var(--font-mono); background: rgba(255,255,255,0.06); color: var(--text-muted);">IDLE</span>
|
||||
</div>
|
||||
<div style="display: flex; gap: 0.4rem; align-items: center;">
|
||||
<label style="font-size: 0.72rem; color: var(--text-muted); display: inline-flex; align-items: center; gap: 0.3rem; margin-right: 0.4rem; cursor: pointer;">
|
||||
<input type="checkbox" id="transcode-term-autoscroll" checked /> Auto-scroll
|
||||
</label>
|
||||
<button class="btn btn-secondary" id="btn-clear-transcode-term" style="font-size: 0.7rem; padding: 0.2rem 0.5rem;">
|
||||
🧹 Clear Log
|
||||
</button>
|
||||
<button class="btn btn-secondary" id="btn-copy-transcode-term" style="font-size: 0.7rem; padding: 0.2rem 0.5rem;">
|
||||
📋 Copy
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="transcode-terminal-body" style="background: #090d16; border: 1px solid #1e293b; border-radius: var(--radius-sm); padding: 0.85rem; font-family: var(--font-mono); font-size: 0.75rem; line-height: 1.5; color: #94a3b8; height: 180px; max-height: 320px; overflow-y: auto; white-space: pre-wrap; word-break: break-all; box-shadow: inset 0 2px 6px rgba(0,0,0,0.5);">
|
||||
<span style="color: #64748b;">[System] Transcode Live Execution Console ready. Start a replacement or batch scan to stream operations here...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@@ -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 = `
|
||||
<div style="padding: 2.5rem; text-align: center; color: var(--text-muted);">
|
||||
@@ -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 = `<div style="color: var(--accent-rose); padding: 1.5rem;">Failed to correlate transcodes: ${err.message}</div>`;
|
||||
@@ -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 = `<span style="color: #64748b;">[${timeStr}]</span> ${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 = '<span style="color: #64748b;">[System] Console cleared. Waiting for operations...</span>';
|
||||
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) {
|
||||
|
||||
Reference in New Issue
Block a user