import fs from 'node:fs'; import https from 'node:https'; import net from 'node:net'; const MPD_HOST = process.env.MPD_HOST ?? '127.0.0.1'; const MPD_PORT = Number(process.env.MPD_PORT ?? '6600'); const MPD_TIMEOUT_MS = Number(process.env.MPD_TIMEOUT_MS ?? '700'); const POLL_INTERVAL_MS = Number(process.env.POLL_INTERVAL_MS ?? '1000'); const TLS_CERT_FILE = process.env.TLS_CERT_FILE ?? '/var/run/tls/tls.crt'; const TLS_KEY_FILE = process.env.TLS_KEY_FILE ?? '/var/run/tls/tls.key'; const state = { available: false, current: null, previous: [], error: 'Waiting for MPD', updatedAt: null, }; let pollRunning = false; const songDisplay = (fields) => { const artist = fields.Artist ?? fields.AlbumArtist ?? ''; const title = fields.Title ?? fields.Name ?? fields.file ?? 'Unknown'; return artist ? `${artist} - ${title}` : title; }; const parseSong = (raw) => { const fields = {}; for (const line of raw.split(/\r?\n/)) { if ( !line || line === 'OK' || line.startsWith('OK MPD ') ) { continue; } if (line.startsWith('ACK ')) { throw new Error(line); } const separator = line.indexOf(': '); if (separator === -1) { continue; } const key = line.slice(0, separator); const value = line.slice(separator + 2); if (fields[key] === undefined) { fields[key] = value; } } if ( !fields.file && !fields.Title && !fields.Name ) { return null; } const display = songDisplay(fields); return { id: [ fields.file ?? '', fields.Artist ?? '', fields.Title ?? '', fields.Name ?? '', ].join('\u0000'), display, artist: fields.Artist ?? null, title: fields.Title ?? null, file: fields.file ?? null, }; }; const getCurrentSong = () => new Promise((resolve, reject) => { const socket = net.createConnection({ host: MPD_HOST, port: MPD_PORT, }); socket.setEncoding('utf8'); socket.setTimeout(MPD_TIMEOUT_MS); let buffer = ''; let commandSent = false; let finished = false; const fail = (error) => { if (finished) { return; } finished = true; socket.destroy(); reject(error); }; const complete = (song) => { if (finished) { return; } finished = true; socket.end(); resolve(song); }; socket.on('timeout', () => { fail( new Error( `MPD did not respond within ${MPD_TIMEOUT_MS} ms`, ), ); }); socket.on('error', fail); socket.on('data', (chunk) => { buffer += chunk; if (!commandSent) { const newline = buffer.indexOf('\n'); if (newline === -1) { return; } const greeting = buffer.slice(0, newline).trim(); if (!greeting.startsWith('OK MPD ')) { fail( new Error( `Invalid MPD greeting: ${greeting}`, ), ); return; } buffer = buffer.slice(newline + 1); commandSent = true; socket.write('currentsong\n'); } if ( buffer === 'OK\n' || buffer.endsWith('\nOK\n') ) { try { complete(parseSong(buffer)); } catch (error) { fail(error); } } if (buffer.includes('\nACK ')) { fail(new Error(buffer.trim())); } }); socket.on('close', () => { if (!finished) { fail( new Error( 'MPD connection closed unexpectedly', ), ); } }); }); const pollMPD = async () => { if (pollRunning) { return; } pollRunning = true; try { const nextSong = await getCurrentSong(); const currentId = state.current?.id ?? null; const nextId = nextSong?.id ?? null; if (currentId !== nextId) { if (state.current) { state.previous = [ { display: state.current.display, artist: state.current.artist, title: state.current.title, file: state.current.file, }, ...state.previous, ].slice(0, 5); } state.current = nextSong; } state.available = true; state.error = null; state.updatedAt = new Date().toISOString(); } catch (error) { state.available = false; state.error = error instanceof Error ? error.message : String(error); state.updatedAt = new Date().toISOString(); } finally { pollRunning = false; } }; await pollMPD(); setInterval( pollMPD, POLL_INTERVAL_MS, ); const server = https.createServer( { cert: fs.readFileSync(TLS_CERT_FILE), key: fs.readFileSync(TLS_KEY_FILE), }, (req, res) => { res.setHeader( 'Content-Type', 'application/json', ); if (req.url === '/healthz') { res.writeHead(200); res.end( JSON.stringify({ status: 'ok', }), ); return; } if (req.url === '/api/status') { const current = state.current ? { display: state.current.display, artist: state.current.artist, title: state.current.title, file: state.current.file, } : null; res.writeHead(200); res.end( JSON.stringify({ available: state.available, current, previous: state.previous, error: state.error, updatedAt: state.updatedAt, }), ); return; } res.writeHead(404); res.end( JSON.stringify({ error: 'Not found', }), ); }, ); server.listen(9443, '0.0.0.0', () => { console.log( `MPD status server listening on :9443, MPD=${MPD_HOST}:${MPD_PORT}`, ); });