diff --git a/feeder-plugin/book-feeder/src/gutenberg.rs b/feeder-plugin/book-feeder/src/gutenberg.rs index 83743cd..5da652e 100644 --- a/feeder-plugin/book-feeder/src/gutenberg.rs +++ b/feeder-plugin/book-feeder/src/gutenberg.rs @@ -44,6 +44,26 @@ struct Binding { by_title: bool, } +/// One etext's binding: the identifier Open Library records, else — only for an +/// etext it lists under no work — the title match. Shared by the search path and +/// the redeem path so the two cannot bind the same book differently. +async fn bind( + client: &OpenLibraryClient, + id: u64, + title: &str, + author: &str, + deadline: std::time::Duration, +) -> Result, GatewayError> { + Ok(match client.gutenberg_etext(id, deadline).await? { + Etext::One(work) => Some(Binding { work, by_title: false }), + Etext::Several => None, + Etext::Unlisted => client + .title_work(title, author, deadline) + .await? + .map(|work| Binding { work, by_title: true }), + }) +} + /// Canonical Gutendex base URL. Overridable via [`GutenbergPlugin::with_base_url`] /// for tests + private mirrors. const DEFAULT_BASE_URL: &str = "https://gutendex.com"; @@ -451,19 +471,7 @@ impl GutenbergPlugin { .collect(); stream::iter(wanted) .map(|(id, title, author)| async move { - // The identifier first; a title match only for an etext Open - // Library lists under no work at all. - let lookup = async { - let deadline = openlibrary_client::BINDING_DEADLINE; - Ok::<_, GatewayError>(match client.gutenberg_etext(id, deadline).await? { - Etext::One(work) => Some(Binding { work, by_title: false }), - Etext::Several => None, - Etext::Unlisted => client - .title_work(&title, &author, deadline) - .await? - .map(|work| Binding { work, by_title: true }), - }) - }; + let lookup = bind(client, id, &title, &author, openlibrary_client::BINDING_DEADLINE); match tokio::time::timeout(BINDING_TIMEOUT, lookup).await { Ok(Ok(binding)) => binding, Ok(Err(e)) => { @@ -628,9 +636,9 @@ impl FeederPlugin for GutenbergPlugin { // Not user-facing, so the binding lookup may queue for the full search // deadline. A failure still leaves the record unbound, never fails the // compute. + let author = book.authors.first().map(|a| a.name.clone()).unwrap_or_default(); let work = match self.openlibrary.as_ref() { - Some(ol) => ol - .gutenberg_work(book.id, openlibrary_client::SEARCH_DEADLINE) + Some(ol) => bind(ol, book.id, &book.title, &author, openlibrary_client::SEARCH_DEADLINE) .await .unwrap_or_else(|e| { debug!( @@ -704,8 +712,8 @@ impl FeederPlugin for GutenbergPlugin { /// Convert a `GutendexBook` into our wire-level `DiscoveryRecord`. /// -/// `openlibrary_work` is the binding from [`OpenLibraryClient::gutenberg_work`]: -/// `Some` only when Open Library records exactly one work for this etext. +/// `openlibrary_work` is the binding from [`bind`]: the one work Open Library +/// records for this etext, or, when it records none, the title match. fn into_discovery_record( book: GutendexBook, cover_cid: Option,