120 lines
4.3 KiB
TypeScript
120 lines
4.3 KiB
TypeScript
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<Response> {
|
|
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<Response> {
|
|
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",
|
|
},
|
|
});
|
|
}
|