From 23bb4cd2d9c7b5bf041d6c9ed483572bcad11467 Mon Sep 17 00:00:00 2001 From: Trouve Alexis Date: Sat, 29 Nov 2025 22:56:58 +0800 Subject: [PATCH] Add video/audio to MP3 conversion feature MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implement drag-and-drop interface for converting video and audio files to MP3 format using FFmpeg. Users can now upload files (MP4, M4A, AVI, MKV, MOV, WAV, FLAC, OGG) and convert them with customizable bitrate and quality settings. - Add conversion service with FFmpeg integration - Add /convert-to-mp3 and /supported-formats API endpoints - Add new "Video to MP3" tab with drag-and-drop UI - Support multiple file uploads with batch conversion - Add bitrate (128k-320k) and VBR quality options 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- public/app.js | 153 +++++++++++++++++++++++++++++++++++++ public/index.html | 66 ++++++++++++++++ public/style.css | 7 ++ src/server.js | 81 ++++++++++++++++++++ src/services/conversion.js | 145 +++++++++++++++++++++++++++++++++++ 5 files changed, 452 insertions(+) create mode 100644 src/services/conversion.js diff --git a/public/app.js b/public/app.js index 8b20402..c1cf74d 100644 --- a/public/app.js +++ b/public/app.js @@ -162,6 +162,159 @@ document.getElementById('download-form').addEventListener('submit', async (e) => }; }); +// ==================== CONVERT TAB (Video to MP3) ==================== + +let convertSelectedFiles = []; +const convertDropZone = document.getElementById('convert-drop-zone'); +const convertFileInput = document.getElementById('convert-file-input'); +const convertSelectedFilesDiv = document.getElementById('convert-selected-files'); +const convertFilesList = document.getElementById('convert-files-list'); +const convertBtn = document.getElementById('convert-btn'); +const convertClearFilesBtn = document.getElementById('convert-clear-files'); + +function updateConvertFilesList() { + if (convertSelectedFiles.length === 0) { + convertSelectedFilesDiv.style.display = 'none'; + convertBtn.disabled = true; + return; + } + + convertSelectedFilesDiv.style.display = 'block'; + convertBtn.disabled = false; + + convertFilesList.innerHTML = convertSelectedFiles.map((file, index) => ` +
  • + ${file.name} + ${formatSize(file.size)} + +
  • + `).join(''); + + // Add remove handlers + convertFilesList.querySelectorAll('.remove-file').forEach(btn => { + btn.addEventListener('click', () => { + convertSelectedFiles.splice(parseInt(btn.dataset.index), 1); + updateConvertFilesList(); + }); + }); +} + +function addConvertFiles(files) { + const videoAudioFiles = Array.from(files).filter(f => + f.type.startsWith('video/') || f.type.startsWith('audio/') || + f.name.match(/\.(mp4|avi|mkv|mov|m4a|wav|flac|ogg|webm|wmv|flv)$/i) + ); + convertSelectedFiles = [...convertSelectedFiles, ...videoAudioFiles]; + updateConvertFilesList(); +} + +// Drag & Drop events for convert +convertDropZone.addEventListener('click', () => convertFileInput.click()); + +convertDropZone.addEventListener('dragover', (e) => { + e.preventDefault(); + convertDropZone.classList.add('drag-over'); +}); + +convertDropZone.addEventListener('dragleave', () => { + convertDropZone.classList.remove('drag-over'); +}); + +convertDropZone.addEventListener('drop', (e) => { + e.preventDefault(); + convertDropZone.classList.remove('drag-over'); + addConvertFiles(e.dataTransfer.files); +}); + +convertFileInput.addEventListener('change', () => { + addConvertFiles(convertFileInput.files); + convertFileInput.value = ''; +}); + +convertClearFilesBtn.addEventListener('click', () => { + convertSelectedFiles = []; + updateConvertFilesList(); +}); + +// Convert form submit +document.getElementById('convert-form').addEventListener('submit', async (e) => { + e.preventDefault(); + if (convertSelectedFiles.length === 0) return; + + const button = convertBtn; + const bitrate = document.getElementById('convert-bitrate').value; + const quality = document.getElementById('convert-quality').value; + + const convertProgress = document.getElementById('convert-progress'); + const convertProgressFill = document.getElementById('convert-progress-fill'); + const convertProgressTitle = document.getElementById('convert-progress-title'); + const convertProgressPercent = document.getElementById('convert-progress-percent'); + const convertProgressInfo = document.getElementById('convert-progress-info'); + const convertProgressCurrent = document.getElementById('convert-progress-current'); + + setLoading(button, true); + convertProgress.style.display = 'block'; + convertProgressFill.style.width = '0%'; + convertProgressTitle.textContent = 'Converting to MP3...'; + convertProgressPercent.textContent = '0%'; + convertProgressInfo.textContent = `0/${convertSelectedFiles.length} files`; + document.getElementById('convert-result').classList.remove('show'); + + const formData = new FormData(); + convertSelectedFiles.forEach(file => formData.append('files', file)); + formData.append('bitrate', bitrate); + formData.append('quality', quality); + + try { + const response = await fetch(`${API_URL}/convert-to-mp3`, { + method: 'POST', + body: formData + }); + + const data = await response.json(); + + if (!response.ok) throw new Error(data.error || 'Conversion failed'); + + convertProgressFill.style.width = '100%'; + convertProgressPercent.textContent = '100%'; + convertProgressTitle.textContent = 'Conversion Complete!'; + convertProgressInfo.textContent = `${data.successCount}/${data.totalFiles} files`; + + showResult('convert-result', true, ` +

    Conversion Complete!

    +

    ${data.successCount}/${data.totalFiles} files converted to MP3

    +
    + ${data.results.map(r => r.success ? ` +
    +
    + ${r.fileName} + ${r.size} +
    + Download MP3 +
    + ` : ` +
    + ${r.fileName} + ${r.error} +
    + `).join('')} +
    + `); + + // Clear selected files after successful conversion + convertSelectedFiles = []; + updateConvertFilesList(); + + } catch (error) { + showResult('convert-result', false, `

    Error

    ${error.message}

    `); + } finally { + setLoading(button, false); + setTimeout(() => { + convertProgress.style.display = 'none'; + }, 1000); + } +}); + // ==================== TRANSCRIBE TAB (Drag & Drop) ==================== let selectedFiles = []; diff --git a/public/index.html b/public/index.html index fa820a2..4c009c7 100644 --- a/public/index.html +++ b/public/index.html @@ -16,6 +16,7 @@