diff --git a/utils/downloader.js b/utils/downloader.js new file mode 100644 index 0000000..b299590 --- /dev/null +++ b/utils/downloader.js @@ -0,0 +1,269 @@ +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 +};