feat: implement initial UI and core structure for cross-platform media downloader with localization support
This commit is contained in:
@@ -80,11 +80,11 @@ ipcMain.handle('get-video-info', async (event, { url, browser }) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// IPC Handler: Download Media
|
// IPC Handler: Download Media
|
||||||
ipcMain.handle('download-media', async (event, { url, format, quality, browser }) => {
|
ipcMain.handle('download-media', async (event, { items, browser }) => {
|
||||||
try {
|
try {
|
||||||
const stats = await downloader.downloadMedia(url, format, quality, selectedOutputDir, browser, (percent, status) => {
|
const stats = await downloader.downloadMedia(items, selectedOutputDir, browser, (progressData) => {
|
||||||
if (mainWindow) {
|
if (mainWindow) {
|
||||||
mainWindow.webContents.send('download-progress', { percent, status });
|
mainWindow.webContents.send('download-progress', progressData);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return { success: true, stats };
|
return { success: true, stats };
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@ contextBridge.exposeInMainWorld('api', {
|
|||||||
checkDependencies: () => ipcRenderer.invoke('check-dependencies'),
|
checkDependencies: () => ipcRenderer.invoke('check-dependencies'),
|
||||||
onDependencyStatus: (callback) => ipcRenderer.on('dependency-status', (_, data) => callback(data)),
|
onDependencyStatus: (callback) => ipcRenderer.on('dependency-status', (_, data) => callback(data)),
|
||||||
getVideoInfo: (url, browser) => ipcRenderer.invoke('get-video-info', { url, browser }),
|
getVideoInfo: (url, browser) => ipcRenderer.invoke('get-video-info', { url, browser }),
|
||||||
downloadMedia: (url, format, quality, browser) => ipcRenderer.invoke('download-media', { url, format, quality, browser }),
|
downloadMedia: (items, browser) => ipcRenderer.invoke('download-media', { items, browser }),
|
||||||
onDownloadProgress: (callback) => ipcRenderer.on('download-progress', (_, data) => callback(data)),
|
onDownloadProgress: (callback) => ipcRenderer.on('download-progress', (_, data) => callback(data)),
|
||||||
selectDirectory: () => ipcRenderer.invoke('select-directory'),
|
selectDirectory: () => ipcRenderer.invoke('select-directory'),
|
||||||
getDownloadsFolder: () => ipcRenderer.invoke('get-downloads-folder'),
|
getDownloadsFolder: () => ipcRenderer.invoke('get-downloads-folder'),
|
||||||
|
|||||||
@@ -135,6 +135,23 @@
|
|||||||
|
|
||||||
<!-- Scrollable Track List Area -->
|
<!-- Scrollable Track List Area -->
|
||||||
<div class="track-list-scroll">
|
<div class="track-list-scroll">
|
||||||
|
<!-- Playlist Bulk Actions and Select All Header -->
|
||||||
|
<div id="playlist-header" class="playlist-header" style="display: none;">
|
||||||
|
<div class="playlist-select-all">
|
||||||
|
<input type="checkbox" id="select-all-checkbox" checked style="cursor: pointer; accent-color: #7000ff; width: 16px; height: 16px;">
|
||||||
|
<label for="select-all-checkbox" id="select-all-label" data-i18n="playlist_select_all" style="cursor: pointer; color: #fff; font-size: 0.9em; font-weight: 500; user-select: none;">Tümünü Seç</label>
|
||||||
|
</div>
|
||||||
|
<div class="playlist-bulk-actions">
|
||||||
|
<button id="bulk-mp3-btn" class="bulk-action-btn">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width: 14px; height: 14px;"><path d="M9 18V5l12-2v13" /><circle cx="6" cy="18" r="3" /><circle cx="18" cy="16" r="3" /></svg>
|
||||||
|
<span data-i18n="bulk_mp3">Tümünü MP3 Yap</span>
|
||||||
|
</button>
|
||||||
|
<button id="bulk-mp4-btn" class="bulk-action-btn">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width: 14px; height: 14px;"><rect x="2" y="2" width="20" height="20" rx="2.18" ry="2.18" /><line x1="7" y1="2" x2="7" y2="22" /><line x1="17" y1="2" x2="17" y2="22" /><line x1="2" y1="12" x2="22" y2="12" /></svg>
|
||||||
|
<span data-i18n="bulk_mp4">Tümünü MP4 Yap</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div id="track-list" class="track-list">
|
<div id="track-list" class="track-list">
|
||||||
<!-- Dynamically populated by Javascript -->
|
<!-- Dynamically populated by Javascript -->
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+7
-1
@@ -55,5 +55,11 @@
|
|||||||
"quality_best": "Highest",
|
"quality_best": "Highest",
|
||||||
"quality_standard": "Standard",
|
"quality_standard": "Standard",
|
||||||
"sec_suffix": "seconds",
|
"sec_suffix": "seconds",
|
||||||
"min_suffix": "minutes"
|
"min_suffix": "minutes",
|
||||||
|
"playlist_select_all": "Select All",
|
||||||
|
"bulk_mp3": "Bulk MP3",
|
||||||
|
"bulk_mp4": "Bulk MP4",
|
||||||
|
"err_no_items_selected": "Please select at least one file to download.",
|
||||||
|
"playlist_title_label": "Playlist Title:",
|
||||||
|
"downloaded_files_label": "Downloaded Files:"
|
||||||
}
|
}
|
||||||
|
|||||||
+7
-1
@@ -55,5 +55,11 @@
|
|||||||
"quality_best": "En Yüksek",
|
"quality_best": "En Yüksek",
|
||||||
"quality_standard": "Standart",
|
"quality_standard": "Standart",
|
||||||
"sec_suffix": "saniye",
|
"sec_suffix": "saniye",
|
||||||
"min_suffix": "dakika"
|
"min_suffix": "dakika",
|
||||||
|
"playlist_select_all": "Tümünü Seç",
|
||||||
|
"bulk_mp3": "Tümünü MP3 Yap",
|
||||||
|
"bulk_mp4": "Tümünü MP4 Yap",
|
||||||
|
"err_no_items_selected": "Lütfen indirmek için en az bir dosya seçin.",
|
||||||
|
"playlist_title_label": "Çalma Listesi:",
|
||||||
|
"downloaded_files_label": "İndirilen Dosyalar:"
|
||||||
}
|
}
|
||||||
|
|||||||
+334
-54
@@ -39,6 +39,15 @@ let currentFormat = 'mp3'; // 'mp3' or 'mp4'
|
|||||||
let currentQuality = '320'; // Selected quality value
|
let currentQuality = '320'; // Selected quality value
|
||||||
let currentVideoData = null;
|
let currentVideoData = null;
|
||||||
|
|
||||||
|
// Playlist State Variables
|
||||||
|
const playlistHeader = document.getElementById('playlist-header');
|
||||||
|
const selectAllCheckbox = document.getElementById('select-all-checkbox');
|
||||||
|
const bulkMp3Btn = document.getElementById('bulk-mp3-btn');
|
||||||
|
const bulkMp4Btn = document.getElementById('bulk-mp4-btn');
|
||||||
|
let playlistTracks = [];
|
||||||
|
let focusedTrackId = null;
|
||||||
|
let isPlaylistMode = false;
|
||||||
|
|
||||||
// Localization State
|
// Localization State
|
||||||
let currentLang = 'tr';
|
let currentLang = 'tr';
|
||||||
let translations = {};
|
let translations = {};
|
||||||
@@ -134,6 +143,41 @@ window.addEventListener('DOMContentLoaded', async () => {
|
|||||||
await loadTranslations(e.target.value);
|
await loadTranslations(e.target.value);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Select All Checkbox Handler
|
||||||
|
selectAllCheckbox.addEventListener('change', () => {
|
||||||
|
const checked = selectAllCheckbox.checked;
|
||||||
|
playlistTracks.forEach(track => {
|
||||||
|
track.checked = checked;
|
||||||
|
});
|
||||||
|
renderTrackList(currentVideoData);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Bulk MP3 Button Handler
|
||||||
|
bulkMp3Btn.addEventListener('click', () => {
|
||||||
|
playlistTracks.forEach(track => {
|
||||||
|
if (track.checked) {
|
||||||
|
track.format = 'mp3';
|
||||||
|
track.quality = '320';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const focusedTrack = playlistTracks.find(t => t.id === focusedTrackId);
|
||||||
|
if (focusedTrack) syncBottomPanelWithTrack(focusedTrack);
|
||||||
|
renderTrackList(currentVideoData);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Bulk MP4 Button Handler
|
||||||
|
bulkMp4Btn.addEventListener('click', () => {
|
||||||
|
playlistTracks.forEach(track => {
|
||||||
|
if (track.checked) {
|
||||||
|
track.format = 'mp4';
|
||||||
|
track.quality = '1080';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const focusedTrack = playlistTracks.find(t => t.id === focusedTrackId);
|
||||||
|
if (focusedTrack) syncBottomPanelWithTrack(focusedTrack);
|
||||||
|
renderTrackList(currentVideoData);
|
||||||
|
});
|
||||||
|
|
||||||
// Get and load persisted or default language
|
// Get and load persisted or default language
|
||||||
const savedLang = localStorage.getItem('vibe_lang') || 'tr';
|
const savedLang = localStorage.getItem('vibe_lang') || 'tr';
|
||||||
languageSelect.value = savedLang;
|
languageSelect.value = savedLang;
|
||||||
@@ -195,6 +239,7 @@ formatMp3Btn.addEventListener('click', () => {
|
|||||||
formatMp4Btn.classList.remove('active');
|
formatMp4Btn.classList.remove('active');
|
||||||
currentQuality = '320'; // Reset to default best quality for MP3
|
currentQuality = '320'; // Reset to default best quality for MP3
|
||||||
renderQualityPills();
|
renderQualityPills();
|
||||||
|
applyBottomSettingsToSelected();
|
||||||
});
|
});
|
||||||
|
|
||||||
formatMp4Btn.addEventListener('click', () => {
|
formatMp4Btn.addEventListener('click', () => {
|
||||||
@@ -204,6 +249,7 @@ formatMp4Btn.addEventListener('click', () => {
|
|||||||
formatMp3Btn.classList.remove('active');
|
formatMp3Btn.classList.remove('active');
|
||||||
currentQuality = '1080'; // Reset to default best quality for MP4
|
currentQuality = '1080'; // Reset to default best quality for MP4
|
||||||
renderQualityPills();
|
renderQualityPills();
|
||||||
|
applyBottomSettingsToSelected();
|
||||||
});
|
});
|
||||||
|
|
||||||
// 4. Quality Pills Renderer
|
// 4. Quality Pills Renderer
|
||||||
@@ -225,12 +271,50 @@ function renderQualityPills() {
|
|||||||
document.querySelectorAll('.quality-pill').forEach(p => p.classList.remove('active'));
|
document.querySelectorAll('.quality-pill').forEach(p => p.classList.remove('active'));
|
||||||
pill.classList.add('active');
|
pill.classList.add('active');
|
||||||
currentQuality = opt.value;
|
currentQuality = opt.value;
|
||||||
|
applyBottomSettingsToSelected();
|
||||||
});
|
});
|
||||||
|
|
||||||
qualityPillsContainer.appendChild(pill);
|
qualityPillsContainer.appendChild(pill);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function syncBottomPanelWithTrack(track) {
|
||||||
|
currentFormat = track.format;
|
||||||
|
currentQuality = track.quality;
|
||||||
|
|
||||||
|
if (currentFormat === 'mp3') {
|
||||||
|
formatMp3Btn.classList.add('active');
|
||||||
|
formatMp4Btn.classList.remove('active');
|
||||||
|
} else {
|
||||||
|
formatMp4Btn.classList.add('active');
|
||||||
|
formatMp3Btn.classList.remove('active');
|
||||||
|
}
|
||||||
|
renderQualityPills();
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyBottomSettingsToSelected() {
|
||||||
|
if (!currentVideoData) return;
|
||||||
|
const checkedTracks = playlistTracks.filter(t => t.checked);
|
||||||
|
if (checkedTracks.length > 0) {
|
||||||
|
checkedTracks.forEach(track => {
|
||||||
|
track.format = currentFormat;
|
||||||
|
track.quality = currentQuality;
|
||||||
|
});
|
||||||
|
} else if (focusedTrackId) {
|
||||||
|
const focusedTrack = playlistTracks.find(t => t.id === focusedTrackId);
|
||||||
|
if (focusedTrack) {
|
||||||
|
focusedTrack.format = currentFormat;
|
||||||
|
focusedTrack.quality = currentQuality;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
renderTrackList(currentVideoData);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateSelectAllCheckboxState() {
|
||||||
|
const checkedCount = playlistTracks.filter(t => t.checked).length;
|
||||||
|
selectAllCheckbox.checked = checkedCount === playlistTracks.length;
|
||||||
|
}
|
||||||
|
|
||||||
// 5. Back Button (Step 2 to Step 1 Transition)
|
// 5. Back Button (Step 2 to Step 1 Transition)
|
||||||
backBtn.addEventListener('click', () => {
|
backBtn.addEventListener('click', () => {
|
||||||
// Reset any error card
|
// Reset any error card
|
||||||
@@ -239,6 +323,8 @@ backBtn.addEventListener('click', () => {
|
|||||||
step2.classList.remove('active');
|
step2.classList.remove('active');
|
||||||
step1.classList.add('active');
|
step1.classList.add('active');
|
||||||
currentVideoData = null;
|
currentVideoData = null;
|
||||||
|
playlistTracks = [];
|
||||||
|
focusedTrackId = null;
|
||||||
urlInput.focus();
|
urlInput.focus();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -263,6 +349,8 @@ analyzeBtn.addEventListener('click', async () => {
|
|||||||
if (result.success) {
|
if (result.success) {
|
||||||
analyzedUrl = url;
|
analyzedUrl = url;
|
||||||
currentVideoData = result.info;
|
currentVideoData = result.info;
|
||||||
|
playlistTracks = [];
|
||||||
|
focusedTrackId = null;
|
||||||
|
|
||||||
// Render the fetched video details inside the scrollable list container
|
// Render the fetched video details inside the scrollable list container
|
||||||
renderTrackList(result.info);
|
renderTrackList(result.info);
|
||||||
@@ -284,30 +372,174 @@ analyzeBtn.addEventListener('click', async () => {
|
|||||||
|
|
||||||
// 7. Render Track List Function
|
// 7. Render Track List Function
|
||||||
function renderTrackList(info) {
|
function renderTrackList(info) {
|
||||||
trackList.innerHTML = `
|
trackList.innerHTML = '';
|
||||||
<div class="track-item">
|
|
||||||
<div class="track-thumb">
|
if (info.isPlaylist) {
|
||||||
|
isPlaylistMode = true;
|
||||||
|
playlistHeader.style.display = 'flex';
|
||||||
|
} else {
|
||||||
|
isPlaylistMode = false;
|
||||||
|
playlistHeader.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (info.entries) {
|
||||||
|
info.entries.forEach(entry => {
|
||||||
|
const existing = playlistTracks.find(t => t.id === entry.id);
|
||||||
|
if (!existing) {
|
||||||
|
playlistTracks.push({
|
||||||
|
id: entry.id,
|
||||||
|
title: entry.title,
|
||||||
|
thumbnail: entry.thumbnail,
|
||||||
|
duration: entry.duration,
|
||||||
|
uploader: entry.uploader,
|
||||||
|
url: entry.url,
|
||||||
|
format: currentFormat,
|
||||||
|
quality: currentQuality,
|
||||||
|
status: 'ready',
|
||||||
|
progress: 0,
|
||||||
|
checked: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (playlistTracks.length > 0 && !focusedTrackId) {
|
||||||
|
focusedTrackId = playlistTracks[0].id;
|
||||||
|
}
|
||||||
|
|
||||||
|
const focusedTrack = playlistTracks.find(t => t.id === focusedTrackId);
|
||||||
|
if (focusedTrack) {
|
||||||
|
syncBottomPanelWithTrack(focusedTrack);
|
||||||
|
}
|
||||||
|
|
||||||
|
playlistTracks.forEach(track => {
|
||||||
|
const itemEl = document.createElement('div');
|
||||||
|
itemEl.className = `track-item ${track.id === focusedTrackId ? 'focused' : ''}`;
|
||||||
|
itemEl.dataset.id = track.id;
|
||||||
|
|
||||||
|
const checkboxWrapper = document.createElement('div');
|
||||||
|
checkboxWrapper.className = 'track-checkbox-wrapper';
|
||||||
|
|
||||||
|
const checkbox = document.createElement('input');
|
||||||
|
checkbox.type = 'checkbox';
|
||||||
|
checkbox.className = 'track-checkbox';
|
||||||
|
checkbox.checked = track.checked;
|
||||||
|
|
||||||
|
checkbox.addEventListener('change', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
track.checked = checkbox.checked;
|
||||||
|
updateSelectAllCheckboxState();
|
||||||
|
});
|
||||||
|
|
||||||
|
checkboxWrapper.appendChild(checkbox);
|
||||||
|
itemEl.appendChild(checkboxWrapper);
|
||||||
|
|
||||||
|
const thumbEl = document.createElement('div');
|
||||||
|
thumbEl.className = 'track-thumb';
|
||||||
|
thumbEl.innerHTML = `
|
||||||
<svg class="track-thumb-fallback" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
<svg class="track-thumb-fallback" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
<polygon points="23 7 16 12 23 17 23 7"></polygon>
|
<polygon points="23 7 16 12 23 17 23 7"></polygon>
|
||||||
<rect x="1" y="5" width="15" height="14" rx="2" ry="2"></rect>
|
<rect x="1" y="5" width="15" height="14" rx="2" ry="2"></rect>
|
||||||
</svg>
|
</svg>
|
||||||
<img src="${info.thumbnail || ''}" onerror="this.style.display='none';" alt="Thumbnail">
|
<img src="${track.thumbnail || ''}" onerror="this.style.display='none';" alt="Thumbnail">
|
||||||
</div>
|
|
||||||
<div class="track-details">
|
|
||||||
<h4>${info.title}</h4>
|
|
||||||
<p class="track-uploader">${info.uploader}</p>
|
|
||||||
<span class="track-duration">${t('duration_label')} ${info.duration || t('duration_unknown')}</span>
|
|
||||||
</div>
|
|
||||||
<div class="track-status">
|
|
||||||
<span id="track-status-badge" class="status-badge ready">${t('badge_ready')}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
`;
|
||||||
|
itemEl.appendChild(thumbEl);
|
||||||
|
|
||||||
|
const detailsEl = document.createElement('div');
|
||||||
|
detailsEl.className = 'track-details';
|
||||||
|
detailsEl.innerHTML = `
|
||||||
|
<h4>${track.title}</h4>
|
||||||
|
<p class="track-uploader">${track.uploader}</p>
|
||||||
|
<span class="track-duration">${t('duration_label')} ${track.duration || t('duration_unknown')}</span>
|
||||||
|
`;
|
||||||
|
itemEl.appendChild(detailsEl);
|
||||||
|
|
||||||
|
const formatSelectorEl = document.createElement('div');
|
||||||
|
formatSelectorEl.className = 'track-format-selector';
|
||||||
|
|
||||||
|
const formatBadgeBtn = document.createElement('button');
|
||||||
|
formatBadgeBtn.className = `format-badge-btn ${track.format}`;
|
||||||
|
formatBadgeBtn.textContent = track.format.toUpperCase();
|
||||||
|
|
||||||
|
formatBadgeBtn.addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
track.format = track.format === 'mp3' ? 'mp4' : 'mp3';
|
||||||
|
track.quality = track.format === 'mp3' ? '320' : '1080';
|
||||||
|
|
||||||
|
if (track.id === focusedTrackId) {
|
||||||
|
syncBottomPanelWithTrack(track);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderTrackList(currentVideoData);
|
||||||
|
});
|
||||||
|
|
||||||
|
formatSelectorEl.appendChild(formatBadgeBtn);
|
||||||
|
itemEl.appendChild(formatSelectorEl);
|
||||||
|
|
||||||
|
const statusEl = document.createElement('div');
|
||||||
|
statusEl.className = 'track-status';
|
||||||
|
|
||||||
|
let statusText = t('badge_ready');
|
||||||
|
let badgeClass = 'ready';
|
||||||
|
|
||||||
|
if (track.status === 'downloading') {
|
||||||
|
statusText = track.progress > 0 ? `${t('badge_downloading')} %${track.progress}` : t('badge_downloading');
|
||||||
|
badgeClass = 'downloading';
|
||||||
|
} else if (track.status === 'completed') {
|
||||||
|
statusText = t('badge_completed');
|
||||||
|
badgeClass = 'completed';
|
||||||
|
} else if (track.status === 'error') {
|
||||||
|
statusText = t('badge_error');
|
||||||
|
badgeClass = 'error';
|
||||||
|
} else if (track.status === 'waiting') {
|
||||||
|
statusText = t('status_connecting');
|
||||||
|
badgeClass = 'ready';
|
||||||
|
}
|
||||||
|
|
||||||
|
const statusBadge = document.createElement('span');
|
||||||
|
statusBadge.id = `badge-${track.id}`;
|
||||||
|
statusBadge.className = `status-badge ${badgeClass}`;
|
||||||
|
|
||||||
|
if (track.status === 'completed') {
|
||||||
|
statusBadge.style.borderColor = 'rgba(0, 255, 135, 0.3)';
|
||||||
|
statusBadge.style.color = '#00ff87';
|
||||||
|
statusBadge.style.background = 'rgba(0, 255, 135, 0.1)';
|
||||||
|
} else if (track.status === 'error') {
|
||||||
|
statusBadge.style.borderColor = 'rgba(255, 77, 77, 0.3)';
|
||||||
|
statusBadge.style.color = '#ff4d4d';
|
||||||
|
statusBadge.style.background = 'rgba(255, 77, 77, 0.1)';
|
||||||
|
}
|
||||||
|
|
||||||
|
statusBadge.textContent = statusText;
|
||||||
|
statusEl.appendChild(statusBadge);
|
||||||
|
itemEl.appendChild(statusEl);
|
||||||
|
|
||||||
|
itemEl.addEventListener('click', () => {
|
||||||
|
focusedTrackId = track.id;
|
||||||
|
syncBottomPanelWithTrack(track);
|
||||||
|
renderTrackList(currentVideoData);
|
||||||
|
});
|
||||||
|
|
||||||
|
trackList.appendChild(itemEl);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 8. Media Download Executor
|
// 8. Media Download Executor
|
||||||
downloadBtn.addEventListener('click', async () => {
|
downloadBtn.addEventListener('click', async () => {
|
||||||
if (!analyzedUrl) return;
|
if (!analyzedUrl || playlistTracks.length === 0) return;
|
||||||
|
|
||||||
|
const selectedItems = playlistTracks.filter(t => t.checked);
|
||||||
|
if (selectedItems.length === 0) {
|
||||||
|
Swal.fire({
|
||||||
|
title: t('badge_error'),
|
||||||
|
text: t('err_no_items_selected') || 'Lütfen indirmek için en az bir dosya seçin.',
|
||||||
|
icon: 'warning',
|
||||||
|
background: 'rgba(23, 20, 38, 0.95)',
|
||||||
|
color: '#fff',
|
||||||
|
confirmButtonColor: '#7000ff'
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Setup download progress panel UI states
|
// Setup download progress panel UI states
|
||||||
downloadProgressPanel.style.display = 'block';
|
downloadProgressPanel.style.display = 'block';
|
||||||
@@ -316,59 +548,112 @@ downloadBtn.addEventListener('click', async () => {
|
|||||||
downloadStatus.textContent = t('status_connecting');
|
downloadStatus.textContent = t('status_connecting');
|
||||||
downloadPercent.textContent = '0%';
|
downloadPercent.textContent = '0%';
|
||||||
downloadProgressBar.style.width = '0%';
|
downloadProgressBar.style.width = '0%';
|
||||||
downloadSubtext.textContent = currentFormat === 'mp3' ? t('subtext_mp3_download') : t('subtext_mp4_download');
|
downloadSubtext.textContent = t('subtext_wait');
|
||||||
|
|
||||||
// Set track status badge as downloading
|
// Disable UI interactive components during download
|
||||||
const statusBadge = document.getElementById('track-status-badge');
|
document.querySelectorAll('.track-checkbox, .format-badge-btn, .quality-pill, .format-btn').forEach(el => {
|
||||||
if (statusBadge) {
|
el.disabled = true;
|
||||||
statusBadge.textContent = t('badge_downloading');
|
el.style.pointerEvents = 'none';
|
||||||
statusBadge.className = 'status-badge downloading';
|
});
|
||||||
|
|
||||||
|
// Mark selected items as waiting
|
||||||
|
selectedItems.forEach(track => {
|
||||||
|
track.status = 'waiting';
|
||||||
|
track.progress = 0;
|
||||||
|
|
||||||
|
const badge = document.getElementById(`badge-${track.id}`);
|
||||||
|
if (badge) {
|
||||||
|
badge.textContent = t('status_connecting');
|
||||||
|
badge.className = 'status-badge ready';
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Subscribe to progress events from the main process
|
// Subscribe to progress events from the main process
|
||||||
window.api.onDownloadProgress(({ percent, status }) => {
|
window.api.onDownloadProgress(({ itemId, index, total, percent, status, title, failed }) => {
|
||||||
|
const track = playlistTracks.find(t => t.id === itemId);
|
||||||
|
if (track) {
|
||||||
|
if (percent === 100) {
|
||||||
|
track.status = 'completed';
|
||||||
|
track.progress = 100;
|
||||||
|
} else if (failed) {
|
||||||
|
track.status = 'error';
|
||||||
|
} else {
|
||||||
|
track.status = 'downloading';
|
||||||
|
track.progress = percent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dynamic row updates
|
||||||
|
const badge = document.getElementById(`badge-${itemId}`);
|
||||||
|
if (badge) {
|
||||||
|
if (percent === 100) {
|
||||||
|
badge.textContent = t('badge_completed');
|
||||||
|
badge.className = 'status-badge completed';
|
||||||
|
badge.style.borderColor = 'rgba(0, 255, 135, 0.3)';
|
||||||
|
badge.style.color = '#00ff87';
|
||||||
|
badge.style.background = 'rgba(0, 255, 135, 0.1)';
|
||||||
|
} else if (failed) {
|
||||||
|
badge.textContent = t('badge_error');
|
||||||
|
badge.className = 'status-badge error';
|
||||||
|
badge.style.borderColor = 'rgba(255, 77, 77, 0.3)';
|
||||||
|
badge.style.color = '#ff4d4d';
|
||||||
|
badge.style.background = 'rgba(255, 77, 77, 0.1)';
|
||||||
|
} else {
|
||||||
|
badge.textContent = `${t('badge_downloading')} %${percent}`;
|
||||||
|
badge.className = 'status-badge downloading';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update main footer progress panel
|
||||||
let stat = status;
|
let stat = status;
|
||||||
if (status.includes('İndiriliyor')) {
|
if (status.includes('İndiriliyor')) {
|
||||||
stat = t('status_downloading', { percent });
|
stat = `${t('btn_downloading')}... %${percent}`;
|
||||||
} else if (status.includes('İşleniyor') || status.includes('Dönüştürülüyor')) {
|
} else if (status.includes('İşleniyor') || status.includes('Dönüştürülüyor')) {
|
||||||
stat = t('status_processing');
|
stat = t('status_processing');
|
||||||
}
|
}
|
||||||
|
|
||||||
downloadStatus.textContent = stat;
|
downloadStatus.textContent = `[${index + 1}/${total}] ${title}`;
|
||||||
downloadPercent.textContent = `${percent}%`;
|
downloadPercent.textContent = `${percent}%`;
|
||||||
downloadProgressBar.style.width = `${percent}%`;
|
downloadProgressBar.style.width = `${percent}%`;
|
||||||
|
downloadSubtext.textContent = `${t('swal_file_size')}: ${stat}`;
|
||||||
if (percent > 90) {
|
|
||||||
downloadSubtext.textContent = currentFormat === 'mp3'
|
|
||||||
? t('subtext_mp3_convert')
|
|
||||||
: t('subtext_mp4_merge');
|
|
||||||
} else {
|
|
||||||
downloadSubtext.textContent = t('subtext_speed_note');
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const browser = useCookiesCheckbox.checked ? cookieBrowserSelect.value : null;
|
const browser = useCookiesCheckbox.checked ? cookieBrowserSelect.value : null;
|
||||||
const result = await window.api.downloadMedia(analyzedUrl, currentFormat, currentQuality, browser);
|
|
||||||
|
// Map selected items into a simplified API task schema
|
||||||
|
const downloadTasks = selectedItems.map(t => ({
|
||||||
|
id: t.id,
|
||||||
|
title: t.title,
|
||||||
|
url: t.url,
|
||||||
|
format: t.format,
|
||||||
|
quality: t.quality
|
||||||
|
}));
|
||||||
|
|
||||||
|
const result = await window.api.downloadMedia(downloadTasks, browser);
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
downloadStatus.textContent = t('status_completed');
|
downloadStatus.textContent = t('status_completed');
|
||||||
downloadPercent.textContent = '100%';
|
downloadPercent.textContent = '100%';
|
||||||
downloadProgressBar.style.width = '100%';
|
downloadProgressBar.style.width = '100%';
|
||||||
downloadSubtext.textContent = t('subtext_success');
|
downloadSubtext.textContent = t('subtext_success');
|
||||||
|
|
||||||
if (statusBadge) {
|
|
||||||
statusBadge.textContent = t('badge_completed');
|
|
||||||
statusBadge.className = 'status-badge ready';
|
|
||||||
statusBadge.style.borderColor = 'rgba(0, 255, 135, 0.3)';
|
|
||||||
statusBadge.style.color = '#00ff87';
|
|
||||||
statusBadge.style.background = 'rgba(0, 255, 135, 0.1)';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Success gradient transitions
|
// Success gradient transitions
|
||||||
downloadProgressBar.style.background = 'linear-gradient(90deg, #00ff87 0%, #60efff 100%)';
|
downloadProgressBar.style.background = 'linear-gradient(90deg, #00ff87 0%, #60efff 100%)';
|
||||||
downloadProgressBar.style.boxShadow = '0 0 15px rgba(0, 255, 135, 0.6)';
|
downloadProgressBar.style.boxShadow = '0 0 15px rgba(0, 255, 135, 0.6)';
|
||||||
|
|
||||||
// Translate duration to localized representation
|
const displayTitle = isPlaylistMode
|
||||||
|
? `${currentVideoData.playlistTitle || 'Playlist'} (${result.stats.completed}/${result.stats.totalItems})`
|
||||||
|
: selectedItems[0].title;
|
||||||
|
|
||||||
|
const displayUploader = isPlaylistMode
|
||||||
|
? 'Playlist Queue'
|
||||||
|
: (selectedItems[0].uploader || t('duration_unknown'));
|
||||||
|
|
||||||
|
const displaySize = isPlaylistMode
|
||||||
|
? `${result.stats.completed} ${t('badge_completed').toLowerCase()}`
|
||||||
|
: (result.stats.results[0]?.size || 'Bilinmiyor');
|
||||||
|
|
||||||
const localizedDuration = result.stats.duration
|
const localizedDuration = result.stats.duration
|
||||||
.replace('saniye', t('sec_suffix'))
|
.replace('saniye', t('sec_suffix'))
|
||||||
.replace('dakika', t('min_suffix'));
|
.replace('dakika', t('min_suffix'));
|
||||||
@@ -378,11 +663,10 @@ downloadBtn.addEventListener('click', async () => {
|
|||||||
title: t('swal_success_title'),
|
title: t('swal_success_title'),
|
||||||
html: `
|
html: `
|
||||||
<div style="text-align: ${currentLang === 'ar' ? 'right' : 'left'}; direction: ${currentLang === 'ar' ? 'rtl' : 'ltr'}; font-family: 'Outfit', sans-serif; color: #fff; line-height: 1.6; font-size: 14px;">
|
<div style="text-align: ${currentLang === 'ar' ? 'right' : 'left'}; direction: ${currentLang === 'ar' ? 'rtl' : 'ltr'}; font-family: 'Outfit', sans-serif; color: #fff; line-height: 1.6; font-size: 14px;">
|
||||||
<p style="margin-bottom: 8px;"><strong>${t('swal_file_name')}</strong> <span style="color: #60efff;">${currentVideoData.title}</span></p>
|
<p style="margin-bottom: 8px;"><strong>${isPlaylistMode ? t('playlist_title_label') || 'Çalma Listesi:' : t('swal_file_name')}</strong> <span style="color: #60efff;">${displayTitle}</span></p>
|
||||||
<p style="margin-bottom: 8px;"><strong>${t('swal_platform_uploader')}</strong> <span style="color: #60efff;">${currentVideoData.uploader || t('duration_unknown')}</span></p>
|
<p style="margin-bottom: 8px;"><strong>${t('swal_platform_uploader')}</strong> <span style="color: #60efff;">${displayUploader}</span></p>
|
||||||
<p style="margin-bottom: 8px;"><strong>${t('swal_file_size')}</strong> <span style="color: #00ff87;">${result.stats.size}</span></p>
|
<p style="margin-bottom: 8px;"><strong>${isPlaylistMode ? t('downloaded_files_label') || 'İndirilen Dosyalar:' : t('swal_file_size')}</strong> <span style="color: #00ff87;">${displaySize}</span></p>
|
||||||
<p style="margin-bottom: 8px;"><strong>${t('swal_duration')}</strong> <span style="color: #00ff87;">${localizedDuration}</span></p>
|
<p style="margin-bottom: 8px;"><strong>${t('swal_duration')}</strong> <span style="color: #00ff87;">${localizedDuration}</span></p>
|
||||||
<p style="margin-bottom: 8px;"><strong>${t('swal_format_quality')}</strong> <span style="color: #ff4da6; text-transform: uppercase;">${currentFormat} (${currentQuality === '320' || currentQuality === '1080' ? t('quality_best') : currentQuality})</span></p>
|
|
||||||
<p style="margin-top: 15px; font-size: 12px; opacity: 0.8; word-break: break-all;"><strong>${t('swal_output_dir')}</strong> <br>${result.stats.outputDir}</p>
|
<p style="margin-top: 15px; font-size: 12px; opacity: 0.8; word-break: break-all;"><strong>${t('swal_output_dir')}</strong> <br>${result.stats.outputDir}</p>
|
||||||
</div>
|
</div>
|
||||||
`,
|
`,
|
||||||
@@ -399,7 +683,6 @@ downloadBtn.addEventListener('click', async () => {
|
|||||||
if (swalResult.isConfirmed) {
|
if (swalResult.isConfirmed) {
|
||||||
window.api.openPath(result.stats.outputDir);
|
window.api.openPath(result.stats.outputDir);
|
||||||
} else {
|
} else {
|
||||||
// Go back to step 1
|
|
||||||
backBtn.click();
|
backBtn.click();
|
||||||
urlInput.value = '';
|
urlInput.value = '';
|
||||||
downloadProgressPanel.style.display = 'none';
|
downloadProgressPanel.style.display = 'none';
|
||||||
@@ -408,13 +691,6 @@ downloadBtn.addEventListener('click', async () => {
|
|||||||
} else {
|
} else {
|
||||||
showError(result.error);
|
showError(result.error);
|
||||||
downloadProgressPanel.style.display = 'none';
|
downloadProgressPanel.style.display = 'none';
|
||||||
if (statusBadge) {
|
|
||||||
statusBadge.textContent = t('badge_error');
|
|
||||||
statusBadge.className = 'status-badge ready';
|
|
||||||
statusBadge.style.borderColor = 'rgba(255, 77, 77, 0.3)';
|
|
||||||
statusBadge.style.color = '#ff4d4d';
|
|
||||||
statusBadge.style.background = 'rgba(255, 77, 77, 0.1)';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showError(err.message);
|
showError(err.message);
|
||||||
@@ -422,6 +698,10 @@ downloadBtn.addEventListener('click', async () => {
|
|||||||
} finally {
|
} finally {
|
||||||
downloadBtn.disabled = false;
|
downloadBtn.disabled = false;
|
||||||
backBtn.disabled = false;
|
backBtn.disabled = false;
|
||||||
|
document.querySelectorAll('.track-checkbox, .format-badge-btn, .quality-pill, .format-btn').forEach(el => {
|
||||||
|
el.disabled = false;
|
||||||
|
el.style.pointerEvents = 'auto';
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+141
@@ -899,3 +899,144 @@ html[dir="rtl"] .error-card {
|
|||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Playlist Header Controls */
|
||||||
|
.playlist-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 12px 18px;
|
||||||
|
background: rgba(255, 255, 255, 0.03);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.06);
|
||||||
|
border-radius: 12px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.playlist-select-all {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.playlist-bulk-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bulk-action-btn {
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 6px 12px;
|
||||||
|
color: #dcd7eb;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bulk-action-btn:hover {
|
||||||
|
background: rgba(255, 0, 128, 0.1);
|
||||||
|
border-color: rgba(255, 0, 128, 0.3);
|
||||||
|
color: #ff0080;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Premium Playlist Track Selection & Layout */
|
||||||
|
.track-checkbox-wrapper {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.track-checkbox {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
accent-color: #7000ff;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Make track item interactive and clickable */
|
||||||
|
.track-item {
|
||||||
|
cursor: pointer;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||||
|
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.track-item:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.04);
|
||||||
|
border-color: rgba(255, 255, 255, 0.12);
|
||||||
|
transform: translateX(2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Highlight active/focused track */
|
||||||
|
.track-item.focused {
|
||||||
|
background: rgba(112, 0, 255, 0.08);
|
||||||
|
border-color: rgba(112, 0, 255, 0.4);
|
||||||
|
box-shadow: 0 0 15px rgba(112, 0, 255, 0.15),
|
||||||
|
inset 0 1px 0 rgba(255, 255, 255, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom interactive format switcher badge inside row */
|
||||||
|
.track-format-selector {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-right: 6px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.format-badge-btn {
|
||||||
|
background: rgba(0, 0, 0, 0.3);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
color: #9e97b8;
|
||||||
|
font-size: 0.72rem;
|
||||||
|
font-weight: 700;
|
||||||
|
padding: 4px 10px;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
min-width: 50px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.format-badge-btn:hover {
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.format-badge-btn.mp3 {
|
||||||
|
background: rgba(255, 0, 128, 0.1);
|
||||||
|
color: #ff0080;
|
||||||
|
border-color: rgba(255, 0, 128, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.format-badge-btn.mp4 {
|
||||||
|
background: rgba(0, 223, 216, 0.1);
|
||||||
|
color: #00dfd8;
|
||||||
|
border-color: rgba(0, 223, 216, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Disabled states during downloading */
|
||||||
|
.track-item.disabled {
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
html[dir="rtl"] .playlist-select-all {
|
||||||
|
flex-direction: row-reverse;
|
||||||
|
}
|
||||||
|
|
||||||
|
html[dir="rtl"] .bulk-action-btn {
|
||||||
|
flex-direction: row-reverse;
|
||||||
|
}
|
||||||
|
|
||||||
|
html[dir="rtl"] .track-format-selector {
|
||||||
|
margin-right: 0;
|
||||||
|
margin-left: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+145
-19
@@ -168,11 +168,22 @@ async function checkDependencies(onStatusUpdate) {
|
|||||||
return true;
|
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.)
|
// Get video metadata (title, thumbnail, duration, etc.)
|
||||||
function getVideoInfo(url, browser) {
|
function getVideoInfo(url, browser) {
|
||||||
console.log(`[DEBUG] Fetching info for URL: ${url}`);
|
console.log(`[DEBUG] Fetching info for URL: ${url}`);
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
let args = ['--no-playlist', '-j'];
|
let args = ['-J', '--flat-playlist'];
|
||||||
if (browser) {
|
if (browser) {
|
||||||
args.push('--cookies-from-browser', 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}`);
|
console.log(`[DEBUG] yt-dlp info process exited with code ${code}`);
|
||||||
if (code !== 0) {
|
if (code !== 0) {
|
||||||
console.error(`[DEBUG] yt-dlp error output:`, stderr);
|
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;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const info = JSON.parse(stdout);
|
const info = JSON.parse(stdout);
|
||||||
console.log(`[DEBUG] Metadata fetched successfully for: "${info.title}"`);
|
|
||||||
resolve({
|
if (info._type === 'playlist' || info.entries) {
|
||||||
title: info.title,
|
console.log(`[DEBUG] Playlist metadata fetched successfully for: "${info.title}" (${info.entries.length} items)`);
|
||||||
thumbnail: info.thumbnail,
|
const entries = info.entries.map((entry, index) => {
|
||||||
duration: info.duration_string,
|
let thumb = '';
|
||||||
uploader: info.uploader
|
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) {
|
} catch (err) {
|
||||||
console.error(`[DEBUG] JSON parsing error on yt-dlp output:`, err);
|
console.error(`[DEBUG] JSON parsing error on yt-dlp output:`, err);
|
||||||
reject(err);
|
reject(err);
|
||||||
@@ -210,9 +259,8 @@ function getVideoInfo(url, browser) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Download video or convert to MP3
|
// Download single item helper
|
||||||
function downloadMedia(url, format, quality, outputDir, browser, onProgress) {
|
function downloadSingle(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) => {
|
return new Promise((resolve, reject) => {
|
||||||
let args = [];
|
let args = [];
|
||||||
const outputTemplate = path.join(outputDir, '%(title)s.%(ext)s');
|
const outputTemplate = path.join(outputDir, '%(title)s.%(ext)s');
|
||||||
@@ -220,7 +268,7 @@ function downloadMedia(url, format, quality, outputDir, browser, onProgress) {
|
|||||||
let totalSize = 'Bilinmiyor';
|
let totalSize = 'Bilinmiyor';
|
||||||
|
|
||||||
if (format === 'mp3') {
|
if (format === 'mp3') {
|
||||||
const q = quality ? `${quality}K` : '320K'; // '320K', '256K', '192K'
|
const q = quality ? `${quality}K` : '320K';
|
||||||
args = [
|
args = [
|
||||||
'--no-playlist',
|
'--no-playlist',
|
||||||
'--ffmpeg-location', FFMPEG_PATH,
|
'--ffmpeg-location', FFMPEG_PATH,
|
||||||
@@ -230,7 +278,7 @@ function downloadMedia(url, format, quality, outputDir, browser, onProgress) {
|
|||||||
'-o', outputTemplate
|
'-o', outputTemplate
|
||||||
];
|
];
|
||||||
} else {
|
} else {
|
||||||
const res = quality || '1080'; // '1080', '720', '480'
|
const res = quality || '1080';
|
||||||
args = [
|
args = [
|
||||||
'--no-playlist',
|
'--no-playlist',
|
||||||
'--ffmpeg-location', FFMPEG_PATH,
|
'--ffmpeg-location', FFMPEG_PATH,
|
||||||
@@ -244,13 +292,12 @@ function downloadMedia(url, format, quality, outputDir, browser, onProgress) {
|
|||||||
}
|
}
|
||||||
args.push(url);
|
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);
|
const proc = spawn(YTDLP_PATH, args);
|
||||||
|
|
||||||
proc.stdout.on('data', (data) => {
|
proc.stdout.on('data', (data) => {
|
||||||
const line = data.toString();
|
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]+)/);
|
const sizeMatch = line.match(/of\s+(\d+(?:\.\d+)?\s*[a-zA-Z]+)/);
|
||||||
if (sizeMatch) {
|
if (sizeMatch) {
|
||||||
totalSize = sizeMatch[1];
|
totalSize = sizeMatch[1];
|
||||||
@@ -261,7 +308,6 @@ function downloadMedia(url, format, quality, outputDir, browser, onProgress) {
|
|||||||
const percent = parseFloat(match[1]);
|
const percent = parseFloat(match[1]);
|
||||||
onProgress(percent, `İndiriliyor... %${percent}`);
|
onProgress(percent, `İndiriliyor... %${percent}`);
|
||||||
} else if (line.includes('[ExtractAudio]') || line.includes('[Merger]')) {
|
} 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...');
|
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) => {
|
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) {
|
if (code === 0) {
|
||||||
const durationSec = ((Date.now() - startTime) / 1000).toFixed(1);
|
const durationSec = ((Date.now() - startTime) / 1000).toFixed(1);
|
||||||
resolve({
|
resolve({
|
||||||
size: totalSize,
|
size: totalSize,
|
||||||
duration: `${durationSec} saniye`,
|
duration: `${durationSec} saniye`
|
||||||
outputDir: outputDir
|
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
reject(new Error(`Download exited with code ${code}`));
|
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 = {
|
module.exports = {
|
||||||
checkDependencies,
|
checkDependencies,
|
||||||
getVideoInfo,
|
getVideoInfo,
|
||||||
|
|||||||
Reference in New Issue
Block a user