From 5a16d8ad53852349f33fa918fffb84c8c00beaa4 Mon Sep 17 00:00:00 2001 From: BoulderBadgeDad Date: Sat, 13 Jun 2026 08:20:18 -0700 Subject: [PATCH] =?UTF-8?q?Server=20playlists:=20'Export=20M3U'=20button?= =?UTF-8?q?=20in=20the=20compare/editor=20toolbar=20=E2=80=94=20exports=20?= =?UTF-8?q?the=20server=20playlist's=20tracks=20via=20the=20shared=20M3U?= =?UTF-8?q?=20writer=20(Music=20Assistant=20etc.)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- webui/index.html | 1 + webui/static/pages-extra.js | 52 +++++++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+) diff --git a/webui/index.html b/webui/index.html index b353f10f..5da85124 100644 --- a/webui/index.html +++ b/webui/index.html @@ -2089,6 +2089,7 @@ +
diff --git a/webui/static/pages-extra.js b/webui/static/pages-extra.js index ecb246ae..4294dff4 100644 --- a/webui/static/pages-extra.js +++ b/webui/static/pages-extra.js @@ -1648,6 +1648,58 @@ function _serverEditorRefresh() { _openServerCompareView(_serverEditorState.playlistId, _serverEditorState.playlistName, _serverEditorState.mirroredPlaylist); } +/** + * Export the currently-open server playlist as an M3U file. Takes the tracks + * physically present ON the server (matched + extra) and reuses the shared M3U + * writer, which resolves each to its real library file path (+ the configured + * entry_base_path prefix) so media servers like Music Assistant can read it. + * force:true bypasses the auto-save "m3u_export.enabled" gate — this is a manual + * on-demand export. + */ +async function exportServerPlaylistM3U() { + const st = _serverEditorState; + const btn = document.getElementById('server-editor-export-btn'); + const tracks = (st && Array.isArray(st.tracks) ? st.tracks : []) + .filter(t => t.server_track) + .map(t => ({ + name: t.server_track.title, + artist: t.server_track.artist || '', + duration_ms: t.server_track.duration || 0, + })); + if (!tracks.length) { + showToast('No server tracks to export', 'warning'); + return; + } + const orig = btn ? btn.textContent : ''; + if (btn) { btn.disabled = true; btn.textContent = '⏳ Exporting…'; } + try { + const res = await fetch('/api/generate-playlist-m3u', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + playlist_name: st.playlistName || 'Playlist', + tracks, + context_type: 'playlist', + save_to_disk: true, + force: true, + }), + }); + const data = await res.json(); + if (!res.ok || data.success === false) { + throw new Error(data.error || 'Export failed'); + } + // `found` = server tracks resolved to a real library file path; any not in + // SoulSync's library are skipped (can't write a path for them). + const found = data.stats && data.stats.found != null ? data.stats.found : tracks.length; + const note = found < tracks.length ? ` (${found}/${tracks.length} in library)` : ` (${found} tracks)`; + showToast(`Exported M3U: ${st.playlistName}${note}`, 'success'); + } catch (e) { + showToast(`M3U export failed: ${e.message}`, 'error'); + } finally { + if (btn) { btn.disabled = false; btn.textContent = orig || '📋 Export M3U'; } + } +} + function serverEditorBack() { const container = document.getElementById('server-playlist-container'); const editor = document.getElementById('server-editor');