import { Channel, type Track } from "../channel"; import { saveChannel, deleteChannelFromDb, saveChannelQueue, updateChannelState, } from "../db"; import { state } from "../state"; import { broadcastChannelList } from "../broadcast"; import { generateChannelId, buildTracksFromIds } from "../init"; import { getOrCreateUser, userHasPermission } from "./helpers"; // Helper for persistence callback function getPersistCallback() { return (channel: Channel, type: "state" | "queue") => { if (type === "state") { updateChannelState(channel.id, { currentIndex: channel.currentIndex, startedAt: channel.startedAt, paused: channel.paused, pausedAt: channel.pausedAt, playbackMode: channel.playbackMode, }); } else if (type === "queue") { saveChannelQueue(channel.id, channel.queue.map(t => t.id)); } }; } // GET /api/channels - list channels export function handleListChannels(req: Request, server: any): Response { const { user, headers } = getOrCreateUser(req, server); if (!user) { return Response.json({ error: "Authentication required" }, { status: 401 }); } const list = [...state.channels.values()].map(c => c.getListInfo()); return Response.json(list, { headers }); } // POST /api/channels - create channel export async function handleCreateChannel(req: Request, server: any): 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 create channels" }, { status: 403 }); } try { const { name, description, trackIds } = 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 }); } let tracks: Track[]; if (trackIds && Array.isArray(trackIds) && trackIds.length > 0) { tracks = buildTracksFromIds(trackIds, state.library); } else { tracks = []; } const channelId = generateChannelId(); const channel = new Channel({ id: channelId, name: name.trim(), description: description || "", tracks, createdBy: user.id, isDefault: false, }); channel.setPersistenceCallback(getPersistCallback()); state.channels.set(channelId, channel); saveChannel({ id: channel.id, name: channel.name, description: channel.description, createdBy: channel.createdBy, isDefault: false, currentIndex: channel.currentIndex, startedAt: channel.startedAt, paused: channel.paused, pausedAt: channel.pausedAt, playbackMode: channel.playbackMode, }); saveChannelQueue(channel.id, tracks.map(t => t.id)); console.log(`[Channel] Created "${name.trim()}" (id=${channelId}) by user ${user.id}`); broadcastChannelList(); return Response.json(channel.getListInfo(), { status: 201 }); } catch { return Response.json({ error: "Invalid request" }, { status: 400 }); } } // DELETE /api/channels/:id - delete channel export function handleDeleteChannel(req: Request, server: any, channelId: string): Response { const { user } = getOrCreateUser(req, server); if (!user) { return Response.json({ error: "Authentication required" }, { status: 401 }); } const channel = state.channels.get(channelId); if (!channel) { return Response.json({ error: "Channel not found" }, { status: 404 }); } if (channel.isDefault) { return Response.json({ error: "Cannot delete default channel" }, { status: 403 }); } if (!user.is_admin && channel.createdBy !== user.id) { return Response.json({ error: "Access denied" }, { status: 403 }); } // Move connected clients to default channel before deleting const defaultChannel = [...state.channels.values()].find(c => c.isDefault); if (defaultChannel && channel.clients.size > 0) { for (const ws of channel.clients) { channel.removeClient(ws); ws.data.channelId = defaultChannel.id; defaultChannel.addClient(ws); ws.send(JSON.stringify({ type: "switched", channelId: defaultChannel.id })); } } state.channels.delete(channelId); deleteChannelFromDb(channelId); broadcastChannelList(); return Response.json({ success: true }); } // GET /api/channels/:id - get channel state export function handleGetChannel(channelId: string): Response { const channel = state.channels.get(channelId); if (!channel) return new Response("Not found", { status: 404 }); return Response.json(channel.getState()); } // POST /api/channels/:id/jump - jump to track export async function handleJump(req: Request, server: any, channelId: string): Promise { const { user } = getOrCreateUser(req, server); if (!userHasPermission(user, "channel", channelId, "control")) { return new Response("Forbidden", { status: 403 }); } const channel = state.channels.get(channelId); if (!channel) return new Response("Not found", { status: 404 }); try { const body = await req.json(); if (typeof body.index === "number") { channel.jumpTo(body.index); return Response.json({ success: true }); } return new Response("Invalid index", { status: 400 }); } catch { return new Response("Invalid JSON", { status: 400 }); } } // POST /api/channels/:id/seek - seek in channel export async function handleSeek(req: Request, server: any, channelId: string): Promise { const { user } = getOrCreateUser(req, server); if (!userHasPermission(user, "channel", channelId, "control")) { return new Response("Forbidden", { status: 403 }); } const channel = state.channels.get(channelId); if (!channel) return new Response("Not found", { status: 404 }); try { const body = await req.json(); if (typeof body.timestamp === "number") { channel.seek(body.timestamp); return Response.json({ success: true }); } return new Response("Invalid timestamp", { status: 400 }); } catch { return new Response("Invalid JSON", { status: 400 }); } } // PATCH /api/channels/:id/queue - modify queue export async function handleModifyQueue(req: Request, server: any, channelId: string): Promise { const { user } = getOrCreateUser(req, server); if (!userHasPermission(user, "channel", channelId, "control")) { return new Response("Forbidden", { status: 403 }); } const channel = state.channels.get(channelId); if (!channel) return new Response("Not found", { status: 404 }); try { const body = await req.json(); const { add, remove, set } = body; if (Array.isArray(set)) { const tracks = buildTracksFromIds(set, state.library); channel.setQueue(tracks); return Response.json({ success: true, queueLength: channel.queue.length }); } if (Array.isArray(remove) && remove.length > 0) { const indices = remove.filter((i: unknown) => typeof i === "number"); channel.removeTracksByIndex(indices); } if (Array.isArray(add) && add.length > 0) { const tracks = buildTracksFromIds(add, state.library); channel.addTracks(tracks); } return Response.json({ success: true, queueLength: channel.queue.length }); } catch { return new Response("Invalid JSON", { status: 400 }); } } // POST /api/channels/:id/mode - set playback mode export async function handleSetMode(req: Request, server: any, channelId: string): Promise { const { user } = getOrCreateUser(req, server); if (!userHasPermission(user, "channel", channelId, "control")) { return new Response("Forbidden", { status: 403 }); } const channel = state.channels.get(channelId); if (!channel) return new Response("Not found", { status: 404 }); try { const body = await req.json(); const validModes = ["once", "repeat-all", "repeat-one", "shuffle"]; if (typeof body.mode === "string" && validModes.includes(body.mode)) { channel.setPlaybackMode(body.mode); return Response.json({ success: true, playbackMode: channel.playbackMode }); } return new Response("Invalid mode", { status: 400 }); } catch { return new Response("Invalid JSON", { status: 400 }); } }