Fetch BSB passage text from Bolls.life API in study lesson view; v1.1.35

Adds /api/bible-passage server route that proxies the Berean Standard Bible
from Bolls.life per-verse, with in-memory caching. Study lesson scripture
block now displays live BSB text instead of static passageText. Adds
bibleBook field to StudyProgram so admins can set the book name explicitly
when the study slug doesn't match (e.g. a study titled "ephesians-part-2").

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
nmemmert
2026-08-10 10:12:25 -04:00
parent 6acc3f1d04
commit 9e602e29ec
8 changed files with 117 additions and 5 deletions
+76
View File
@@ -0,0 +1,76 @@
const passageCache = new Map()
const BOLLS_BOOK_MAP = {
genesis: 1, exodus: 2, leviticus: 3, numbers: 4, deuteronomy: 5,
joshua: 6, judges: 7, ruth: 8, '1samuel': 9, '2samuel': 10,
'1kings': 11, '2kings': 12, '1chronicles': 13, '2chronicles': 14,
ezra: 15, nehemiah: 16, esther: 17, job: 18, psalms: 19, proverbs: 20,
ecclesiastes: 21, songofsolomon: 22, isaiah: 23, jeremiah: 24,
lamentations: 25, ezekiel: 26, daniel: 27, hosea: 28, joel: 29,
amos: 30, obadiah: 31, jonah: 32, micah: 33, nahum: 34, habakkuk: 35,
zephaniah: 36, haggai: 37, zechariah: 38, malachi: 39,
matthew: 40, mark: 41, luke: 42, john: 43, acts: 44,
romans: 45, '1corinthians': 46, '2corinthians': 47, galatians: 48,
ephesians: 49, philippians: 50, colossians: 51, '1thessalonians': 52,
'2thessalonians': 53, '1timothy': 54, '2timothy': 55, titus: 56,
philemon: 57, hebrews: 58, james: 59, '1peter': 60, '2peter': 61,
'1john': 62, '2john': 63, '3john': 64, jude: 65, revelation: 66,
}
function parseReference(reference) {
const match = reference.match(/^(\d+):(\d+)(?:-(\d+))?$/)
if (!match) return null
return {
chapter: parseInt(match[1], 10),
verseStart: parseInt(match[2], 10),
verseEnd: match[3] ? parseInt(match[3], 10) : parseInt(match[2], 10),
}
}
export function register(app) {
app.get('/api/bible-passage', async (req, res) => {
const book = String(req.query.book ?? '').trim().toLowerCase().replace(/[\s-]/g, '')
const reference = String(req.query.reference ?? '').trim()
const bookNum = BOLLS_BOOK_MAP[book]
if (!bookNum) {
res.status(400).json({ message: 'Unknown book.' })
return
}
const parsed = parseReference(reference)
if (!parsed) {
res.status(400).json({ message: 'Invalid reference format. Expected format: chapter:verseStart-verseEnd' })
return
}
const cacheKey = `BSB:${bookNum}:${parsed.chapter}:${parsed.verseStart}:${parsed.verseEnd}`
if (passageCache.has(cacheKey)) {
res.json({ text: passageCache.get(cacheKey) })
return
}
try {
const verseNums = []
for (let v = parsed.verseStart; v <= parsed.verseEnd; v++) verseNums.push(v)
const results = await Promise.all(
verseNums.map(v =>
fetch(`https://bolls.life/get-verse/BSB/${bookNum}/${parsed.chapter}/${v}/`, { signal: AbortSignal.timeout(8000) })
.then(r => r.ok ? r.json() : Promise.reject(new Error(`${r.status}`)))
)
)
if (results.length === 0) {
res.status(404).json({ message: 'Passage not found.' })
return
}
const text = results.map(v => `v. ${v.verse} ${String(v.text).trim()}`).join('\n\n')
passageCache.set(cacheKey, text)
res.json({ text })
} catch {
res.status(502).json({ message: 'Unable to fetch passage right now.' })
}
})
}