Files
VibeDownloader/utils/downloader.js
T

294 lines
10 KiB
JavaScript

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, browser) {
console.log(`[DEBUG] Fetching info for URL: ${url}`);
return new Promise((resolve, reject) => {
let args = ['--no-playlist', '-j'];
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 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, quality, outputDir, browser, onProgress) {
console.log(`[DEBUG] Media download requested. Format: ${format}, Quality: ${quality}, Target Dir: ${outputDir}, Browser Cookies: ${browser || 'none'}`);
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'; // '320K', '256K', '192K'
args = [
'--no-playlist',
'--ffmpeg-location', FFMPEG_PATH,
'-x',
'--audio-format', 'mp3',
'--audio-quality', q,
'-o', outputTemplate
];
} else {
const res = quality || '1080'; // '1080', '720', '480'
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 downloader process: ${YTDLP_PATH} ${args.join(' ')}`);
const proc = spawn(YTDLP_PATH, args);
proc.stdout.on('data', (data) => {
const line = data.toString();
// Parse total size: e.g. [download] 1.2% of 8.37MiB at...
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]')) {
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) {
const durationSec = ((Date.now() - startTime) / 1000).toFixed(1);
resolve({
size: totalSize,
duration: `${durationSec} saniye`,
outputDir: outputDir
});
} else {
reject(new Error(`Download exited with code ${code}`));
}
});
});
}
module.exports = {
checkDependencies,
getVideoInfo,
downloadMedia
};