// 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);
}
};
})();