diff --git a/webui/index.html b/webui/index.html index b40a19f6..1adc10bf 100644 --- a/webui/index.html +++ b/webui/index.html @@ -29,6 +29,10 @@ 📥 Search + - @@ -3986,6 +3989,57 @@ async function openDownloadMissingModal(playlistId) { hideLoadingOverlay(); } +function exportPlaylistAsM3U(playlistId) { + /** + * Export the tracks from the download missing tracks modal as an M3U playlist file + */ + console.log(`📋 Exporting playlist ${playlistId} as M3U`); + + // Get the process data + const process = activeDownloadProcesses[playlistId]; + if (!process || !process.tracks || process.tracks.length === 0) { + showToast('No tracks available to export', 'warning'); + return; + } + + const tracks = process.tracks; + const playlistName = process.playlistName || 'Playlist'; + + // Generate M3U8 content + let m3uContent = '#EXTM3U\n'; + m3uContent += `#PLAYLIST:${playlistName}\n\n`; + + tracks.forEach(track => { + // Get duration in seconds + const durationSeconds = track.duration_ms ? Math.floor(track.duration_ms / 1000) : -1; + + // Get artist names + const artists = Array.isArray(track.artists) ? track.artists.join(', ') : (track.artists || 'Unknown Artist'); + + // Add track info + m3uContent += `#EXTINF:${durationSeconds},${artists} - ${track.name}\n`; + + // Add a placeholder path (user will need to replace with actual file paths) + const sanitizedArtist = artists.replace(/[/\\?%*:|"<>]/g, '-'); + const sanitizedTrack = track.name.replace(/[/\\?%*:|"<>]/g, '-'); + m3uContent += `${sanitizedArtist} - ${sanitizedTrack}.mp3\n\n`; + }); + + // Create a Blob and download it + const blob = new Blob([m3uContent], { type: 'audio/x-mpegurl;charset=utf-8' }); + const url = URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + link.download = `${playlistName.replace(/[/\\?%*:|"<>]/g, '-')}.m3u8`; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + URL.revokeObjectURL(url); + + showToast(`Exported ${tracks.length} tracks as M3U playlist`, 'success'); + console.log(`✅ Exported ${tracks.length} tracks to ${link.download}`); +} + async function openDownloadMissingModalForYouTube(virtualPlaylistId, playlistName, spotifyTracks) { showLoadingOverlay('Loading YouTube playlist...'); // Check if a process is already active for this virtual playlist @@ -4147,6 +4201,9 @@ async function openDownloadMissingModalForYouTube(virtualPlaylistId, playlistNam @@ -17432,6 +17489,9 @@ async function openDownloadMissingModalForArtistAlbum(virtualPlaylistId, playlis diff --git a/webui/static/style.css b/webui/static/style.css index 79a4c615..9fb3e5db 100644 --- a/webui/static/style.css +++ b/webui/static/style.css @@ -8619,6 +8619,19 @@ body { transform: translateY(-1px); } +.download-control-btn.export { + background: linear-gradient(135deg, #667eea, #764ba2); + color: #ffffff; + border: 1px solid rgba(118, 75, 162, 0.3); + box-shadow: 0 4px 16px rgba(102, 126, 234, 0.25); +} + +.download-control-btn.export:hover { + background: linear-gradient(135deg, #7c8ff0, #8a5ab8); + transform: translateY(-1px); + box-shadow: 0 6px 20px rgba(102, 126, 234, 0.35); +} + .download-control-btn:disabled { background: #333333; color: #666666; @@ -8714,6 +8727,7 @@ body { .modal-close-section { display: flex; align-items: center; + gap: 12px; }