// MusicRoom - Playlist module // Playlist CRUD, library rendering, playlist rendering (function() { const M = window.MusicRoom; // Render the current playlist M.renderPlaylist = function() { const container = M.$("#playlist"); container.innerHTML = ""; if (M.playlist.length === 0) { container.innerHTML = '
Playlist empty
'; return; } M.playlist.forEach((track, i) => { const div = document.createElement("div"); div.className = "track" + (i === M.currentIndex ? " active" : ""); const title = track.title?.trim() || (track.id || track.filename || "Unknown").replace(/\.[^.]+$/, ""); const trackId = track.id || track.filename; // Show remove button only for user playlists (not stream playlists) const removeBtn = M.selectedPlaylistId ? `×` : ""; div.innerHTML = `${title}${removeBtn}${M.fmt(track.duration)}`; div.querySelector(".track-title").onclick = async () => { if (M.synced && M.currentStreamId) { const res = await fetch("/api/streams/" + M.currentStreamId + "/jump", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ index: i }) }); if (res.status === 403) M.flashPermissionDenied(); } else { M.currentIndex = i; M.currentFilename = trackId; M.serverTrackDuration = track.duration; M.$("#track-title").textContent = title; M.loadingSegments.clear(); const cachedUrl = await M.loadTrackBlob(trackId); M.audio.src = cachedUrl || M.getTrackUrl(trackId); M.audio.currentTime = 0; M.localTimestamp = 0; M.audio.play(); M.renderPlaylist(); } }; const removeEl = div.querySelector(".btn-remove"); if (removeEl) { removeEl.onclick = (e) => { e.stopPropagation(); M.removeTrackFromCurrentPlaylist(i); }; } container.appendChild(div); }); }; // Render the library M.renderLibrary = function() { const container = M.$("#library"); container.innerHTML = ""; if (M.library.length === 0) { container.innerHTML = '
No tracks discovered
'; return; } const canAdd = M.selectedPlaylistId && M.selectedPlaylistId !== "all"; M.library.forEach((track) => { const div = document.createElement("div"); div.className = "track"; const title = track.title?.trim() || track.filename.replace(/\.[^.]+$/, ""); const addBtn = canAdd ? `+` : ""; div.innerHTML = `${title}${addBtn}${M.fmt(track.duration)}`; div.querySelector(".track-title").onclick = async () => { // Play directly from library (uses track ID) if (!M.synced) { M.currentFilename = track.id; M.serverTrackDuration = track.duration; M.$("#track-title").textContent = title; M.loadingSegments.clear(); const cachedUrl = await M.loadTrackBlob(track.id); M.audio.src = cachedUrl || M.getTrackUrl(track.id); M.audio.currentTime = 0; M.localTimestamp = 0; M.audio.play(); } }; const addBtnEl = div.querySelector(".btn-add"); if (addBtnEl) { addBtnEl.onclick = (e) => { e.stopPropagation(); M.addTrackToCurrentPlaylist(track.id); }; } container.appendChild(div); }); }; // Load library from server M.loadLibrary = async function() { try { const res = await fetch("/api/library"); M.library = await res.json(); M.renderLibrary(); } catch (e) { console.warn("Failed to load library"); } }; // Load user playlists from server M.loadPlaylists = async function() { try { const res = await fetch("/api/playlists"); M.userPlaylists = await res.json(); M.renderPlaylistSelector(); } catch (e) { console.warn("Failed to load playlists"); } }; // Render playlist selector sidebar M.renderPlaylistSelector = function() { const list = M.$("#playlists-list"); if (!list) return; list.innerHTML = ""; // Add "All Tracks" as default option const allItem = document.createElement("div"); allItem.className = "playlist-item" + (M.selectedPlaylistId === "all" ? " active" : ""); allItem.textContent = "All Tracks"; allItem.onclick = () => M.loadSelectedPlaylist("all"); list.appendChild(allItem); // Add user playlists for (const pl of M.userPlaylists) { const item = document.createElement("div"); item.className = "playlist-item" + (pl.id === M.selectedPlaylistId ? " active" : ""); item.textContent = pl.name; item.onclick = () => M.loadSelectedPlaylist(pl.id); list.appendChild(item); } // Update playlist panel title const titleEl = M.$("#playlist-title"); if (M.selectedPlaylistId === "all") { titleEl.textContent = "Playlist - All Tracks"; } else if (M.selectedPlaylistId) { const pl = M.userPlaylists.find(p => p.id === M.selectedPlaylistId); titleEl.textContent = pl ? "Playlist - " + pl.name : "Playlist"; } else { titleEl.textContent = "Playlist"; } }; // Load and display a specific playlist M.loadSelectedPlaylist = async function(playlistId) { if (!playlistId) { M.playlist = []; M.selectedPlaylistId = null; M.renderPlaylist(); M.renderPlaylistSelector(); M.renderLibrary(); return; } if (playlistId === "all") { // Use library as playlist M.playlist = [...M.library]; M.selectedPlaylistId = "all"; M.currentIndex = 0; M.renderPlaylist(); M.renderPlaylistSelector(); M.renderLibrary(); return; } try { const res = await fetch("/api/playlists/" + playlistId); if (!res.ok) throw new Error("Failed to load playlist"); const data = await res.json(); M.playlist = data.tracks || []; M.selectedPlaylistId = playlistId; M.currentIndex = 0; M.renderPlaylist(); M.renderPlaylistSelector(); M.renderLibrary(); } catch (e) { console.warn("Failed to load playlist:", e); } }; // Create a new playlist M.createNewPlaylist = async function() { const header = M.$("#playlists-panel .panel-header"); const btn = M.$("#btn-new-playlist"); // Already in edit mode? if (header.querySelector(".new-playlist-input")) return; // Hide button, show input btn.style.display = "none"; const input = document.createElement("input"); input.type = "text"; input.className = "new-playlist-input"; input.placeholder = "Playlist name..."; const submit = document.createElement("button"); submit.className = "btn-submit-playlist"; submit.textContent = "›"; header.appendChild(input); header.appendChild(submit); input.focus(); const cleanup = () => { input.remove(); submit.remove(); btn.style.display = ""; }; const doCreate = async () => { const name = input.value.trim(); if (!name) { cleanup(); return; } try { const res = await fetch("/api/playlists", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name, visibility: "private" }) }); if (!res.ok) throw new Error("Failed to create playlist"); const pl = await res.json(); await M.loadPlaylists(); M.selectedPlaylistId = pl.id; M.renderPlaylistSelector(); await M.loadSelectedPlaylist(pl.id); } catch (e) { alert("Failed to create playlist"); } cleanup(); }; submit.onclick = doCreate; input.onkeydown = (e) => { if (e.key === "Enter") doCreate(); if (e.key === "Escape") cleanup(); }; input.onblur = (e) => { // Delay to allow click on submit button setTimeout(() => { if (document.activeElement !== submit) cleanup(); }, 100); }; }; // Add a track to current playlist M.addTrackToCurrentPlaylist = async function(trackId) { if (!M.selectedPlaylistId || M.selectedPlaylistId === "all") { alert("Select or create a playlist first"); return; } try { const res = await fetch("/api/playlists/" + M.selectedPlaylistId + "/tracks", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ trackIds: [trackId] }) }); if (!res.ok) throw new Error("Failed to add track"); await M.loadSelectedPlaylist(M.selectedPlaylistId); } catch (e) { console.warn("Failed to add track:", e); } }; // Remove a track from current playlist M.removeTrackFromCurrentPlaylist = async function(position) { if (!M.selectedPlaylistId || M.selectedPlaylistId === "all") return; try { const res = await fetch("/api/playlists/" + M.selectedPlaylistId + "/tracks/" + position, { method: "DELETE" }); if (!res.ok) throw new Error("Failed to remove track"); await M.loadSelectedPlaylist(M.selectedPlaylistId); } catch (e) { console.warn("Failed to remove track:", e); } }; })();