import fs from 'fs'; import path from 'path'; import mime from 'mime'; import { parseFile as mmParseFile } from 'music-metadata'; import { DATA_DIR } from '../config.js'; export function registerAudioRoutes(app) { // Simple in-memory cover cache: name -> { mime, buf } const coverCache = new Map(); app.head('/audio/:name', (req, res) => { const name = req.params.name; const filePath = path.join(DATA_DIR, name); if (!filePath.startsWith(DATA_DIR)) return res.status(400).end(); if (!fs.existsSync(filePath)) return res.status(404).end(); const stat = fs.statSync(filePath); const type = mime.getType(filePath) || 'audio/mpeg'; res.setHeader('Accept-Ranges', 'bytes'); res.setHeader('Content-Type', type); res.setHeader('Content-Length', stat.size); res.setHeader('Cache-Control', 'no-store'); return res.status(200).end(); }); app.get('/audio/:name', (req, res) => { const name = req.params.name; const filePath = path.join(DATA_DIR, name); if (!filePath.startsWith(DATA_DIR)) return res.status(400).send('Invalid path'); if (!fs.existsSync(filePath)) return res.status(404).send('Not found'); const stat = fs.statSync(filePath); const range = req.headers.range; const type = mime.getType(filePath) || 'audio/mpeg'; res.setHeader('Accept-Ranges', 'bytes'); res.setHeader('Cache-Control', 'no-store'); if (range) { const match = /bytes=(\d+)-(\d+)?/.exec(range); let start = match && match[1] ? parseInt(match[1], 10) : 0; let end = match && match[2] ? parseInt(match[2], 10) : stat.size - 1; if (Number.isNaN(start)) start = 0; if (Number.isNaN(end)) end = stat.size - 1; start = Math.min(Math.max(0, start), Math.max(0, stat.size - 1)); end = Math.min(Math.max(start, end), Math.max(0, stat.size - 1)); if (start > end || start >= stat.size) { res.setHeader('Content-Range', `bytes */${stat.size}`); return res.status(416).end(); } const chunkSize = end - start + 1; res.writeHead(206, { 'Content-Range': `bytes ${start}-${end}/${stat.size}`, 'Content-Length': chunkSize, 'Content-Type': type, }); fs.createReadStream(filePath, { start, end }).pipe(res); } else { res.writeHead(200, { 'Content-Length': stat.size, 'Content-Type': type, }); fs.createReadStream(filePath).pipe(res); } }); // Serve embedded cover art from audio files, if present app.get('/cover/:name', async (req, res) => { try { const name = req.params.name; const filePath = path.join(DATA_DIR, name); const resolved = path.resolve(filePath); if (!resolved.startsWith(DATA_DIR)) return res.status(400).send('Invalid path'); if (!fs.existsSync(resolved)) return res.status(404).send('Not found'); if (coverCache.has(resolved)) { const c = coverCache.get(resolved); res.setHeader('Content-Type', c.mime || 'image/jpeg'); res.setHeader('Cache-Control', 'no-store'); return res.status(200).end(c.buf); } const meta = await mmParseFile(resolved, { duration: false }); const pic = meta.common?.picture?.[0]; if (!pic || !pic.data || !pic.data.length) { return res.status(404).send('No cover'); } const mimeType = pic.format || 'image/jpeg'; const buf = Buffer.from(pic.data); coverCache.set(resolved, { mime: mimeType, buf }); res.setHeader('Content-Type', mimeType); res.setHeader('Cache-Control', 'no-store'); return res.status(200).end(buf); } catch (e) { return res.status(500).send('Error reading cover'); } }); }