Files
VibeDownloader/utils/downloader.js
T

420 lines
14 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;
}
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}`));
}
});
});
}
// Download list of items sequentially
async function downloadMedia(items, outputDir, browser, onProgress) {
console.log(`[DEBUG] Media download requested for ${items.length} items.`);
const results = [];
const startTime = Date.now();
let completedCount = 0;
for (let i = 0; i < items.length; i++) {
const item = items[i];
console.log(`[DEBUG] Queue processing [${i+1}/${items.length}]: "${item.title}"`);
// Notify start of this item
onProgress({
itemId: item.id,
index: i,
total: items.length,
percent: 0,
status: `İndiriliyor...`,
title: item.title
});
try {
const result = await downloadSingle(item.url, item.format, item.quality, outputDir, browser, (percent, status) => {
onProgress({
itemId: item.id,
index: i,
total: items.length,
percent: percent,
status: status,
title: item.title
});
});
completedCount++;
results.push({
success: true,
item,
size: result.size,
duration: result.duration
});
// Notify completion of this item
onProgress({
itemId: item.id,
index: i,
total: items.length,
percent: 100,
status: `Tamamlandı`,
title: item.title
});
} catch (error) {
console.error(`[DEBUG] Queue error on item "${item.title}":`, error);
results.push({
success: false,
item,
error: error.message
});
onProgress({
itemId: item.id,
index: i,
total: items.length,
percent: 0,
status: `Hata`,
title: item.title,
failed: true
});
}
}
const durationSec = ((Date.now() - startTime) / 1000).toFixed(1);
return {
totalItems: items.length,
completed: completedCount,
duration: `${durationSec} saniye`,
outputDir: outputDir,
results: results
};
}
module.exports = {
checkDependencies,
getVideoInfo,
downloadMedia
};