feat: add endpoint to serve embedded cover art from audio files
All checks were successful
Build and Push Docker Image / docker (push) Successful in 9s
All checks were successful
Build and Push Docker Image / docker (push) Successful in 9s
This commit is contained in:
@@ -57,9 +57,7 @@ function handlePlayTrack(msg) {
|
|||||||
if ($guessTitle) $guessTitle.value = '';
|
if ($guessTitle) $guessTitle.value = '';
|
||||||
if ($guessArtist) $guessArtist.value = '';
|
if ($guessArtist) $guessArtist.value = '';
|
||||||
if ($answerResult) { $answerResult.textContent=''; $answerResult.className='mt-1 text-sm'; }
|
if ($answerResult) { $answerResult.textContent=''; $answerResult.className='mt-1 text-sm'; }
|
||||||
try {
|
try { $audio.preload = 'auto'; } catch {}
|
||||||
$audio.preload = 'auto';
|
|
||||||
} catch {}
|
|
||||||
$audio.src = t.url;
|
$audio.src = t.url;
|
||||||
const pf = document.getElementById('progressFill');
|
const pf = document.getElementById('progressFill');
|
||||||
if (pf) {
|
if (pf) {
|
||||||
@@ -68,6 +66,8 @@ function handlePlayTrack(msg) {
|
|||||||
const rd = document.getElementById('recordDisc');
|
const rd = document.getElementById('recordDisc');
|
||||||
if (rd) {
|
if (rd) {
|
||||||
rd.classList.remove('spin-record');
|
rd.classList.remove('spin-record');
|
||||||
|
// Reset cover to default logo at the start of a new track
|
||||||
|
rd.src = '/hitstar.png';
|
||||||
}
|
}
|
||||||
const { startAt, serverNow } = msg;
|
const { startAt, serverNow } = msg;
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
@@ -134,6 +134,15 @@ function handleReveal(msg) {
|
|||||||
if ($placeArea) {
|
if ($placeArea) {
|
||||||
$placeArea.classList.add('hidden');
|
$placeArea.classList.add('hidden');
|
||||||
}
|
}
|
||||||
|
// Try to load embedded cover art and replace the center image
|
||||||
|
const rd = document.getElementById('recordDisc');
|
||||||
|
if (rd && track?.id) {
|
||||||
|
const coverUrl = `/cover/${encodeURIComponent(track.id)}`;
|
||||||
|
const img = new Image();
|
||||||
|
img.onload = () => { rd.src = coverUrl; };
|
||||||
|
img.onerror = () => { /* keep default logo */ };
|
||||||
|
img.src = coverUrl + `?t=${Date.now()}`; // bypass cache just in case
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleGameEnded(msg) {
|
function handleGameEnded(msg) {
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import mime from 'mime';
|
import mime from 'mime';
|
||||||
|
import { parseFile as mmParseFile } from 'music-metadata';
|
||||||
import { DATA_DIR } from '../config.js';
|
import { DATA_DIR } from '../config.js';
|
||||||
|
|
||||||
export function registerAudioRoutes(app) {
|
export function registerAudioRoutes(app) {
|
||||||
|
// Simple in-memory cover cache: name -> { mime, buf }
|
||||||
|
const coverCache = new Map();
|
||||||
|
|
||||||
app.head('/audio/:name', (req, res) => {
|
app.head('/audio/:name', (req, res) => {
|
||||||
const name = req.params.name;
|
const name = req.params.name;
|
||||||
const filePath = path.join(DATA_DIR, name);
|
const filePath = path.join(DATA_DIR, name);
|
||||||
@@ -55,4 +59,36 @@ export function registerAudioRoutes(app) {
|
|||||||
fs.createReadStream(filePath).pipe(res);
|
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');
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user