221 lines
9.2 KiB
HTML
221 lines
9.2 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="es">
|
|
<head>
|
|
<meta charset="utf-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
<title>Musify · Consola de pruebas</title>
|
|
<style>
|
|
body { font-family: system-ui, sans-serif; max-width: 920px; margin: 1.5rem auto; padding: 0 1rem; color: #eee; background: #121212; }
|
|
h1 { font-size: 1.3rem; } h2 { font-size: 1rem; color: #1db954; margin: 0 0 .6rem; }
|
|
fieldset { border: 1px solid #333; border-radius: 8px; margin: 0 0 1rem; padding: .8rem 1rem 1rem; }
|
|
legend { padding: 0 .4rem; color: #aaa; font-size: .85rem; }
|
|
label { display: block; margin: .5rem 0 .15rem; font-size: .8rem; color: #aaa; }
|
|
input, textarea, select { width: 100%; box-sizing: border-box; padding: .45rem; background: #1e1e1e; color: #eee; border: 1px solid #333; border-radius: 6px; font-family: monospace; font-size: .82rem; }
|
|
textarea { resize: vertical; }
|
|
select[multiple], select[size] { height: auto; }
|
|
button { margin: .5rem .4rem 0 0; padding: .45rem .9rem; background: #1db954; color: #000; border: 0; border-radius: 16px; font-weight: 600; cursor: pointer; font-size: .82rem; }
|
|
button.sec { background: #333; color: #eee; }
|
|
button.danger { background: #c0392b; color: #fff; }
|
|
.row { display: flex; gap: 1rem; } .row > * { flex: 1; }
|
|
video { width: 100%; margin-top: .6rem; background: #000; border-radius: 8px; }
|
|
#log { margin-top: .5rem; padding: .6rem; background: #000; border-radius: 6px; font-family: monospace; font-size: .76rem; white-space: pre-wrap; max-height: 220px; overflow: auto; border: 1px solid #333; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<h1>Musify · Consola de pruebas</h1>
|
|
|
|
<fieldset>
|
|
<legend>Conexión</legend>
|
|
<div class="row">
|
|
<div><label>API base</label><input id="api" value="http://localhost:5111" /></div>
|
|
</div>
|
|
<label>Access token (Bearer JWT de Zitadel)</label>
|
|
<textarea id="token" rows="3" placeholder="eyJ..."></textarea>
|
|
</fieldset>
|
|
|
|
<fieldset>
|
|
<legend>Playlists</legend>
|
|
<h2>Crear</h2>
|
|
<div class="row">
|
|
<div><label>Nombre</label><input id="plName" placeholder="Mi lista" /></div>
|
|
<div><label>Descripción</label><input id="plDesc" placeholder="descripción" /></div>
|
|
</div>
|
|
<button onclick="createPlaylist()">Crear playlist</button>
|
|
|
|
<h2 style="margin-top:1rem">Mis playlists</h2>
|
|
<button class="sec" onclick="refreshPlaylists()">Refrescar mis playlists</button>
|
|
<select id="plList" size="6"></select>
|
|
<h2 style="margin-top:1rem">Actualizar / eliminar (la seleccionada arriba)</h2>
|
|
<div class="row">
|
|
<div><label>Nuevo nombre</label><input id="plNewName" placeholder="(dejar vacío = no cambiar)" /></div>
|
|
<div><label>Nueva descripción</label><input id="plNewDesc" placeholder="(dejar vacío = no cambiar)" /></div>
|
|
</div>
|
|
<button onclick="updatePlaylist()">Actualizar</button>
|
|
<button class="danger" onclick="deletePlaylist()">Eliminar playlist</button>
|
|
|
|
<h2 style="margin-top:1rem">Canciones de la playlist seleccionada</h2>
|
|
<button class="sec" onclick="refreshPlaylistTracks()">Ver canciones de la playlist</button>
|
|
<select id="plTracks" size="6"></select>
|
|
<div>
|
|
<button onclick="playPlaylistTrack()">▶ Reproducir seleccionada</button>
|
|
<button class="danger" onclick="removeFromPlaylistView()">Quitar de esta playlist</button>
|
|
</div>
|
|
</fieldset>
|
|
|
|
<fieldset>
|
|
<legend>Canciones</legend>
|
|
<button class="sec" onclick="refreshTracks()">Refrescar canciones</button>
|
|
<select id="trackList" size="6"></select>
|
|
<div>
|
|
<button onclick="playSelected()">▶ Reproducir seleccionada</button>
|
|
<button onclick="addTrack()">Añadir a la playlist seleccionada</button>
|
|
<button class="danger" onclick="removeTrack()">Quitar de la playlist seleccionada</button>
|
|
</div>
|
|
<video id="video" controls></video>
|
|
</fieldset>
|
|
|
|
<div id="log">Listo.</div>
|
|
|
|
<script>
|
|
const $ = (id) => document.getElementById(id);
|
|
const apiBase = () => $("api").value.replace(/\/+$/, "");
|
|
const token = () => $("token").value.trim();
|
|
|
|
function log(msg) {
|
|
const el = $("log");
|
|
el.textContent = "[" + new Date().toLocaleTimeString() + "] " + msg + "\n" + el.textContent;
|
|
}
|
|
|
|
function tokenSub() {
|
|
try {
|
|
let p = token().split(".")[1].replace(/-/g, "+").replace(/_/g, "/");
|
|
p += "=".repeat((4 - p.length % 4) % 4);
|
|
return JSON.parse(atob(p)).sub;
|
|
} catch { return null; }
|
|
}
|
|
|
|
async function api(method, path, body) {
|
|
const opt = { method, headers: {} };
|
|
if (token()) opt.headers.Authorization = "Bearer " + token();
|
|
if (body !== undefined) { opt.headers["Content-Type"] = "application/json"; opt.body = JSON.stringify(body); }
|
|
let res;
|
|
try {
|
|
res = await fetch(apiBase() + path, opt);
|
|
} catch (err) {
|
|
log(method + " " + path + " -> ERROR DE RED: " + err.message + " (¿WebApi y CORS arriba?)");
|
|
return { ok: false, status: 0, data: null };
|
|
}
|
|
const text = await res.text();
|
|
let data = null;
|
|
try { data = text ? JSON.parse(text) : null; } catch { data = text; }
|
|
log(method + " " + path + " -> " + res.status + (res.ok ? "" : " " + (typeof data === "string" ? data : JSON.stringify(data))));
|
|
return { ok: res.ok, status: res.status, data };
|
|
}
|
|
|
|
function selectedPlaylist() { return $("plList").value || null; }
|
|
function selectedTrack() { return $("trackList").value || null; }
|
|
|
|
async function refreshPlaylists() {
|
|
const sub = tokenSub();
|
|
if (!sub) { log("Token inválido: no puedo leer el sub."); return; }
|
|
const r = await api("GET", "/playLists/users/" + sub + "?pageSize=100");
|
|
if (!r.ok) return;
|
|
const items = (r.data && r.data.items) || [];
|
|
$("plList").innerHTML = items.map(p => `<option value="${p.id}">${p.name} — ${p.description || ""} [${p.id}]</option>`).join("");
|
|
log("Playlists: " + items.length);
|
|
}
|
|
|
|
async function refreshTracks() {
|
|
const r = await api("GET", "/tracks?pageSize=100");
|
|
if (!r.ok) return;
|
|
const items = (r.data && r.data.items) || [];
|
|
$("trackList").innerHTML = items.map(t => `<option value="${t.id}">${t.title} [${t.id}]</option>`).join("");
|
|
log("Canciones: " + items.length);
|
|
}
|
|
|
|
async function createPlaylist() {
|
|
const r = await api("POST", "/playLists", { name: $("plName").value, description: $("plDesc").value, pictureIntentId: null });
|
|
if (r.ok) { $("plName").value = ""; $("plDesc").value = ""; refreshPlaylists(); }
|
|
}
|
|
|
|
async function updatePlaylist() {
|
|
const id = selectedPlaylist();
|
|
if (!id) { log("Selecciona una playlist."); return; }
|
|
const body = {
|
|
newName: $("plNewName").value || null,
|
|
newDescription: $("plNewDesc").value || null,
|
|
newPictureIntentId: null
|
|
};
|
|
const r = await api("PUT", "/playLists/" + id, body);
|
|
if (r.ok) { $("plNewName").value = ""; $("plNewDesc").value = ""; refreshPlaylists(); }
|
|
}
|
|
|
|
async function deletePlaylist() {
|
|
const id = selectedPlaylist();
|
|
if (!id) { log("Selecciona una playlist."); return; }
|
|
const r = await api("DELETE", "/playLists/" + id);
|
|
if (r.ok) refreshPlaylists();
|
|
}
|
|
|
|
async function addTrack() {
|
|
const pl = selectedPlaylist(), tr = selectedTrack();
|
|
if (!pl || !tr) { log("Selecciona playlist y canción."); return; }
|
|
await api("POST", "/playLists/" + pl + "/tracks/" + tr);
|
|
}
|
|
|
|
async function removeTrack() {
|
|
const pl = selectedPlaylist(), tr = selectedTrack();
|
|
if (!pl || !tr) { log("Selecciona playlist y canción."); return; }
|
|
await api("DELETE", "/playLists/" + pl + "/tracks/" + tr);
|
|
}
|
|
|
|
function selectedPlaylistTrack() { return $("plTracks").value || null; }
|
|
|
|
async function refreshPlaylistTracks() {
|
|
const pl = selectedPlaylist();
|
|
if (!pl) { log("Selecciona una playlist."); return; }
|
|
const r = await api("GET", "/playLists/" + pl + "/tracks?pageSize=100");
|
|
if (!r.ok) { $("plTracks").innerHTML = ""; return; }
|
|
const items = (r.data && r.data.items) || [];
|
|
$("plTracks").innerHTML = items.map(t => `<option value="${t.id}">${t.title} [${t.id}]</option>`).join("");
|
|
log("Canciones en la playlist: " + items.length);
|
|
}
|
|
|
|
async function removeFromPlaylistView() {
|
|
const pl = selectedPlaylist(), tr = selectedPlaylistTrack();
|
|
if (!pl || !tr) { log("Selecciona una playlist y una canción de la lista."); return; }
|
|
const r = await api("DELETE", "/playLists/" + pl + "/tracks/" + tr);
|
|
if (r.ok) refreshPlaylistTracks();
|
|
}
|
|
|
|
function playPlaylistTrack() {
|
|
const tr = selectedPlaylistTrack();
|
|
if (!tr) { log("Selecciona una canción de la playlist."); return; }
|
|
play(tr);
|
|
}
|
|
|
|
function playSelected() {
|
|
const tr = selectedTrack();
|
|
if (!tr) { log("Selecciona una canción."); return; }
|
|
play(tr);
|
|
}
|
|
|
|
async function play(trackId) {
|
|
const r = await api("GET", "/tracks/" + trackId + "/stream");
|
|
if (!r.ok) return;
|
|
const { manifestUrl, ticket } = r.data;
|
|
const sep = manifestUrl.indexOf("?") === -1 ? "?" : "&";
|
|
const el = $("video");
|
|
el.onerror = () => log("audio error: " + (el.error ? el.error.message || el.error.code : "desconocido"));
|
|
el.src = manifestUrl + sep + "t=" + encodeURIComponent(ticket);
|
|
el.play().catch((e) => log("play error: " + e.message));
|
|
log("Reproduciendo " + trackId);
|
|
}
|
|
|
|
document.addEventListener("DOMContentLoaded", () => {
|
|
$("plList").addEventListener("change", refreshPlaylistTracks);
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|