Add search functionality and make other improvements
This commit is contained in:
parent
6e71909c6e
commit
12d5d9dcea
2 changed files with 148 additions and 140 deletions
84
esv.d
84
esv.d
|
@ -46,6 +46,7 @@ int lFlag; /* line length */
|
||||||
bool nFlag, NFlag; /* verse numbers */
|
bool nFlag, NFlag; /* verse numbers */
|
||||||
bool PFlag; /* disable pager */
|
bool PFlag; /* disable pager */
|
||||||
bool rFlag, RFlag; /* passage references */
|
bool rFlag, RFlag; /* passage references */
|
||||||
|
string sFlag; /* search passages */
|
||||||
bool VFlag; /* show version */
|
bool VFlag; /* show version */
|
||||||
|
|
||||||
int
|
int
|
||||||
|
@ -89,21 +90,18 @@ run(string[] args)
|
||||||
|
|
||||||
/* Parse command-line options */
|
/* Parse command-line options */
|
||||||
try {
|
try {
|
||||||
args.getopt(
|
getopt(args,
|
||||||
getoptConfig.bundling,
|
getoptConfig.bundling,
|
||||||
getoptConfig.caseSensitive,
|
getoptConfig.caseSensitive,
|
||||||
"a", &aFlag,
|
"a", &aFlag,
|
||||||
"C", &CFlag,
|
"C", &CFlag,
|
||||||
"F", &FFlag,
|
"F", &FFlag, "f", &fFlag,
|
||||||
"f", &fFlag,
|
"H", &HFlag, "h", &hFlag,
|
||||||
"H", &HFlag,
|
|
||||||
"h", &hFlag,
|
|
||||||
"l", &lFlag,
|
"l", &lFlag,
|
||||||
"N", &NFlag,
|
"N", &NFlag, "n", &nFlag,
|
||||||
"n", &nFlag,
|
|
||||||
"P", &PFlag,
|
"P", &PFlag,
|
||||||
"R", &RFlag,
|
"R", &RFlag, "r", &rFlag,
|
||||||
"r", &rFlag,
|
"s", &sFlag,
|
||||||
"V", &VFlag,
|
"V", &VFlag,
|
||||||
);
|
);
|
||||||
} catch (GetOptException e) {
|
} catch (GetOptException e) {
|
||||||
|
@ -115,7 +113,7 @@ run(string[] args)
|
||||||
throw new Exception(e.msg); /* catch-all */
|
throw new Exception(e.msg); /* catch-all */
|
||||||
} catch (ConvException e) {
|
} catch (ConvException e) {
|
||||||
throw new Exception(
|
throw new Exception(
|
||||||
"illegal argument to -l option -- integer required");
|
"illegal argument to -l option -- must be integer");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (VFlag) {
|
if (VFlag) {
|
||||||
|
@ -123,8 +121,13 @@ run(string[] args)
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (sFlag != "") {
|
||||||
|
/* skip argument validation */
|
||||||
|
goto config;
|
||||||
|
}
|
||||||
|
|
||||||
if (args.length < 3) {
|
if (args.length < 3) {
|
||||||
stderr.writefln("usage: %s [-C config] [-l length] [-aFfHhNnPRrV] book verses", args[0].baseName());
|
stderr.writefln("usage: %s [-aFfHhNnPRrV] [-C config] [-l length] [-s query] book verses", args[0].baseName());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -136,6 +139,7 @@ run(string[] args)
|
||||||
/* determine configuration file
|
/* determine configuration file
|
||||||
* Options have first priority, then environment variables,
|
* Options have first priority, then environment variables,
|
||||||
* then the default path */
|
* then the default path */
|
||||||
|
config:
|
||||||
configPath = environment.get(ENV_CONFIG, DEFAULT_CONFIGPATH)
|
configPath = environment.get(ENV_CONFIG, DEFAULT_CONFIGPATH)
|
||||||
.expandTilde();
|
.expandTilde();
|
||||||
try {
|
try {
|
||||||
|
@ -162,8 +166,8 @@ key = %s
|
||||||
#[passage]
|
#[passage]
|
||||||
#footnotes = false
|
#footnotes = false
|
||||||
#headings = false
|
#headings = false
|
||||||
#passage_references = false
|
#passage-references = false
|
||||||
#verse_numbers = false
|
#verse-numbers = false
|
||||||
"(DEFAULT_APIKEY));
|
"(DEFAULT_APIKEY));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -182,16 +186,21 @@ key = %s
|
||||||
|
|
||||||
esv = new ESVApi(apiKey);
|
esv = new ESVApi(apiKey);
|
||||||
|
|
||||||
|
enforce(!(aFlag && sFlag), "cannot specify both -a and -s options");
|
||||||
if (aFlag) {
|
if (aFlag) {
|
||||||
string tmpf, mpegPlayer;
|
string tmpf, mpegPlayer;
|
||||||
|
|
||||||
/* check for mpg123 */
|
tmpf = esv.getAudioPassage(args[1], args[2]);
|
||||||
enforce(executeShell(
|
|
||||||
format!"command -v %s >/dev/null 2>&1"(DEFAULT_MPEGPLAYER)).status == 0,
|
|
||||||
DEFAULT_MPEGPLAYER ~ " is required for audio mode; cannot continue");
|
|
||||||
|
|
||||||
tmpf = esv.getAudioVerses(args[1], args[2]);
|
|
||||||
mpegPlayer = environment.get(ENV_PLAYER, DEFAULT_MPEGPLAYER);
|
mpegPlayer = environment.get(ENV_PLAYER, DEFAULT_MPEGPLAYER);
|
||||||
|
|
||||||
|
/* check for an audio player */
|
||||||
|
enforce(
|
||||||
|
executeShell(
|
||||||
|
format!"command -v %s >/dev/null 2>&1"(mpegPlayer)
|
||||||
|
).status == 0,
|
||||||
|
format!"%s is required for audio mode; cannot continue"(mpegPlayer)
|
||||||
|
);
|
||||||
|
|
||||||
/* esv has built-in support for mpg123 and mpv.
|
/* esv has built-in support for mpg123 and mpv.
|
||||||
* other players will work, just recompile with
|
* other players will work, just recompile with
|
||||||
* the DEFAULT_MPEGPLAYER enum set differently
|
* the DEFAULT_MPEGPLAYER enum set differently
|
||||||
|
@ -203,6 +212,10 @@ key = %s
|
||||||
executeShell(mpegPlayer ~ tmpf);
|
executeShell(mpegPlayer ~ tmpf);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
if (sFlag) {
|
||||||
|
writeln(esv.searchFormat(sFlag));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
esv.extraParameters = iniData["api"].getKey("parameters");
|
esv.extraParameters = iniData["api"].getKey("parameters");
|
||||||
|
|
||||||
|
@ -213,9 +226,9 @@ key = %s
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Get [passage] keys */
|
/* Get [passage] keys */
|
||||||
foreach (string key; ["footnotes", "headings", "passage_references", "verse_numbers"]) {
|
foreach (string key; ["footnotes", "headings", "passage-references", "verse-numbers"]) {
|
||||||
try {
|
try {
|
||||||
esv.opts.b["include_" ~ key] =
|
esv.opts.b["include-" ~ key] =
|
||||||
returnValid("true", iniData["passage"].getKey(key)).to!bool();
|
returnValid("true", iniData["passage"].getKey(key)).to!bool();
|
||||||
} catch (ConvException e) {
|
} catch (ConvException e) {
|
||||||
throw new Exception(format!
|
throw new Exception(format!
|
||||||
|
@ -224,27 +237,30 @@ key = %s
|
||||||
);
|
);
|
||||||
} catch (IniException e) {} // just do nothing; use the default settings
|
} catch (IniException e) {} // just do nothing; use the default settings
|
||||||
}
|
}
|
||||||
/* Get line_length ([passage]) */
|
/* Get line-length ([passage]) */
|
||||||
try esv.opts.i["line_length"] = returnValid("0", iniData["passage"].getKey("line_length")).to!int();
|
try {
|
||||||
|
esv.opts.i["line-length"] =
|
||||||
|
returnValid("0", iniData["passage"].getKey("line-length")).to!int();
|
||||||
|
}
|
||||||
catch (ConvException e) {
|
catch (ConvException e) {
|
||||||
throw new Exception(
|
throw new Exception(
|
||||||
format!"%s: illegal value '%s' -- must be an integer"(
|
format!"%s: illegal value '%s' -- must be an integer"(
|
||||||
configPath,
|
configPath,
|
||||||
iniData["passage"].getKey("line_length"))
|
iniData["passage"].getKey("line-length"))
|
||||||
);
|
);
|
||||||
} catch (IniException e) {} // just do nothing; use the default setting
|
} catch (IniException e) {} // just do nothing; use the default setting
|
||||||
|
|
||||||
if (fFlag) esv.opts.b["include_footnotes"] = true;
|
if (fFlag) esv.opts.b["include-footnotes"] = true;
|
||||||
if (hFlag) esv.opts.b["include_headings"] = true;
|
if (hFlag) esv.opts.b["include-headings"] = true;
|
||||||
if (nFlag) esv.opts.b["include_verse_numbers"] = true;
|
if (nFlag) esv.opts.b["include-verse-numbers"] = true;
|
||||||
if (rFlag) esv.opts.b["include_passage_references"] = true;
|
if (rFlag) esv.opts.b["include-passage-references"] = true;
|
||||||
if (FFlag) esv.opts.b["include_footnotes"] = false;
|
if (FFlag) esv.opts.b["include-footnotes"] = false;
|
||||||
if (HFlag) esv.opts.b["include_headings"] = false;
|
if (HFlag) esv.opts.b["include-headings"] = false;
|
||||||
if (NFlag) esv.opts.b["include_verse_numbers"] = false;
|
if (NFlag) esv.opts.b["include-verse-numbers"] = false;
|
||||||
if (RFlag) esv.opts.b["include_passage_references"] = false;
|
if (RFlag) esv.opts.b["include-passage-references"] = false;
|
||||||
if (lFlag != 0) esv.opts.i["line_length"] = lFlag;
|
if (lFlag != 0) esv.opts.i["line-length"] = lFlag;
|
||||||
|
|
||||||
verses = esv.getVerses(args[1].parseBook(), args[2]);
|
verses = esv.getPassage(args[1].parseBook(), args[2]);
|
||||||
foreach (string line; verses.splitLines())
|
foreach (string line; verses.splitLines())
|
||||||
++lines;
|
++lines;
|
||||||
|
|
||||||
|
|
204
esvapi.d
204
esvapi.d
|
@ -27,7 +27,7 @@ import std.format : format;
|
||||||
import std.json : JSONValue, parseJSON;
|
import std.json : JSONValue, parseJSON;
|
||||||
import std.regex : regex, matchAll;
|
import std.regex : regex, matchAll;
|
||||||
import std.stdio : File;
|
import std.stdio : File;
|
||||||
import std.string : capitalize, tr;
|
import std.string : capitalize, tr, wrap;
|
||||||
import std.net.curl : HTTP;
|
import std.net.curl : HTTP;
|
||||||
|
|
||||||
enum ESVIndent
|
enum ESVIndent
|
||||||
|
@ -184,7 +184,11 @@ class ESVApi
|
||||||
_url = ESVAPI_URL;
|
_url = ESVAPI_URL;
|
||||||
opts = ESVApiOptions(true);
|
opts = ESVApiOptions(true);
|
||||||
extraParameters = "";
|
extraParameters = "";
|
||||||
onProgress = (size_t dlTotal, size_t dlNow, size_t ulTotal, size_t ulNow) { return 0; };
|
onProgress = (size_t dlTotal, size_t dlNow,
|
||||||
|
size_t ulTotal, size_t ulNow)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -222,15 +226,15 @@ class ESVApi
|
||||||
_url = url;
|
_url = url;
|
||||||
}
|
}
|
||||||
/*
|
/*
|
||||||
* Requests the verse(s) in text format from the API and returns it.
|
* Requests the passage in text format from the API and returns it.
|
||||||
* The (case-insensitive) name of the book being searched are
|
* The (case-insensitive) name of the book being searched are
|
||||||
* contained in the argument book. The verse(s) being looked up are
|
* contained in the argument book. The verse(s) being looked up are
|
||||||
* contained in the argument verses.
|
* contained in the argument verses.
|
||||||
*
|
*
|
||||||
* Example: getVerses("John", "3:16-21")
|
* Example: getPassage("John", "3:16-21")
|
||||||
*/
|
*/
|
||||||
string
|
string
|
||||||
getVerses(in char[] book, in char[] verse) const
|
getPassage(in char[] book, in char[] verse)
|
||||||
in (bookValid(book), "Invalid book")
|
in (bookValid(book), "Invalid book")
|
||||||
in (verseValid(verse), "Invalid verse format")
|
in (verseValid(verse), "Invalid verse format")
|
||||||
{
|
{
|
||||||
|
@ -242,60 +246,37 @@ class ESVApi
|
||||||
{
|
{
|
||||||
void *o;
|
void *o;
|
||||||
string[] parambuf;
|
string[] parambuf;
|
||||||
foreach (string opt; ESVAPI_PARAMETERS) {
|
|
||||||
switch (opt) {
|
void
|
||||||
case "indent-using":
|
addParams(R)(R item)
|
||||||
o = cast(void *)opts.indent_using;
|
{
|
||||||
break;
|
parambuf[parambuf.length] =
|
||||||
case "indent-poetry":
|
format!"&%s=%s"(item.key, item.value);
|
||||||
case "include-passage-references":
|
}
|
||||||
case "include-verse-numbers":
|
|
||||||
case "include-first-verse-numbers":
|
/* integers booleans indent_using */
|
||||||
case "include-footnotes":
|
parambuf = new string[opts.i.length + opts.b.length + 1];
|
||||||
case "include-footnote-body":
|
|
||||||
case "include-headings":
|
foreach (item; opts.i.byKeyValue())
|
||||||
case "include-short-copyright":
|
addParams(item);
|
||||||
case "include-copyright":
|
foreach (item; opts.b.byKeyValue())
|
||||||
case "include-passage-horizontal-lines":
|
addParams(item);
|
||||||
case "include-heading-horizontal-lines":
|
|
||||||
case "include-selahs":
|
parambuf[parambuf.length] =
|
||||||
o = cast(void *)opts.b[opt];
|
format!"&indent-using=%s"(
|
||||||
break;
|
opts.indent_using == ESVIndent.TAB ? "tab" : "space");
|
||||||
case "line-length":
|
|
||||||
case "horizontal-line-length":
|
/* assemble string from string buffer */
|
||||||
case "indent-paragraphs":
|
foreach (string param; parambuf) {
|
||||||
case "indent-poetry-lines":
|
params ~= param;
|
||||||
case "indent-declares":
|
|
||||||
case "indent-psalm-doxology":
|
|
||||||
o = cast(void *)opts.i[opt];
|
|
||||||
break;
|
|
||||||
default: break;
|
|
||||||
}
|
|
||||||
parambuf[parambuf.length] = format!"&%s=%s"(
|
|
||||||
opt,
|
|
||||||
opt == "indent-using" ?
|
|
||||||
opts.indent_using == ESVIndent.TAB ? "tab" : "space"
|
|
||||||
: o.to!string()
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
request = HTTP(format!"%s/text/?q=%s+%s%s%s"(
|
response = makeRequest(format!"text/?q=%s+%s"(
|
||||||
_url,
|
|
||||||
book
|
book
|
||||||
.capitalize()
|
.capitalize()
|
||||||
.tr(" ", "+"),
|
.tr(" ", "+"),
|
||||||
verse, params, extraParameters)
|
verse) ~ params ~ extraParameters);
|
||||||
);
|
|
||||||
request.onProgress = onProgress;
|
|
||||||
request.onReceive =
|
|
||||||
(ubyte[] data)
|
|
||||||
{
|
|
||||||
response ~= data;
|
|
||||||
return data.length;
|
|
||||||
};
|
|
||||||
request.addRequestHeader("Authorization", "Token " ~ _key);
|
|
||||||
request.perform();
|
|
||||||
return response.parseJSON()["passages"][0].str;
|
return response.parseJSON()["passages"][0].str;
|
||||||
}
|
}
|
||||||
/*
|
/*
|
||||||
|
@ -305,23 +286,81 @@ class ESVApi
|
||||||
* contained in the argument book. The verse(s) being looked up are
|
* contained in the argument book. The verse(s) being looked up are
|
||||||
* contained in the argument verses.
|
* contained in the argument verses.
|
||||||
*
|
*
|
||||||
* Example: getAudioVerses("John", "3:16-21")
|
* Example: getAudioPassage("John", "3:16-21")
|
||||||
*/
|
*/
|
||||||
string
|
string
|
||||||
getAudioVerses(in char[] book, in char[] verse) const
|
getAudioPassage(in char[] book, in char[] verse)
|
||||||
in (bookValid(book), "Invalid book")
|
in (bookValid(book), "Invalid book")
|
||||||
in (verseValid(verse), "Invalid verse format")
|
in (verseValid(verse), "Invalid verse format")
|
||||||
{
|
{
|
||||||
char[] response;
|
char[] response;
|
||||||
File tmpFile;
|
File tmpFile;
|
||||||
|
HTTP request;
|
||||||
|
|
||||||
auto request = HTTP(format!"%s/audio/?q=%s+%s"(
|
response = makeRequest(format!"audio/?q=%s+%s"(
|
||||||
_url,
|
|
||||||
book
|
book
|
||||||
.capitalize()
|
.capitalize()
|
||||||
.tr(" ", "+"),
|
.tr(" ", "+"),
|
||||||
verse)
|
verse)
|
||||||
);
|
);
|
||||||
|
tmpFile = File(_tmp, "w");
|
||||||
|
tmpFile.write(response);
|
||||||
|
return _tmp;
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
* Requests a passage search for the given query.
|
||||||
|
* Returns a string containing JSON data representing
|
||||||
|
* the results of the search.
|
||||||
|
*
|
||||||
|
* Example: search("It is finished")
|
||||||
|
*/
|
||||||
|
char[]
|
||||||
|
search(in string query)
|
||||||
|
{
|
||||||
|
char[] response;
|
||||||
|
HTTP request;
|
||||||
|
JSONValue json;
|
||||||
|
|
||||||
|
response = makeRequest("search/?q=" ~ query.tr(" ", "+"));
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
* Calls search() and formats the results nicely as plain text.
|
||||||
|
*/
|
||||||
|
char[]
|
||||||
|
searchFormat(alias fmt = "\033[1m%s\033[0m\n %s\n")
|
||||||
|
(in string query, int lineLength = 0) /* 0 means default */
|
||||||
|
{
|
||||||
|
JSONValue resp;
|
||||||
|
char[] layout;
|
||||||
|
|
||||||
|
resp = parseJSON(search(query));
|
||||||
|
layout = [];
|
||||||
|
|
||||||
|
enforce!ESVException(resp["total_results"].integer != 0,
|
||||||
|
"No results for search");
|
||||||
|
|
||||||
|
lineLength = lineLength == 0 ? 80 : lineLength;
|
||||||
|
|
||||||
|
foreach (JSONValue item; resp["results"].array) {
|
||||||
|
layout ~= format!fmt(
|
||||||
|
item["reference"].str,
|
||||||
|
item["content"].str
|
||||||
|
.wrap(lineLength)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return layout;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected char[]
|
||||||
|
makeRequest(in char[] query)
|
||||||
|
{
|
||||||
|
char[] response;
|
||||||
|
HTTP request;
|
||||||
|
|
||||||
|
response = [];
|
||||||
|
request = HTTP(_url ~ "/" ~ query);
|
||||||
request.onProgress = onProgress;
|
request.onProgress = onProgress;
|
||||||
request.onReceive =
|
request.onReceive =
|
||||||
(ubyte[] data)
|
(ubyte[] data)
|
||||||
|
@ -331,55 +370,8 @@ class ESVApi
|
||||||
};
|
};
|
||||||
request.addRequestHeader("Authorization", "Token " ~ _key);
|
request.addRequestHeader("Authorization", "Token " ~ _key);
|
||||||
request.perform();
|
request.perform();
|
||||||
tmpFile = File(_tmp, "w");
|
|
||||||
tmpFile.write(response);
|
|
||||||
return _tmp;
|
|
||||||
}
|
|
||||||
/*
|
|
||||||
* Requests a passage search for the given query.
|
|
||||||
* If raw is false, formats the passage search as
|
|
||||||
* plain text, otherwise returns JSON from the API.
|
|
||||||
*
|
|
||||||
* Example: search("It is finished")
|
|
||||||
*/
|
|
||||||
char[]
|
|
||||||
search(in string query, in bool raw = true)
|
|
||||||
{
|
|
||||||
ulong i;
|
|
||||||
char[] response;
|
|
||||||
char[] layout;
|
|
||||||
HTTP request;
|
|
||||||
JSONValue json;
|
|
||||||
|
|
||||||
request = HTTP(format!"%s/search/?q=%s"(
|
return response;
|
||||||
_url, query.tr(" ", "+"))
|
|
||||||
);
|
|
||||||
request.onProgress = onProgress;
|
|
||||||
request.onReceive =
|
|
||||||
(ubyte[] data)
|
|
||||||
{
|
|
||||||
response ~= cast(char[])data;
|
|
||||||
return data.length;
|
|
||||||
};
|
|
||||||
request.addRequestHeader("Authorization", "Token " ~ _key);
|
|
||||||
request.perform();
|
|
||||||
|
|
||||||
if (raw)
|
|
||||||
return response;
|
|
||||||
|
|
||||||
json = response.parseJSON();
|
|
||||||
layout = cast(char[])"";
|
|
||||||
|
|
||||||
enforce!ESVException(json["total_results"].integer == 0, "No results for search");
|
|
||||||
|
|
||||||
foreach (ulong i, JSONValue result; json["results"]) {
|
|
||||||
layout ~= format!"%s\n %s\n"(
|
|
||||||
result["reference"].str,
|
|
||||||
result["content"].str
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return layout;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue