import { file } from "bun"; import { join } from "path"; import { config, MUSIC_DIR } from "../config"; import { state } from "../state"; import { getOrCreateUser } from "./helpers"; // GET /api/library - list all tracks export function handleGetLibrary(req: Request, server: any): Response { const { user, headers } = getOrCreateUser(req, server); if (!user) { return Response.json({ error: "Authentication required" }, { status: 401 }); } const tracks = state.library.getAllTracks().map(t => ({ id: t.id, filename: t.filename, title: t.title, artist: t.artist, album: t.album, duration: t.duration, available: t.available, })); return Response.json(tracks, { headers }); } // POST /api/upload - upload audio file export async function handleUpload(req: Request, server: any): Promise { const { user, headers } = getOrCreateUser(req, server); if (!user) { return Response.json({ error: "Authentication required" }, { status: 401 }); } try { const formData = await req.formData(); const uploadedFile = formData.get("file"); if (!uploadedFile || !(uploadedFile instanceof File)) { state.library.logActivity("upload_failed", { filename: "unknown" }, { id: user.id, username: user.username }); return Response.json({ error: "No file provided" }, { status: 400 }); } const validExts = [".mp3", ".ogg", ".flac", ".wav", ".m4a", ".aac", ".opus", ".wma", ".mp4"]; const ext = uploadedFile.name.toLowerCase().match(/\.[^.]+$/)?.[0]; if (!ext || !validExts.includes(ext)) { state.library.logActivity("upload_rejected", { filename: uploadedFile.name }, { id: user.id, username: user.username }); return Response.json({ error: "Invalid audio format" }, { status: 400 }); } const safeName = uploadedFile.name.replace(/[^a-zA-Z0-9._-]/g, "_"); const destPath = join(config.musicDir, safeName); const existingFile = Bun.file(destPath); if (await existingFile.exists()) { state.library.logActivity("upload_duplicate", { filename: safeName }, { id: user.id, username: user.username }); return Response.json({ error: "File already exists" }, { status: 409 }); } const arrayBuffer = await uploadedFile.arrayBuffer(); await Bun.write(destPath, arrayBuffer); console.log(`[Upload] ${user.username} uploaded: ${safeName}`); state.library.logActivity("upload", { filename: safeName }, { id: user.id, username: user.username }); return Response.json({ success: true, filename: safeName }, { headers }); } catch (e) { console.error("[Upload] Error:", e); state.library.logActivity("upload_error", { filename: "unknown" }, { id: user.id, username: user.username }); return Response.json({ error: "Upload failed" }, { status: 500 }); } } // GET /api/tracks/:id - serve audio file export async function handleGetTrack(req: Request, server: any, identifier: string): Promise { const { user } = getOrCreateUser(req, server); if (!user) { return Response.json({ error: "Authentication required" }, { status: 401 }); } if (identifier.includes("..")) return new Response("Forbidden", { status: 403 }); let filepath: string; if (identifier.startsWith("sha256:")) { const trackPath = state.library.getFilePath(identifier); if (!trackPath) return new Response("Not found", { status: 404 }); filepath = trackPath; } else { filepath = join(MUSIC_DIR, identifier); } const f = file(filepath); if (!(await f.exists())) return new Response("Not found", { status: 404 }); const size = f.size; const range = req.headers.get("range"); if (range) { const match = range.match(/bytes=(\d+)-(\d*)/); if (match) { const start = parseInt(match[1]); const end = match[2] ? parseInt(match[2]) : size - 1; const chunk = f.slice(start, end + 1); return new Response(chunk, { status: 206, headers: { "Content-Range": `bytes ${start}-${end}/${size}`, "Accept-Ranges": "bytes", "Content-Length": String(end - start + 1), "Content-Type": f.type || "audio/mpeg", }, }); } } return new Response(f, { headers: { "Accept-Ranges": "bytes", "Content-Length": String(size), "Content-Type": f.type || "audio/mpeg", }, }); }