From 48cdf912bce7920c3fae95b6b46077717c9e17ec Mon Sep 17 00:00:00 2001 From: peterino2 Date: Thu, 5 Feb 2026 23:29:27 -0800 Subject: [PATCH] updated agents and created the ability to rename --- AGENTS.md | 18 +++++++++++- db.ts | 4 +++ public/channelSync.js | 65 ++++++++++++++++++++++++++++++++++++++++++- public/index.html | 5 +++- public/styles.css | 10 +++++++ public/ui.js | 4 +++ routes/channels.ts | 33 ++++++++++++++++++++++ routes/index.ts | 6 ++++ 8 files changed, 142 insertions(+), 3 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 12704be..a80943c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -110,10 +110,26 @@ GET /api/library → List all tracks with id, filename, title, dura ## Files ### Server -- **server.ts** — Bun entrypoint. HTTP routes and WebSocket handlers. +- **server.ts** — Bun entrypoint. Imports and starts the server. +- **config.ts** — Config types, loading, and exports (port, musicDir, etc). +- **state.ts** — Shared application state (channels Map, userConnections Map, library). +- **init.ts** — Server initialization, channel loading, tick interval, library events. +- **broadcast.ts** — Broadcast utilities (broadcastToAll, sendToUser, broadcastChannelList). +- **websocket.ts** — WebSocket open/close/message handlers. - **channel.ts** — `Channel` class. Queue, current index, time tracking, broadcasting. - **library.ts** — `Library` class. Scans music directory, computes content hashes. - **db.ts** — SQLite database for users, sessions, tracks. +- **auth.ts** — Auth helpers (getUser, requirePermission, session cookies). +- **ytdlp.ts** — yt-dlp integration for fetching audio from URLs. + +### Routes (routes/) +- **index.ts** — Main router, dispatches to route handlers. +- **helpers.ts** — Shared route helpers (getOrCreateUser, userHasPermission). +- **auth.ts** — Auth endpoints (signup, login, logout, me, admin). +- **channels.ts** — Channel CRUD and control (list, create, delete, jump, seek, queue, mode). +- **tracks.ts** — Library listing, file upload, audio serving with range support. +- **fetch.ts** — yt-dlp fetch endpoints (check URL, confirm playlist, queue status). +- **static.ts** — Static file serving (index.html, styles.css, JS files). ### Client (public/) - **core.js** — Global state namespace (`window.MusicRoom`) diff --git a/db.ts b/db.ts index 775addc..ab8eb8d 100644 --- a/db.ts +++ b/db.ts @@ -384,6 +384,10 @@ export function deleteChannelFromDb(id: string): void { db.query("DELETE FROM channels WHERE id = ?").run(id); } +export function updateChannelName(id: string, name: string): void { + db.query("UPDATE channels SET name = ? WHERE id = ?").run(name, id); +} + // Queue persistence functions export function saveChannelQueue(channelId: string, trackIds: string[]): void { db.query("BEGIN").run(); diff --git a/public/channelSync.js b/public/channelSync.js index 3d9db5b..a4b955a 100644 --- a/public/channelSync.js +++ b/public/channelSync.js @@ -158,16 +158,60 @@ (M.currentUser.isAdmin || ch.createdBy === M.currentUser.id); const deleteBtn = canDelete ? `` : ""; + // Show rename button for signed-in non-guests + const canRename = M.currentUser && !M.currentUser.isGuest; + const renameBtn = canRename ? `` : ""; + div.innerHTML = `
${ch.name} + + ${renameBtn} ${deleteBtn} ${ch.listenerCount}
${listenersHtml}
`; const headerEl = div.querySelector(".channel-header"); - headerEl.querySelector(".channel-name").onclick = () => M.switchChannel(ch.id); + const nameSpan = headerEl.querySelector(".channel-name"); + const nameInput = headerEl.querySelector(".channel-name-input"); + nameSpan.onclick = () => M.switchChannel(ch.id); + + const renBtn = headerEl.querySelector(".btn-rename-channel"); + if (renBtn) { + renBtn.onclick = (e) => { + e.stopPropagation(); + nameSpan.style.display = "none"; + renBtn.style.display = "none"; + nameInput.style.display = "inline-block"; + nameInput.focus(); + nameInput.select(); + }; + } + + // Handle inline rename + const submitRename = async () => { + const newName = nameInput.value.trim(); + if (newName && newName !== ch.name) { + await M.renameChannel(ch.id, newName); + } else { + // Restore original display + nameSpan.style.display = ""; + if (renBtn) renBtn.style.display = ""; + nameInput.style.display = "none"; + } + }; + nameInput.onblur = submitRename; + nameInput.onkeydown = (e) => { + if (e.key === "Enter") { + e.preventDefault(); + nameInput.blur(); + } else if (e.key === "Escape") { + nameInput.value = ch.name; + nameInput.blur(); + } + }; + const delBtn = headerEl.querySelector(".btn-delete-channel"); if (delBtn) { delBtn.onclick = (e) => { @@ -411,4 +455,23 @@ } M.updateUI(); }; + + // Rename channel + M.renameChannel = async function(channelId, newName) { + try { + const res = await fetch(`/api/channels/${channelId}`, { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ name: newName.trim() }) + }); + const data = await res.json(); + if (!res.ok) { + M.showToast(data.error || "Failed to rename channel", "error"); + return; + } + M.showToast(`Channel renamed to "${data.name}"`); + } catch (e) { + M.showToast("Failed to rename channel", "error"); + } + }; })(); diff --git a/public/index.html b/public/index.html index 4b274d9..aafff6f 100644 --- a/public/index.html +++ b/public/index.html @@ -8,7 +8,10 @@
-

Blastoise

+

Sign in to continue

diff --git a/public/styles.css b/public/styles.css index 5a0262f..b3b0a3e 100644 --- a/public/styles.css +++ b/public/styles.css @@ -15,8 +15,14 @@ h3 { font-size: 0.8rem; color: #666; margin-bottom: 0.3rem; text-transform: uppe #auth-section .admin-badge { background: #c4f; color: #111; padding: 0.1rem 0.3rem; border-radius: 3px; font-size: 0.65rem; } #btn-logout { background: none; border: none; color: #e44; font-size: 0.65rem; cursor: pointer; padding: 0; opacity: 0.7; } #btn-logout:hover { opacity: 1; text-decoration: underline; } +#btn-logout.guest-signin { background: #4e8; color: #111; font-size: 0.85rem; font-weight: 600; padding: 0.4rem 1rem; border-radius: 4px; opacity: 1; } +#btn-logout.guest-signin:hover { background: #5fa; text-decoration: none; } #btn-kick-others { background: none; border: none; color: #ea4; font-size: 0.65rem; cursor: pointer; padding: 0; opacity: 0.7; } #btn-kick-others:hover { opacity: 1; text-decoration: underline; } +#site-header { display: flex; align-items: baseline; gap: 1rem; margin-bottom: 0.5rem; } +#site-header h1 { margin: 0; } +#btn-report-bug { color: #4e8; font-size: 0.8rem; text-decoration: none; padding: 0.3rem 0.6rem; border: 1px solid #4e8; border-radius: 4px; } +#btn-report-bug:hover { background: #4e8; color: #111; } #stream-select select { background: #222; color: #eee; border: 1px solid #333; padding: 0.3rem 0.6rem; border-radius: 4px; font-size: 0.85rem; } /* Main content - library and queue */ @@ -28,10 +34,14 @@ h3 { font-size: 0.8rem; color: #666; margin-bottom: 0.3rem; text-transform: uppe #channels-list .channel-header { display: flex; justify-content: space-between; align-items: center; cursor: pointer; padding: 0.1rem 0; border-radius: 3px; } #channels-list .channel-header:hover { background: #222; } #channels-list .channel-name { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 0.8rem; } +#channels-list .channel-name-input { flex: 1; font-size: 0.8rem; background: #111; color: #eee; border: 1px solid #4e8; border-radius: 3px; padding: 0.1rem 0.3rem; outline: none; min-width: 0; } #channels-list .listener-count { font-size: 0.65rem; color: #666; flex-shrink: 0; margin-left: 0.3rem; } #channels-list .btn-delete-channel { background: none; border: none; color: #666; font-size: 0.9rem; cursor: pointer; padding: 0 0.2rem; line-height: 1; opacity: 0; transition: opacity 0.15s; } #channels-list .channel-header:hover .btn-delete-channel { opacity: 1; } #channels-list .btn-delete-channel:hover { color: #e44; } +#channels-list .btn-rename-channel { background: none; border: none; color: #666; font-size: 0.7rem; cursor: pointer; padding: 0 0.2rem; line-height: 1; opacity: 0; transition: opacity 0.15s; } +#channels-list .channel-header:hover .btn-rename-channel { opacity: 1; } +#channels-list .btn-rename-channel:hover { color: #4e8; } #channels-list .channel-listeners { display: flex; flex-direction: column; margin-left: 0.5rem; border-left: 1px solid #333; padding-left: 0.3rem; } #channels-list .listener { font-size: 0.65rem; color: #aaa; padding: 0.05rem 0; position: relative; } #channels-list .listener::before { content: ""; position: absolute; left: -0.3rem; top: 50%; width: 0.2rem; height: 1px; background: #333; } diff --git a/public/ui.js b/public/ui.js index 26587cd..be05807 100644 --- a/public/ui.js +++ b/public/ui.js @@ -40,11 +40,15 @@ if (M.currentUser.isGuest) { M.$("#current-username").textContent = "Guest"; M.$("#btn-logout").textContent = "Sign In"; + M.$("#btn-logout").classList.add("guest-signin"); } else { M.$("#current-username").textContent = M.currentUser.username; M.$("#btn-logout").textContent = "Logout"; + M.$("#btn-logout").classList.remove("guest-signin"); } M.$("#admin-badge").style.display = M.currentUser.isAdmin ? "inline" : "none"; + // Re-render channel list to update rename/delete buttons + if (M.renderChannelList) M.renderChannelList(); } else { M.$("#login-panel").classList.remove("hidden"); M.$("#player-content").classList.remove("visible"); diff --git a/routes/channels.ts b/routes/channels.ts index 2bac563..29c29d2 100644 --- a/routes/channels.ts +++ b/routes/channels.ts @@ -4,6 +4,7 @@ import { deleteChannelFromDb, saveChannelQueue, updateChannelState, + updateChannelName, } from "../db"; import { state } from "../state"; import { broadcastChannelList } from "../broadcast"; @@ -130,6 +131,38 @@ export function handleDeleteChannel(req: Request, server: any, channelId: string return Response.json({ success: true }); } +// PATCH /api/channels/:id - rename channel +export async function handleRenameChannel(req: Request, server: any, channelId: string): Promise { + const { user } = getOrCreateUser(req, server); + if (!user) { + return Response.json({ error: "Authentication required" }, { status: 401 }); + } + if (user.is_guest) { + return Response.json({ error: "Guests cannot rename channels" }, { status: 403 }); + } + const channel = state.channels.get(channelId); + if (!channel) { + return Response.json({ error: "Channel not found" }, { status: 404 }); + } + try { + const { name } = await req.json(); + if (!name || typeof name !== "string" || name.trim().length === 0) { + return Response.json({ error: "Name is required" }, { status: 400 }); + } + if (name.trim().length > 64) { + return Response.json({ error: "Name must be 64 characters or less" }, { status: 400 }); + } + const newName = name.trim(); + channel.name = newName; + updateChannelName(channelId, newName); + console.log(`[Channel] Renamed "${channelId}" to "${newName}" by user ${user.id}`); + broadcastChannelList(); + return Response.json({ success: true, name: newName }); + } catch { + return Response.json({ error: "Invalid request" }, { status: 400 }); + } +} + // GET /api/channels/:id - get channel state export function handleGetChannel(channelId: string): Response { const channel = state.channels.get(channelId); diff --git a/routes/index.ts b/routes/index.ts index eba25f7..9f9d33e 100644 --- a/routes/index.ts +++ b/routes/index.ts @@ -25,6 +25,7 @@ import { handleSeek, handleModifyQueue, handleSetMode, + handleRenameChannel, } from "./channels"; // Track routes @@ -85,6 +86,11 @@ export function createRouter() { return handleDeleteChannel(req, server, channelDeleteMatch[1]); } + const channelPatchMatch = path.match(/^\/api\/channels\/([^/]+)$/); + if (channelPatchMatch && req.method === "PATCH") { + return handleRenameChannel(req, server, channelPatchMatch[1]); + } + const channelGetMatch = path.match(/^\/api\/channels\/([^/]+)$/); if (channelGetMatch && req.method === "GET") { return handleGetChannel(channelGetMatch[1]);