import path from "node:path"; import { parseArgs } from "node:util"; import commandExists from "command-exists"; import type { Metadata, VideoExtras } from "music-server-shared/types"; export const coverRegex = process.env.COVER_REGEX === undefined ? /(cover|folder)\.(png|jpe?g)$/i : process.env.COVER_REGEX === "" ? "" : new RegExp(process.env.COVER_REGEX, "i"); export const musicRoot = process.env.MUSIC_ROOT || ""; if (musicRoot === "") { console.error("Please define the environment variable MUSIC_ROOT"); process.exit(1); } export const [username, password] = (() => { if (!process.env.AUTH) return [undefined, undefined]; const decoded = Buffer.from(process.env.AUTH, "base64").toString(); const separator = decoded.indexOf(":"); if (separator <= 0 || separator === decoded.length - 1) { console.error("AUTH is set but invalid: expected base64 of 'username:password' with both parts non-empty"); process.exit(1); } return [decoded.slice(0, separator), decoded.slice(separator + 1)]; })(); export const authTokens = new Map(); //checks presence *and* expiry - the daily sweep below only bounds memory, it is not an //enforcement mechanism, so a token must not stay valid past its end date while awaiting it export function isValidAuthToken(token: string): boolean { const endDate = authTokens.get(token); if (!endDate) return false; if (endDate.getTime() < Date.now()) { authTokens.delete(token); return false; } return true; } //clear outdated tokens once a day setInterval( () => { for (const [token, endDate] of authTokens.entries()) { if (endDate.getTime() < Date.now()) authTokens.delete(token); } }, 1000 * 60 * 60 * 24, ); if (!path.isAbsolute(musicRoot)) { console.error(`Music root ${musicRoot} is not absolute`); process.exit(1); } export const excludeExtension = process.env.EXCLUDE_EXTENSION === undefined ? ["txt", "log", "nfo", "m3u", "htm", "html"] : process.env.EXCLUDE_EXTENSION.split(","); //number of parallel mediainfo processes used while scanning uncached folders export const scanConcurrency = (() => { if (process.env.SCAN_CONCURRENCY === undefined) return 8; const parsed = Number.parseInt(process.env.SCAN_CONCURRENCY, 10); if (Number.isNaN(parsed) || parsed <= 0) return 8; return parsed; })(); if (!commandExists.sync("ffmpeg")) { console.error("Required command ffmpeg not found in PATH"); process.exit(1); } if (!commandExists.sync("tar")) { console.error("Required command tar not found in PATH"); process.exit(1); } if (!commandExists.sync("avifenc")) { console.error("Required command avifenc not found in PATH"); process.exit(1); } if (!commandExists.sync("mediainfo")) { console.error("Required command mediainfo not found in PATH"); process.exit(1); } export const { values: args } = parseArgs({ args: Bun.argv, options: { serve: { type: "string", }, }, strict: true, allowPositionals: true, }); if (!args.serve) { console.error("Missing --serve, set it to the directory containing the frontend code"); process.exit(1); } export const mediaTypes = ["audio", "video"]; export const allowedTypes = [...mediaTypes, "image"]; //simple cache with a per-entry TTL: stale entries are evicted on access and swept periodically export class TTLCache { private map = new Map(); constructor(private ttl: number) { //periodic sweep to bound memory even for entries that are never accessed again setInterval( () => { const now = Date.now(); for (const [key, entry] of this.map.entries()) { if (now - entry.timestamp > this.ttl) this.map.delete(key); } }, 1000 * 60 * 60, ); } get(key: string): V | undefined { const entry = this.map.get(key); if (!entry) return undefined; if (Date.now() - entry.timestamp > this.ttl) { this.map.delete(key); return undefined; } return entry.value; } set(key: string, value: V): void { this.map.set(key, { value, timestamp: Date.now() }); } clear(): void { this.map.clear(); } } const oneDay = 1000 * 60 * 60 * 24; export const fileTypeCache = new TTLCache(oneDay); export const probeCache = new TTLCache(oneDay); export const videoExtrasCache = new TTLCache(oneDay); export const generatedPlaylistIds = new Map(); export class PathInfo { constructor(public mimeType: string) {} } export class ServerError { constructor( public status: number, public error: string, ) {} }