diff --git a/booru_viewer/core/api/base.py b/booru_viewer/core/api/base.py index 62c3fd7..be3b0b4 100644 --- a/booru_viewer/core/api/base.py +++ b/booru_viewer/core/api/base.py @@ -37,6 +37,9 @@ class BooruClient(ABC): api_type: str = "" + # Shared client across all BooruClient instances for connection reuse + _shared_client: httpx.AsyncClient | None = None + def __init__( self, base_url: str, @@ -46,26 +49,25 @@ class BooruClient(ABC): self.base_url = base_url.rstrip("/") self.api_key = api_key self.api_user = api_user - self._client: httpx.AsyncClient | None = None @property def client(self) -> httpx.AsyncClient: - if self._client is None or self._client.is_closed: - self._client = httpx.AsyncClient( + if BooruClient._shared_client is None or BooruClient._shared_client.is_closed: + BooruClient._shared_client = httpx.AsyncClient( headers={"User-Agent": USER_AGENT}, follow_redirects=True, timeout=20.0, event_hooks={"request": [self._log_request]}, + limits=httpx.Limits(max_connections=10, max_keepalive_connections=5), ) - return self._client + return BooruClient._shared_client @staticmethod async def _log_request(request: httpx.Request) -> None: log_connection(str(request.url)) async def close(self) -> None: - if self._client and not self._client.is_closed: - await self._client.aclose() + pass # shared client stays open @abstractmethod async def search( diff --git a/booru_viewer/core/api/detect.py b/booru_viewer/core/api/detect.py index c34c995..418be37 100644 --- a/booru_viewer/core/api/detect.py +++ b/booru_viewer/core/api/detect.py @@ -23,11 +23,17 @@ async def detect_site_type( """ url = url.rstrip("/") - async with httpx.AsyncClient( - headers={"User-Agent": USER_AGENT}, - follow_redirects=True, - timeout=10.0, - ) as client: + from .base import BooruClient as _BC + # Reuse shared client for site detection + if _BC._shared_client is None or _BC._shared_client.is_closed: + _BC._shared_client = httpx.AsyncClient( + headers={"User-Agent": USER_AGENT}, + follow_redirects=True, + timeout=20.0, + limits=httpx.Limits(max_connections=10, max_keepalive_connections=5), + ) + client = _BC._shared_client + if True: # keep indent level # Try Danbooru / e621 first — /posts.json is a definitive endpoint try: params: dict = {"limit": 1} diff --git a/booru_viewer/core/api/e621.py b/booru_viewer/core/api/e621.py index b465ea1..b7975fe 100644 --- a/booru_viewer/core/api/e621.py +++ b/booru_viewer/core/api/e621.py @@ -15,19 +15,23 @@ log = logging.getLogger("booru") class E621Client(BooruClient): api_type = "e621" + _e621_client: httpx.AsyncClient | None = None + _e621_ua: str = "" + @property def client(self) -> httpx.AsyncClient: - if self._client is None or self._client.is_closed: - # e621 requires a descriptive User-Agent with username - ua = USER_AGENT - if self.api_user: - ua = f"{USER_AGENT} (by {self.api_user} on e621)" - self._client = httpx.AsyncClient( + ua = USER_AGENT + if self.api_user: + ua = f"{USER_AGENT} (by {self.api_user} on e621)" + if E621Client._e621_client is None or E621Client._e621_client.is_closed or E621Client._e621_ua != ua: + E621Client._e621_ua = ua + E621Client._e621_client = httpx.AsyncClient( headers={"User-Agent": ua}, follow_redirects=True, timeout=20.0, + limits=httpx.Limits(max_connections=10, max_keepalive_connections=5), ) - return self._client + return E621Client._e621_client async def search( self, tags: str = "", page: int = 1, limit: int = DEFAULT_PAGE_SIZE