'use strict'; // ---- Radio: The Journey FM on top, your saved stations, and a search of thousands more (Radio Browser). The hub passes the stream on (and reads the song title // out of it), so stations that only offer plain http still play, and everything plays in the same player as Music. const rdoApi = (p, opts) => jfetch('/api/radio' + p, opts); const rdoOpt = (m, o) => ({ method: m, headers: { 'content-type': 'application/json' }, body: o ? JSON.stringify(o) : undefined }); const rdoIcon = s => s.favicon ? '/api/radio/icon?u=' + encodeURIComponent(s.favicon) : ''; const rdoTrack = s => ({ key: 'radio:' + s.id, title: s.name, artist: 'Live radio', album: [s.state, s.country].filter(Boolean).join(', '), thumb: rdoIcon(s), src: '/api/radio/play/' + encodeURIComponent(s.id), nocache: true, dur: 0, station: s.name }); const rdoPlaying = () => { const t = cur(); return t && String(t.key).startsWith('radio:') ? t : null; }; const rdoMeta = s => [s.tags && s.tags.split(',').slice(0, 2).join(', '), s.state || s.country, s.codec && (s.codec + (s.bitrate ? ' ' + s.bitrate + 'k' : ''))].filter(Boolean).join(' · '); const rdoChips = [['Christian', 'christian'], ['Worship', 'worship'], ['Gospel', 'gospel'], ['Talk', 'talk'], ['News', 'news'], ['Classical', 'classical'], ['Jazz', 'jazz'], ['Country', 'country'], ['Oldies', 'oldies']]; let rdoSaved = [], rdoNow = {}; /** While a station plays, ask what song is on every 20 seconds and show it in the player. */ async function rdoPoll() { const t = rdoPlaying(); if (!t) return; const id = t.key.slice(6); try { const n = await rdoApi('/now/' + encodeURIComponent(id)); if (!rdoPlaying() || rdoPlaying().key !== t.key) return; rdoNow[id] = n; t.title = n.title || t.station; t.artist = n.title ? (n.artist || t.station) : 'Live radio'; t.album = n.title ? t.station : t.album; if (typeof updateNow === 'function') updateNow(); const el = document.getElementById('rdonow'); if (el && rdoPlaying()) el.dataset.id === id && rdoDrawNow(el, n); } catch {} } setInterval(rdoPoll, 20000); pl.a.addEventListener('playing', () => { if (rdoPlaying()) setTimeout(rdoPoll, 1500); }); function rdoDrawNow(el, n) { el.innerHTML = n && n.title ? `Now playing${esc(n.title)}${n.artist ? ' · ' + esc(n.artist) + '' : ''}` : 'Live'; } async function viewRadio() { const v = $('#view'); v.innerHTML = '

Radio

Loading…

'; let feat, mine; try { ({ featured: feat, mine } = await rdoApi('/stations')); } catch (e) { v.innerHTML = `

Radio

${esc(e.message)}

`; return; } if (!location.hash.startsWith('#/radio')) return; rdoSaved = mine; const S = { q: '', tag: 'christian', items: [], more: false, busy: false, seq: 0, off: 0 }; const saved = id => rdoSaved.some(x => x.id === id); const row = (s, fav) => `
${s.featured ? '' : ``}
`; const j = feat[0]; v.innerHTML = `

Radio

Featured

${esc(j.name)}

${esc(rdoMeta(j))}

Live
${icon('ext')}Website

Find stations

${rdoChips.map(([l, t]) => `${l}`).join('')}
`; const playStation = s => { playQueue([rdoTrack(s)], 0); toast('Tuning in to ' + s.name); drawAll(); setTimeout(rdoPoll, 2500); }; const bindRows = (root, byId) => { root.querySelectorAll('.rdorow').forEach(r => { const s = byId(r.dataset.id); r.querySelector('[data-play]').onclick = () => playStation(s); const f = r.querySelector('[data-fav]'); if (f) f.onclick = async () => { try { if (saved(s.id)) { await rdoApi('/stations/' + encodeURIComponent(s.id), rdoOpt('DELETE')); rdoSaved = rdoSaved.filter(x => x.id !== s.id); } else { await rdoApi('/stations', rdoOpt('POST', { id: s.id })); rdoSaved.push(s); } drawAll(); } catch (e) { toast(e.message); } }; }); }; const drawMine = () => { $('#rdomine').innerHTML = rdoSaved.length ? `

My stations

${rdoSaved.map(s => row(s)).join('')}
` : ''; bindRows($('#rdomine'), id => rdoSaved.find(x => x.id === id)); }; const drawRes = () => { $('#rdores').innerHTML = S.items.length ? S.items.map(s => row(s)).join('') : S.busy ? '

Searching…

' : '

No stations found.

'; bindRows($('#rdores'), id => S.items.find(x => x.id === id)); $('#rdomore').innerHTML = S.more && !S.busy ? '' : ''; if ($('#rdomb')) $('#rdomb').onclick = () => run(false); }; const drawAll = () => { drawMine(); drawRes(); }; const run = async reset => { const my = ++S.seq; if (reset) { S.off = 0; S.items = []; } S.busy = true; if (reset) drawRes(); try { const r = await rdoApi(`/search?q=${encodeURIComponent(S.q)}&tag=${encodeURIComponent(S.q ? '' : S.tag)}&offset=${S.off}`); if (my !== S.seq) return; S.items = S.items.concat(r.items); S.more = r.more; S.off += 30; } catch (e) { if (my === S.seq) { $('#rdores').innerHTML = `

${esc(e.message)}

`; S.busy = false; return; } } if (my === S.seq) { S.busy = false; drawRes(); } }; $('#rdojp').onclick = () => playStation(j); $('#rdojr').onclick = async () => { const el = openSheet('Recently played', j.name, '

Loading…

'); try { const n = await rdoApi('/now/journeyfm'); el.querySelector('.empty').outerHTML = n.recent.length ? `
${n.recent.map(x => `
${esc(x.title)}${esc(x.artist)}
`).join('')}
` : '

Nothing to show right now.

'; } catch (e) { el.querySelector('.empty').outerHTML = `

${esc(e.message)}

`; } }; $('#rdochips').querySelectorAll('a').forEach(a => a.onclick = e => { e.preventDefault(); S.tag = a.dataset.t; S.q = ''; $('#rdoq').value = ''; $('#rdochips').querySelectorAll('a').forEach(x => x.classList.toggle('on', x === a)); run(true); }); let t; $('#rdoq').oninput = e => { clearTimeout(t); t = setTimeout(() => { S.q = e.target.value.trim(); $('#rdochips').querySelectorAll('a').forEach(x => x.classList.toggle('on', !S.q && x.dataset.t === S.tag)); run(true); }, 450); }; drawMine(); run(true); rdoApi('/now/journeyfm').then(n => { const el = $('#rdonow'); if (el) { rdoNow.journeyfm = n; rdoDrawNow(el, n); } }).catch(() => {}); }