Add player functionality with HLS support and API integration

- Implemented a new Player class in player.js to handle audio playback, including HLS support using hls.js.
- Created a shared API module in api.js for making HTTP requests with proper error handling.
- Added DOM utility functions in dom.js for creating and clearing elements.
- Introduced WebSocket connection handling in ws.js for real-time updates.
- Developed a comprehensive CSS stylesheet for styling the application, including a high-contrast theme.
This commit is contained in:
Marco Mooren
2026-05-10 14:43:00 +02:00
commit e0a60f7b64
51 changed files with 9022 additions and 0 deletions

68
server/streams/probe.js Normal file
View File

@@ -0,0 +1,68 @@
// Low-level stream probe.
// Icecast/SHOUTcast servers commonly answer with `ICY 200 OK` instead of
// `HTTP/1.1 200 OK`, which Node's built-in fetch refuses to parse. We open
// a raw TCP/TLS socket, send a minimal HTTP/1.0 GET, and inspect the first
// status line ourselves.
import net from 'node:net';
import tls from 'node:tls';
const TIMEOUT = 8000;
const UA = 'Mozilla/5.0 OnlineRadioExplorer/0.1';
export function probeStream(rawUrl) {
return new Promise((resolve) => {
let url;
try { url = new URL(rawUrl); } catch { return resolve('err-badurl'); }
const isTls = url.protocol === 'https:';
const port = Number(url.port) || (isTls ? 443 : 80);
const path = (url.pathname || '/') + (url.search || '');
const host = url.hostname;
const opts = { host, port, servername: host };
const connect = isTls ? tls.connect : net.connect;
const sock = connect(opts);
let settled = false;
const finish = (status) => {
if (settled) return;
settled = true;
try { sock.destroy(); } catch {}
resolve(status);
};
sock.setTimeout(TIMEOUT);
sock.on('timeout', () => finish('err-timeout'));
sock.on('error', () => finish('err-fetch'));
sock.on('connect', () => {
const req =
`GET ${path} HTTP/1.0\r\n` +
`Host: ${host}\r\n` +
`User-Agent: ${UA}\r\n` +
`Icy-MetaData: 1\r\n` +
`Accept: */*\r\n` +
`Connection: close\r\n\r\n`;
sock.write(req);
});
let buf = '';
sock.on('data', (chunk) => {
buf += chunk.toString('latin1');
const eol = buf.indexOf('\n');
if (eol < 0) return;
const statusLine = buf.slice(0, eol).trim();
// Accept: HTTP/1.x 2xx, ICY 2xx, SOURCE 2xx
const m = statusLine.match(/^(?:HTTP\/\d\.\d|ICY|SOURCE)\s+(\d{3})/i);
if (!m) return finish(`bad-${statusLine.slice(0, 16)}`);
const code = Number(m[1]);
if (code >= 200 && code < 400) finish('up');
else finish(`http-${code}`);
});
sock.on('end', () => {
if (!settled) finish(buf ? 'err-empty' : 'err-fetch');
});
});
}