Radio starts empty and fills from the Store (Add to Radio, including The Journey FM); iCloud connect falls back to the calendar home beside the principal
build-container / build (push) Successful in 6m9s

- Store > Radio is the station finder with Add to Radio / Added; Radio under Listen lists only what was added
- Fix the Radio tab going blank (the router passed its arguments as the Store host)
- iCloud: when the account reports no calendar home, try /<id>/calendars/, and say what iCloud answered if that fails

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
nmemmertandClaude Sonnet 5.5 committed 2026-10-26 09:37:04 -04:00
1 parent a31fdcdabd
commit a804f9a8db
9 files changed
+76 -38

No files matched your search

+7 -3
View File
@@ -57,6 +57,10 @@ async function station(id) {
const favFile = () => path.join(users.dir(), 'radio.json');
const mine = () => { try { const l = JSON.parse(fs.readFileSync(favFile(), 'utf8')); return Array.isArray(l) ? l : []; } catch { return []; } };
const saveMine = l => { const f = favFile(), tmp = f + '.tmp'; fs.writeFileSync(tmp, JSON.stringify(l), { mode: 0o600 }); fs.renameSync(tmp, f); };
// The featured stations (The Journey FM) are not on anyone's Radio until they add them from the Store, so each person has a list of the ones they added.
const addedFile = () => path.join(users.dir(), 'radio-featured.json');
const added = () => { try { const l = JSON.parse(fs.readFileSync(addedFile(), 'utf8')); return Array.isArray(l) ? l.filter(id => FEATURED.some(f => f.id === id)) : []; } catch { return []; } };
const saveAdded = l => { const f = addedFile(), tmp = f + '.tmp'; fs.writeFileSync(tmp, JSON.stringify([...new Set(l)]), { mode: 0o600 }); fs.renameSync(tmp, f); };
const readBody = async req => { const c = []; let n = 0; for await (const d of req) { n += d.length; if (n > 1e5) throw fail(413, 'Too large'); c.push(d); } try { return JSON.parse(Buffer.concat(c).toString('utf8') || '{}'); } catch { throw fail(400, 'Bad request'); } };
// ---- now playing: the Journey's own feed, or the song title carried inside the stream
@@ -124,12 +128,12 @@ async function handle(req, res) {
for (const st of items) known.set(st.id, { t: Date.now(), st }); if (known.size > 500) known.clear(); // saving or playing one of these needs no second look-up
return json(res, 200, { items, more: rows.length >= 30 });
}
if (p === '/stations' && req.method === 'GET') return json(res, 200, { featured: FEATURED, mine: mine() });
if (p === '/stations' && req.method === 'GET') return json(res, 200, { featured: FEATURED, added: added(), mine: mine() });
if (p === '/stations' && req.method === 'POST') {
const b = await readBody(req), s = await station(String(b.id || '')); if (s.featured) return json(res, 200, { saved: true });
const b = await readBody(req), s = await station(String(b.id || '')); if (s.featured) { saveAdded([...added(), s.id]); return json(res, 200, { saved: true }); }
const l = mine().filter(x => x.id !== s.id); l.push(s); saveMine(l.slice(-200)); return json(res, 200, { saved: true });
}
if ((m = /^\/stations\/([\w-]+)$/.exec(p)) && req.method === 'DELETE') { saveMine(mine().filter(x => x.id !== m[1])); return json(res, 200, { removed: true }); }
if ((m = /^\/stations\/([\w-]+)$/.exec(p)) && req.method === 'DELETE') { saveMine(mine().filter(x => x.id !== m[1])); saveAdded(added().filter(x => x !== m[1])); return json(res, 200, { removed: true }); }
if ((m = /^\/now\/([\w-]+)$/.exec(p)) && req.method === 'GET') {
if (m[1] === 'journeyfm') { let now = null, recent = []; try { recent = await journeyNow(true); now = recent[0] || null; } catch {} return json(res, 200, { title: now ? now.title : '', artist: now ? now.artist : '', startedAt: now ? now.startedAt : 0, recent: recent.slice(0, 12) }); }
const n = nowMap.get(me.id + ':' + m[1]); const fresh = n && Date.now() - n.t < 600e3, sp = fresh ? /^(.+?)\s+-\s+(.+)$/.exec(n.title) : null; // many stations send "Artist - Title"