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 new ElysiaFile(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 (
71 !matchesType(fileScan.mimeType, mediaTypes) ||
72 !needsTranscoding(await probeFile(filePath), bitrate)
73 ) {
74 set.status = "Temporary Redirect";
75 set.headers.Location = `/download/${params["*"]}`;
76 return "No transcoding necessary or probe failed, redirecting to normal endpoint";
77 }
78
79 const cmd = convertWithFFmpeg(filePath, bitrate);
80 request.signal.addEventListener("abort", () => cmd.kill("SIGKILL"));
81 set.headers["content-type"] = "audio/opus";
82 return new Response(cmd.stdout);
83 })
84 .get(
85 "/list/*",
86 async ({ params, set, query }) => {
87 const dirPath = path.join(musicRoot, decodeURI(params["*"]));
88 const fileList = await listFiles(dirPath, query.recursive || false);
89 if (fileList instanceof ServerError) {
90 set.status = fileList.status;
91 return fileList.error;
92 }
93 const json = JSON.stringify(fileList);
94 set.headers["Content-Type"] = "application/json";
95 set.status = "OK";
96 return json;
97 },
98 { query: t.Optional(t.Object({ recursive: t.Boolean() })) },
99 )
100 .get("/cover/*", async ({ params, set }) => {
101 set.status = "Not Found";
102 const dirPath = path.join(musicRoot, decodeURI(params["*"]));
103 const fileScanResult = await findCover(dirPath);
104 if ("path" in fileScanResult) {
105 set.headers["Content-Type"] = fileScanResult.info.mimeType;
106 set.status = "OK";
107 return new ElysiaFile(fileScanResult.path);
108 }
109 set.status = fileScanResult.info.status;
110 return fileScanResult.info.error;
111 })
112 .listen(3000);
113
114console.log(`🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}`);
115