saving
This commit is contained in:
parent
629deaab3f
commit
12856b439c
|
|
@ -154,6 +154,7 @@ interface Track {
|
||||||
playlist: Track[],
|
playlist: Track[],
|
||||||
currentIndex: number,
|
currentIndex: number,
|
||||||
listenerCount: number,
|
listenerCount: number,
|
||||||
|
listeners: string[], // usernames of connected users
|
||||||
isDefault: boolean
|
isDefault: boolean
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
|
||||||
16
channel.ts
16
channel.ts
|
|
@ -16,7 +16,7 @@ export interface ChannelConfig {
|
||||||
isDefault?: boolean;
|
isDefault?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type WsData = { channelId: string; userId: number | null };
|
export type WsData = { channelId: string; userId: number | null; username: string };
|
||||||
|
|
||||||
export class Channel {
|
export class Channel {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
@ -131,6 +131,15 @@ export class Channel {
|
||||||
this.playlistDirty = true;
|
this.playlistDirty = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setPlaylist(tracks: Track[]) {
|
||||||
|
this.playlist = tracks;
|
||||||
|
this.currentIndex = 0;
|
||||||
|
this.startedAt = Date.now();
|
||||||
|
this.pausedAt = 0;
|
||||||
|
this.playlistDirty = true;
|
||||||
|
this.broadcast();
|
||||||
|
}
|
||||||
|
|
||||||
broadcast() {
|
broadcast() {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const includePlaylist = this.playlistDirty || (now - this.lastPlaylistBroadcast >= 60000);
|
const includePlaylist = this.playlistDirty || (now - this.lastPlaylistBroadcast >= 60000);
|
||||||
|
|
@ -140,9 +149,6 @@ export class Channel {
|
||||||
}
|
}
|
||||||
const msg = JSON.stringify(this.getState(includePlaylist));
|
const msg = JSON.stringify(this.getState(includePlaylist));
|
||||||
|
|
||||||
const clientIds = Array.from(this.clients).map(ws => ws.data?.sessionId ?? 'unknown');
|
|
||||||
console.log(`[Channel] "${this.name}" broadcasting to ${this.clients.size} clients: [${clientIds.join(', ')}]`);
|
|
||||||
|
|
||||||
for (const ws of this.clients) {
|
for (const ws of this.clients) {
|
||||||
ws.send(msg);
|
ws.send(msg);
|
||||||
}
|
}
|
||||||
|
|
@ -164,12 +170,14 @@ export class Channel {
|
||||||
}
|
}
|
||||||
|
|
||||||
getListInfo() {
|
getListInfo() {
|
||||||
|
const listeners = Array.from(this.clients).map(ws => ws.data?.username ?? 'Unknown');
|
||||||
return {
|
return {
|
||||||
id: this.id,
|
id: this.id,
|
||||||
name: this.name,
|
name: this.name,
|
||||||
description: this.description,
|
description: this.description,
|
||||||
trackCount: this.playlist.length,
|
trackCount: this.playlist.length,
|
||||||
listenerCount: this.clients.size,
|
listenerCount: this.clients.size,
|
||||||
|
listeners,
|
||||||
isDefault: this.isDefault,
|
isDefault: this.isDefault,
|
||||||
createdBy: this.createdBy,
|
createdBy: this.createdBy,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
BIN
musicroom.db
BIN
musicroom.db
Binary file not shown.
|
|
@ -4,7 +4,7 @@
|
||||||
(function() {
|
(function() {
|
||||||
const M = window.MusicRoom;
|
const M = window.MusicRoom;
|
||||||
|
|
||||||
// Load available channels and connect to first one
|
// Load available channels and connect to saved or default
|
||||||
M.loadChannels = async function() {
|
M.loadChannels = async function() {
|
||||||
try {
|
try {
|
||||||
const res = await fetch("/api/channels");
|
const res = await fetch("/api/channels");
|
||||||
|
|
@ -15,9 +15,11 @@
|
||||||
}
|
}
|
||||||
M.channels = channels;
|
M.channels = channels;
|
||||||
M.renderChannelList();
|
M.renderChannelList();
|
||||||
// Connect to first (default) channel
|
// Try saved channel first, fall back to default
|
||||||
const defaultChannel = channels.find(c => c.isDefault) || channels[0];
|
const savedChannelId = localStorage.getItem("musicroom_channel");
|
||||||
M.connectChannel(defaultChannel.id);
|
const savedChannel = savedChannelId && channels.find(c => c.id === savedChannelId);
|
||||||
|
const targetChannel = savedChannel || channels.find(c => c.isDefault) || channels[0];
|
||||||
|
M.connectChannel(targetChannel.id);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
M.$("#track-title").textContent = "Server unavailable";
|
M.$("#track-title").textContent = "Server unavailable";
|
||||||
M.$("#status").textContent = "Local (offline)";
|
M.$("#status").textContent = "Local (offline)";
|
||||||
|
|
@ -48,16 +50,65 @@
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// New channel creation with slideout input
|
||||||
|
M.createNewChannel = async function() {
|
||||||
|
const header = M.$("#channels-panel .panel-header");
|
||||||
|
const btn = M.$("#btn-new-channel");
|
||||||
|
|
||||||
|
// Already in edit mode?
|
||||||
|
if (header.querySelector(".new-channel-input")) return;
|
||||||
|
|
||||||
|
// Hide button, show input
|
||||||
|
btn.style.display = "none";
|
||||||
|
|
||||||
|
const input = document.createElement("input");
|
||||||
|
input.type = "text";
|
||||||
|
input.className = "new-channel-input";
|
||||||
|
input.placeholder = "Channel name...";
|
||||||
|
|
||||||
|
const submit = document.createElement("button");
|
||||||
|
submit.className = "btn-submit-channel";
|
||||||
|
submit.textContent = "›";
|
||||||
|
|
||||||
|
header.appendChild(input);
|
||||||
|
header.appendChild(submit);
|
||||||
|
input.focus();
|
||||||
|
|
||||||
|
const cleanup = () => {
|
||||||
|
input.remove();
|
||||||
|
submit.remove();
|
||||||
|
btn.style.display = "";
|
||||||
|
};
|
||||||
|
|
||||||
|
const doCreate = async () => {
|
||||||
|
const name = input.value.trim();
|
||||||
|
if (!name) {
|
||||||
|
cleanup();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await M.createChannel(name);
|
||||||
|
} catch (e) {
|
||||||
|
M.showToast("Failed to create channel");
|
||||||
|
}
|
||||||
|
cleanup();
|
||||||
|
};
|
||||||
|
|
||||||
|
submit.onclick = doCreate;
|
||||||
|
input.onkeydown = (e) => {
|
||||||
|
if (e.key === "Enter") doCreate();
|
||||||
|
if (e.key === "Escape") cleanup();
|
||||||
|
};
|
||||||
|
input.onblur = (e) => {
|
||||||
|
if (e.relatedTarget !== submit) cleanup();
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
// New channel button handler
|
// New channel button handler
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
const btn = M.$("#btn-new-channel");
|
const btn = M.$("#btn-new-channel");
|
||||||
if (btn) {
|
if (btn) {
|
||||||
btn.onclick = () => {
|
btn.onclick = () => M.createNewChannel();
|
||||||
const name = prompt("Channel name:");
|
|
||||||
if (name && name.trim()) {
|
|
||||||
M.createChannel(name.trim());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -69,11 +120,23 @@
|
||||||
for (const ch of M.channels || []) {
|
for (const ch of M.channels || []) {
|
||||||
const div = document.createElement("div");
|
const div = document.createElement("div");
|
||||||
div.className = "channel-item" + (ch.id === M.currentChannelId ? " active" : "");
|
div.className = "channel-item" + (ch.id === M.currentChannelId ? " active" : "");
|
||||||
|
const listeners = ch.listeners || [];
|
||||||
|
// Count occurrences of each user
|
||||||
|
const counts = {};
|
||||||
|
for (const name of listeners) {
|
||||||
|
counts[name] = (counts[name] || 0) + 1;
|
||||||
|
}
|
||||||
|
const listenersHtml = Object.entries(counts).map(([name, count]) =>
|
||||||
|
`<div class="listener">${name}${count > 1 ? ` <span class="listener-mult">x${count}</span>` : ""}</div>`
|
||||||
|
).join("");
|
||||||
div.innerHTML = `
|
div.innerHTML = `
|
||||||
|
<div class="channel-header">
|
||||||
<span class="channel-name">${ch.name}</span>
|
<span class="channel-name">${ch.name}</span>
|
||||||
<span class="listener-count">${ch.listenerCount} 👤</span>
|
<span class="listener-count">${ch.listenerCount}</span>
|
||||||
|
</div>
|
||||||
|
<div class="channel-listeners">${listenersHtml}</div>
|
||||||
`;
|
`;
|
||||||
div.onclick = () => M.switchChannel(ch.id);
|
div.querySelector(".channel-header").onclick = () => M.switchChannel(ch.id);
|
||||||
container.appendChild(div);
|
container.appendChild(div);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -95,6 +158,7 @@
|
||||||
oldWs.close();
|
oldWs.close();
|
||||||
}
|
}
|
||||||
M.currentChannelId = id;
|
M.currentChannelId = id;
|
||||||
|
localStorage.setItem("musicroom_channel", id);
|
||||||
const proto = location.protocol === "https:" ? "wss:" : "ws:";
|
const proto = location.protocol === "https:" ? "wss:" : "ws:";
|
||||||
M.ws = new WebSocket(proto + "//" + location.host + "/api/channels/" + id + "/ws");
|
M.ws = new WebSocket(proto + "//" + location.host + "/api/channels/" + id + "/ws");
|
||||||
|
|
||||||
|
|
@ -234,10 +298,18 @@
|
||||||
M.audio.currentTime = data.currentTimestamp;
|
M.audio.currentTime = data.currentTimestamp;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (!wasServerPaused && M.serverPaused) {
|
} else {
|
||||||
// Server just paused
|
// Server is paused - ensure we're paused too
|
||||||
|
if (!M.audio.paused) {
|
||||||
M.audio.pause();
|
M.audio.pause();
|
||||||
}
|
}
|
||||||
|
// Sync to paused position
|
||||||
|
if (isNewTrack || !M.audio.src) {
|
||||||
|
const cachedUrl = await M.loadTrackBlob(M.currentTrackId);
|
||||||
|
M.audio.src = cachedUrl || M.getTrackUrl(M.currentTrackId);
|
||||||
|
M.audio.currentTime = data.currentTimestamp;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
M.updateUI();
|
M.updateUI();
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -275,6 +275,30 @@
|
||||||
M.renderLibrary();
|
M.renderLibrary();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If synced to a channel, broadcast playlist change to server
|
||||||
|
if (M.synced && M.currentChannelId) {
|
||||||
|
try {
|
||||||
|
const res = await fetch("/api/channels/" + M.currentChannelId + "/playlist", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ playlistId: playlistId === "all" ? "all" : parseInt(playlistId) })
|
||||||
|
});
|
||||||
|
if (res.status === 403) {
|
||||||
|
M.flashPermissionDenied();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!res.ok) throw new Error("Failed to set playlist");
|
||||||
|
M.selectedPlaylistId = playlistId;
|
||||||
|
M.renderPlaylistSelector();
|
||||||
|
// Server will broadcast new playlist via WebSocket
|
||||||
|
return;
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("Failed to set channel playlist:", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Local mode - load playlist directly
|
||||||
if (playlistId === "all") {
|
if (playlistId === "all") {
|
||||||
// Use library as playlist
|
// Use library as playlist
|
||||||
M.playlist = [...M.library];
|
M.playlist = [...M.library];
|
||||||
|
|
|
||||||
|
|
@ -19,11 +19,16 @@ h3 { font-size: 0.9rem; color: #666; margin-bottom: 0.5rem; text-transform: uppe
|
||||||
#main-content { display: flex; gap: 1rem; flex: 1; min-height: 0; margin-bottom: 1rem; }
|
#main-content { display: flex; gap: 1rem; flex: 1; min-height: 0; margin-bottom: 1rem; }
|
||||||
#channels-panel { flex: 0 0 180px; background: #1a1a1a; border-radius: 8px; padding: 1rem; display: flex; flex-direction: column; min-height: 300px; max-height: 60vh; }
|
#channels-panel { flex: 0 0 180px; background: #1a1a1a; border-radius: 8px; padding: 1rem; display: flex; flex-direction: column; min-height: 300px; max-height: 60vh; }
|
||||||
#channels-list { flex: 1; overflow-y: auto; }
|
#channels-list { flex: 1; overflow-y: auto; }
|
||||||
#channels-list .channel-item { padding: 0.5rem 0.75rem; border-radius: 4px; cursor: pointer; font-size: 0.9rem; display: flex; justify-content: space-between; align-items: center; }
|
#channels-list .channel-item { padding: 0.5rem 0.75rem; border-radius: 4px; font-size: 0.9rem; display: flex; flex-direction: column; gap: 0.25rem; }
|
||||||
#channels-list .channel-item:hover { background: #222; }
|
|
||||||
#channels-list .channel-item.active { background: #2a4a3a; color: #4e8; }
|
#channels-list .channel-item.active { background: #2a4a3a; color: #4e8; }
|
||||||
|
#channels-list .channel-header { display: flex; justify-content: space-between; align-items: center; cursor: pointer; padding: 0.25rem 0; border-radius: 4px; }
|
||||||
|
#channels-list .channel-header:hover { background: #222; }
|
||||||
#channels-list .channel-name { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
#channels-list .channel-name { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
#channels-list .listener-count { font-size: 0.75rem; color: #666; flex-shrink: 0; margin-left: 0.5rem; }
|
#channels-list .listener-count { font-size: 0.75rem; color: #666; flex-shrink: 0; margin-left: 0.5rem; }
|
||||||
|
#channels-list .channel-listeners { display: flex; flex-direction: column; margin-left: 1rem; border-left: 2px solid #333; padding-left: 0.5rem; }
|
||||||
|
#channels-list .listener { font-size: 0.75rem; color: #aaa; padding: 0.15rem 0; position: relative; }
|
||||||
|
#channels-list .listener::before { content: ""; position: absolute; left: -0.5rem; top: 50%; width: 0.4rem; height: 2px; background: #333; }
|
||||||
|
#channels-list .listener-mult { color: #666; font-size: 0.65rem; }
|
||||||
#library-panel, #playlist-panel { flex: 2; background: #1a1a1a; border-radius: 8px; padding: 1rem; display: flex; flex-direction: column; min-height: 300px; max-height: 60vh; }
|
#library-panel, #playlist-panel { flex: 2; background: #1a1a1a; border-radius: 8px; padding: 1rem; display: flex; flex-direction: column; min-height: 300px; max-height: 60vh; }
|
||||||
#playlists-panel { flex: 1; background: #1a1a1a; border-radius: 8px; padding: 1rem; display: flex; flex-direction: column; min-height: 300px; max-height: 60vh; min-width: 180px; max-width: 250px; }
|
#playlists-panel { flex: 1; background: #1a1a1a; border-radius: 8px; padding: 1rem; display: flex; flex-direction: column; min-height: 300px; max-height: 60vh; min-width: 180px; max-width: 250px; }
|
||||||
#playlists-list { flex: 1; overflow-y: auto; }
|
#playlists-list { flex: 1; overflow-y: auto; }
|
||||||
|
|
@ -36,9 +41,9 @@ h3 { font-size: 0.9rem; color: #666; margin-bottom: 0.5rem; text-transform: uppe
|
||||||
.panel-header select { flex: 1; background: #222; color: #eee; border: 1px solid #333; padding: 0.3rem 0.5rem; border-radius: 4px; font-size: 0.8rem; }
|
.panel-header select { flex: 1; background: #222; color: #eee; border: 1px solid #333; padding: 0.3rem 0.5rem; border-radius: 4px; font-size: 0.8rem; }
|
||||||
.panel-header button { background: #333; color: #eee; border: 1px solid #444; width: 28px; height: 28px; border-radius: 4px; cursor: pointer; font-size: 1rem; line-height: 1; padding: 0; }
|
.panel-header button { background: #333; color: #eee; border: 1px solid #444; width: 28px; height: 28px; border-radius: 4px; cursor: pointer; font-size: 1rem; line-height: 1; padding: 0; }
|
||||||
.panel-header button:hover { background: #444; }
|
.panel-header button:hover { background: #444; }
|
||||||
.new-playlist-input { flex: 1; background: #222; color: #eee; border: 1px solid #333; padding: 0.3rem 0.5rem; border-radius: 4px; font-size: 0.8rem; }
|
.new-playlist-input, .new-channel-input { flex: 1; background: #222; color: #eee; border: 1px solid #333; padding: 0.3rem 0.5rem; border-radius: 4px; font-size: 0.8rem; }
|
||||||
.btn-submit-playlist { background: #2a4a3a; color: #4e8; border: 1px solid #3a5a4a; width: 28px; height: 28px; border-radius: 4px; cursor: pointer; font-size: 1.2rem; line-height: 1; padding: 0; }
|
.btn-submit-playlist, .btn-submit-channel { background: #2a4a3a; color: #4e8; border: 1px solid #3a5a4a; width: 28px; height: 28px; border-radius: 4px; cursor: pointer; font-size: 1.2rem; line-height: 1; padding: 0; }
|
||||||
.btn-submit-playlist:hover { background: #3a5a4a; }
|
.btn-submit-playlist:hover, .btn-submit-channel:hover { background: #3a5a4a; }
|
||||||
#library, #playlist { flex: 1; overflow-y: auto; }
|
#library, #playlist { flex: 1; overflow-y: auto; }
|
||||||
#library .track, #playlist .track { padding: 0.5rem 0.75rem; border-radius: 4px; cursor: pointer; font-size: 0.9rem; display: flex; justify-content: space-between; align-items: center; position: relative; }
|
#library .track, #playlist .track { padding: 0.5rem 0.75rem; border-radius: 4px; cursor: pointer; font-size: 0.9rem; display: flex; justify-content: space-between; align-items: center; position: relative; }
|
||||||
#library .track:hover, #playlist .track:hover { background: #222; }
|
#library .track:hover, #playlist .track:hover { background: #222; }
|
||||||
|
|
|
||||||
|
|
@ -48,8 +48,8 @@
|
||||||
if (!M.currentUser) return false;
|
if (!M.currentUser) return false;
|
||||||
if (M.currentUser.isAdmin) return true;
|
if (M.currentUser.isAdmin) return true;
|
||||||
return M.currentUser.permissions?.some(p =>
|
return M.currentUser.permissions?.some(p =>
|
||||||
p.resource_type === "stream" &&
|
p.resource_type === "channel" &&
|
||||||
(p.resource_id === M.currentStreamId || p.resource_id === null) &&
|
(p.resource_id === M.currentChannelId || p.resource_id === null) &&
|
||||||
p.permission === "control"
|
p.permission === "control"
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
47
server.ts
47
server.ts
|
|
@ -125,7 +125,7 @@ function broadcastToAll(message: object) {
|
||||||
// Broadcast channel list to all clients
|
// Broadcast channel list to all clients
|
||||||
function broadcastChannelList() {
|
function broadcastChannelList() {
|
||||||
const list = [...channels.values()].map(c => c.getListInfo());
|
const list = [...channels.values()].map(c => c.getListInfo());
|
||||||
console.log(`[Broadcast] Sending channel_list to all clients (${list.length} channels)`);
|
console.log(`[Broadcast] Sending channel_list to all clients (${list.length} channels)`, JSON.stringify(list.map(c => ({ id: c.id, listeners: c.listeners }))));
|
||||||
broadcastToAll({ type: "channel_list", channels: list });
|
broadcastToAll({ type: "channel_list", channels: list });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -174,8 +174,6 @@ setInterval(() => {
|
||||||
}
|
}
|
||||||
}, 1000);
|
}, 1000);
|
||||||
|
|
||||||
type WsData = { streamId: string; userId: number | null };
|
|
||||||
|
|
||||||
// Helper to get or create guest session
|
// Helper to get or create guest session
|
||||||
function getOrCreateUser(req: Request, server: any): { user: ReturnType<typeof getUser>, headers?: Headers } {
|
function getOrCreateUser(req: Request, server: any): { user: ReturnType<typeof getUser>, headers?: Headers } {
|
||||||
let user = getUser(req, server);
|
let user = getUser(req, server);
|
||||||
|
|
@ -221,7 +219,7 @@ serve({
|
||||||
const id = path.split("/")[3];
|
const id = path.split("/")[3];
|
||||||
if (!channels.has(id)) return new Response("Channel not found", { status: 404 });
|
if (!channels.has(id)) return new Response("Channel not found", { status: 404 });
|
||||||
const { user } = getOrCreateUser(req, server);
|
const { user } = getOrCreateUser(req, server);
|
||||||
const ok = server.upgrade(req, { data: { channelId: id, userId: user?.id ?? null } });
|
const ok = server.upgrade(req, { data: { channelId: id, userId: user?.id ?? null, username: user?.username ?? 'Guest' } });
|
||||||
if (ok) return undefined;
|
if (ok) return undefined;
|
||||||
return new Response("WebSocket upgrade failed", { status: 500 });
|
return new Response("WebSocket upgrade failed", { status: 500 });
|
||||||
}
|
}
|
||||||
|
|
@ -698,6 +696,35 @@ serve({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// API: set channel playlist
|
||||||
|
const playlistMatch = path.match(/^\/api\/channels\/([^/]+)\/playlist$/);
|
||||||
|
if (playlistMatch && req.method === "POST") {
|
||||||
|
const channelId = playlistMatch[1];
|
||||||
|
const { user } = getOrCreateUser(req, server);
|
||||||
|
if (!userHasPermission(user, "channel", channelId, "control")) {
|
||||||
|
return new Response("Forbidden", { status: 403 });
|
||||||
|
}
|
||||||
|
const channel = channels.get(channelId);
|
||||||
|
if (!channel) return new Response("Not found", { status: 404 });
|
||||||
|
try {
|
||||||
|
const body = await req.json();
|
||||||
|
let tracks: Track[] = [];
|
||||||
|
if (body.playlistId === "all") {
|
||||||
|
tracks = library.getAllTracks();
|
||||||
|
} else if (typeof body.playlistId === "number") {
|
||||||
|
const playlist = getPlaylist(body.playlistId);
|
||||||
|
if (!playlist) return new Response("Playlist not found", { status: 404 });
|
||||||
|
tracks = getPlaylistTracks(body.playlistId);
|
||||||
|
} else {
|
||||||
|
return new Response("Invalid playlistId", { status: 400 });
|
||||||
|
}
|
||||||
|
channel.setPlaylist(tracks);
|
||||||
|
return Response.json({ success: true, trackCount: tracks.length });
|
||||||
|
} catch {
|
||||||
|
return new Response("Invalid JSON", { status: 400 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// API: serve audio file (requires auth or guest)
|
// API: serve audio file (requires auth or guest)
|
||||||
// Supports both filename and track ID (sha256:...)
|
// Supports both filename and track ID (sha256:...)
|
||||||
const trackMatch = path.match(/^\/api\/tracks\/(.+)$/);
|
const trackMatch = path.match(/^\/api\/tracks\/(.+)$/);
|
||||||
|
|
@ -782,19 +809,20 @@ serve({
|
||||||
const channel = channels.get(ws.data.channelId);
|
const channel = channels.get(ws.data.channelId);
|
||||||
if (channel) {
|
if (channel) {
|
||||||
channel.addClient(ws);
|
channel.addClient(ws);
|
||||||
// Send channel list on connect
|
// Broadcast updated channel list to all clients
|
||||||
const list = [...channels.values()].map(c => c.getListInfo());
|
broadcastChannelList();
|
||||||
ws.send(JSON.stringify({ type: "channel_list", channels: list }));
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
close(ws: ServerWebSocket<WsData>) {
|
close(ws: ServerWebSocket<WsData>) {
|
||||||
const channel = channels.get(ws.data.channelId);
|
const channel = channels.get(ws.data.channelId);
|
||||||
if (channel) channel.removeClient(ws);
|
if (channel) {
|
||||||
|
channel.removeClient(ws);
|
||||||
|
broadcastChannelList();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
message(ws: ServerWebSocket<WsData>, message: string | Buffer) {
|
message(ws: ServerWebSocket<WsData>, message: string | Buffer) {
|
||||||
try {
|
try {
|
||||||
const data = JSON.parse(String(message));
|
const data = JSON.parse(String(message));
|
||||||
console.log("[WS] Received message:", data.action, "from streamId:", ws.data.streamId);
|
|
||||||
|
|
||||||
// Handle channel switching
|
// Handle channel switching
|
||||||
if (data.action === "switch" && data.channelId) {
|
if (data.action === "switch" && data.channelId) {
|
||||||
|
|
@ -808,6 +836,7 @@ serve({
|
||||||
ws.data.channelId = data.channelId;
|
ws.data.channelId = data.channelId;
|
||||||
newChannel.addClient(ws);
|
newChannel.addClient(ws);
|
||||||
ws.send(JSON.stringify({ type: "switched", channelId: data.channelId }));
|
ws.send(JSON.stringify({ type: "switched", channelId: data.channelId }));
|
||||||
|
broadcastChannelList();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue