Radio: artist and song in the player, lyrics for the song that is on (LRCLIB), synced for The Journey FM
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UaC9HmtHMdRP8dAbN4wAKj
This commit is contained in:
11 files changed
+129
-43
No files matched your search
@@ -0,0 +1,34 @@
|
||||
// Lyrics without any extra service: LRCLIB (free, no key) is asked for the exact song, then for the closest title; synced lines when it has them, plain words otherwise.
|
||||
const http = require('http'), assert = require('assert'), fs = require('fs'), os = require('os'), path = require('path');
|
||||
const seen = [];
|
||||
const fake = http.createServer((req, res) => {
|
||||
const u = new URL(req.url, 'http://x'), J = (o, c = 200) => { res.writeHead(c, { 'content-type': 'application/json' }); res.end(JSON.stringify(o)); }; seen.push(u.pathname + '?' + u.searchParams.get('track_name'));
|
||||
const t = u.searchParams.get('track_name'), a = u.searchParams.get('artist_name');
|
||||
if (u.pathname === '/api/get') {
|
||||
if (t === 'Synced') return J({ trackName: 'Synced', artistName: a, albumName: 'Alb', duration: 200, syncedLyrics: '[00:10.00]First line\n[00:20.50]Second line', plainLyrics: 'First line\nSecond line' });
|
||||
if (t === 'Plain') return J({ trackName: 'Plain', artistName: a, plainLyrics: 'Just words\n\nMore words' });
|
||||
if (t === 'Quiet') return J({ trackName: 'Quiet', artistName: a, instrumental: true });
|
||||
if (t === 'Long') return J({ trackName: 'Long', artistName: a, duration: 500, plainLyrics: 'words' });
|
||||
return J({ statusCode: 404, message: 'not found' }, 404);
|
||||
}
|
||||
if (u.pathname === '/api/search') return J(t === 'Fuzzy' ? [{ trackName: 'Other', artistName: 'Someone' }, { trackName: 'Fuzzy', artistName: 'The Band', plainLyrics: 'found by search' }] : []);
|
||||
res.writeHead(404); res.end();
|
||||
}).listen(0, async () => {
|
||||
process.env.LRCLIB_URL = 'http://127.0.0.1:' + fake.address().port; delete process.env.LYRICS_LRCLIB;
|
||||
const lyrics = require('../lyrics'); lyrics.init({ dataDir: fs.mkdtempSync(path.join(os.tmpdir(), 'nehub-lrclib-')), apiUrl: '' });
|
||||
const srv = http.createServer((q, r) => lyrics.handle(q, r)).listen(0, async () => {
|
||||
const ask = async (a, t, d = '') => (await fetch(`http://127.0.0.1:${srv.address().port}/api/lyrics?artist=${encodeURIComponent(a)}&title=${encodeURIComponent(t)}${d ? '&duration=' + d : ''}`)).json();
|
||||
try {
|
||||
assert.ok(lyrics.enabled(), 'lyrics work with no Lyrica service');
|
||||
let r = await ask('Band', 'Synced'); assert.equal(r.found, true); assert.equal(r.source, 'LRCLIB'); assert.equal(r.sync, 'line'); assert.deepEqual(r.lines.map(l => [l.t, l.text]), [[10000, 'First line'], [20500, 'Second line']]);
|
||||
r = await ask('Band', 'Plain'); assert.equal(r.sync, 'none'); assert.equal(r.lines.filter(l => l.text).length, 2, 'plain words are kept, blank lines too');
|
||||
assert.equal((await ask('Band', 'Quiet')).instrumental, true);
|
||||
r = await ask('The Band', 'Fuzzy'); assert.equal(r.found, true, 'when the exact song is not there, the search finds the closest title'); assert.equal(r.lines[0].text, 'found by search');
|
||||
assert.equal((await ask('Band', 'Nothing here')).found, false);
|
||||
assert.equal((await ask('Band', 'Long', '200')).found, false, 'a much longer or shorter version is not the same recording'); assert.equal((await ask('Band', 'Long')).found, true, 'a live radio song has no length to compare');
|
||||
const n = seen.length; await ask('Band', 'Synced'); assert.equal(seen.length, n, 'a song is looked up once, then remembered');
|
||||
console.log('lrclib test ok');
|
||||
} catch (e) { console.error(e); process.exitCode = 1; }
|
||||
srv.close(); fake.close();
|
||||
});
|
||||
});
|
||||
@@ -45,7 +45,7 @@ const lrcLib = require('../lyrics');
|
||||
fs.writeFileSync(path.join(dataDir, 'users.json'), JSON.stringify({ users: [
|
||||
{ id: 'uadmin1', name: 'Admin', role: 'admin', tabs: [], handle: 'h', epoch: 1, disabled: false, created: 1 },
|
||||
{ id: 'ufriend1', name: 'Friend', role: 'user', tabs: ['mail'], handle: 'h2', epoch: 1, disabled: false, created: 2 }] }));
|
||||
const child = spawn(process.execPath, ['server.js'], { cwd: root, env: { ...process.env, PORT: String(port), DATA_DIR: dataDir, COOKIE_SECRET: SECRET, LYRICS_API_URL: 'http://127.0.0.1:' + fake.address().port, SEAFILE_URL: 'http://127.0.0.1:9' } });
|
||||
const child = spawn(process.execPath, ['server.js'], { cwd: root, env: { ...process.env, PORT: String(port), DATA_DIR: dataDir, COOKIE_SECRET: SECRET, LYRICS_LRCLIB: '0', LYRICS_API_URL: 'http://127.0.0.1:' + fake.address().port, SEAFILE_URL: 'http://127.0.0.1:9' } });
|
||||
child.stdout.on('data', () => {}); child.stderr.on('data', () => {}); await wait(1500);
|
||||
const base = 'http://127.0.0.1:' + port, exp = Date.now() + 864e5;
|
||||
const ck = uid => `hub_session=${uid}.1.${exp}.${crypto.createHmac('sha256', SECRET).update(`s${uid}.1.${exp}`).digest('base64url')}`;
|
||||
|
||||
+4
-3
@@ -2,7 +2,7 @@
|
||||
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='Great Song - Some Band';"); let port;
|
||||
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)); };
|
||||
if (p === '/json/stations/search') return J([
|
||||
@@ -32,11 +32,12 @@ const fake = http.createServer((req, res) => {
|
||||
r = await call('/play/journeyfm'); assert.equal(r.status, 200); assert.equal(r.headers.get('content-type'), 'audio/mpeg'); let body = Buffer.from(await r.arrayBuffer());
|
||||
assert.equal(body.length, 3072, 'the song-title blocks are taken out of the audio'); assert.ok(body.equals(AUDIO.subarray(0, 3072)), 'and the audio is untouched');
|
||||
await new Promise(ok => setTimeout(ok, 100)); r = await j(await call('/now/aaaaaaaa-1111')); assert.equal(r.title, '', 'nothing yet for a station not played');
|
||||
r = await call('/play/aaaaaaaa-1111'); assert.equal(r.status, 200); await r.arrayBuffer(); r = await j(await call('/now/aaaaaaaa-1111')); assert.equal(r.title, 'Great Song - Some Band', 'the title carried in the stream is reported');
|
||||
r = await call('/play/aaaaaaaa-1111'); assert.equal(r.status, 200); await r.arrayBuffer(); r = await j(await call('/now/aaaaaaaa-1111')); assert.equal(r.title, 'Great Song', 'the title carried in the stream is reported'); assert.equal(r.artist, 'Some Band', '"Artist - Title" is split into the artist and the title'); assert.equal(r.startedAt, 0, 'with no clock from the station there is no start time');
|
||||
r = await call('/play/eeeeeeee-5555'); assert.equal(r.status, 502, 'a playlist file is not a stream');
|
||||
assert.equal((await call('/play/..%2Fx')).status, 404); assert.equal((await call('/play/zzzzzzzz-9999')).status, 502, 'an unknown station is an error, not a crash');
|
||||
// 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(r.recent.length, 2, "the station's own sound bites (no artist) are not listed songs");
|
||||
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);
|
||||
// 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');
|
||||
|
||||
Reference in new issue
Block a user