Heavily refactor esvapi.d (add ESVMode enum, ...) and apply changes to main.d
This commit is contained in:
parent
45890a6051
commit
8564fd3003
2 changed files with 117 additions and 90 deletions
185
esvapi.d
185
esvapi.d
|
@ -30,11 +30,18 @@ import std.format : format;
|
||||||
import std.json : JSONValue, parseJSON;
|
import std.json : JSONValue, parseJSON;
|
||||||
import std.random : rndGen;
|
import std.random : rndGen;
|
||||||
import std.range : take;
|
import std.range : take;
|
||||||
import std.regex : matchAll, replaceAll, regex;
|
import std.regex : matchAll, replaceAll, replaceFirst, regex;
|
||||||
import std.string : capitalize;
|
import std.string : capitalize;
|
||||||
import std.utf : toUTF8;
|
import std.utf : toUTF8;
|
||||||
import std.net.curl;
|
import std.net.curl;
|
||||||
|
|
||||||
|
public enum ESVMode
|
||||||
|
{
|
||||||
|
TEXT,
|
||||||
|
AUDIO
|
||||||
|
}
|
||||||
|
|
||||||
|
const enum ESVAPI_KEY = "abfb7456fa52ec4292c79e435890cfa3df14dc2b";
|
||||||
const enum ESVAPI_URL = "https://api.esv.org/v3/passage";
|
const enum ESVAPI_URL = "https://api.esv.org/v3/passage";
|
||||||
const string[] BIBLE_BOOKS = [
|
const string[] BIBLE_BOOKS = [
|
||||||
// Old Testament
|
// Old Testament
|
||||||
|
@ -56,8 +63,8 @@ const string[] BIBLE_BOOKS = [
|
||||||
"Nehemiah",
|
"Nehemiah",
|
||||||
"Esther",
|
"Esther",
|
||||||
"Job",
|
"Job",
|
||||||
"Psalm", // <-
|
"Psalm",
|
||||||
"Psalms", // <- both are valid
|
"Psalms", // both are valid
|
||||||
"Proverbs",
|
"Proverbs",
|
||||||
"Ecclesiastes",
|
"Ecclesiastes",
|
||||||
"Song of Solomon",
|
"Song of Solomon",
|
||||||
|
@ -110,54 +117,38 @@ const string[] BIBLE_BOOKS = [
|
||||||
|
|
||||||
class ESVApi
|
class ESVApi
|
||||||
{
|
{
|
||||||
private string _key;
|
private {
|
||||||
private string _url;
|
int _mode;
|
||||||
private string _mode;
|
string _key;
|
||||||
|
string _tmp;
|
||||||
|
string _url;
|
||||||
|
}
|
||||||
ESVApiOptions opts;
|
ESVApiOptions opts;
|
||||||
string extraParameters;
|
string extraParameters;
|
||||||
int delegate(size_t dlTotal, size_t dlNow, size_t ulTotal, size_t ulNow) onProgress;
|
int delegate(size_t, size_t, size_t, size_t) onProgress;
|
||||||
string tmpDir;
|
this(immutable(string) key = ESVAPI_KEY, bool audio = false)
|
||||||
this(immutable(string) key)
|
|
||||||
{
|
{
|
||||||
_url = ESVAPI_URL;
|
|
||||||
_key = key;
|
_key = key;
|
||||||
_mode = "text";
|
_mode = audio ? ESVMode.AUDIO : ESVMode.TEXT;
|
||||||
opts.setDefaults();
|
_tmp = tempDir() ~ "esv";
|
||||||
|
_url = ESVAPI_URL;
|
||||||
|
opts.defaults();
|
||||||
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; };
|
||||||
tmpDir = tempDir() ~ "esvapi";
|
tmpName = "esv";
|
||||||
}
|
|
||||||
/*
|
|
||||||
* Returns the API URL currently in use.
|
|
||||||
*/
|
|
||||||
final string getURL() const nothrow @nogc @safe
|
|
||||||
{
|
|
||||||
return _url;
|
|
||||||
}
|
|
||||||
/*
|
|
||||||
* If the url argument is a valid HTTP URL, sets the API URL currently in use
|
|
||||||
* to the given url argument. Otherwise, throws an EsvException .
|
|
||||||
*/
|
|
||||||
final void setURL(immutable(string) url) @safe
|
|
||||||
{
|
|
||||||
auto matches = url.matchAll("^https?://.+\\..+(/.+)?");
|
|
||||||
if (matches.empty)
|
|
||||||
throw new EsvException("Invalid URL format");
|
|
||||||
else
|
|
||||||
_url = url;
|
|
||||||
}
|
}
|
||||||
/*
|
/*
|
||||||
* Returns the API authentication key that was given when the API object was instantiated.
|
* Returns the API authentication key that was given when the API object was instantiated.
|
||||||
* This authentication key cannot be changed after instantiation.
|
* This authentication key cannot be changed after instantiation.
|
||||||
*/
|
*/
|
||||||
final string getKey() const nothrow @nogc @safe
|
@nogc @property @safe string key() const nothrow
|
||||||
{
|
{
|
||||||
return _key;
|
return _key;
|
||||||
}
|
}
|
||||||
/*
|
/*
|
||||||
* Returns the API authentication key currently in use.
|
* Returns the API authentication key currently in use.
|
||||||
*/
|
*/
|
||||||
final string getMode() const nothrow @nogc @safe
|
@nogc @property @safe int mode() const nothrow
|
||||||
{
|
{
|
||||||
return _mode;
|
return _mode;
|
||||||
}
|
}
|
||||||
|
@ -165,27 +156,54 @@ class ESVApi
|
||||||
* If the mode argument is either "text" or "html",
|
* If the mode argument is either "text" or "html",
|
||||||
* sets the text API mode to the given mode argument.
|
* sets the text API mode to the given mode argument.
|
||||||
* If the mode argument is not one of those,
|
* If the mode argument is not one of those,
|
||||||
* then this function will do nothing.
|
* throws an ESVException.
|
||||||
*/
|
*/
|
||||||
final void setMode(immutable(string) mode) nothrow @nogc @safe
|
@property @safe void mode(immutable(int) mode)
|
||||||
{
|
{
|
||||||
foreach (string m; ["text", "html"] )
|
if (mode == ESVMode.TEXT || mode == ESVMode.AUDIO)
|
||||||
{
|
_mode = mode;
|
||||||
if (mode == m)
|
else
|
||||||
{
|
throw new ESVException("Invalid mode");
|
||||||
_mode = mode;
|
}
|
||||||
return;
|
/*
|
||||||
}
|
* Returns the API URL currently in use.
|
||||||
}
|
*/
|
||||||
|
@nogc @property @safe string url() const nothrow
|
||||||
|
{
|
||||||
|
return _url;
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
* If the url argument is a valid HTTP URL, sets the API URL currently in use
|
||||||
|
* to the given url argument. Otherwise, throws an ESVException.
|
||||||
|
*/
|
||||||
|
@property @safe void url(immutable(string) url)
|
||||||
|
{
|
||||||
|
if (url.matchAll("^https?://.+\\..+(/.+)?").empty)
|
||||||
|
throw new ESVException("Invalid URL format");
|
||||||
|
else
|
||||||
|
_url = url;
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
* Returns the temp directory name.
|
||||||
|
*/
|
||||||
|
@property @safe tmpName() const
|
||||||
|
{
|
||||||
|
return _tmp.replaceFirst(regex('^' ~ tempDir()), "");
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
* Sets the temp directory name to the given string.
|
||||||
|
*/
|
||||||
|
@property @safe void tmpName(immutable(string) name)
|
||||||
|
{
|
||||||
|
_tmp = tempDir() ~ name;
|
||||||
}
|
}
|
||||||
/*
|
/*
|
||||||
* Returns true if the argument book is a valid book of the Bible.
|
* Returns true if the argument book is a valid book of the Bible.
|
||||||
* Otherwise, returns false.
|
* Otherwise, returns false.
|
||||||
*/
|
*/
|
||||||
final bool validateBook(in char[] book) const nothrow @safe
|
@safe bool validateBook(in char[] book) const nothrow
|
||||||
{
|
{
|
||||||
foreach (string b; BIBLE_BOOKS)
|
foreach (b; BIBLE_BOOKS) {
|
||||||
{
|
|
||||||
if (book.capitalize() == b.capitalize())
|
if (book.capitalize() == b.capitalize())
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -195,42 +213,47 @@ class ESVApi
|
||||||
* Returns true if the argument book is a valid verse format.
|
* Returns true if the argument book is a valid verse format.
|
||||||
* Otherwise, returns false.
|
* Otherwise, returns false.
|
||||||
*/
|
*/
|
||||||
final bool validateVerse(in char[] verse) const @safe
|
@safe bool validateVerse(in char[] verse) const
|
||||||
{
|
{
|
||||||
bool attemptRegex(string re) const @safe
|
@safe bool attemptRegex(string re) const
|
||||||
{
|
{
|
||||||
auto matches = verse.matchAll(re);
|
return !verse.matchAll(re).empty;
|
||||||
return !matches.empty;
|
|
||||||
}
|
}
|
||||||
if (attemptRegex("^\\d{1,3}$") ||
|
if (attemptRegex("^\\d{1,3}$") ||
|
||||||
attemptRegex("^\\d{1,3}-\\d{1,3}$") ||
|
attemptRegex("^\\d{1,3}-\\d{1,3}$") ||
|
||||||
attemptRegex("^\\d{1,3}:\\d{1,3}$") ||
|
attemptRegex("^\\d{1,3}:\\d{1,3}$") ||
|
||||||
attemptRegex("^\\d{1,3}:\\d{1,3}-\\d{1,3}$"))
|
attemptRegex("^\\d{1,3}:\\d{1,3}-\\d{1,3}$"))
|
||||||
{
|
|
||||||
return true;
|
return true;
|
||||||
}
|
|
||||||
else
|
else
|
||||||
{
|
|
||||||
return false;
|
return false;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
/*
|
/*
|
||||||
* Requests the verse(s) from the API and returns it.
|
* Requests the verse(s) 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.
|
||||||
|
*
|
||||||
|
* If the mode is ESVMode.AUDIO, requests an audio passage instead.
|
||||||
|
* A file path to an MP3 audio track is returned.
|
||||||
|
* To explicitly get an audio passage without setting the mode,
|
||||||
|
* use getAudioVerses().
|
||||||
*
|
*
|
||||||
* Example: getVerses("John", "3:16-21")
|
* Example: getVerses("John", "3:16-21")
|
||||||
*/
|
*/
|
||||||
final string getVerses(in char[] book, in char[] verse) const
|
string getVerses(in char[] book, in char[] verse) const
|
||||||
{
|
{
|
||||||
|
if (_mode == ESVMode.AUDIO) {
|
||||||
|
return getAudioVerses(book, verse);
|
||||||
|
}
|
||||||
|
|
||||||
if (!validateBook(book))
|
if (!validateBook(book))
|
||||||
throw new EsvException("Invalid book");
|
throw new ESVException("Invalid book");
|
||||||
if (!validateVerse(verse))
|
if (!validateVerse(verse))
|
||||||
throw new EsvException("Invalid verse format");
|
throw new ESVException("Invalid verse format");
|
||||||
|
|
||||||
string apiURL = format!"%s/%s/?q=%s+%s%s%s"(_url, _mode,
|
string apiURL = format!"%s/%s/?q=%s+%s%s%s"(_url, _mode,
|
||||||
book.capitalize().replaceAll(regex(" "), "+"), verse, assembleParameters(), extraParameters);
|
book.capitalize().replaceAll(regex(" "), "+"), verse,
|
||||||
|
assembleParameters(), extraParameters);
|
||||||
auto request = HTTP(apiURL);
|
auto request = HTTP(apiURL);
|
||||||
string response;
|
string response;
|
||||||
request.onProgress = onProgress;
|
request.onProgress = onProgress;
|
||||||
|
@ -252,12 +275,12 @@ class ESVApi
|
||||||
*
|
*
|
||||||
* Example: getVerses("John", "3:16-21")
|
* Example: getVerses("John", "3:16-21")
|
||||||
*/
|
*/
|
||||||
final string getAudioVerses(in char[] book, in char[] verse) const
|
string getAudioVerses(in char[] book, in char[] verse) const
|
||||||
{
|
{
|
||||||
if (!validateBook(book))
|
if (!validateBook(book))
|
||||||
throw new EsvException("Invalid book");
|
throw new ESVException("Invalid book");
|
||||||
if (!validateVerse(verse))
|
if (!validateVerse(verse))
|
||||||
throw new EsvException("Invalid verse format");
|
throw new ESVException("Invalid verse format");
|
||||||
|
|
||||||
string apiURL = format!"%s/audio/?q=%s+%s"(_url, book.capitalize().replaceAll(regex(" "), "+"), verse);
|
string apiURL = format!"%s/audio/?q=%s+%s"(_url, book.capitalize().replaceAll(regex(" "), "+"), verse);
|
||||||
auto request = HTTP(apiURL);
|
auto request = HTTP(apiURL);
|
||||||
|
@ -274,7 +297,8 @@ class ESVApi
|
||||||
tmpFile.write(response);
|
tmpFile.write(response);
|
||||||
return tmpFile;
|
return tmpFile;
|
||||||
}
|
}
|
||||||
private string assembleParameters() const @safe
|
private:
|
||||||
|
@safe string assembleParameters() const
|
||||||
{
|
{
|
||||||
string params = "";
|
string params = "";
|
||||||
string addParam(string param, string value) const
|
string addParam(string param, string value) const
|
||||||
|
@ -302,14 +326,13 @@ class ESVApi
|
||||||
params = addParam("indent-using", opts.indent_using.to!string);
|
params = addParam("indent-using", opts.indent_using.to!string);
|
||||||
return params;
|
return params;
|
||||||
}
|
}
|
||||||
private string tempFile() const @safe
|
@safe string tempFile() const
|
||||||
{
|
{
|
||||||
auto rndNums = rndGen().map!(a => cast(ubyte)a)().take(32);
|
auto rndNums = rndGen().map!(a => cast(ubyte)a)().take(32);
|
||||||
auto result = appender!string();
|
auto result = appender!string();
|
||||||
Base64.encode(rndNums, result);
|
Base64.encode(rndNums, result);
|
||||||
tmpDir.mkdirRecurse();
|
_tmp.mkdirRecurse();
|
||||||
string f = tmpDir ~ "/" ~ result.data.filter!isAlphaNum().to!string();
|
string f = _tmp ~ "/" ~ result.data.filter!isAlphaNum().to!string();
|
||||||
f.write("");
|
|
||||||
return f;
|
return f;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -319,7 +342,7 @@ struct ESVApiOptions
|
||||||
bool[string] boolOpts;
|
bool[string] boolOpts;
|
||||||
int[string] intOpts;
|
int[string] intOpts;
|
||||||
string indent_using;
|
string indent_using;
|
||||||
void setDefaults() nothrow @safe
|
@safe void defaults() nothrow
|
||||||
{
|
{
|
||||||
boolOpts["include_passage_references"] = true;
|
boolOpts["include_passage_references"] = true;
|
||||||
boolOpts["include_verse_numbers"] = true;
|
boolOpts["include_verse_numbers"] = true;
|
||||||
|
@ -333,19 +356,19 @@ struct ESVApiOptions
|
||||||
boolOpts["include_heading_horizontal_lines"] = false;
|
boolOpts["include_heading_horizontal_lines"] = false;
|
||||||
boolOpts["include_selahs"] = true;
|
boolOpts["include_selahs"] = true;
|
||||||
boolOpts["indent_poetry"] = true;
|
boolOpts["indent_poetry"] = true;
|
||||||
intOpts["horizontal_line_length"] = 55;
|
intOpts["horizontal_line_length"] = 55;
|
||||||
intOpts["indent_paragraphs"] = 2;
|
intOpts["indent_paragraphs"] = 2;
|
||||||
intOpts["indent_poetry_lines"] = 4;
|
intOpts["indent_poetry_lines"] = 4;
|
||||||
intOpts["indent_declares"] = 40;
|
intOpts["indent_declares"] = 40;
|
||||||
intOpts["indent_psalm_doxology"] = 30;
|
intOpts["indent_psalm_doxology"] = 30;
|
||||||
intOpts["line_length"] = 0;
|
intOpts["line_length"] = 0;
|
||||||
indent_using = "space";
|
indent_using = "space";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class EsvException : Exception
|
class ESVException : Exception
|
||||||
{
|
{
|
||||||
this(string msg, string file = __FILE__, size_t line = __LINE__) @safe pure
|
@safe this(string msg, string file = __FILE__, size_t line = __LINE__) pure
|
||||||
{
|
{
|
||||||
super(msg, file, line);
|
super(msg, file, line);
|
||||||
}
|
}
|
||||||
|
|
22
main.d
22
main.d
|
@ -33,7 +33,7 @@ import dini;
|
||||||
|
|
||||||
enum VERSION = "0.2.0";
|
enum VERSION = "0.2.0";
|
||||||
|
|
||||||
enum DEFAULT_APIKEY = "abfb7456fa52ec4292c79e435890cfa3df14dc2b"; // crossway approved ;)
|
enum DEFAULT_APIKEY = "abfb7456fa52ec4292c79e435890cfa3df14dc2b";
|
||||||
enum DEFAULT_CONFIGPATH = "~/.config/esv.conf";
|
enum DEFAULT_CONFIGPATH = "~/.config/esv.conf";
|
||||||
enum DEFAULT_MPEGPLAYER = "mpg123";
|
enum DEFAULT_MPEGPLAYER = "mpg123";
|
||||||
enum DEFAULT_PAGER = "less";
|
enum DEFAULT_PAGER = "less";
|
||||||
|
@ -158,14 +158,15 @@ key = " ~ DEFAULT_APIKEY ~ "
|
||||||
panic(e.msg);
|
panic(e.msg);
|
||||||
}
|
}
|
||||||
string apiKey;
|
string apiKey;
|
||||||
try apiKey = iniData["api"].getKey("key");
|
try
|
||||||
|
apiKey = iniData["api"].getKey("key");
|
||||||
catch (IniException e)
|
catch (IniException e)
|
||||||
panic("API key not present in configuration file; cannot proceed");
|
panic("API key not present in configuration file; cannot proceed");
|
||||||
if (apiKey == "")
|
if (apiKey == "")
|
||||||
panic("API key not present in configuration file; cannot proceed");
|
panic("API key not present in configuration file; cannot proceed");
|
||||||
|
|
||||||
// Initialise API object and validate the book and verse
|
// Initialise API object and validate the book and verse
|
||||||
ESVApi esv = new ESVApi(apiKey);
|
ESVApi esv = new ESVApi(apiKey, optAudio);
|
||||||
if (!esv.validateBook(args[1].extractBook()))
|
if (!esv.validateBook(args[1].extractBook()))
|
||||||
panic("book '" ~ args[1] ~ "' does not exist");
|
panic("book '" ~ args[1] ~ "' does not exist");
|
||||||
if (!esv.validateVerse(args[2]))
|
if (!esv.validateVerse(args[2]))
|
||||||
|
@ -179,10 +180,12 @@ key = " ~ DEFAULT_APIKEY ~ "
|
||||||
} else {
|
} else {
|
||||||
string tmpf = esv.getAudioVerses(args[1], args[2]);
|
string tmpf = esv.getAudioVerses(args[1], args[2]);
|
||||||
string mpegPlayer = environment.get(ENV_PLAYER, DEFAULT_MPEGPLAYER);
|
string mpegPlayer = environment.get(ENV_PLAYER, DEFAULT_MPEGPLAYER);
|
||||||
// esv has built-in support for mpg123 and mpv
|
/*
|
||||||
// other players will work, just recompile with
|
* esv has built-in support for mpg123 and mpv;
|
||||||
// the DEFAULT_MPEGPLAYER enum set differently
|
* other players will work, just recompile with
|
||||||
// or use the ESV_PLAYER environment variable
|
* the DEFAULT_MPEGPLAYER enum set differently
|
||||||
|
* or use the ESV_PLAYER environment variable
|
||||||
|
*/
|
||||||
if (mpegPlayer == "mpg123")
|
if (mpegPlayer == "mpg123")
|
||||||
mpegPlayer = mpegPlayer ~ " -q ";
|
mpegPlayer = mpegPlayer ~ " -q ";
|
||||||
else if (mpegPlayer == "mpv")
|
else if (mpegPlayer == "mpv")
|
||||||
|
@ -191,8 +194,8 @@ key = " ~ DEFAULT_APIKEY ~ "
|
||||||
mpegPlayer = DEFAULT_MPEGPLAYER ~ " ";
|
mpegPlayer = DEFAULT_MPEGPLAYER ~ " ";
|
||||||
// spawn mpg123
|
// spawn mpg123
|
||||||
executeShell(mpegPlayer ~ tmpf);
|
executeShell(mpegPlayer ~ tmpf);
|
||||||
return 0;
|
|
||||||
}
|
}
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
esv.extraParameters = iniData["api"].getKey("parameters");
|
esv.extraParameters = iniData["api"].getKey("parameters");
|
||||||
|
@ -219,7 +222,8 @@ key = " ~ DEFAULT_APIKEY ~ "
|
||||||
} 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.intOpts["line_length"] = returnValid("0", iniData["passage"].getKey("line_length")).to!int();
|
try
|
||||||
|
esv.opts.intOpts["line_length"] = returnValid("0", iniData["passage"].getKey("line_length")).to!int();
|
||||||
catch (ConvException e) {
|
catch (ConvException e) {
|
||||||
panic(configPath ~ ": value '" ~ iniData["passage"].getKey("line_length")
|
panic(configPath ~ ": value '" ~ iniData["passage"].getKey("line_length")
|
||||||
~ "' is not convertible to an integer value; must be a non-decimal number");
|
~ "' is not convertible to an integer value; must be a non-decimal number");
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue