added lite version
This commit is contained in:
@@ -9,6 +9,14 @@ import { dirname, resolve } from 'node:path';
|
||||
import { mkdirSync } from 'node:fs';
|
||||
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));
|
||||
|
||||
// Pick data location BEFORE importing the server so its module-level path
|
||||
|
||||
BIN
node_modules.zip
Normal file
BIN
node_modules.zip
Normal file
Binary file not shown.
File diff suppressed because one or more lines are too long
1
server/public/assets/master-x_buNpFo.js
Normal file
1
server/public/assets/master-x_buNpFo.js
Normal file
File diff suppressed because one or more lines are too long
@@ -5,7 +5,7 @@
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Radio Master</title>
|
||||
|
||||
|
||||
<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/debug-B-FoNBZ5.js">
|
||||
|
||||
@@ -90,6 +90,7 @@ router.post('/:id/play/end', requireUser, (req, res) => {
|
||||
});
|
||||
|
||||
router.post('/:id/resolve', requireUser, async (req, res) => {
|
||||
try {
|
||||
const id = Number(req.params.id);
|
||||
const streams = getStreamsForStation(id);
|
||||
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' });
|
||||
const resolved = await resolveStream({ url: preferred.url, format: preferred.format });
|
||||
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
|
||||
@@ -116,8 +121,14 @@ router.get('/:id/proxy', requireUser, async (req, res) => {
|
||||
const resolved = await resolveStream({ url: preferred.url, format: preferred.format });
|
||||
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();
|
||||
req.on('close', () => { try { controller.abort(); } catch { } });
|
||||
const abortFetch = () => { try { controller.abort(); } catch { } };
|
||||
req.once('close', abortFetch);
|
||||
|
||||
let upstream;
|
||||
try {
|
||||
@@ -127,10 +138,14 @@ router.get('/:id/proxy', requireUser, async (req, res) => {
|
||||
headers: { 'User-Agent': 'oradio-kiosk/1.0', 'Icy-MetaData': '0' }
|
||||
});
|
||||
} 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}` });
|
||||
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) {
|
||||
return res.status(502).json({ error: `upstream HTTP ${upstream.status}` });
|
||||
}
|
||||
|
||||
@@ -338,6 +338,17 @@ function handleCommand(msg) {
|
||||
if (id === state.np.stationId && (state.np.playing || state.np.loading)) return;
|
||||
if (id === _pendingStationId) return;
|
||||
_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;
|
||||
api.get(`/api/stations/${id}`).then((st) => {
|
||||
// Bail if a newer command superseded this one while the
|
||||
|
||||
Reference in New Issue
Block a user