Phase 4 consume LibreBible catalogs
This commit is contained in:
+315
-17
@@ -26,7 +26,7 @@ use std::collections::HashMap;
|
||||
use std::env;
|
||||
use std::fs::{self, File};
|
||||
use std::io::{BufRead, BufReader};
|
||||
use std::path::PathBuf;
|
||||
use std::path::{Path, PathBuf};
|
||||
use tauri::{AppHandle, Manager};
|
||||
|
||||
#[derive(Serialize)]
|
||||
@@ -79,6 +79,46 @@ struct ImportedStrongLink {
|
||||
strong: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct LibreBibleIndexCatalog {
|
||||
resources: Vec<LibreBibleResourceSummary>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct LibreBibleResourceSummary {
|
||||
id: String,
|
||||
resource_type: Option<String>,
|
||||
title: Option<String>,
|
||||
package_path: Option<String>,
|
||||
files: Option<LibreBiblePackageFiles>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct LibreBibleResourceCatalog {
|
||||
id: String,
|
||||
title: Option<String>,
|
||||
files: LibreBiblePackageFiles,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct LibreBiblePackageFiles {
|
||||
verses_jsonl: Option<LibreBiblePackageFile>,
|
||||
strongs_jsonl: Option<LibreBiblePackageFile>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct LibreBiblePackageFile {
|
||||
path: String,
|
||||
}
|
||||
|
||||
struct LibreBiblePackageSource {
|
||||
resource_id: String,
|
||||
title: String,
|
||||
package_dir: PathBuf,
|
||||
verses_path: PathBuf,
|
||||
strongs_path: Option<PathBuf>,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn get_library_state(app: AppHandle) -> Result<LibraryState, String> {
|
||||
let conn = open_db(&app)?;
|
||||
@@ -183,7 +223,7 @@ fn migrate(conn: &Connection) -> Result<(), String> {
|
||||
verse_id INTEGER NOT NULL,
|
||||
strong TEXT NOT NULL,
|
||||
position INTEGER NOT NULL,
|
||||
PRIMARY KEY (verse_id, strong),
|
||||
PRIMARY KEY (verse_id, strong, position),
|
||||
FOREIGN KEY (verse_id) REFERENCES verses(id),
|
||||
FOREIGN KEY (strong) REFERENCES lexicon(strong)
|
||||
);
|
||||
@@ -194,6 +234,14 @@ fn migrate(conn: &Connection) -> Result<(), String> {
|
||||
color TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS library_sources (
|
||||
id INTEGER PRIMARY KEY,
|
||||
resource_id TEXT NOT NULL UNIQUE,
|
||||
title TEXT NOT NULL,
|
||||
package_dir TEXT NOT NULL,
|
||||
imported_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS verse_tags (
|
||||
verse_id INTEGER NOT NULL,
|
||||
tag_id INTEGER NOT NULL,
|
||||
@@ -212,11 +260,20 @@ fn seed(conn: &mut Connection) -> Result<(), String> {
|
||||
.map_err(|err| err.to_string())?;
|
||||
|
||||
if verse_count > 0 {
|
||||
let source_count: i64 = conn
|
||||
.query_row("SELECT COUNT(*) FROM library_sources", [], |row| row.get(0))
|
||||
.map_err(|err| err.to_string())?;
|
||||
if source_count == 0 {
|
||||
if let Some(package_source) = find_default_package_source() {
|
||||
reset_resource_cache(conn)?;
|
||||
import_package(conn, package_source)?;
|
||||
}
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if let Some(package_dir) = find_default_package_dir() {
|
||||
import_package(conn, package_dir)?;
|
||||
if let Some(package_source) = find_default_package_source() {
|
||||
import_package(conn, package_source)?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -250,12 +307,29 @@ fn seed(conn: &mut Connection) -> Result<(), String> {
|
||||
conn.execute_batch(seed_sql).map_err(|err| err.to_string())
|
||||
}
|
||||
|
||||
fn find_default_package_dir() -> Option<PathBuf> {
|
||||
fn reset_resource_cache(conn: &Connection) -> Result<(), String> {
|
||||
conn.execute_batch(
|
||||
"
|
||||
DELETE FROM verse_tags;
|
||||
DELETE FROM verse_strongs;
|
||||
DELETE FROM lexicon;
|
||||
DELETE FROM verses;
|
||||
DELETE FROM library_sources;
|
||||
",
|
||||
)
|
||||
.map_err(|err| err.to_string())
|
||||
}
|
||||
|
||||
fn find_default_package_source() -> Option<LibreBiblePackageSource> {
|
||||
if let Ok(path) = env::var("LIBREBIBLE_CATALOG_PATH") {
|
||||
if let Ok(source) = load_package_source_from_catalog(PathBuf::from(path)) {
|
||||
return Some(source);
|
||||
}
|
||||
}
|
||||
|
||||
if let Ok(path) = env::var("LIBRE_BIBLE_DATA_PACKAGE") {
|
||||
let candidate = PathBuf::from(path);
|
||||
if candidate.join("verses.jsonl").exists() && candidate.join("strongs-links.jsonl").exists()
|
||||
{
|
||||
return Some(candidate);
|
||||
if let Ok(source) = load_package_source_from_package_dir(PathBuf::from(path)) {
|
||||
return Some(source);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -266,7 +340,7 @@ fn find_default_package_dir() -> Option<PathBuf> {
|
||||
.join("libre-bible-data")
|
||||
.join("packages")
|
||||
.join("json")
|
||||
.join("kjv-eng-kjv2006"),
|
||||
.join("catalog.json"),
|
||||
);
|
||||
|
||||
if let Some(parent) = cwd.parent() {
|
||||
@@ -275,21 +349,204 @@ fn find_default_package_dir() -> Option<PathBuf> {
|
||||
.join("libre-bible-data")
|
||||
.join("packages")
|
||||
.join("json")
|
||||
.join("kjv-eng-kjv2006"),
|
||||
.join("catalog.json"),
|
||||
);
|
||||
}
|
||||
|
||||
candidates.into_iter().find(|path| {
|
||||
path.join("verses.jsonl").exists() && path.join("strongs-links.jsonl").exists()
|
||||
for candidate in candidates {
|
||||
if let Ok(source) = load_package_source_from_catalog(candidate) {
|
||||
return Some(source);
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn load_package_source_from_catalog(
|
||||
catalog_path: PathBuf,
|
||||
) -> Result<LibreBiblePackageSource, String> {
|
||||
let catalog_text = fs::read_to_string(&catalog_path).map_err(|err| {
|
||||
format!(
|
||||
"Unable to read LibreBible catalog {}: {err}",
|
||||
catalog_path.display()
|
||||
)
|
||||
})?;
|
||||
let catalog_dir = catalog_path
|
||||
.parent()
|
||||
.ok_or_else(|| format!("Catalog path has no parent: {}", catalog_path.display()))?;
|
||||
|
||||
if let Ok(index) = serde_json::from_str::<LibreBibleIndexCatalog>(&catalog_text) {
|
||||
let selected_id =
|
||||
env::var("LIBREBIBLE_RESOURCE_ID").unwrap_or_else(|_| "kjv-eng-kjv2006".to_string());
|
||||
let resource = index
|
||||
.resources
|
||||
.iter()
|
||||
.find(|resource| resource.id == selected_id)
|
||||
.or_else(|| {
|
||||
index
|
||||
.resources
|
||||
.iter()
|
||||
.find(|resource| resource.resource_type.as_deref() == Some("translation"))
|
||||
})
|
||||
.ok_or_else(|| {
|
||||
format!(
|
||||
"LibreBible catalog {} does not contain an importable translation.",
|
||||
catalog_path.display()
|
||||
)
|
||||
})?;
|
||||
|
||||
if let Some(package_path) = &resource.package_path {
|
||||
let resource_catalog_path = resolve_catalog_relative(catalog_dir, package_path);
|
||||
return load_package_source_from_resource_catalog(resource_catalog_path);
|
||||
}
|
||||
|
||||
if let Some(files) = &resource.files {
|
||||
let package_dir = catalog_dir.join(&resource.id);
|
||||
return package_source_from_files(
|
||||
resource.id.clone(),
|
||||
resource
|
||||
.title
|
||||
.clone()
|
||||
.unwrap_or_else(|| resource.id.clone()),
|
||||
package_dir,
|
||||
files,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
load_package_source_from_resource_catalog(catalog_path)
|
||||
}
|
||||
|
||||
fn load_package_source_from_resource_catalog(
|
||||
catalog_path: PathBuf,
|
||||
) -> Result<LibreBiblePackageSource, String> {
|
||||
let catalog_text = fs::read_to_string(&catalog_path).map_err(|err| {
|
||||
format!(
|
||||
"Unable to read LibreBible resource catalog {}: {err}",
|
||||
catalog_path.display()
|
||||
)
|
||||
})?;
|
||||
let catalog: LibreBibleResourceCatalog =
|
||||
serde_json::from_str(&catalog_text).map_err(|err| {
|
||||
format!(
|
||||
"Invalid LibreBible resource catalog {}: {err}",
|
||||
catalog_path.display()
|
||||
)
|
||||
})?;
|
||||
let package_dir = catalog_path
|
||||
.parent()
|
||||
.ok_or_else(|| {
|
||||
format!(
|
||||
"Resource catalog path has no parent: {}",
|
||||
catalog_path.display()
|
||||
)
|
||||
})?
|
||||
.to_path_buf();
|
||||
|
||||
package_source_from_files(
|
||||
catalog.id,
|
||||
catalog
|
||||
.title
|
||||
.unwrap_or_else(|| "LibreBible resource".to_string()),
|
||||
package_dir,
|
||||
&catalog.files,
|
||||
)
|
||||
}
|
||||
|
||||
fn load_package_source_from_package_dir(
|
||||
package_dir: PathBuf,
|
||||
) -> Result<LibreBiblePackageSource, String> {
|
||||
let catalog_path = package_dir.join("catalog.json");
|
||||
if catalog_path.exists() {
|
||||
return load_package_source_from_resource_catalog(catalog_path);
|
||||
}
|
||||
|
||||
let files = LibreBiblePackageFiles {
|
||||
verses_jsonl: Some(LibreBiblePackageFile {
|
||||
path: "verses.jsonl".to_string(),
|
||||
}),
|
||||
strongs_jsonl: Some(LibreBiblePackageFile {
|
||||
path: "strongs-links.jsonl".to_string(),
|
||||
}),
|
||||
};
|
||||
|
||||
package_source_from_files(
|
||||
package_dir
|
||||
.file_name()
|
||||
.and_then(|name| name.to_str())
|
||||
.unwrap_or("legacy-package")
|
||||
.to_string(),
|
||||
"Legacy LibreBible package".to_string(),
|
||||
package_dir,
|
||||
&files,
|
||||
)
|
||||
}
|
||||
|
||||
fn package_source_from_files(
|
||||
resource_id: String,
|
||||
title: String,
|
||||
package_dir: PathBuf,
|
||||
files: &LibreBiblePackageFiles,
|
||||
) -> Result<LibreBiblePackageSource, String> {
|
||||
let verses_file = files.verses_jsonl.as_ref().ok_or_else(|| {
|
||||
format!("LibreBible resource {resource_id} does not define verses_jsonl.")
|
||||
})?;
|
||||
let verses_path = package_dir.join(&verses_file.path);
|
||||
if !verses_path.exists() {
|
||||
return Err(format!(
|
||||
"LibreBible verses file is missing: {}",
|
||||
verses_path.display()
|
||||
));
|
||||
}
|
||||
|
||||
let strongs_path = files
|
||||
.strongs_jsonl
|
||||
.as_ref()
|
||||
.map(|file| package_dir.join(&file.path))
|
||||
.filter(|path| path.exists());
|
||||
|
||||
Ok(LibreBiblePackageSource {
|
||||
resource_id,
|
||||
title,
|
||||
package_dir,
|
||||
verses_path,
|
||||
strongs_path,
|
||||
})
|
||||
}
|
||||
|
||||
fn import_package(conn: &mut Connection, package_dir: PathBuf) -> Result<(), String> {
|
||||
let verses_path = package_dir.join("verses.jsonl");
|
||||
let strongs_path = package_dir.join("strongs-links.jsonl");
|
||||
fn resolve_catalog_relative(catalog_dir: &Path, package_path: &str) -> PathBuf {
|
||||
let candidate = PathBuf::from(package_path);
|
||||
if candidate.is_absolute() {
|
||||
return candidate;
|
||||
}
|
||||
|
||||
let from_repo_root = catalog_dir.join("..").join("..").join(package_path);
|
||||
if from_repo_root.exists() {
|
||||
return from_repo_root;
|
||||
}
|
||||
|
||||
catalog_dir.join(package_path)
|
||||
}
|
||||
|
||||
fn import_package(
|
||||
conn: &mut Connection,
|
||||
package_source: LibreBiblePackageSource,
|
||||
) -> Result<(), String> {
|
||||
let verses_path = package_source.verses_path.clone();
|
||||
let tx = conn.transaction().map_err(|err| err.to_string())?;
|
||||
let mut verse_ids = HashMap::new();
|
||||
|
||||
tx.execute(
|
||||
"INSERT OR REPLACE INTO library_sources (resource_id, title, package_dir, imported_at)
|
||||
VALUES (?1, ?2, ?3, CURRENT_TIMESTAMP)",
|
||||
params![
|
||||
package_source.resource_id,
|
||||
package_source.title,
|
||||
package_source.package_dir.to_string_lossy().to_string()
|
||||
],
|
||||
)
|
||||
.map_err(|err| err.to_string())?;
|
||||
|
||||
{
|
||||
let file = File::open(&verses_path)
|
||||
.map_err(|err| format!("Unable to open {}: {err}", verses_path.display()))?;
|
||||
@@ -318,7 +575,7 @@ fn import_package(conn: &mut Connection, package_dir: PathBuf) -> Result<(), Str
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
if let Some(strongs_path) = package_source.strongs_path {
|
||||
let file = File::open(&strongs_path)
|
||||
.map_err(|err| format!("Unable to open {}: {err}", strongs_path.display()))?;
|
||||
let reader = BufReader::new(file);
|
||||
@@ -561,3 +818,44 @@ fn looks_like_strongs(query: &str) -> bool {
|
||||
let mut chars = query.chars();
|
||||
matches!(chars.next(), Some('G' | 'g' | 'H' | 'h')) && chars.all(|char| char.is_ascii_digit())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn imports_sibling_librebible_catalog_when_available() -> Result<(), String> {
|
||||
let catalog_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("..")
|
||||
.join("..")
|
||||
.join("libre-bible-data")
|
||||
.join("packages")
|
||||
.join("json")
|
||||
.join("catalog.json");
|
||||
|
||||
if !catalog_path.exists() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let source = load_package_source_from_catalog(catalog_path)?;
|
||||
let mut conn = Connection::open_in_memory().map_err(|err| err.to_string())?;
|
||||
migrate(&conn)?;
|
||||
import_package(&mut conn, source)?;
|
||||
|
||||
let verse_count: i64 = conn
|
||||
.query_row("SELECT COUNT(*) FROM verses", [], |row| row.get(0))
|
||||
.map_err(|err| err.to_string())?;
|
||||
let strongs_count: i64 = conn
|
||||
.query_row("SELECT COUNT(*) FROM verse_strongs", [], |row| row.get(0))
|
||||
.map_err(|err| err.to_string())?;
|
||||
let source_count: i64 = conn
|
||||
.query_row("SELECT COUNT(*) FROM library_sources", [], |row| row.get(0))
|
||||
.map_err(|err| err.to_string())?;
|
||||
|
||||
assert_eq!(verse_count, 31_102);
|
||||
assert_eq!(strongs_count, 348_884);
|
||||
assert_eq!(source_count, 1);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user