v4.3: file-based media storage + manual VACUUM maintenance

This commit is contained in:
Ashim Kumar
2026-06-12 13:24:00 +06:00
parent 965470853e
commit cc57204aff
10 changed files with 789 additions and 164 deletions

View File

@@ -1,6 +1,6 @@
/**
* Audiobook Maker Pro v4.2 - Main Application
* UPDATED: Lazy audio loading to avoid large response truncation
* Audiobook Maker Pro v4.3 - Main Application
* UPDATED: Lazy audio loading + Storage & Maintenance (VACUUM)
*/
// ============================================
@@ -17,6 +17,7 @@ let voices = [];
let archiveModal = null;
let ttsEditModal = null;
let publishModal = null;
let dbMaintenanceModal = null;
let publishingProjectId = null;
let currentWorkflowStage = 'upload';
let allArchiveProjects = [];
@@ -26,7 +27,7 @@ let allArchiveProjects = [];
// ============================================
document.addEventListener('DOMContentLoaded', function() {
console.log('🎧 Audiobook Maker Pro v4.2 initializing...');
console.log('🎧 Audiobook Maker Pro v4.3 initializing...');
archiveModal = new bootstrap.Modal(document.getElementById('archiveModal'));
ttsEditModal = new bootstrap.Modal(document.getElementById('ttsEditModal'));
@@ -979,6 +980,32 @@ async function loadAudioBlocksInBackground(projectId, blockIds) {
async function fetchOne(blockId) {
try {
const resp = await fetch(`/api/projects/${projectId}/audio/${blockId}`);
// v4.3: এন্ডপয়েন্ট বাইনারি অডিও (audio/*) অথবা legacy base64 JSON দিতে পারে
const contentType = resp.headers.get('content-type') || '';
const blockData = editorBlocks.find(b => b.db_id === blockId);
if (contentType.startsWith('audio/')) {
// নতুন: ফাইল আছে — শুধু indicator আপডেট করি, base64 মেমরিতে রাখি না
// (reader নিজেই lazy fetch করবে)
if (blockData) {
blockData.has_audio = true;
const blockEl = document.getElementById(blockData.id);
if (blockEl) {
const indicator = blockEl.querySelector('.audio-indicator');
if (indicator) {
indicator.classList.remove('no-audio');
indicator.classList.add('has-audio');
indicator.title = 'Audio available';
}
}
}
loaded++;
return;
}
// Legacy base64 JSON
const data = await resp.json();
if (data.error || !data.audio_data) {
@@ -986,13 +1013,11 @@ async function loadAudioBlocksInBackground(projectId, blockIds) {
return;
}
// Update editorBlocks state by db_id
const blockData = editorBlocks.find(b => b.db_id === blockId);
if (blockData) {
blockData.audio_data = data.audio_data;
blockData.audio_format = data.audio_format;
blockData.has_audio = true;
// Update DOM indicator (green dot)
const blockEl = document.getElementById(blockData.id);
if (blockEl) {
const indicator = blockEl.querySelector('.audio-indicator');
@@ -1017,8 +1042,8 @@ async function loadAudioBlocksInBackground(projectId, blockIds) {
}
const msg = failed > 0
? `Loaded ${loaded}/${blockIds.length} audio blocks (${failed} failed)`
: `All ${loaded} audio blocks loaded ✓`;
? `Verified ${loaded}/${blockIds.length} audio blocks (${failed} failed)`
: `All ${loaded} audio blocks verified ✓`;
showNotification(msg, failed > 0 ? 'warning' : 'success');
if (typeof updatePanelUI === 'function') {
@@ -1028,7 +1053,7 @@ async function loadAudioBlocksInBackground(projectId, blockIds) {
async function deleteProject(projectId) {
if (!confirm('Are you sure you want to delete this project? This action cannot be undone.')) return;
if (!confirm('Are you sure you want to delete this project? This action cannot be undone.\n\nএই প্রজেক্টের অডিও ও ইমেজ ফাইলগুলোও মুছে যাবে।')) return;
showLoader('Deleting...');
@@ -1046,6 +1071,111 @@ async function deleteProject(projectId) {
}
}
// ============================================
// v4.3: Storage & Maintenance (VACUUM)
// ============================================
function openDbMaintenance() {
if (!dbMaintenanceModal) {
dbMaintenanceModal = new bootstrap.Modal(document.getElementById('dbMaintenanceModal'));
}
dbMaintenanceModal.show();
loadDbStats();
}
async function loadDbStats() {
const loadingEl = document.getElementById('dbStatsLoading');
const contentEl = document.getElementById('dbStatsContent');
if (loadingEl) loadingEl.style.display = 'block';
if (contentEl) contentEl.style.display = 'none';
try {
const resp = await fetch('/api/maintenance/db-stats');
const s = await resp.json();
if (s.error) {
showNotification(s.error, 'error');
return;
}
document.getElementById('dbmFileSize').textContent = `${s.file_size_mb} MB`;
document.getElementById('dbmMediaSize').textContent = `${s.media_size_mb} MB`;
document.getElementById('dbmFreeText').textContent =
`${s.free_mb} MB (${s.free_percent}%)`;
const bar = document.getElementById('dbmFreeBar');
const pct = Math.min(s.free_percent, 100);
bar.style.width = pct + '%';
bar.textContent = `${s.free_percent}%`;
// রঙ: কম হলে সবুজ, বেশি হলে হলুদ/লাল
bar.className = 'progress-bar';
if (s.free_percent >= 30) {
bar.classList.add('bg-danger');
} else if (s.free_percent >= 15) {
bar.classList.add('bg-warning');
} else {
bar.classList.add('bg-success');
}
const advice = document.getElementById('dbmAdvice');
if (s.free_percent >= 15) {
advice.className = 'alert alert-warning';
advice.style.display = 'block';
advice.innerHTML = `<i class="bi bi-exclamation-triangle me-1"></i>` +
`ডেটাবেসে <strong>${s.free_percent}%</strong> ফাঁকা স্পেস জমেছে। ` +
`<strong>Run VACUUM</strong> চালিয়ে এটি reclaim করতে পারেন।`;
} else {
advice.className = 'alert alert-success';
advice.style.display = 'block';
advice.innerHTML = `<i class="bi bi-check-circle me-1"></i>` +
`ফাঁকা স্পেস কম (<strong>${s.free_percent}%</strong>) — এখন VACUUM চালানোর দরকার নেই।`;
}
if (loadingEl) loadingEl.style.display = 'none';
if (contentEl) contentEl.style.display = 'block';
} catch (e) {
console.error(e);
showNotification('Failed to load storage info', 'error');
}
}
async function runDbVacuum() {
const vacuumBtn = document.getElementById('dbmVacuumBtn');
const refreshBtn = document.getElementById('dbmRefreshBtn');
if (!confirm('VACUUM এখন চালাবেন? এটি ডেটাবেস ছোট করবে কিন্তু কিছু সময় (ডেটাবেস বড় হলে কয়েক মিনিট) নিতে পারে।')) {
return;
}
if (vacuumBtn) {
vacuumBtn.disabled = true;
vacuumBtn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Running...';
}
if (refreshBtn) refreshBtn.disabled = true;
try {
const resp = await fetch('/api/maintenance/vacuum', { method: 'POST' });
const data = await resp.json();
if (data.error) {
showNotification(data.error, 'error');
} else {
showNotification(data.message || 'VACUUM complete', 'success');
loadDbStats();
}
} catch (e) {
showNotification('VACUUM failed', 'error');
} finally {
if (vacuumBtn) {
vacuumBtn.disabled = false;
vacuumBtn.innerHTML = '<i class="bi bi-stars me-1"></i>Run VACUUM';
}
if (refreshBtn) refreshBtn.disabled = false;
}
}
// ============================================
// TTS Text Editing
// ============================================