9e602e29ec
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>
77 lines
2.8 KiB
JavaScript
77 lines
2.8 KiB
JavaScript
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.' })
|
|
}
|
|
})
|
|
}
|