index.ts
⎇
Raw
1import staticPlugin from "@elysiajs/static";
2import { Elysia, ElysiaFile, t } from "elysia";
3import path from "node:path";
4import { parseArgs } from "node:util";
5import { musicRoot, fileTypeCache, ServerError, allowedTypes, probeCache, mediaTypes } from "./shared";
6import {
7 getPathInfo,
8 probeFile,
9 listFiles,
10 findCover,
11 matchesType,
12 needsTranscoding,
13 convertWithFFmpeg,
14} from "./utils";
15
16//TODO: transcoding cache?
17//TODO: add reasonable timeouts for caches
18//TODO: better ffmpeg errors
19
20const { values } = parseArgs({
21 args: Bun.argv,
22 options: {
23 serve: {
24 type: "string",
25 },
26 },
27 strict: true,
28 allowPositionals: true,
29});
30
31if (!values.serve) {
32 console.error("Please define the serve option");
33 process.exit(1);
34}
35
36const app = new Elysia()
37 .use(staticPlugin({ assets: values.serve, prefix: "/" }))
38 .post("/reset-cache", () => {
39 fileTypeCache.clear();
40 probeCache.clear();
41 })
42 .get("/download/*", async ({ params, set }) => {
43 const filePath = path.join(musicRoot, decodeURI(params["*"]));
44 const fileScan = await getPathInfo(filePath);
45 if (!fileScan || fileScan instanceof ServerError) {
46 set.status = fileScan?.error ? "Internal Server Error" : "Bad Request";
47 return fileScan?.error || "Not a file";
48 }
49 if (!matchesType(fileScan.mimeType, allowedTypes)) {
50 set.status = "Forbidden";
51 return "Forbidden file type";
52 }
53
54 set.status = "OK";
55 set.headers["content-type"] = fileScan.mimeType;
56 return Bun.file(filePath);
57 })
58 .get("/transcode/*", async ({ request, query, set, params }) => {
59 const filePath = path.join(musicRoot, decodeURI(params["*"]));
60 const fileScan = await getPathInfo(filePath);
61 if (!fileScan || fileScan instanceof ServerError) {
62 set.status = fileScan?.error ? "Internal Server Error" : "Bad Request";
63 return fileScan?.error || "Not a file";
64 }
65 if (!matchesType(fileScan.mimeType, allowedTypes)) {
66 set.status = "Forbidden";
67 return "Forbidden file type";
68 }
69 const bitrate = Number.parseInt(query.bitrate) || 96000;
70 if (!matchesType(fileScan.mimeType, mediaTypes) || !needsTranscoding(await probeFile(filePath), bitrate)) {
71 set.status = "Temporary Redirect";
72 set.headers.Location = `/download/${params["*"]}`;
73 return "No transcoding necessary or probe failed, redirecting to normal endpoint";
74 }
75
76 const cmd = convertWithFFmpeg(filePath, bitrate);
77 request.signal.addEventListener("abort", () => cmd.kill("SIGKILL"));
78 set.headers["content-type"] = "audio/opus";
79 return new Response(cmd.stdout);
80 })
81 .get(
82 "/list/*",
83 async ({ params, set, query }) => {
84 const dirPath = path.join(musicRoot, decodeURI(params["*"]));
85 const fileList = await listFiles(dirPath, query.recursive || false);
86 if (fileList instanceof ServerError) {
87 set.status = fileList.status;
88 return fileList.error;
89 }
90 const json = JSON.stringify(fileList);
91 set.headers["Content-Type"] = "application/json";
92 set.status = "OK";
93 return json;
94 },
95 { query: t.Optional(t.Object({ recursive: t.Boolean() })) },
96 )
97 .get("/cover/*", async ({ params, set }) => {
98 set.status = "Not Found";
99 const dirPath = path.join(musicRoot, decodeURI(params["*"]));
100 const fileScanResult = await findCover(dirPath);
101 if ("path" in fileScanResult) {
102 set.headers["Content-Type"] = fileScanResult.info.mimeType;
103 set.status = "OK";
104 return new ElysiaFile(fileScanResult.path);
105 }
106 set.status = fileScanResult.info.status;
107 return fileScanResult.info.error;
108 })
109 .listen(3000);
110
111console.log(`🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}`);
112