Add song library with Git LFS, DJ chat, and tinqs/live-radio publish path.

Songs persist under songs/ (MP3 via LFS, metadata in git). Player shows saved library.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-07 14:18:17 +01:00
parent 4924db5617
commit b8ff25f370
18 changed files with 357 additions and 12 deletions
+90
View File
@@ -205,6 +205,44 @@
min-width: 72px;
padding: 0.6rem 1rem;
}
.library {
margin-top: 1.25rem;
border-top: 1px solid #2a2a3a;
padding-top: 1rem;
}
.library h2 {
font-size: 0.8rem;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--muted);
margin-bottom: 0.5rem;
}
.library-list {
display: flex;
flex-direction: column;
gap: 0.35rem;
max-height: 140px;
overflow-y: auto;
}
.song-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.5rem;
padding: 0.5rem 0.65rem;
background: #1a1a28;
border: 1px solid #2a2a3a;
border-radius: 10px;
cursor: pointer;
font-size: 0.82rem;
text-align: left;
color: var(--text);
width: 100%;
}
.song-row:hover { border-color: var(--accent); }
.song-row.playing { border-color: var(--accent); background: rgba(255,107,53,0.08); }
.song-meta { color: var(--muted); font-size: 0.72rem; }
.library-empty { color: var(--muted); font-size: 0.8rem; }
</style>
</head>
<body>
@@ -232,6 +270,13 @@
<div class="status" id="status">Connecting…</div>
<section class="library">
<h2>Saved songs</h2>
<div class="library-list" id="libraryList">
<div class="library-empty">Loading library…</div>
</div>
</section>
<section class="chat">
<h2>Talk to the DJ</h2>
<div class="chat-log" id="chatLog"></div>
@@ -261,6 +306,48 @@
const chatForm = document.getElementById('chatForm');
const chatInput = document.getElementById('chatInput');
const chatSend = document.getElementById('chatSend');
const libraryList = document.getElementById('libraryList');
let currentTrackId = null;
async function loadLibrary() {
try {
const res = await fetch(`${API}/api/songs`);
const data = await res.json();
libraryList.innerHTML = '';
if (!data.songs || data.songs.length === 0) {
libraryList.innerHTML = '<div class="library-empty">No saved songs yet — generate one!</div>';
return;
}
for (const song of data.songs) {
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'song-row' + (song.id === currentTrackId ? ' playing' : '');
const sizeMb = song.size_bytes ? (song.size_bytes / 1e6).toFixed(1) + ' MB' : '';
btn.innerHTML = `<span>${song.title}</span><span class="song-meta">${sizeMb}</span>`;
btn.addEventListener('click', () => playSong(song.id));
libraryList.appendChild(btn);
}
} catch (_) {
libraryList.innerHTML = '<div class="library-empty">Library offline</div>';
}
}
async function playSong(trackId) {
try {
const res = await fetch(`${API}/api/songs/${trackId}/play`, { method: 'POST' });
const data = await res.json();
if (data.status === 'ok' && data.track) {
currentTrackId = data.track.track_id;
titleEl.textContent = data.track.title;
moodEl.textContent = data.track.mood ? `Mood: ${data.track.mood}` : '';
djEl.textContent = data.track.dj_line ? `"${data.track.dj_line}"` : '';
player.src = `${API}${data.track.audio_url}`;
player.play().catch(() => {});
statusEl.textContent = `Playing · ${data.track.track_id}`;
loadLibrary();
}
} catch (_) {}
}
function addBubble(role, text) {
const el = document.createElement('div');
@@ -329,6 +416,7 @@
return;
}
const t = data.track;
currentTrackId = t.track_id;
titleEl.textContent = t.title;
moodEl.textContent = t.mood ? `Mood: ${t.mood}` : '';
djEl.textContent = t.dj_line ? `"${t.dj_line}"` : '';
@@ -352,6 +440,7 @@
statusEl.textContent = data.message;
} else if (data.status === 'ok') {
await refreshNow();
await loadLibrary();
} else {
statusEl.textContent = 'Generation failed — check server logs';
}
@@ -385,6 +474,7 @@
});
refreshNow();
loadLibrary();
loadChat();
setInterval(refreshNow, 15000);
</script>
Before
After