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 = ` -
-
- - - - - Thumbnail -
-
-

${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 = ` + + + + + Thumbnail + `; + 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,