added lite version

This commit is contained in:
Marco Mooren
2026-05-27 15:04:14 +02:00
parent 3def9e0aac
commit 2e833b0bae
7 changed files with 47 additions and 13 deletions

View File

@@ -9,6 +9,14 @@ import { dirname, resolve } from 'node:path';
import { mkdirSync } from 'node:fs'; import { mkdirSync } from 'node:fs';
import 'dotenv/config'; import 'dotenv/config';
// AbortErrors from undici/node-fetch can escape async route handlers when a
// client disconnects mid-stream (station switch). They are benign — the socket
// is already gone. Log everything else so genuine bugs are still visible.
process.on('unhandledRejection', (reason) => {
if (reason?.name === 'AbortError') return;
console.error('[electron] unhandledRejection:', reason);
});
const __dirname = dirname(fileURLToPath(import.meta.url)); const __dirname = dirname(fileURLToPath(import.meta.url));
// Pick data location BEFORE importing the server so its module-level path // Pick data location BEFORE importing the server so its module-level path

BIN
node_modules.zip Normal file

Binary file not shown.

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -5,7 +5,7 @@
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Radio Master</title> <title>Radio Master</title>
<script type="module" crossorigin src="/assets/master-x_buNpFo.js"></script> <script type="module" crossorigin src="/assets/master-x_buNpFo.js"></script>
<link rel="modulepreload" crossorigin href="/assets/modulepreload-polyfill-B5Qt9EMX.js"> <link rel="modulepreload" crossorigin href="/assets/modulepreload-polyfill-B5Qt9EMX.js">
<link rel="modulepreload" crossorigin href="/assets/debug-B-FoNBZ5.js"> <link rel="modulepreload" crossorigin href="/assets/debug-B-FoNBZ5.js">

View File

@@ -90,6 +90,7 @@ router.post('/:id/play/end', requireUser, (req, res) => {
}); });
router.post('/:id/resolve', requireUser, async (req, res) => { router.post('/:id/resolve', requireUser, async (req, res) => {
try {
const id = Number(req.params.id); const id = Number(req.params.id);
const streams = getStreamsForStation(id); const streams = getStreamsForStation(id);
if (!streams.length) return res.status(404).json({ error: 'no streams' }); if (!streams.length) return res.status(404).json({ error: 'no streams' });
@@ -99,6 +100,10 @@ router.post('/:id/resolve', requireUser, async (req, res) => {
if (!preferred) return res.status(404).json({ error: 'stream not found' }); if (!preferred) return res.status(404).json({ error: 'stream not found' });
const resolved = await resolveStream({ url: preferred.url, format: preferred.format }); const resolved = await resolveStream({ url: preferred.url, format: preferred.format });
res.json({ stream: preferred, resolved }); res.json({ stream: preferred, resolved });
} catch (err) {
if (err?.name === 'AbortError' || res.headersSent) return;
res.status(500).json({ error: 'resolve failed' });
}
}); });
// Same-origin streaming proxy. Adds the CORS headers Icecast/SHOUTcast servers // Same-origin streaming proxy. Adds the CORS headers Icecast/SHOUTcast servers
@@ -116,8 +121,14 @@ router.get('/:id/proxy', requireUser, async (req, res) => {
const resolved = await resolveStream({ url: preferred.url, format: preferred.format }); const resolved = await resolveStream({ url: preferred.url, format: preferred.format });
if (resolved.format === 'hls') return res.status(415).json({ error: 'hls not proxied' }); if (resolved.format === 'hls') return res.status(415).json({ error: 'hls not proxied' });
// Use an AbortController only for the fetch-connection phase. Once the
// upstream connection is established the listener is removed and streaming
// teardown switches to reader.cancel(). Keeping the controller active after
// fetch() resolves causes undici to emit an unhandled AbortError on the
// response-body stream when the client disconnects.
const controller = new AbortController(); const controller = new AbortController();
req.on('close', () => { try { controller.abort(); } catch { } }); const abortFetch = () => { try { controller.abort(); } catch { } };
req.once('close', abortFetch);
let upstream; let upstream;
try { try {
@@ -127,10 +138,14 @@ router.get('/:id/proxy', requireUser, async (req, res) => {
headers: { 'User-Agent': 'oradio-kiosk/1.0', 'Icy-MetaData': '0' } headers: { 'User-Agent': 'oradio-kiosk/1.0', 'Icy-MetaData': '0' }
}); });
} catch (err) { } catch (err) {
if (err.name === 'AbortError') { res.end(); return; } req.off('close', abortFetch);
if (err?.name === 'AbortError') { res.end(); return; }
if (!res.headersSent) res.status(502).json({ error: `upstream: ${err.message || err}` }); if (!res.headersSent) res.status(502).json({ error: `upstream: ${err.message || err}` });
return; return;
} }
// Fetch resolved — detach the abort listener so controller.abort() is
// never called on an already-resolved fetch.
req.off('close', abortFetch);
if (!upstream.ok || !upstream.body) { if (!upstream.ok || !upstream.body) {
return res.status(502).json({ error: `upstream HTTP ${upstream.status}` }); return res.status(502).json({ error: `upstream HTTP ${upstream.status}` });
} }

View File

@@ -338,6 +338,17 @@ function handleCommand(msg) {
if (id === state.np.stationId && (state.np.playing || state.np.loading)) return; if (id === state.np.stationId && (state.np.playing || state.np.loading)) return;
if (id === _pendingStationId) return; if (id === _pendingStationId) return;
_pendingStationId = id; _pendingStationId = id;
// Stop old audio immediately — mirror the UI path where the station
// object is already available and player.stop() fires synchronously
// inside player.play(). Without this, the old stream keeps playing
// for the full round-trip of the metadata fetch below.
endCurrentSession();
try { player.stop(); } catch { /* ignore */ }
state.np.playing = false;
state.np.loading = true;
state.np.station = null;
state.np.stationId = id;
render();
const gen = ++_cmdGen; const gen = ++_cmdGen;
api.get(`/api/stations/${id}`).then((st) => { api.get(`/api/stations/${id}`).then((st) => {
// Bail if a newer command superseded this one while the // Bail if a newer command superseded this one while the