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;
}