feat: implement initial UI and core structure for cross-platform media downloader with localization support

This commit is contained in:
Ümit Tunç
2026-05-28 07:33:41 +03:00
parent 67d833d7d7
commit 25b6d693ae
8 changed files with 662 additions and 86 deletions
+146 -20
View File
@@ -168,11 +168,22 @@ async function checkDependencies(onStatusUpdate) {
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 = ['--no-playlist', '-j'];
let args = ['-J', '--flat-playlist'];
if (browser) {
args.push('--cookies-from-browser', browser);
}
@@ -190,18 +201,56 @@ function getVideoInfo(url, browser) {
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'));
reject(new Error(stderr || 'Failed to fetch 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
});
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);
@@ -210,9 +259,8 @@ function getVideoInfo(url, browser) {
});
}
// 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'}`);
// 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');
@@ -220,7 +268,7 @@ function downloadMedia(url, format, quality, outputDir, browser, onProgress) {
let totalSize = 'Bilinmiyor';
if (format === 'mp3') {
const q = quality ? `${quality}K` : '320K'; // '320K', '256K', '192K'
const q = quality ? `${quality}K` : '320K';
args = [
'--no-playlist',
'--ffmpeg-location', FFMPEG_PATH,
@@ -230,7 +278,7 @@ function downloadMedia(url, format, quality, outputDir, browser, onProgress) {
'-o', outputTemplate
];
} else {
const res = quality || '1080'; // '1080', '720', '480'
const res = quality || '1080';
args = [
'--no-playlist',
'--ffmpeg-location', FFMPEG_PATH,
@@ -244,13 +292,12 @@ function downloadMedia(url, format, quality, outputDir, browser, onProgress) {
}
args.push(url);
console.log(`[DEBUG] Spawning downloader process: ${YTDLP_PATH} ${args.join(' ')}`);
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();
// 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];
@@ -261,7 +308,6 @@ function downloadMedia(url, format, quality, outputDir, browser, 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...');
}
});
@@ -271,13 +317,12 @@ function downloadMedia(url, format, quality, outputDir, browser, onProgress) {
});
proc.on('close', (code) => {
console.log(`[DEBUG] Downloader process exited with code ${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`,
outputDir: outputDir
duration: `${durationSec} saniye`
});
} else {
reject(new Error(`Download exited with code ${code}`));
@@ -286,6 +331,87 @@ function downloadMedia(url, format, quality, outputDir, browser, onProgress) {
});
}
// 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,