From efba2f28253ef08858b052e8d430a024e9320495 Mon Sep 17 00:00:00 2001 From: Manielle <75130972+Manielle@users.noreply.github.com> Date: Mon, 19 Oct 2026 19:38:22 +0000 Subject: [PATCH] gutenberg: one bind() for the search and redeem paths compute_outcomes still passed the identifier-only lookup's Option to into_discovery_record, which now takes the binding (CI: E0308). Both paths now call bind(): the identifier, else the title match for an etext Open Library lists under no work, so a redeemed record is bound exactly as its search record was. --- feeder-plugin/book-feeder/src/gutenberg.rs | 42 +++++++++++++--------- 1 file changed, 25 insertions(+), 17 deletions(-) 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,