Files
manga-mover-and-metadata-co…/src/MangaBakaRateLimit.py
T
2026-05-26 21:03:37 +02:00

93 lines
3.0 KiB
Python

"""
mangabaka_rate_limit.py
=======================
Process-wide rate limiter for the MangaBaka API.
Apply via:
from MangaBakaRateLimit import apply_to_session
apply_to_session(session)
This mounts a custom ``requests.adapters.HTTPAdapter`` on the given
``requests.Session`` for the ``api.mangabaka.dev`` host. Every request
going through that adapter is:
* throttled so that no two requests are dispatched within
``_MIN_INTERVAL`` seconds of one another, and
* retried on HTTP 429, honouring the ``Retry-After`` header when
present, otherwise exponential backoff capped at ``_MAX_BACKOFF``.
Throttle state is module-global, so even if several sessions exist in
the same process they share one budget — important because they all hit
the same upstream IP-based limit.
"""
from __future__ import annotations
import threading
import time
from requests.adapters import HTTPAdapter
# Tune these if MangaBaka tightens or loosens limits.
_MIN_INTERVAL = 1.1 # seconds between consecutive requests
_MAX_RETRIES = 6 # retries on 429 before giving up
_MAX_BACKOFF = 60.0 # cap on per-attempt backoff sleep
# --- shared throttle state --------------------------------------------------
_state_lock = threading.Lock()
_last_request_time = 0.0
def _wait_for_slot() -> None:
"""Block until the next request slot is available, then reserve it."""
global _last_request_time
while True:
with _state_lock:
now = time.monotonic()
wait = _MIN_INTERVAL - (now - _last_request_time)
if wait <= 0:
_last_request_time = now
return
time.sleep(wait)
class _MangaBakaRateLimitAdapter(HTTPAdapter):
def send(self, request, **kwargs):
response = None
for attempt in range(_MAX_RETRIES + 1):
_wait_for_slot()
response = super().send(request, **kwargs)
if response.status_code != 429:
return response
retry_after = response.headers.get("Retry-After")
try:
wait = (float(retry_after) if retry_after
else min(_MAX_BACKOFF, 2.0 * (2 ** attempt)))
except ValueError:
wait = min(_MAX_BACKOFF, 2.0 * (2 ** attempt))
print(f"[MangaBaka] 429 — backing off {wait:.1f}s "
f"(attempt {attempt + 1}/{_MAX_RETRIES})",
flush=True)
response.close()
time.sleep(wait)
# Retries exhausted — let the caller deal with the last 429.
return response
def apply_to_session(session) -> None:
"""
Mount the rate-limit adapter on ``session`` so every MangaBaka call
is automatically throttled. Safe to call multiple times (later mounts
just replace the earlier adapter for the same prefix).
"""
adapter = _MangaBakaRateLimitAdapter()
session.mount("https://api.mangabaka.dev/", adapter)
session.mount("http://api.mangabaka.dev/", adapter)