const { spawn, exec } = require('child_process'); const path = require('path'); const fs = require('fs'); const https = require('https'); const BIN_DIR = 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; } // Get video metadata (title, thumbnail, duration, etc.) function getVideoInfo(url) { console.log(`[DEBUG] Fetching info for URL: ${url}`); return new Promise((resolve, reject) => { // Adding '--no-playlist' prevents yt-dlp from crawling the entire playlist (which hangs/takes extremely long) console.log(`[DEBUG] Spawning process: ${YTDLP_PATH} --no-playlist -j ${url}`); const proc = spawn(YTDLP_PATH, ['--no-playlist', '-j', url]); 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 video details')); return; } try { const info = JSON.parse(stdout); console.log(`[DEBUG] Metadata fetched successfully for: "${info.title}"`); resolve({ title: info.title, thumbnail: info.thumbnail, duration: info.duration_string, uploader: info.uploader }); } catch (err) { console.error(`[DEBUG] JSON parsing error on yt-dlp output:`, err); reject(err); } }); }); } // Download video or convert to MP3 function downloadMedia(url, format, outputDir, onProgress) { console.log(`[DEBUG] Media download requested. Format: ${format}, Target Dir: ${outputDir}`); return new Promise((resolve, reject) => { let args = []; const outputTemplate = path.join(outputDir, '%(title)s.%(ext)s'); if (format === 'mp3') { args = [ '--no-playlist', '--ffmpeg-location', FFMPEG_PATH, '-x', '--audio-format', 'mp3', '--audio-quality', '320K', '-o', outputTemplate, url ]; } else { args = [ '--no-playlist', '--ffmpeg-location', FFMPEG_PATH, '-f', 'bv*[ext=mp4]+ba[ext=m4a]/b[ext=mp4] / bv*+ba/b', '-o', outputTemplate, url ]; } console.log(`[DEBUG] Spawning downloader process: ${YTDLP_PATH} ${args.join(' ')}`); const proc = spawn(YTDLP_PATH, args); proc.stdout.on('data', (data) => { const line = data.toString(); 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]')) { console.log('[DEBUG] Audio extraction or Video merger phase started.'); 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] Downloader process exited with code ${code}`); if (code === 0) { resolve(); } else { reject(new Error(`Download exited with code ${code}`)); } }); }); } module.exports = { checkDependencies, getVideoInfo, downloadMedia };