All checks were successful
Build and Push Docker Image / docker (push) Successful in 9s
95 lines
3.6 KiB
JavaScript
95 lines
3.6 KiB
JavaScript
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');
|
|
}
|
|
});
|
|
}
|