const { spawn, exec } = require('child_process'); const path = require('path'); const fs = require('fs'); const https = require('https'); const { app } = require('electron'); const BIN_DIR = app && app.isPackaged ? path.join(process.resourcesPath, 'app.asar.unpacked', 'bin') : path.join(__dirname, '..', 'bin'); const YTDLP_PATH = path.join(BIN_DIR, 'yt-dlp.exe'); const FFMPEG_PATH = path.join(BIN_DIR, 'ffmpeg.exe'); console.log('[DEBUG] Downloader Module initialized.'); console.log('[DEBUG] Binary directory path:', BIN_DIR); console.log('[DEBUG] yt-dlp path:', YTDLP_PATH); console.log('[DEBUG] ffmpeg path:', FFMPEG_PATH); // Ensure binary directory exists if (!fs.existsSync(BIN_DIR)) { console.log('[DEBUG] Binary directory does not exist, creating:', BIN_DIR); fs.mkdirSync(BIN_DIR, { recursive: true }); } // Download helper function with progress callback function downloadFile(url, dest, onProgress) { console.log(`[DEBUG] Starting download for: ${url} -> Target: ${dest}`); return new Promise((resolve, reject) => { let file; function makeRequest(requestUrl) { console.log(`[DEBUG] Requesting URL: ${requestUrl}`); https.get(requestUrl, (response) => { console.log(`[DEBUG] Received status code: ${response.statusCode} for ${requestUrl}`); // Follow HTTP redirects (301, 302, 307, 308) if (response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) { const redirectUrl = response.headers.location; console.log(`[DEBUG] Redirecting to: ${redirectUrl}`); makeRequest(redirectUrl); return; } if (response.statusCode !== 200) { console.error(`[DEBUG] Error: Download failed with status ${response.statusCode}`); reject(new Error(`Failed to download: status ${response.statusCode}`)); return; } console.log(`[DEBUG] Connection successful. Writing to disk: ${dest}`); file = fs.createWriteStream(dest); const totalSize = parseInt(response.headers['content-length'], 10); console.log(`[DEBUG] Total file size: ${totalSize ? (totalSize / (1024 * 1024)).toFixed(2) + ' MB' : 'unknown'}`); let downloaded = 0; response.on('data', (chunk) => { downloaded += chunk.length; file.write(chunk); if (onProgress && totalSize) { const percent = Math.round((downloaded / totalSize) * 100); onProgress(percent); } }); response.on('end', () => { file.end(); console.log(`[DEBUG] File download completed successfully: ${dest}`); resolve(); }); }).on('error', (err) => { console.error(`[DEBUG] Network error during request:`, err); if (file) { fs.unlink(dest, () => { console.log(`[DEBUG] Cleaned up partial file on error: ${dest}`); }); } reject(err); }); } makeRequest(url); }); } // Unzip using PowerShell (Windows native) function unzipWindows(zipPath, destDir) { console.log(`[DEBUG] Extracting archive: ${zipPath} to ${destDir}`); return new Promise((resolve, reject) => { const cmd = `powershell -Command "Expand-Archive -Path '${zipPath}' -DestinationPath '${destDir}' -Force"`; console.log(`[DEBUG] Executing extract command: ${cmd}`); exec(cmd, (error) => { if (error) { console.error(`[DEBUG] Extraction failed:`, error); reject(error); } else { console.log(`[DEBUG] Extraction completed successfully.`); resolve(); } }); }); } // Check and install dependencies async function checkDependencies(onStatusUpdate) { console.log('[DEBUG] Checking applications and libraries dependencies...'); let needsYtDlp = true; if (fs.existsSync(YTDLP_PATH)) { const stats = fs.statSync(YTDLP_PATH); console.log(`[DEBUG] Found yt-dlp.exe. Size: ${(stats.size / (1024 * 1024)).toFixed(2)} MB`); if (stats.size > 1024 * 1024) { needsYtDlp = false; console.log(`[DEBUG] yt-dlp.exe is valid.`); } else { console.log(`[DEBUG] yt-dlp.exe is corrupted or incomplete. Deleting to redownload...`); try { fs.unlinkSync(YTDLP_PATH); } catch (e) { console.error('[DEBUG] Failed to delete corrupted yt-dlp:', e); } } } else { console.log(`[DEBUG] yt-dlp.exe not found.`); } let needsFfmpeg = true; if (fs.existsSync(FFMPEG_PATH)) { const stats = fs.statSync(FFMPEG_PATH); console.log(`[DEBUG] Found ffmpeg.exe. Size: ${(stats.size / (1024 * 1024)).toFixed(2)} MB`); if (stats.size > 1024 * 1024) { needsFfmpeg = false; console.log(`[DEBUG] ffmpeg.exe is valid.`); } else { console.log(`[DEBUG] ffmpeg.exe is corrupted or incomplete. Deleting to redownload...`); try { fs.unlinkSync(FFMPEG_PATH); } catch (e) { console.error('[DEBUG] Failed to delete corrupted ffmpeg:', e); } } } else { console.log(`[DEBUG] ffmpeg.exe not found.`); } if (!needsYtDlp && !needsFfmpeg) { console.log('[DEBUG] All dependencies are verified and ready.'); return true; } if (needsYtDlp) { onStatusUpdate('Downloading yt-dlp...', 10); const ytDlpUrl = 'https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp.exe'; await downloadFile(ytDlpUrl, YTDLP_PATH, (pct) => { onStatusUpdate(`Downloading yt-dlp... ${pct}%`, Math.round(pct * 0.4)); }); } if (needsFfmpeg) { onStatusUpdate('Downloading ffmpeg...', 45); const ffmpegZipUrl = 'https://github.com/ffbinaries/ffbinaries-prebuilt/releases/download/v4.4.1/ffmpeg-4.4.1-win-64.zip'; const tempZip = path.join(BIN_DIR, 'ffmpeg.zip'); await downloadFile(ffmpegZipUrl, tempZip, (pct) => { onStatusUpdate(`Downloading ffmpeg... ${pct}%`, 40 + Math.round(pct * 0.5)); }); onStatusUpdate('Extracting ffmpeg...', 95); try { await unzipWindows(tempZip, BIN_DIR); } finally { if (fs.existsSync(tempZip)) { console.log(`[DEBUG] Cleaning up temporary archive: ${tempZip}`); try { fs.unlinkSync(tempZip); } catch (e) {} } } } onStatusUpdate('Dependencies ready!', 100); console.log('[DEBUG] Dependencies installation completed successfully.'); return true; } function formatDuration(sec) { if (!sec) return ''; const hrs = Math.floor(sec / 3600); const mins = Math.floor((sec % 3600) / 60); const secs = Math.floor(sec % 60); if (hrs > 0) { return `${hrs}:${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`; } return `${mins}:${secs.toString().padStart(2, '0')}`; } // Get video metadata (title, thumbnail, duration, etc.) function getVideoInfo(url, browser) { console.log(`[DEBUG] Fetching info for URL: ${url}`); return new Promise((resolve, reject) => { let args = ['-J', '--flat-playlist']; if (browser) { args.push('--cookies-from-browser', browser); } args.push(url); console.log(`[DEBUG] Spawning process: ${YTDLP_PATH} ${args.join(' ')}`); const proc = spawn(YTDLP_PATH, args); let stdout = ''; let stderr = ''; proc.stdout.on('data', (data) => { stdout += data; }); proc.stderr.on('data', (data) => { stderr += data; }); proc.on('close', (code) => { console.log(`[DEBUG] yt-dlp info process exited with code ${code}`); if (code !== 0) { console.error(`[DEBUG] yt-dlp error output:`, stderr); reject(new Error(stderr || 'Failed to fetch details')); return; } try { const info = JSON.parse(stdout); if (info._type === 'playlist' || info.entries) { console.log(`[DEBUG] Playlist metadata fetched successfully for: "${info.title}" (${info.entries.length} items)`); const entries = info.entries.map((entry, index) => { let thumb = ''; if (entry.thumbnail) thumb = entry.thumbnail; else if (entry.thumbnails && entry.thumbnails.length > 0) thumb = entry.thumbnails[0].url; let durStr = ''; if (entry.duration_string) durStr = entry.duration_string; else if (entry.duration) durStr = formatDuration(entry.duration); return { id: entry.id || `track-${index}`, title: entry.title || `Track ${index + 1}`, thumbnail: thumb, duration: durStr, uploader: entry.uploader || info.uploader || 'Playlist', url: entry.url || `https://www.youtube.com/watch?v=${entry.id}` }; }); resolve({ isPlaylist: true, playlistTitle: info.title, entries: entries }); } else { console.log(`[DEBUG] Single video metadata fetched successfully for: "${info.title}"`); let thumb = ''; if (info.thumbnail) thumb = info.thumbnail; else if (info.thumbnails && info.thumbnails.length > 0) thumb = info.thumbnails[0].url; resolve({ isPlaylist: false, entries: [{ id: info.id || 'video', title: info.title, thumbnail: thumb, duration: info.duration_string || (info.duration ? formatDuration(info.duration) : ''), uploader: info.uploader || 'Bilinmiyor', url: url }] }); } } catch (err) { console.error(`[DEBUG] JSON parsing error on yt-dlp output:`, err); reject(err); } }); }); } // Download single item helper function downloadSingle(url, format, quality, outputDir, browser, onProgress) { return new Promise((resolve, reject) => { let args = []; const outputTemplate = path.join(outputDir, '%(title)s.%(ext)s'); const startTime = Date.now(); let totalSize = 'Bilinmiyor'; if (format === 'mp3') { const q = quality ? `${quality}K` : '320K'; args = [ '--no-playlist', '--ffmpeg-location', FFMPEG_PATH, '-x', '--audio-format', 'mp3', '--audio-quality', q, '-o', outputTemplate ]; } else { const res = quality || '1080'; args = [ '--no-playlist', '--ffmpeg-location', FFMPEG_PATH, '-f', `bv*[height<=${res}][ext=mp4]+ba[ext=m4a]/b[height<=${res}][ext=mp4]/bv*[height<=${res}]+ba/b[height<=${res}]/bestvideo+bestaudio/best`, '-o', outputTemplate ]; } if (browser) { args.push('--cookies-from-browser', browser); } args.push(url); console.log(`[DEBUG] Spawning single downloader process: ${YTDLP_PATH} ${args.join(' ')}`); const proc = spawn(YTDLP_PATH, args); proc.stdout.on('data', (data) => { const line = data.toString(); const sizeMatch = line.match(/of\s+(\d+(?:\.\d+)?\s*[a-zA-Z]+)/); if (sizeMatch) { totalSize = sizeMatch[1]; } const match = line.match(/\[download\]\s+(\d+\.\d+)%/); if (match && onProgress) { const percent = parseFloat(match[1]); onProgress(percent, `İndiriliyor... %${percent}`); } else if (line.includes('[ExtractAudio]') || line.includes('[Merger]')) { onProgress(95, 'İşleniyor/Dönüştürülüyor...'); } }); proc.stderr.on('data', (data) => { console.warn(`[DEBUG] [yt-dlp stderr]:`, data.toString()); }); proc.on('close', (code) => { console.log(`[DEBUG] Single downloader process exited with code ${code}`); if (code === 0) { const durationSec = ((Date.now() - startTime) / 1000).toFixed(1); resolve({ size: totalSize, duration: `${durationSec} saniye` }); } else { reject(new Error(`Download exited with code ${code}`)); } }); }); } // Module-level Queue State variables let currentProcess = null; let isPaused = false; let isStopped = false; let currentItemIndex = 0; let downloadQueue = []; let queueOutputDir = ''; let queueBrowser = null; let queueOnProgressCallback = null; let queueResults = []; let queueStartTime = 0; let activeResolve = null; let activeReject = null; function getQueueStats() { const durationSec = ((Date.now() - queueStartTime) / 1000).toFixed(1); const completedCount = queueResults.filter(r => r.success).length; return { totalItems: downloadQueue.length, completed: completedCount, duration: `${durationSec} saniye`, outputDir: queueOutputDir, results: queueResults }; } function downloadSingleItemWithProcessRef(item) { return new Promise((resolve, reject) => { let args = []; const outputTemplate = path.join(queueOutputDir, '%(title)s.%(ext)s'); const startTime = Date.now(); let totalSize = 'Bilinmiyor'; if (item.format === 'mp3') { const q = item.quality ? `${item.quality}K` : '320K'; args = [ '--no-playlist', '--ffmpeg-location', FFMPEG_PATH, '-x', '--audio-format', 'mp3', '--audio-quality', q, '-o', outputTemplate ]; } else { const res = item.quality || '1080'; args = [ '--no-playlist', '--ffmpeg-location', FFMPEG_PATH, '-f', `bv*[height<=${res}][ext=mp4]+ba[ext=m4a]/b[height<=${res}][ext=mp4]/bv*[height<=${res}]+ba/b[height<=${res}]/bestvideo+bestaudio/best`, '-o', outputTemplate ]; } if (queueBrowser) { args.push('--cookies-from-browser', queueBrowser); } args.push(item.url); console.log(`[DEBUG] Spawning single downloader process: ${YTDLP_PATH} ${args.join(' ')}`); currentProcess = spawn(YTDLP_PATH, args); currentProcess.stdout.on('data', (data) => { const line = data.toString(); const sizeMatch = line.match(/of\s+(\d+(?:\.\d+)?\s*[a-zA-Z]+)/); if (sizeMatch) { totalSize = sizeMatch[1]; } const match = line.match(/\[download\]\s+(\d+\.\d+)%/); if (match) { const percent = parseFloat(match[1]); queueOnProgressCallback({ itemId: item.id, index: currentItemIndex, total: downloadQueue.length, percent: percent, status: `İndiriliyor...`, title: item.title }); } else if (line.includes('[ExtractAudio]') || line.includes('[Merger]')) { queueOnProgressCallback({ itemId: item.id, index: currentItemIndex, total: downloadQueue.length, percent: 95, status: `İşleniyor/Dönüştürülüyor...`, title: item.title }); } }); currentProcess.stderr.on('data', (data) => { console.warn(`[DEBUG] [yt-dlp stderr]:`, data.toString()); }); currentProcess.on('close', (code) => { console.log(`[DEBUG] Single downloader process exited with code ${code}`); currentProcess = null; if (code === 0) { const durationSec = ((Date.now() - startTime) / 1000).toFixed(1); resolve({ size: totalSize, duration: `${durationSec} saniye` }); } else { reject(new Error(`Download exited with code ${code}`)); } }); }); } async function processQueue() { if (isStopped) { activeResolve({ success: false, stopped: true, stats: getQueueStats() }); return; } if (isPaused) { queueOnProgressCallback({ itemId: downloadQueue[currentItemIndex].id, index: currentItemIndex, total: downloadQueue.length, percent: 0, status: `Duraklatıldı`, title: downloadQueue[currentItemIndex].title }); return; } if (currentItemIndex >= downloadQueue.length) { activeResolve({ success: true, stats: getQueueStats() }); return; } const item = downloadQueue[currentItemIndex]; console.log(`[DEBUG] Queue processing [${currentItemIndex+1}/${downloadQueue.length}]: "${item.title}"`); queueOnProgressCallback({ itemId: item.id, index: currentItemIndex, total: downloadQueue.length, percent: 0, status: `İndiriliyor...`, title: item.title }); try { const result = await downloadSingleItemWithProcessRef(item); queueResults.push({ success: true, item, size: result.size, duration: result.duration }); queueOnProgressCallback({ itemId: item.id, index: currentItemIndex, total: downloadQueue.length, percent: 100, status: `Tamamlandı`, title: item.title }); currentItemIndex++; processQueue(); // Process next item } catch (error) { if (isStopped) { console.log('[DEBUG] Process aborted by user stop command.'); activeResolve({ success: false, stopped: true, stats: getQueueStats() }); return; } if (isPaused) { console.log('[DEBUG] Process suspended by user pause command.'); return; } console.error(`[DEBUG] Queue error on item "${item.title}":`, error); queueResults.push({ success: false, item, error: error.message }); queueOnProgressCallback({ itemId: item.id, index: currentItemIndex, total: downloadQueue.length, percent: 0, status: `Hata`, title: item.title, failed: true }); currentItemIndex++; processQueue(); // Continue queue on item error } } // Download list of items sequentially async function downloadMedia(items, outputDir, browser, onProgress) { return new Promise((resolve, reject) => { console.log(`[DEBUG] Media download requested for ${items.length} items.`); downloadQueue = items; queueOutputDir = outputDir; queueBrowser = browser; queueOnProgressCallback = onProgress; isPaused = false; isStopped = false; currentItemIndex = 0; queueResults = []; queueStartTime = Date.now(); activeResolve = resolve; activeReject = reject; processQueue(); }); } function pauseDownload() { console.log('[DEBUG] Pause request received.'); isPaused = true; if (currentProcess) { try { currentProcess.kill('SIGINT'); } catch (e) { console.error('[DEBUG] Failed to interrupt process on pause:', e); } } return { success: true }; } function resumeDownload() { console.log('[DEBUG] Resume request received.'); if (!isPaused) return { success: false, error: 'Not paused' }; isPaused = false; processQueue(); return { success: true }; } function stopDownload() { console.log('[DEBUG] Stop request received.'); isStopped = true; if (currentProcess) { try { currentProcess.kill('SIGINT'); } catch (e) { console.error('[DEBUG] Failed to interrupt process on stop:', e); } } else if (activeResolve) { activeResolve({ success: false, stopped: true, stats: getQueueStats() }); } return { success: true }; } module.exports = { checkDependencies, getVideoInfo, downloadMedia, pauseDownload, resumeDownload, stopDownload };