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:
@@ -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
|
Reference in New Issue
Block a user