diff --git a/main.js b/main.js
index b5952d1..cc82cf1 100644
--- a/main.js
+++ b/main.js
@@ -80,11 +80,11 @@ ipcMain.handle('get-video-info', async (event, { url, browser }) => {
});
// IPC Handler: Download Media
-ipcMain.handle('download-media', async (event, { url, format, quality, browser }) => {
+ipcMain.handle('download-media', async (event, { items, browser }) => {
try {
- const stats = await downloader.downloadMedia(url, format, quality, selectedOutputDir, browser, (percent, status) => {
+ const stats = await downloader.downloadMedia(items, selectedOutputDir, browser, (progressData) => {
if (mainWindow) {
- mainWindow.webContents.send('download-progress', { percent, status });
+ mainWindow.webContents.send('download-progress', progressData);
}
});
return { success: true, stats };
diff --git a/preload.js b/preload.js
index a4496a1..290ad38 100644
--- a/preload.js
+++ b/preload.js
@@ -4,7 +4,7 @@ contextBridge.exposeInMainWorld('api', {
checkDependencies: () => ipcRenderer.invoke('check-dependencies'),
onDependencyStatus: (callback) => ipcRenderer.on('dependency-status', (_, data) => callback(data)),
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)),
selectDirectory: () => ipcRenderer.invoke('select-directory'),
getDownloadsFolder: () => ipcRenderer.invoke('get-downloads-folder'),
diff --git a/src/index.html b/src/index.html
index 8abc80c..74e6699 100644
--- a/src/index.html
+++ b/src/index.html
@@ -135,6 +135,23 @@
+
+
diff --git a/src/locales/en.json b/src/locales/en.json
index 5f62865..8b3e182 100644
--- a/src/locales/en.json
+++ b/src/locales/en.json
@@ -55,5 +55,11 @@
"quality_best": "Highest",
"quality_standard": "Standard",
"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:"
}
diff --git a/src/locales/tr.json b/src/locales/tr.json
index 3687401..c34dd41 100644
--- a/src/locales/tr.json
+++ b/src/locales/tr.json
@@ -55,5 +55,11 @@
"quality_best": "En Yüksek",
"quality_standard": "Standart",
"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:"
}
diff --git a/src/renderer.js b/src/renderer.js
index 8f1240c..172741c 100644
--- a/src/renderer.js
+++ b/src/renderer.js
@@ -39,6 +39,15 @@ let currentFormat = 'mp3'; // 'mp3' or 'mp4'
let currentQuality = '320'; // Selected quality value
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
let currentLang = 'tr';
let translations = {};
@@ -134,6 +143,41 @@ window.addEventListener('DOMContentLoaded', async () => {
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
const savedLang = localStorage.getItem('vibe_lang') || 'tr';
languageSelect.value = savedLang;
@@ -195,6 +239,7 @@ formatMp3Btn.addEventListener('click', () => {
formatMp4Btn.classList.remove('active');
currentQuality = '320'; // Reset to default best quality for MP3
renderQualityPills();
+ applyBottomSettingsToSelected();
});
formatMp4Btn.addEventListener('click', () => {
@@ -204,6 +249,7 @@ formatMp4Btn.addEventListener('click', () => {
formatMp3Btn.classList.remove('active');
currentQuality = '1080'; // Reset to default best quality for MP4
renderQualityPills();
+ applyBottomSettingsToSelected();
});
// 4. Quality Pills Renderer
@@ -225,12 +271,50 @@ function renderQualityPills() {
document.querySelectorAll('.quality-pill').forEach(p => p.classList.remove('active'));
pill.classList.add('active');
currentQuality = opt.value;
+ applyBottomSettingsToSelected();
});
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)
backBtn.addEventListener('click', () => {
// Reset any error card
@@ -239,6 +323,8 @@ backBtn.addEventListener('click', () => {
step2.classList.remove('active');
step1.classList.add('active');
currentVideoData = null;
+ playlistTracks = [];
+ focusedTrackId = null;
urlInput.focus();
});
@@ -263,6 +349,8 @@ analyzeBtn.addEventListener('click', async () => {
if (result.success) {
analyzedUrl = url;
currentVideoData = result.info;
+ playlistTracks = [];
+ focusedTrackId = null;
// Render the fetched video details inside the scrollable list container
renderTrackList(result.info);
@@ -284,30 +372,174 @@ analyzeBtn.addEventListener('click', async () => {
// 7. Render Track List Function
function renderTrackList(info) {
- trackList.innerHTML = `
-
-
-
-

-
-
-
${info.title}
-
${info.uploader}
-
${t('duration_label')} ${info.duration || t('duration_unknown')}
-
-
- ${t('badge_ready')}
-
-
- `;
+ trackList.innerHTML = '';
+
+ 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 = `
+
+

+ `;
+ itemEl.appendChild(thumbEl);
+
+ const detailsEl = document.createElement('div');
+ detailsEl.className = 'track-details';
+ detailsEl.innerHTML = `
+
${track.title}
+
${track.uploader}
+
${t('duration_label')} ${track.duration || t('duration_unknown')}
+ `;
+ 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
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
downloadProgressPanel.style.display = 'block';
@@ -316,59 +548,112 @@ downloadBtn.addEventListener('click', async () => {
downloadStatus.textContent = t('status_connecting');
downloadPercent.textContent = '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
- const statusBadge = document.getElementById('track-status-badge');
- if (statusBadge) {
- statusBadge.textContent = t('badge_downloading');
- statusBadge.className = 'status-badge downloading';
- }
+ // Disable UI interactive components during download
+ document.querySelectorAll('.track-checkbox, .format-badge-btn, .quality-pill, .format-btn').forEach(el => {
+ el.disabled = true;
+ el.style.pointerEvents = 'none';
+ });
+
+ // 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
- 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;
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')) {
stat = t('status_processing');
}
- downloadStatus.textContent = stat;
+ downloadStatus.textContent = `[${index + 1}/${total}] ${title}`;
downloadPercent.textContent = `${percent}%`;
downloadProgressBar.style.width = `${percent}%`;
-
- if (percent > 90) {
- downloadSubtext.textContent = currentFormat === 'mp3'
- ? t('subtext_mp3_convert')
- : t('subtext_mp4_merge');
- } else {
- downloadSubtext.textContent = t('subtext_speed_note');
- }
+ downloadSubtext.textContent = `${t('swal_file_size')}: ${stat}`;
});
try {
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) {
downloadStatus.textContent = t('status_completed');
downloadPercent.textContent = '100%';
downloadProgressBar.style.width = '100%';
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
downloadProgressBar.style.background = 'linear-gradient(90deg, #00ff87 0%, #60efff 100%)';
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
.replace('saniye', t('sec_suffix'))
.replace('dakika', t('min_suffix'));
@@ -378,11 +663,10 @@ downloadBtn.addEventListener('click', async () => {
title: t('swal_success_title'),
html: `
-
${t('swal_file_name')} ${currentVideoData.title}
-
${t('swal_platform_uploader')} ${currentVideoData.uploader || t('duration_unknown')}
-
${t('swal_file_size')} ${result.stats.size}
+
${isPlaylistMode ? t('playlist_title_label') || 'Çalma Listesi:' : t('swal_file_name')} ${displayTitle}
+
${t('swal_platform_uploader')} ${displayUploader}
+
${isPlaylistMode ? t('downloaded_files_label') || 'İndirilen Dosyalar:' : t('swal_file_size')} ${displaySize}
${t('swal_duration')} ${localizedDuration}
-
${t('swal_format_quality')} ${currentFormat} (${currentQuality === '320' || currentQuality === '1080' ? t('quality_best') : currentQuality})
${t('swal_output_dir')}
${result.stats.outputDir}
`,
@@ -399,7 +683,6 @@ downloadBtn.addEventListener('click', async () => {
if (swalResult.isConfirmed) {
window.api.openPath(result.stats.outputDir);
} else {
- // Go back to step 1
backBtn.click();
urlInput.value = '';
downloadProgressPanel.style.display = 'none';
@@ -408,13 +691,6 @@ downloadBtn.addEventListener('click', async () => {
} else {
showError(result.error);
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) {
showError(err.message);
@@ -422,6 +698,10 @@ downloadBtn.addEventListener('click', async () => {
} finally {
downloadBtn.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';
+ });
}
});
diff --git a/src/style.css b/src/style.css
index d625a82..62f8d73 100644
--- a/src/style.css
+++ b/src/style.css
@@ -899,3 +899,144 @@ html[dir="rtl"] .error-card {
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;
+}
+
+
diff --git a/utils/downloader.js b/utils/downloader.js
index 6463355..f72c495 100644
--- a/utils/downloader.js
+++ b/utils/downloader.js
@@ -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,