palico-bot/public/scripts/jukebox.js
2026-06-28 22:44:04 +02:00

319 lines
10 KiB
JavaScript

import {
createApiClient,
createStatusManager,
escapeHtml,
getStoredApiKey,
getStoredDisplayName,
formatBytes,
formatDate,
setStoredApiKey,
setStoredDisplayName,
} from "./common.js";
const state = {
apiKey: getStoredApiKey(),
displayName: getStoredDisplayName(),
queue: [],
current: null,
library: [],
status: "idle",
};
const elements = {
status: document.getElementById("status"),
apiKeyInput: document.getElementById("apiKey"),
displayNameInput: document.getElementById("displayName"),
settingsForm: document.getElementById("settingsForm"),
uploadForm: document.getElementById("uploadForm"),
fileInput: document.getElementById("musicFile"),
notesInput: document.getElementById("uploadNotes"),
permanentCheckbox: document.getElementById("permanentFile"),
enqueueCheckbox: document.getElementById("enqueueFile"),
currentTrack: document.getElementById("currentTrack"),
queueList: document.getElementById("queueList"),
queueEmpty: document.getElementById("queueEmpty"),
libraryList: document.getElementById("libraryList"),
libraryEmpty: document.getElementById("libraryEmpty"),
refreshBtn: document.getElementById("refreshBtn"),
pauseResumeBtn: document.getElementById("pauseResumeBtn"),
playerButtons: document.querySelectorAll("[data-player-action]"),
heroUploadBtn: document.getElementById("heroUploadBtn"),
heroQueueBtn: document.getElementById("heroQueueBtn"),
uploadSection: document.getElementById("uploadSection"),
queueSection: document.getElementById("queueSection"),
};
let pollIntervalId = null;
const { setStatus, clearStatus } = createStatusManager(elements.status);
const apiFetch = createApiClient({
basePath: "/api",
getApiKey: () => state.apiKey,
});
async function refreshAll({ silent = false } = {}) {
if (!silent) {
clearStatus();
}
try {
const [queueData, libraryData] = await Promise.all([
apiFetch("/queue"),
apiFetch("/library"),
]);
state.current = queueData.current;
state.queue = queueData.upcoming || [];
state.library = libraryData.tracks || [];
state.status = queueData.status || "idle";
renderCurrentTrack();
renderQueue();
renderLibrary();
updatePauseResumeButton();
} catch (error) {
setStatus(`Erreur: ${error.message}`, "error", false);
}
}
function renderCurrentTrack() {
if (!elements.currentTrack) return;
if (!state.current) {
elements.currentTrack.classList.add("player-current--empty");
elements.currentTrack.innerHTML = "En attente de lecture...";
return;
}
elements.currentTrack.classList.remove("player-current--empty");
const track = state.current;
const permanence = track.permanent ? "Permanent" : "Temporaire";
elements.currentTrack.innerHTML = `
<div class="media-item__title">${escapeHtml(track.title || track.originalName)}</div>
<div class="media-item__meta">
${permanence}${formatBytes(track.size)} • Ajouté par ${escapeHtml(
track.uploader || "inconnu"
)}
</div>
<div class="media-item__meta">Commencé à ${formatDate(track.createdAt)}</div>
`;
}
function renderQueue() {
if (!elements.queueList) return;
elements.queueList.innerHTML = "";
if (!state.queue || state.queue.length === 0) {
elements.queueEmpty.style.display = "block";
return;
}
elements.queueEmpty.style.display = "none";
state.queue.forEach((track, index) => {
const li = document.createElement("li");
li.className = "media-item media-item--queue";
const permanence = track.permanent ? "Permanent" : "Temporaire";
li.innerHTML = `
<div class="media-item__info">
<div class="media-item__title">${escapeHtml(track.title || track.originalName)}</div>
<div class="media-item__meta">
${permanence}${formatBytes(track.size)} • Ajouté par ${escapeHtml(
track.uploader || "inconnu"
)}
</div>
</div>
<div class="media-item__actions">
<button class="btn btn--secondary" data-action="up" data-track="${track.id}">▲</button>
<button class="btn btn--secondary" data-action="down" data-track="${track.id}">▼</button>
<button class="btn btn--danger" data-action="remove" data-track="${track.id}">Supprimer</button>
</div>
`;
li.querySelectorAll("button").forEach((btn) => {
btn.addEventListener("click", (event) => {
const action = event.currentTarget.dataset.action;
const trackId = event.currentTarget.dataset.track;
handleQueueAction(action, trackId, index);
});
});
elements.queueList.appendChild(li);
});
}
function renderLibrary() {
if (!elements.libraryList) return;
elements.libraryList.innerHTML = "";
if (!state.library || state.library.length === 0) {
elements.libraryEmpty.style.display = "block";
return;
}
elements.libraryEmpty.style.display = "none";
state.library.forEach((track) => {
const li = document.createElement("li");
li.className = "media-item media-item--library";
li.innerHTML = `
<div class="media-item__info">
<div class="media-item__title">${escapeHtml(track.originalName)}</div>
<div class="media-item__meta">
${formatBytes(track.size)} • Ajouté le ${formatDate(track.createdAt)}
</div>
</div>
<div class="media-item__actions">
<button class="btn" data-library-action="enqueue" data-library-id="${track.id}">Ajouter</button>
<button class="btn btn--danger" data-library-action="delete" data-library-id="${track.id}">
Supprimer
</button>
</div>
`;
li.querySelectorAll("button").forEach((btn) => {
btn.addEventListener("click", (event) => {
const action = event.currentTarget.dataset.libraryAction;
const libraryId = event.currentTarget.dataset.libraryId;
handleLibraryAction(action, libraryId);
});
});
elements.libraryList.appendChild(li);
});
}
function updatePauseResumeButton() {
if (!elements.pauseResumeBtn) return;
const normalizedStatus =
typeof state.status === "string" ? state.status.toLowerCase() : "idle";
const isPaused = normalizedStatus === "paused";
const isPlaying = normalizedStatus === "playing";
if (isPaused) {
elements.pauseResumeBtn.textContent = "Reprendre";
elements.pauseResumeBtn.dataset.playerAction = "resume";
} else {
elements.pauseResumeBtn.textContent = "Pause";
elements.pauseResumeBtn.dataset.playerAction = "pause";
}
const actionable = isPaused || isPlaying;
elements.pauseResumeBtn.disabled = !actionable;
}
async function handleQueueAction(action, trackId, index) {
try {
if (action === "remove") {
await apiFetch(`/queue/${trackId}`, { method: "DELETE" });
} else if (action === "up" || action === "down") {
const delta = action === "up" ? -1 : 1;
const newPosition = Math.max(0, Math.min(index + delta, state.queue.length - 1));
if (newPosition === index) return;
await apiFetch(`/queue/${trackId}/move`, {
method: "PATCH",
body: { position: newPosition },
});
}
await refreshAll({ silent: true });
} catch (error) {
setStatus(`Impossible de mettre à jour la file: ${error.message}`, "error");
}
}
async function handleLibraryAction(action, libraryId) {
try {
if (action === "enqueue") {
await apiFetch(`/library/${libraryId}/enqueue`, {
method: "POST",
body: { requestedBy: state.displayName || "web-ui" },
});
} else if (action === "delete") {
if (!confirm("Supprimer ce fichier permanent ?")) {
return;
}
await apiFetch(`/library/${libraryId}`, { method: "DELETE" });
}
await refreshAll({ silent: true });
} catch (error) {
setStatus(`Action bibliothèque impossible: ${error.message}`, "error");
}
}
function startPolling() {
clearInterval(pollIntervalId);
pollIntervalId = setInterval(() => refreshAll({ silent: true }), 5000);
}
function bindEvents() {
elements.settingsForm?.addEventListener("submit", (event) => {
event.preventDefault();
state.apiKey = elements.apiKeyInput.value.trim();
state.displayName = elements.displayNameInput.value.trim();
setStoredApiKey(state.apiKey);
setStoredDisplayName(state.displayName);
setStatus("Paramètres sauvegardés", "success");
refreshAll({ silent: true });
});
elements.uploadForm?.addEventListener("submit", async (event) => {
event.preventDefault();
const file = elements.fileInput.files[0];
if (!file) {
setStatus("Merci de sélectionner un fichier audio", "error");
return;
}
try {
const data = new FormData();
data.append("file", file);
data.append("permanent", elements.permanentCheckbox.checked ? "true" : "false");
data.append("enqueue", elements.enqueueCheckbox.checked ? "true" : "false");
if (state.displayName) {
data.append("uploader", state.displayName);
}
if (elements.notesInput.value) {
data.append("notes", elements.notesInput.value.trim());
}
setStatus("Upload en cours...", "info", false);
await apiFetch("/uploads", { method: "POST", body: data });
elements.uploadForm.reset();
elements.enqueueCheckbox.checked = true;
setStatus("Fichier envoyé avec succès", "success");
await refreshAll({ silent: true });
} catch (error) {
setStatus(`Échec de l'upload: ${error.message}`, "error");
}
});
elements.refreshBtn?.addEventListener("click", () => refreshAll());
elements.heroUploadBtn?.addEventListener("click", () =>
elements.uploadSection?.scrollIntoView({ behavior: "smooth" })
);
elements.heroQueueBtn?.addEventListener("click", () =>
elements.queueSection?.scrollIntoView({ behavior: "smooth" })
);
elements.playerButtons?.forEach((button) => {
button.addEventListener("click", async () => {
const action = button.dataset.playerAction;
try {
await apiFetch(`/player/${action}`, { method: "POST" });
await refreshAll({ silent: true });
} catch (error) {
setStatus(`Action player impossible: ${error.message}`, "error");
}
});
});
}
function initialize() {
elements.apiKeyInput.value = state.apiKey;
elements.displayNameInput.value = state.displayName;
bindEvents();
updatePauseResumeButton();
refreshAll();
startPolling();
}
initialize();