bookmeta, bibfetch, bindings: what a caller needs to trust a cover

bookmeta: Cover::guessed says the file left the choice open — a Kindle
book's first image record, or an EPUB image picked because its name says
cover — so a caller with a register to ask can prefer that. A cover the
file names is never a guess. Cover::extension matches bibfetch's.

bibfetch: Open Library answers 403 to more than a hundred covers by ISBN
in five minutes from one address. That is now Error::Limited, not
Unreachable: the other form of the ISBN is not asked, the cover id still
is, since those are not limited, and the CLI exits 75. Covers asked by
ISBN are kept three seconds apart across the process (Covers::pace), which
stays under the limit however long a run goes. A deadline too long to add
to the clock no longer panics; it is no deadline.

bindings: fetch_cover and search_title for Python, and catena.Limited,
which fetch_identifier raises too when a register refuses for being asked
too often.

corpus-core: exit 75, a service refused for now.
This commit is contained in:
funkpower committed 2026-11-13 15:44:50 -05:00
1 parent 7aa1783a3e
commit de23a7abad
14 files changed
+334 -16

No files matched your search

+9 -2
View File
@@ -350,7 +350,8 @@ fn cover<R: std::io::Read + std::io::Seek>(
&& (item.id.to_lowercase().contains("cover")
|| item.href.to_lowercase().contains("cover"))
}) {
if let Some(found) = look.image(base, &item.href) {
if let Some(mut found) = look.image(base, &item.href) {
found.guessed = true;
return Some(found);
}
}
@@ -469,6 +470,7 @@ impl<R: std::io::Read + std::io::Seek> Look<'_, R> {
Some(Cover {
media_type: media_type.to_string(),
bytes,
guessed: false,
})
}
}
@@ -767,6 +769,8 @@ mod tests {
let cover = meta.cover.expect("a cover");
assert_eq!(cover.media_type, "image/jpeg");
assert_eq!(cover.bytes, JPEG);
assert!(!cover.guessed, "a declared cover is not a guess");
assert_eq!(cover.extension(), "jpg");
assert!(meta.notes.is_empty(), "{:?}", meta.notes);
}
@@ -861,7 +865,10 @@ mod tests {
("OEBPS/images/i2.gif", GIF),
],
);
assert_eq!(meta.cover.expect("a cover").bytes, GIF);
let cover = meta.cover.expect("a cover");
assert_eq!(cover.bytes, GIF);
assert!(cover.guessed, "a cover found by its name is a guess");
assert_eq!(cover.extension(), "gif");
let by_address = opf(
r#"<item id="i9" href="images/Book_COVER.jpeg" media-type="image/jpeg"/>"#,
+18
View File
@@ -110,6 +110,24 @@ pub struct Cover {
pub media_type: String,
/// The image itself, never more than [`MAX_COVER`] bytes.
pub bytes: Vec<u8>,
/// Whether the file left the choice open. A MOBI that names no cover, or
/// an EPUB whose only cover is an image with "cover" in its name, has one
/// picked by position or by name, and that pick can be an illustration
/// from inside the book. A caller with a better source should prefer it.
pub guessed: bool,
}
impl Cover {
/// The usual file extension for the image, without the dot.
pub fn extension(&self) -> &'static str {
match self.media_type.as_str() {
"image/png" => "png",
"image/gif" => "gif",
"image/webp" => "webp",
"image/bmp" => "bmp",
_ => "jpg",
}
}
}
/// How large a cover is worth keeping.
+2
View File
@@ -342,6 +342,7 @@ fn cover(
Some(Cover {
media_type: media_type.to_string(),
bytes: data.to_vec(),
guessed: false,
})
}
@@ -361,6 +362,7 @@ fn first_image_record(bytes: &[u8], records: &[usize], first_image: Option<u32>)
Some(Cover {
media_type: media_type.to_string(),
bytes: data.to_vec(),
guessed: true,
})
}