fix: allow colons in login passwords, clamp volume keys, stop serving backend binary, async avif tmp I/O, kill ffmpeg on stream cancel

AuthorKonata <konata@posteo.jp>
Date
Commita809b1887905b833d97f2a2ecf108fa2b64929d6
Parent7385039
7 files changed, 18 insertions(+), 18 deletions(-)
▾MContainerfile
@@ -8,7 +8,8 @@ RUN bun run --cwd server compile
FROM docker.io/alpine:3.22
RUN apk add catatonit ffmpeg tar libavif-apps mediainfo
WORKDIR /app
COPY --from=compile /app/server/dist .
COPY --from=compile /app/server/dist /app/public
RUN mv /app/public/server /app/server
ENTRYPOINT ["catatonit", "--"]
CMD ["/app/server", "--serve", "/app"]
CMD ["/app/server", "--serve", "/app/public"]
▾MREADME.md
@@ -1,6 +1,6 @@
# Pico Pixel Player ![icon](webclient/public/assets/favicon-any.svg)
Pico Pixel Player is a lightweight, self-hostable audio player with an [ElysiaJS](https://elysiajs.com) backend and [SolidJS](https://www.solidjs.com) frontend. It lets you play audio files directly from your server with a clean, responsive UI.
Pico Pixel Player is a lightweight, self-hostable media player with an [ElysiaJS](https://elysiajs.com) backend and [SolidJS](https://www.solidjs.com) frontend. It lets you play your audio and video files directly from your server with a clean, responsive UI.
## Features
- **Lightweight**: Minimal dependencies, fast performance
@@ -33,7 +33,7 @@ docker-compose up # or podman-compose up
If you choose to not run this via the include Containerfile, you need to ensure these dependencies are installed:
- `bun`: Used for building, also the backend is using some bun-specific APIs
- `tar`: To bundle playlists to a single file when downloading them
- `ffmpeg`: For transcoding audio
- `ffmpeg`: For transcoding audio and video
- `avifenc`: For transcoding cover art
- `mediainfo`: For probing media file metadata
▾Mserver/package.json
@@ -26,6 +26,5 @@
"music-metadata": "^11.14.0",
"music-server-shared": "file:../shared",
"tmp": "^0.2.7"
},
"module": "src/index.js"
}
}
▾Mserver/src/index.ts
@@ -40,7 +40,6 @@ import {
} from "./utils";
//TODO: transcoding cache?
//TODO: add reasonable timeouts for caches
//TODO: better ffmpeg errors
//TODO: more cover detection
@@ -67,7 +66,7 @@ async function resolveMediaFile(encodedPath: string): Promise<{ filePath: string
const info = await getPathInfo(filePath);
//undefined means it is a directory rather than a file
if (!info || info instanceof ServerError)
return info ?? new ServerError(StatusMap["Internal Server Error"], "Not a file");
return info ?? new ServerError(StatusMap["Not Found"], "Path is a directory, not a file");
if (!matchesType(info.mimeType, allowedTypes)) return new ServerError(StatusMap.Forbidden, "Forbidden file type");
return { filePath, info };
}
@@ -129,7 +128,9 @@ const app = setup
.post(
"/login",
({ body, set }) => {
const [givenUser, givenPassword] = (body as string).split(":", 2);
const separator = body.indexOf(":");
const givenUser = separator === -1 ? body : body.slice(0, separator);
const givenPassword = separator === -1 ? "" : body.slice(separator + 1);
if (givenUser === username && givenPassword === password) {
set.status = 200;
const millisInYear = 365 * 24 * 60 * 60 * 1000;
@@ -247,6 +248,7 @@ const app = setup
else controller.enqueue(value);
},
cancel(reason) {
cmd.kill("SIGKILL");
void reader.cancel(reason);
},
}),
@@ -363,7 +365,7 @@ const app = setup
},
{ params: t.Object({ id: t.String({ minLength: 1 }) }) },
)
.post("remote-log", ({ body }) => {
.post("/remote-log", ({ body }) => {
console.log(body);
}),
)
▾Mserver/src/utils.ts
@@ -1,5 +1,4 @@
import { readFileSync, writeFileSync } from "node:fs";
import { readdir, stat } from "node:fs/promises";
import { readFile, readdir, stat, writeFile } from "node:fs/promises";
import path from "node:path";
import { StatusMap } from "elysia";
import { fileTypeFromBlob } from "file-type";
@@ -352,7 +351,7 @@ export async function toAvif(
});
const tmpFile2 = tmp.fileSync({ postfix: ".avif" });
try {
writeFileSync(tmpFile.fd, data);
await writeFile(tmpFile.name, data);
const cmd = Bun.spawn(["avifenc", "-q", "50", tmpFile.name, tmpFile2.name], {
stdin: "ignore",
stdout: "pipe",
@@ -367,7 +366,7 @@ export async function toAvif(
const exitCode = await cmd.exited;
if (exitCode !== 0) throw new Error(`avifenc failed with status code ${exitCode}`);
setContentType("image/avif");
return readFileSync(tmpFile2.fd) as Uint8Array<ArrayBuffer>;
return (await readFile(tmpFile2.name)) as Uint8Array<ArrayBuffer>;
} catch {
setContentType(mimeType);
return data;
▾Mwebclient/src/App.tsx
@@ -38,7 +38,6 @@ import { formatFilename, handleUnauthorized, LocalStorageValues, setUnauthorized
//TODO: keep synced-aborted in downloadmanager
//TODO: Add documentation to functions
//TODO: allow syncing playlists to server
//TODO: video support
//TODO: virtual rendering for playlist
//TODO: custom serialization?
@@ -752,10 +751,10 @@ const App: Component = () => {
videoElement.currentTime += 10;
e.preventDefault();
} else if (e.key === "ArrowUp") {
videoElement.volume += 0.1;
videoElement.volume = Math.min(1, videoElement.volume + 0.1);
e.preventDefault();
} else if (e.key === "ArrowDown") {
videoElement.volume -= 0.1;
videoElement.volume = Math.max(0, videoElement.volume - 0.1);
e.preventDefault();
}
});
▾Mwebclient/vite.common.config.ts
@@ -19,7 +19,7 @@ export const pwaOptions: Partial<VitePWAOptions> = {
scope: "/",
name: "Pico Pixel Player",
short_name: "Pico Pixel Player",
description: "A directory based audio player with offline functionality",
description: "A directory based media player with offline functionality",
theme_color: "#3f4d4e",
screenshots: [
{