From 0d97110f0132d78147213d11902298c516c32f16 Mon Sep 17 00:00:00 2001 From: Kris Anderson <1562427+kris-anderson@users.noreply.github.com> Date: Mon, 9 Nov 2026 15:58:01 -0800 Subject: [PATCH] fix(network): apply HTTP proxy to Anna's Archive download-count lookups (#1686) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #1685 `_fetch_download_count_inline` fetches `/dyn/md5/inline_info/` for every search result (5 workers) through a module-level `requests.Session`, with no `proxies=`. `get_proxies()` reads the app config rather than the environment, so with the proxy set in Settings → Network the search page and the bypasser used it while these requests left from the host's own IP, about 50 per search. It now passes `network.get_proxies(url)` on each request, as the Libgen fast path in the same file already does. The session stays module-level for keep-alive, and proxies are resolved per request, so a settings change applies without a restart. Verification (macOS, `uv run --frozen pytest`): - New `tests/direct_download/test_download_count_proxy.py` passes. With the source change reverted it fails (`proxies` is `None`). - `tests/direct_download`: 135 passed, including `test_download_count_reuse.py`. - `ruff check` and `ruff format --check` on the changed files pass. - Not re-run live with this branch built. The behaviour before the change was observed on a running v1.6.2 container: 5 sockets from the `ThreadPoolExecutor` threads straight to the AA mirror's IP on :443, and 1 to the proxy. 🤖 Generated with [Claude Code](https://claude.com/claude-code) ## Summary by CodeRabbit * **Bug Fixes** * Anna’s Archive download counts are now retrieved using the configured network proxies. This improves access to download-count information in environments where requests need a proxy, while preserving the existing response handling and timeout behavior. Co-authored-by: Claude Sonnet 5.5 --- .../direct_download/annas_archive.py | 7 ++- .../test_download_count_proxy.py | 43 +++++++++++++++++++ 2 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 tests/direct_download/test_download_count_proxy.py diff --git a/shelfmark/release_sources/direct_download/annas_archive.py b/shelfmark/release_sources/direct_download/annas_archive.py index 101ee8c5..a6b4f402 100644 --- a/shelfmark/release_sources/direct_download/annas_archive.py +++ b/shelfmark/release_sources/direct_download/annas_archive.py @@ -840,7 +840,12 @@ def _fetch_download_count_inline(book_id: str) -> int | None: return cached try: url = f"{network.get_aa_base_url()}/dyn/md5/inline_info/{book_id}" - resp = _inline_info_session.get(url, timeout=5, headers={"Accept": "application/json"}) + resp = _inline_info_session.get( + url, + timeout=5, + headers={"Accept": "application/json"}, + proxies=network.get_proxies(url), + ) if resp.status_code == 200: data = resp.json() count = data.get("downloads_total") diff --git a/tests/direct_download/test_download_count_proxy.py b/tests/direct_download/test_download_count_proxy.py new file mode 100644 index 00000000..6bd23d89 --- /dev/null +++ b/tests/direct_download/test_download_count_proxy.py @@ -0,0 +1,43 @@ +"""AA download-count lookups must honor the Network proxy settings. + +`_fetch_download_count_inline` runs for every search result, on its own session, so a +lookup that skips `get_proxies` sends the whole list straight from the host's IP while +the search page itself goes through the configured proxy. +""" + +from unittest.mock import MagicMock + +import pytest +import requests + +from shelfmark.release_sources.direct_download import annas_archive as aa + +_PROXIES = {"http": "http://10.0.80.3:8118", "https": "http://10.0.80.3:8118"} + + +@pytest.fixture(autouse=True) +def _fresh_cache(monkeypatch): + aa._download_count_cache.clear() + monkeypatch.setattr(aa.network, "get_aa_base_url", lambda: "https://annas-archive.gl") + yield + aa._download_count_cache.clear() + + +def test_download_count_lookup_passes_configured_proxies(monkeypatch): + asked_for: list[str] = [] + captured: dict[str, object] = {} + + def fake_get_proxies(url=""): + asked_for.append(url) + return _PROXIES + + def fake_request(_self, _method, _url, *_args, **kwargs): + captured.update(kwargs) + return MagicMock(status_code=200, json=MagicMock(return_value={"downloads_total": 7})) + + monkeypatch.setattr(aa.network, "get_proxies", fake_get_proxies) + monkeypatch.setattr(requests.Session, "request", fake_request) + + assert aa._fetch_download_count_inline("aaa") == 7 + assert captured.get("proxies") == _PROXIES + assert asked_for == ["https://annas-archive.gl/dyn/md5/inline_info/aaa"]