blastoise-archive/public/playlist.js

291 lines
9.5 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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 = '<div class="empty">Playlist empty</div>';
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 ? `<span class="btn-remove" title="Remove">×</span>` : "";
div.innerHTML = `<span class="track-title">${title}</span><span class="track-actions">${removeBtn}<span class="duration">${M.fmt(track.duration)}</span></span>`;
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 = '<div class="empty">No tracks discovered</div>';
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 ? `<span class="btn-add" title="Add to playlist">+</span>` : "";
div.innerHTML = `<span class="track-title">${title}</span><span class="track-actions">${addBtn}<span class="duration">${M.fmt(track.duration)}</span></span>`;
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);
}
};
})();