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.
+
+
+
@@ -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) {