Radio: cover art for the song that is on (iTunes), else the station logo or a tile; LIVE instead of a length; no seek bar or queue for a live station

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UaC9HmtHMdRP8dAbN4wAKj
This commit is contained in:
Claude committed 2026-10-25 16:51:49 +00:00
1 parent 424d797ecf
commit 2d975baea2
6 files changed
+50 -12

No files matched your search

+9 -3
View File
@@ -2,9 +2,9 @@
const http = require('http'), assert = require('assert'), fs = require('fs'), os = require('os'), path = require('path');
process.env.DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), 'nehub-radio-'));
process.env.RADIO_ALLOW_PRIVATE = '1'; // the fake servers below are on this machine
const AUDIO = Buffer.alloc(4096, 0x55), META = Buffer.from("StreamTitle='Some Band - Great Song';"); let port;
const seen = []; const AUDIO = Buffer.alloc(4096, 0x55), META = Buffer.from("StreamTitle='Some Band - Great Song';"); let port;
const fake = http.createServer((req, res) => {
const u = new URL(req.url, 'http://x'), p = u.pathname, J = o => { res.writeHead(200, { 'content-type': 'application/json' }); res.end(JSON.stringify(o)); };
const u = new URL(req.url, 'http://x'), p = u.pathname; seen.push(p); const J = o => { res.writeHead(200, { 'content-type': 'application/json' }); res.end(JSON.stringify(o)); };
if (p === '/json/stations/search') return J([
{ stationuuid: 'aaaaaaaa-1111', name: 'Good FM', url_resolved: `http://127.0.0.1:${port}/stream`, codec: 'MP3', bitrate: 128, tags: 'christian,worship,pop,extra,more', country: 'US', state: 'Ohio', hls: 0, homepage: 'https://good.fm', favicon: `http://127.0.0.1:${port}/logo.png` },
{ stationuuid: 'bbbbbbbb-2222', name: 'HLS only', url_resolved: `http://127.0.0.1:${port}/x.m3u8`, codec: 'AAC', hls: 1 }, { stationuuid: 'cccccccc-3333', name: 'Odd codec', url_resolved: 'http://a/b', codec: 'FLAC', hls: 0 }, { stationuuid: 'dddddddd-4444', name: ' ', url_resolved: 'http://a/b', codec: 'MP3', hls: 0 }]);
@@ -15,11 +15,13 @@ const fake = http.createServer((req, res) => {
return res.end(Buffer.concat([AUDIO.subarray(0, 1024), Buffer.from([blk.length / 16]), blk, AUDIO.subarray(1024, 2048), Buffer.from([0]), AUDIO.subarray(2048, 3072)]));
}
if (p === '/pls') { res.writeHead(200, { 'content-type': 'audio/x-scpls' }); return res.end('[playlist]'); }
if (p === '/search') { const term = u.searchParams.get('term') || ''; return J({ results: /Great Song/.test(term) ? [{ trackName: 'Other Thing', artistName: 'Else', artworkUrl100: `http://127.0.0.1:${port}/art/wrong-100x100bb.gif` }, { trackName: 'Great Song (Radio Edit)', artistName: 'Some Band', artworkUrl100: `http://127.0.0.1:${port}/art/100x100bb.gif` }] : [] }); }
if (p === '/art/600x600bb.gif') { res.writeHead(200, { 'content-type': 'image/gif' }); return res.end(Buffer.from('GIF89a-cover')); }
if (p === '/logo.png') { res.writeHead(200, { 'content-type': 'image/png' }); return res.end(Buffer.from('89504e470d0a1a0a', 'hex')); }
if (p === '/journey/getRecentlyPlayed' || p === '/journey/getNowPlaying') return J([{ TITLE: 'THE JOURNEY', ARTIST: null, ACTIVITY_DATE: '10/24/2026 22:01:21' }, { TITLE: "A LIL' CHURCH (NOBODY'S TOO LOST)", ARTIST: 'TOBYMAC', ACTIVITY_DATE: '10/24/2026 22:01:37' }, { TITLE: 'RESCUE', ARTIST: 'JORDAN ST. CYR', ACTIVITY_DATE: '10/24/2026 21:57:43' }]);
res.writeHead(404); res.end();
}).listen(0, () => {
port = fake.address().port; Object.assign(process.env, { RADIO_BROWSER_URL: `http://127.0.0.1:${port}`, JOURNEYFM_NOW_URL: `http://127.0.0.1:${port}/journey`, JOURNEYFM_STREAM_URL: `http://127.0.0.1:${port}/stream` });
port = fake.address().port; Object.assign(process.env, { RADIO_BROWSER_URL: `http://127.0.0.1:${port}`, JOURNEYFM_NOW_URL: `http://127.0.0.1:${port}/journey`, JOURNEYFM_STREAM_URL: `http://127.0.0.1:${port}/stream`, ITUNES_URL: `http://127.0.0.1:${port}` });
const radio = require('../radio'), users = require('../users');
const people = { a: users.create({ name: 'Ann', role: 'admin', handle: 'YQ' }), b: users.create({ name: 'Bob', role: 'user', tabs: ['radio'], handle: 'Yg' }) };
const hub = http.createServer((req, res) => users.runAs(req.headers['x-user'] === 'b' ? people.b : people.a, () => radio.handle(req, res))).listen(0, async () => {
@@ -38,6 +40,10 @@ const fake = http.createServer((req, res) => {
// The Journey's own feed
r = await j(await call('/now/journeyfm')); assert.equal(r.title, "A Lil' Church (Nobody's Too Lost)"); assert.equal(r.artist, 'Tobymac'); assert.equal(new Date(r.startedAt).toISOString(), '2026-10-25T02:01:37.000Z', "the station's clock (US Eastern) becomes a real moment in time"); assert.equal(r.recent.length, 2, "the station's own sound bites (no artist) are not listed songs");
const E = radio.easternEpoch; assert.equal(new Date(E('01/15/2026 08:00:00')).toISOString(), '2026-01-15T13:00:00.000Z', 'winter: Eastern time is 5 hours behind'); assert.equal(new Date(E('07/04/2026 12:00:00')).toISOString(), '2026-07-04T16:00:00.000Z', 'summer: 4 hours behind'); assert.equal(new Date(E('03/08/2026 03:30:00')).toISOString(), '2026-03-08T07:30:00.000Z', 'just after the clocks go forward'); assert.equal(E('nonsense'), 0);
r = await call('/art?s=aaaaaaaa-1111&artist=Some%20Band&title=Great%20Song'); assert.equal(r.headers.get('content-type'), 'image/gif'); assert.equal((await r.text()), 'GIF89a-cover', "the cover of the song that is on (the right match, at the larger size)"); assert.ok(seen.includes('/art/600x600bb.gif'));
r = await call('/art?s=aaaaaaaa-1111&artist=Nobody&title=Nothing'); assert.equal(r.headers.get('content-type'), 'image/png', 'no cover found: the station logo');
r = await call('/art?s=eeeeeeee-5555'); assert.equal(r.headers.get('content-type'), 'image/svg+xml', 'no logo either: a coloured tile'); assert.match(await r.text(), />PF</, 'with the station initials');
r = await call('/art'); assert.equal(r.status, 200); assert.equal(r.headers.get('content-type'), 'image/svg+xml', 'never a broken picture');
// saved stations
assert.equal((await call('/stations', { method: 'POST', body: JSON.stringify({ id: 'aaaaaaaa-1111' }) })).status, 200); r = await j(await call('/stations')); assert.deepEqual(r.mine.map(s => s.name), ['Good FM']);
await call('/stations', { method: 'POST', body: JSON.stringify({ id: 'aaaaaaaa-1111' }) }); assert.equal((await j(await call('/stations'))).mine.length, 1, 'saving twice keeps one');