Add API.Bible translation support

This commit is contained in:
2026-06-13 07:12:14 -05:00
parent ffe3118ddf
commit 17f953882f
4 changed files with 177 additions and 20 deletions
+1
View File
@@ -1,2 +1,3 @@
esv_api esv_api
bibleref
*.sw? *.sw?
+103
View File
@@ -0,0 +1,103 @@
import std/[httpclient, json, logging, strutils, uri]
const apiBibleRoot* = "https://rest.api.bible/v1"
proc configBibleIdKey(translation: string): string =
let normalizedTranslation = translation.toLowerAscii
"apiBible" & normalizedTranslation[0].toUpperAscii &
normalizedTranslation[1..^1] & "BibleId"
proc defaultBibleId*(translation: string): string =
case translation.toLowerAscii
of "niv": "78a9f6124f344018-01"
else: ""
proc apiGet(apiRoot, path, query, apiKey: string): JsonNode =
var root = apiRoot
while root.endsWith("/"): root.setLen(root.len - 1)
var urlPath = root & path
if query.len > 0:
urlPath &= "?" & query
debug "requesting " & urlPath
let http = newHttpClient()
http.headers = newHttpHeaders({"api-key": apiKey})
parseJson(http.getContent(urlPath))
proc resolveBibleId(translation, apiKey, apiRoot, configuredBibleId: string): string =
let normalizedTranslation = translation.toLowerAscii
if configuredBibleId.strip.len > 0:
return configuredBibleId.strip
let defaultId = defaultBibleId(normalizedTranslation)
if defaultId.len > 0:
return defaultId
let translationCode = normalizedTranslation.toUpperAscii
let respJson = apiGet(
apiRoot,
"/bibles",
"language=eng&abbreviation=" & encodeUrl(translationCode) &
"&include-full-details=false",
apiKey)
for bible in respJson["data"].getElems:
let abbreviation =
if bible.hasKey("abbreviation"): bible["abbreviation"].getStr else: ""
let abbreviationLocal =
if bible.hasKey("abbreviationLocal"): bible["abbreviationLocal"].getStr else: ""
if abbreviation.toLowerAscii == normalizedTranslation or
abbreviationLocal.toLowerAscii == normalizedTranslation:
return bible["id"].getStr
if respJson["data"].getElems.len > 0:
return respJson["data"].getElems[0]["id"].getStr
raise newException(ValueError,
"could not find an API.Bible Bible ID for '" & translation &
"'; configure " & configBibleIdKey(normalizedTranslation))
proc resolvePassageId(reference, bibleId, apiKey, apiRoot: string): string =
let respJson = apiGet(
apiRoot,
"/bibles/" & encodeUrl(bibleId) & "/search",
"query=" & encodeUrl(reference) & "&limit=1&sort=canonical",
apiKey)
if respJson["data"].hasKey("passages"):
let passages = respJson["data"]["passages"].getElems
if passages.len > 0:
return passages[0]["id"].getStr
if respJson["data"].hasKey("verses"):
let verses = respJson["data"]["verses"].getElems
if verses.len == 1:
return verses[0]["id"].getStr
raise newException(ValueError,
"could not resolve passage reference '" & reference & "' using API.Bible")
proc fetchPassages*(
reference,
translation,
apiKey,
apiRoot,
configuredBibleId: string): seq[string] =
let bibleId = resolveBibleId(translation, apiKey, apiRoot, configuredBibleId)
let passageId = resolvePassageId(reference, bibleId, apiKey, apiRoot)
let respJson = apiGet(
apiRoot,
"/bibles/" & encodeUrl(bibleId) & "/passages/" & encodeUrl(passageId),
"content-type=text&include-notes=false&include-titles=true" &
"&include-chapter-numbers=false&include-verse-numbers=true" &
"&include-verse-spans=false",
apiKey)
let passage = respJson["data"]
@[passage["reference"].getStr & "\n" & passage["content"].getStr]
+60 -20
View File
@@ -3,10 +3,13 @@
## Simple command-line tool for retrieving Biblical passages. ## Simple command-line tool for retrieving Biblical passages.
import std/[httpclient, json, logging, os, re, strutils, uri, wordwrap] import std/[json, logging, os, re, strutils, wordwrap]
import cliutils, docopt, zero_functional import cliutils, docopt, zero_functional
proc formatMarkdown(raw: string): string = import ./api_bible
import ./esv
proc formatMarkdown(raw, translation: string): string =
var reference = "" var reference = ""
var inVerse = false var inVerse = false
var verseLines = newSeq[string]() var verseLines = newSeq[string]()
@@ -30,9 +33,14 @@ proc formatMarkdown(raw: string): string =
map(wrapWords(it, maxLineWidth = 74, newLine = "\p"))).join("\p") map(wrapWords(it, maxLineWidth = 74, newLine = "\p"))).join("\p")
result = (wrapped.splitLines --> map("> " & it)). result = (wrapped.splitLines --> map("> " & it)).
join("\p") & "\p> -- *" & reference & " (ESV)*" join("\p") & "\p> -- *" & reference & " (" &
translation.toUpperAscii & ")*"
proc formatPlain(
raw,
translation: string,
keepVerseNumbers = true): string =
proc formatPlain(raw: string, keepVerseNumbers = true): string =
var reference = "" var reference = ""
var inVerse = false var inVerse = false
var verseLines = newSeq[string]() var verseLines = newSeq[string]()
@@ -60,7 +68,28 @@ proc formatPlain(raw: string, keepVerseNumbers = true): string =
map(wrapWords(it, maxLineWidth = 74, newLine = "\p"))).join("\p") map(wrapWords(it, maxLineWidth = 74, newLine = "\p"))).join("\p")
result = (wrapped.splitLines --> map(it)). result = (wrapped.splitLines --> map(it)).
join("\p") & "\p " & reference & " (ESV)" join("\p") & "\p " & reference & " (" & translation.toUpperAscii & ")"
proc fetchPassages(reference, translation: string, cfg: CombinedConfig): seq[string] =
case translation
of "esv":
esv.fetchPassages(
reference,
cfg.getVal("esv-api-token"),
cfg.getVal("esv-api-root", "https://api.esv.org"))
of "amp", "nkjv", "niv":
api_bible.fetchPassages(
reference,
translation,
cfg.getVal("api-bible-api-key"),
cfg.getVal("api-bible-root", api_bible.apiBibleRoot),
cfg.getVal(
"api-bible-" & translation & "-bible-id",
api_bible.defaultBibleId(translation)))
else:
raise newException(ValueError,
"unsupported translation '" & translation &
"'; supported translations: amp, esv, nkjv, niv")
when isMainModule: when isMainModule:
const USAGE = """Usage: const USAGE = """Usage:
@@ -76,10 +105,27 @@ Options:
-f, --output-format <format> Select a specific output format. Valid values -f, --output-format <format> Select a specific output format. Valid values
are 'raw', 'markdown', 'plain', 'reading'. are 'raw', 'markdown', 'plain', 'reading'.
--translation <translation> Select a specific translation. Supported values
are 'amp', 'esv', 'nkjv', and 'niv'. Defaults
to 'esv'.
-t, --esv-api-token <token> Provide the API token on the command line. By -t, --esv-api-token <token> Provide the API token on the command line. By
default this will be read either from the default this will be read either from the
.bibleref.cfg.json file or the ESV_API_TOKEN .bibleref.cfg.json file or the ESV_API_TOKEN
envionment variable. envionment variable.
--api-bible-api-key <key> Provide the API.Bible API key for translations
backed by api.bible.
--api-bible-root <url> Override the API.Bible API root. Defaults to
https://rest.api.bible/v1.
--api-bible-amp-bible-id <id> Override the API.Bible Bible ID for AMP.
--api-bible-niv-bible-id <id> Override the API.Bible Bible ID for NIV.
--api-bible-nkjv-bible-id <id>
Override the API.Bible Bible ID for NKJV.
""" """
let consoleLogger = newConsoleLogger( let consoleLogger = newConsoleLogger(
@@ -103,30 +149,24 @@ Options:
cfgFileJson = parseFile(cfgFilePath) cfgFileJson = parseFile(cfgFilePath)
let cfg = CombinedConfig(docopt: args, json: cfgFileJson) let cfg = CombinedConfig(docopt: args, json: cfgFileJson)
let apiToken = cfg.getVal("esv-api-token") let translation = cfg.getVal("translation", "esv").strip.toLowerAscii
let apiRoot = cfg.getVal("esv-api-root", "https://api.esv.org")
let reference = $args["<reference>"] let reference = $args["<reference>"]
let http = newHttpClient() let passages = fetchPassages(reference, translation, cfg)
http.headers = newHttpHeaders({"Authorization": "Token " & apiToken})
let urlPath = apiRoot & "/v3/passage/text/?q=" & encodeUrl(reference)
debug "requesting " & urlPath
let respJson = parseJson(http.getContent(urlPath))
let formattedPassages = let formattedPassages =
case $args["--output-format"]: case $args["--output-format"]:
of "plain": of "plain":
respJson["passages"].getElems --> map(formatPlain(it.getStr)) passages --> map(formatPlain(it, translation))
of "reading": of "reading":
respJson["passages"].getElems --> passages -->
map(formatPlain(it.getStr, keepVerseNumbers = false)) map(formatPlain(it, translation, keepVerseNumbers = false))
of "text": of "text":
respJson["passages"].getElems --> passages -->
map(it.getStr.multiReplace([(re"\[(\d+)\]", "$1")])) map(it.multiReplace([(re"\[(\d+)\]", "$1")]))
of "raw": respJson["passages"].getElems --> map(it.getStr) of "raw": passages
else: else:
respJson["passages"].getElems --> map(formatMarkdown(it.getStr)) passages --> map(formatMarkdown(it, translation))
echo formattedPassages.join("\p\p") echo formattedPassages.join("\p\p")
+13
View File
@@ -0,0 +1,13 @@
import std/[httpclient, json, logging, uri]
proc fetchPassages*(reference, apiToken, apiRoot: string): seq[string] =
let http = newHttpClient()
http.headers = newHttpHeaders({"Authorization": "Token " & apiToken})
let urlPath = apiRoot & "/v3/passage/text/?q=" & encodeUrl(reference)
debug "requesting " & urlPath
let respJson = parseJson(http.getContent(urlPath))
result = @[]
for passage in respJson["passages"].getElems:
result.add(passage.getStr)