mirror of
https://git.pumpin.online/funkpower/catena.git
synced 2026-11-13 23:55:40 +00:00
bibfetch: a book's cover from Open Library, by its ISBN
`bibfetch::cover(&agent, isbn, cover_id, size)` asks Open Library's covers store by the ISBN's thirteen-digit form, then its ten-digit form, then by a cover id a lookup record named, and returns the image or `Ok(None)`. The answer is checked before it is trusted: JPEG or PNG by content type and by magic bytes, between 1 KB and 10 MB, at most three redirects, none of them off HTTPS. One request at a time per process; a 429 or 503 is waited out for its Retry-After, up to a minute. `idnorm::isbn` converts between the two forms, and only for an ISBN whose check digit agrees. Lookup records carry `cover_id` from the edition record and the search index. The CLI gains `bibfetch cover <isbn> -o <file>`.
This commit is contained in:
7 files changed
+1162
-14
No files matched your search
@@ -88,7 +88,7 @@ impl Source {
|
||||
Source::OpenLibrarySearch => format!(
|
||||
"https://openlibrary.org/search.json?q=isbn:{}\
|
||||
&fields=title,subtitle,author_name,first_publish_year,publisher,language,subject,\
|
||||
number_of_pages_median&limit=1",
|
||||
number_of_pages_median,cover_i&limit=1",
|
||||
encode(id)
|
||||
),
|
||||
Source::GoogleBooks => format!(
|
||||
@@ -245,6 +245,9 @@ struct OpenLibraryBook {
|
||||
languages: Vec<OpenLibraryKey>,
|
||||
#[serde(default)]
|
||||
number_of_pages: Option<u32>,
|
||||
/// Cover ids, best first. A deleted cover is left in the list as `-1`.
|
||||
#[serde(default)]
|
||||
covers: Vec<i64>,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
@@ -275,6 +278,10 @@ fn open_library(id: &str, kind: &str, body: &str) -> Result<Record, Error> {
|
||||
.find_map(|l| l.key.rsplit('/').next().map(str::to_string))
|
||||
.filter(|l| !l.is_empty());
|
||||
record.pages = book.number_of_pages;
|
||||
record.cover_id = book
|
||||
.covers
|
||||
.into_iter()
|
||||
.find_map(|id| u64::try_from(id).ok().filter(|id| *id > 0));
|
||||
Ok(record)
|
||||
}
|
||||
|
||||
@@ -302,6 +309,8 @@ struct SearchDoc {
|
||||
subject: Vec<String>,
|
||||
#[serde(rename = "number_of_pages_median", default)]
|
||||
pages: Option<u32>,
|
||||
#[serde(default)]
|
||||
cover_i: Option<i64>,
|
||||
}
|
||||
|
||||
/// How many of a work's subject headings are worth carrying.
|
||||
@@ -333,6 +342,10 @@ fn open_library_search(id: &str, kind: &str, body: &str) -> Result<Record, Error
|
||||
record.subjects = doc.subject;
|
||||
record.subjects.truncate(SUBJECT_LIMIT);
|
||||
record.pages = doc.pages;
|
||||
record.cover_id = doc
|
||||
.cover_i
|
||||
.and_then(|id| u64::try_from(id).ok())
|
||||
.filter(|id| *id > 0);
|
||||
Ok(record)
|
||||
}
|
||||
|
||||
@@ -520,6 +533,29 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_cover_id_a_record_names_is_kept_for_the_cover_lookup() {
|
||||
// A deleted cover stays in an edition's list as -1, ahead of the
|
||||
// ones still there.
|
||||
let record = open_library("1", "isbn", r#"{"title":"A","covers":[-1,1162784,99]}"#)
|
||||
.expect("a record");
|
||||
assert_eq!(record.cover_id, Some(1162784));
|
||||
let record = open_library("1", "isbn", r#"{"title":"A"}"#).expect("a record");
|
||||
assert_eq!(record.cover_id, None);
|
||||
|
||||
let record = open_library_search("1", "isbn", r#"{"docs":[{"title":"A","cover_i":2468}]}"#)
|
||||
.expect("a record");
|
||||
assert_eq!(record.cover_id, Some(2468));
|
||||
let record = open_library_search("1", "isbn", r#"{"docs":[{"title":"A","cover_i":-1}]}"#)
|
||||
.expect("a record");
|
||||
assert_eq!(record.cover_id, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_index_is_asked_for_the_cover_id() {
|
||||
assert!(Source::OpenLibrarySearch.url("1").contains("cover_i"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_answer_that_is_not_what_it_should_be_says_so() {
|
||||
assert!(matches!(
|
||||
|
||||
Reference in new issue
Block a user