'use strict'; // ---- Verse By Verse Ministry (Podcast -> Verse By Verse): free verse-by-verse Bible studies from versebyverseministry.org. Pick a study, play a lesson (the rest of the // study follows), open its handout. Where you got to in each lesson is kept on the server, so it follows you between devices. const vbApi = (p, opts) => jfetch('/api/vbv' + p, opts); const vbOpt = (m, o) => ({ method: m, headers: { 'content-type': 'application/json' }, body: JSON.stringify(o) }); let vbProg = {}, vbQueued = {}, vbLast = 0; const vbPlaying = () => { const t = cur(), m = t && /^vbv:([\w-]+):(\d+)$/.exec(String(t.key)); return m ? { slug: m[1], n: +m[2], key: t.key } : null; }; const vbLoadProg = async () => { try { for (const [k, p] of Object.entries(await vbApi('/progress') || {})) if (!vbProg[k] || p.t > vbProg[k].t) vbProg[k] = p; } catch {} }; const vbPush = keepalive => { if (!Object.keys(vbQueued).length) return; const body = JSON.stringify(vbQueued); vbQueued = {}; fetch('/api/vbv/progress', { method: 'POST', headers: { 'content-type': 'application/json' }, body, keepalive: !!keepalive }).then(r => { if (!r.ok) throw 0; }).catch(() => { vbQueued = { ...JSON.parse(body), ...vbQueued }; }); }; const vbSave = keepalive => { const n = vbPlaying(), d = pl.a.duration; if (!n || !isFinite(d) || d <= 0) return; vbProg[n.key] = vbQueued[n.key] = { pos: pl.a.currentTime, dur: d, t: Date.now() }; vbPush(keepalive); }; pl.a.addEventListener('timeupdate', () => { if (Date.now() - vbLast > 8000) { vbLast = Date.now(); vbSave(); } }); pl.a.addEventListener('pause', () => vbSave()); pl.a.addEventListener('ended', () => vbSave()); addEventListener('pagehide', () => vbSave(true)); const vbLeft = key => { const p = vbProg[key]; return p && p.pos > 10 && p.dur && p.pos < p.dur - 30 ? p : null; }; const vbDone = key => { const p = vbProg[key]; return !!(p && p.dur && p.pos >= p.dur - 30); }; const vbDate = d => { const t = Date.parse(d); return t > 0 ? new Date(t).toLocaleDateString([], { month: 'short', day: 'numeric', year: 'numeric' }) : ''; }; const vbCover = (s, big) => ``; async function viewVbv(args = []) { const v = $('#view'); v.innerHTML = '

Verse By Verse

Loading…

'; try { await vbLoadProg(); if (args[0] === 'study' && args[1]) return await vbStudy(args[1]); return await vbHome(); } catch (e) { if (!location.hash.startsWith('#/podcast/vbv')) return; v.innerHTML = `
${icon('back')}Podcast

Verse By Verse

${esc(e.message)}

Open the website
`; $('#vbretry').onclick = () => route(); } } async function vbHome() { const all = await vbApi('/studies'); if (!location.hash.startsWith('#/podcast/vbv')) return; const S = { q: '', cat: '' }, bySlug = new Map(all.studies.map(s => [s.slug, s])); const recent = Object.entries(vbProg).map(([k, p]) => { const m = /^vbv:([\w-]+):(\d+)$/.exec(k); return m && { slug: m[1], n: +m[2], key: k, t: p.t }; }).filter(x => x && bySlug.has(x.slug) && vbLeft(x.key)).sort((a, b) => b.t - a.t) .filter((x, i, a) => a.findIndex(y => y.slug === x.slug) === i).slice(0, 8); const card = s => `${vbCover(s)}${esc(s.title)}${esc([s.lessons ? s.lessons + ' lessons' : '', s.teacher].filter(Boolean).join(' · '))}`; $('#view').innerHTML = `
${icon('back')}Podcast

Verse By Verse

Free verse-by-verse Bible teaching from Verse By Verse Ministry International. Each study plays lesson after lesson, and most lessons have a handout.

${recent.length ? `

Continue listening

${recent.map(x => { const s = bySlug.get(x.slug), p = vbProg[x.key]; return `${vbCover(s).replace('', ``)}${esc(s.title)}Lesson ${x.n + 1}`; }).join('')}
` : ''}
All${all.categories.map(c => `${esc(c.name)}`).join('')}
`; const draw = () => { const q = S.q.toLowerCase(), list = all.studies.filter(s => (!S.cat || s.categories.includes(S.cat)) && (!q || (s.title + ' ' + s.teacher).toLowerCase().includes(q))); $('#vbgrid').innerHTML = list.map(card).join('') || '

No studies match.

'; }; $('#vbq').oninput = e => { S.q = e.target.value.trim(); draw(); }; $('#vbchips').querySelectorAll('a').forEach(a => a.onclick = e => { e.preventDefault(); S.cat = a.dataset.c; $('#vbchips').querySelectorAll('a').forEach(x => x.classList.toggle('on', x === a)); draw(); }); draw(); } async function vbStudy(slug) { const s = await vbApi('/study/' + encodeURIComponent(slug)); if (!location.hash.startsWith('#/podcast/vbv')) return; const key = l => `vbv:${slug}:${l.n}`, partial = s.lessons.filter(l => vbLeft(key(l))).sort((a, b) => vbProg[key(b)].t - vbProg[key(a)].t)[0], started = s.lessons.some(l => vbProg[key(l)]); const nextUp = () => partial || s.lessons.find(l => !vbDone(key(l))) || s.lessons[0]; const row = l => { const p = vbLeft(key(l)), done = vbDone(key(l)); return `
${l.n + 1}. ${esc(l.title)}${esc([vbDate(l.date), done ? 'played' : p ? Math.max(1, Math.round((p.dur - p.pos) / 60)) + ' min left' : ''].filter(Boolean).join(' · '))} Notes
`; }; const resume = nextUp(), rp = vbLeft(key(resume)); $('#view').innerHTML = `
${icon('back')}Studies
${vbCover(s)}

${esc(s.title)}

${esc(s.teacher)} · ${s.lessons.length} lessons

${esc(s.description)}

Lessons

${s.lessons.map(row).join('')}

Teaching and handouts © Verse By Verse Ministry International, shared freely at versebyverseministry.org.

`; const play = l => { // this lesson (from where you left off in it), then the rest of the study in order const p = vbLeft(key(l)), np = vbPlaying(); if (np && np.key === key(l)) { if (pl.a.paused) pl.a.play().catch(() => {}); return; } const q = s.lessons.slice(l.n).map(x => ({ key: key(x), title: x.title, artist: s.title, album: 'Verse By Verse Ministry', thumb: s.cover, src: `/api/vbv/audio/${encodeURIComponent(slug)}/${x.n}`, nocache: true, dur: 0 })); playQueue(q, 0); if (p) pl.a.addEventListener('loadedmetadata', () => { const t = vbPlaying(); if (t && t.key === key(l)) pl.a.currentTime = p.pos; }, { once: true }); }; $('#vbgo').onclick = () => play(resume); document.querySelectorAll('.vbrow').forEach(r => { const l = s.lessons[+r.dataset.n]; r.querySelector('[data-play]').onclick = () => play(l); r.querySelector('[data-notes]').onclick = async e => { e.preventDefault(); try { const res = await fetch(e.currentTarget.href, { redirect: 'manual' }); if (res.status === 404) return toast('This lesson has no handout'); window.open(e.currentTarget.href, '_blank', 'noopener'); } catch { window.open(e.currentTarget.href, '_blank', 'noopener'); } }; }); }