diff --git a/shelfmark/release_sources/direct_download/annas_archive.py b/shelfmark/release_sources/direct_download/annas_archive.py index c3855c2..6978663 100644 --- a/shelfmark/release_sources/direct_download/annas_archive.py +++ b/shelfmark/release_sources/direct_download/annas_archive.py @@ -768,6 +768,9 @@ def search_books(query: str, filters: SearchFilters) -> list[BrowseRecord]: provider_id="annas_archive", item_selector="tr", extract_item=_extract_aa_search_result, + # AA already applied &lang= server-side; only the path-language pass below + # (which skips &lang=) needs a local language filter. + filter_languages=False, ) if path_language_enabled and requested_langs: diff --git a/shelfmark/release_sources/direct_download/common.py b/shelfmark/release_sources/direct_download/common.py index e134894..f68d77e 100644 --- a/shelfmark/release_sources/direct_download/common.py +++ b/shelfmark/release_sources/direct_download/common.py @@ -188,13 +188,19 @@ def parse_search_items( *, provider_id: str, extract_item: Callable[[Tag], ParsedSearchResult | None], + filter_languages: bool = True, ) -> list[BrowseRecord]: """Normalize provider-specific HTML elements into Direct Download records. Providers only describe how fields are extracted from their DOM. Language and format filtering, stable IDs, and BrowseRecord construction stay shared. + + Pass ``filter_languages=False`` when the site already filtered by language: its + language cells are free text, and re-matching them locally drops rows it matched. """ - requested_languages = normalize_requested_languages(filters.lang) if filters else set() + requested_languages = ( + normalize_requested_languages(filters.lang) if filters and filter_languages else set() + ) requested_formats = ( {value.casefold() for value in (filters.format or get_supported_formats())} if filters @@ -253,6 +259,7 @@ def parse_search_page( provider_id: str, item_selector: str, extract_item: Callable[[Tag], ParsedSearchResult | None], + filter_languages: bool = True, ) -> list[BrowseRecord]: """Parse a result page using provider-specific selectors and extraction.""" root = BeautifulSoup(page, "html.parser") if isinstance(page, str) else page @@ -261,4 +268,5 @@ def parse_search_page( filters, provider_id=provider_id, extract_item=extract_item, + filter_languages=filter_languages, ) diff --git a/shelfmark/release_sources/direct_download/source.py b/shelfmark/release_sources/direct_download/source.py index 9a23bfd..022cd14 100644 --- a/shelfmark/release_sources/direct_download/source.py +++ b/shelfmark/release_sources/direct_download/source.py @@ -173,7 +173,7 @@ class DirectDownloadSource(ReleaseSource): raise DirectDownloadUnavailableError(unavailable_reason) releases: list[Release] = [] - unavailable_errors: list[SourceUnavailableError] = [] + failures: list[Exception] = [] for provider in registry.enabled_providers(self._providers): try: records = provider.search( @@ -183,7 +183,7 @@ class DirectDownloadSource(ReleaseSource): content_type=content_type, ) except SourceUnavailableError as exc: - unavailable_errors.append(exc) + failures.append(exc) continue except ( RuntimeError, @@ -192,11 +192,14 @@ class DirectDownloadSource(ReleaseSource): requests.exceptions.RequestException, ) as exc: logger.warning("%s search failed: %s", provider.display_name, exc) + failures.append(exc) continue releases.extend(_browse_record_to_release(record) for record in records) - if unavailable_errors and not releases: - raise unavailable_errors[0] + # A provider failure is only quiet when another provider answered. Otherwise the + # caller has to see it, or a failed search reads as a search with no hits. + if failures and not releases: + raise failures[0] return releases def is_available(self) -> bool: diff --git a/tests/direct_download/test_search_queries.py b/tests/direct_download/test_search_queries.py index fef3fcd..0a7ad3c 100644 --- a/tests/direct_download/test_search_queries.py +++ b/tests/direct_download/test_search_queries.py @@ -350,3 +350,38 @@ def test_book_matches_requested_languages_logic(): assert aa._book_matches_requested_languages(None, set()) is True assert aa._book_matches_requested_languages("en", {"fr"}) is False assert aa._book_matches_requested_languages("fr", {"fr"}) is True + + +def test_search_books_keeps_server_language_matches_when_path_language_disabled(monkeypatch): + monkeypatch.setattr(aa, "_is_language_from_path_enabled", lambda: False) + monkeypatch.setattr(aa.network, "get_aa_base_url", lambda: "https://mirror.example") + monkeypatch.setattr(aa.network, "AAMirrorSelector", lambda: object()) + + captured_url: dict[str, str] = {} + + def _row(record_id: str, language: str) -> str: + return f""" +
