fix(direct_download): Anna's Archive search 404 on ISBN queries (#1456)

When ISBN filters are present, the search query is now the deduplicated
bare ISBN strings, with the free-text query appended only if it is not
already one of those tokens. Searches with no ISBN still send the quoted
free-text query alone. An Anna's Archive lookup that includes an ISBN
comes back as 404 Not Found, so the direct-download release search fails
and the book cannot be retrieved. The failing request puts an
isbn13:/isbn10: boolean expression in the q parameter.

The direct-download search query tests passed.

Closes #1444

Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
This commit is contained in:
Matt Van HornandMatt Van Horn authored and GitHub committed 2026-10-20 01:10:03 -04:00
1 parent 84d1d7196c
commit d3bcf0674d
2 files changed
+90 -4

No files matched your search

@@ -722,11 +722,17 @@ def search_books(query: str, filters: SearchFilters) -> tuple[list[BrowseRecord]
Exception: If parsing fails
"""
query_html = quote(query)
# isbn13:/isbn10: boolean syntax 404s; search the bare ISBN strings instead.
if filters.isbn:
isbns = " || ".join([f"('isbn13:{isbn}' || 'isbn10:{isbn}')" for isbn in filters.isbn])
query_html = quote(f"({isbns}) {query}")
tokens: list[str] = []
for isbn in filters.isbn:
if isbn not in tokens:
tokens.append(isbn)
if query not in tokens:
tokens.append(query)
query_html = quote(" ".join(tokens))
else:
query_html = quote(query)
filters_query = ""
@@ -1,3 +1,7 @@
from urllib.parse import parse_qs, urlparse
import pytest
from shelfmark.core.config import config
from shelfmark.core.models import SearchFilters
from shelfmark.core.search_plan import build_release_search_plan
@@ -5,6 +9,7 @@ from shelfmark.metadata_providers import BookMetadata
from shelfmark.release_sources import BrowseRecord
from shelfmark.release_sources.direct_download import DirectDownloadSource
from shelfmark.release_sources.direct_download import annas_archive as aa
from shelfmark.release_sources.direct_download.common import get_supported_formats
def _browse_record(record_id: str, title: str) -> BrowseRecord:
@@ -186,6 +191,81 @@ class TestDirectDownloadSearchQueries:
]
def _record_search_pages(monkeypatch, body: str) -> list[str]:
urls: list[str] = []
def _fake_html_get_page(url: str, selector, **_kwargs):
del selector
urls.append(url)
return body
monkeypatch.setattr(aa.network, "get_aa_base_url", lambda: "https://mirror.example")
monkeypatch.setattr(aa.network, "AAMirrorSelector", lambda: object())
monkeypatch.setattr(
aa.network, "get_available_aa_urls", lambda: ["https://a.example", "https://b.example"]
)
monkeypatch.setattr(aa, "_enrich_search_results_with_downloads", lambda _books: None)
monkeypatch.setattr(aa.downloader, "html_get_page", _fake_html_get_page)
return urls
def _decoded_search_query(url: str) -> str:
return parse_qs(urlparse(url).query)["q"][0]
def test_isbn_search_requests_the_bare_isbn(monkeypatch):
urls = _record_search_pages(monkeypatch, "No files found.")
aa.search_books("9788845269837", SearchFilters(isbn=["9788845269837"]))
url = urls[0]
assert _decoded_search_query(url) == "9788845269837"
assert "display=table" in url
assert f"&ext={'&ext='.join(get_supported_formats())}" in url
assert "isbn13" not in url
assert "isbn10" not in url
assert "||" not in url
assert "%7C" not in url
def test_isbn_search_includes_a_distinct_free_text_query(monkeypatch):
urls = _record_search_pages(monkeypatch, "No files found.")
aa.search_books("Dune", SearchFilters(isbn=["9780441172719"]))
query = _decoded_search_query(urls[0])
assert query == "9780441172719 Dune"
assert "isbn13" not in query
assert "isbn10" not in query
assert "||" not in query
def test_search_without_isbn_keeps_the_free_text_query(monkeypatch):
urls = _record_search_pages(monkeypatch, "No files found.")
aa.search_books("Dune Messiah", SearchFilters())
assert _decoded_search_query(urls[0]) == "Dune Messiah"
assert "q=Dune%20Messiah" in urls[0]
def test_duplicate_isbn_filters_are_sent_once(monkeypatch):
urls = _record_search_pages(monkeypatch, "No files found.")
aa.search_books("Dune", SearchFilters(isbn=["9780441172719", "9780441172719"]))
assert _decoded_search_query(urls[0]) == "9780441172719 Dune"
def test_empty_isbn_search_page_is_terminal(monkeypatch):
urls = _record_search_pages(monkeypatch, "")
with pytest.raises(aa.SearchUnavailableError):
aa.search_books("9788845269837", SearchFilters(isbn=["9788845269837"]))
assert len(urls) == 1
# --- Distant-path language detection tests ---