feat: implement initial UI and core structure for cross-platform media downloader with localization support
This commit is contained in:
+146
-20
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user