From 68ed8924ef7ffe52fb8750ec0d54975410d1e7fb Mon Sep 17 00:00:00 2001 From: Alok Saboo Date: Fri, 12 Jun 2026 09:43:23 -0400 Subject: [PATCH 1/2] TimeoutAndRetrySession: fold in RateLimitAdapter and 429 retry --- beetsplug/_utils/requests.py | 6 +++--- beetsplug/lyrics.py | 38 +----------------------------------- docs/changelog.rst | 4 ++++ 3 files changed, 8 insertions(+), 40 deletions(-) diff --git a/beetsplug/_utils/requests.py b/beetsplug/_utils/requests.py index fd5f8dee4..2d801d3f0 100644 --- a/beetsplug/_utils/requests.py +++ b/beetsplug/_utils/requests.py @@ -71,22 +71,22 @@ class TimeoutAndRetrySession(requests.Session, metaclass=SingletonMeta): * raises exceptions for HTTP error status codes """ - def __init__(self, *args, **kwargs) -> None: + def __init__(self, rate_limit: float = 0.25, *args, **kwargs) -> None: super().__init__(*args, **kwargs) self.headers["User-Agent"] = f"beets/{__version__} https://beets.io/" retry = Retry( total=6, backoff_factor=0.5, - # Retry on server errors status_forcelist=[ HTTPStatus.INTERNAL_SERVER_ERROR, HTTPStatus.BAD_GATEWAY, HTTPStatus.SERVICE_UNAVAILABLE, HTTPStatus.GATEWAY_TIMEOUT, + HTTPStatus.TOO_MANY_REQUESTS, ], ) - adapter = HTTPAdapter(max_retries=retry) + adapter = RateLimitAdapter(rate_limit=rate_limit, max_retries=retry) self.mount("https://", adapter) self.mount("http://", adapter) diff --git a/beetsplug/lyrics.py b/beetsplug/lyrics.py index ee3188577..0a34fa60e 100644 --- a/beetsplug/lyrics.py +++ b/beetsplug/lyrics.py @@ -24,7 +24,6 @@ from contextlib import contextmanager, suppress from dataclasses import dataclass from functools import cached_property, partial, total_ordering from html import unescape -from http import HTTPStatus from itertools import filterfalse, groupby from pathlib import Path from typing import TYPE_CHECKING, ClassVar, NamedTuple @@ -33,7 +32,6 @@ from urllib.parse import quote, quote_plus, urlencode, urlparse import requests from bs4 import BeautifulSoup from unidecode import unidecode -from urllib3.util.retry import Retry from beets import plugins, ui from beets.autotag import string_dist @@ -45,7 +43,6 @@ from beets.util.lyrics import INSTRUMENTAL_LYRICS, Lyrics from ._utils.requests import ( HTTPNotFoundError, - RateLimitAdapter, RequestHandler, TimeoutAndRetrySession, ) @@ -172,45 +169,12 @@ def slug(text: str) -> str: return re.sub(r"\W+", "-", unidecode(text).lower().strip()).strip("-") -class LyricsSession(TimeoutAndRetrySession): - """HTTP session for lyrics backends with rate limiting and exponential backoff. - - Builds on :class:`TimeoutAndRetrySession` — inherits User-Agent header, - default timeout, and response status checking. Adds a :class:`RateLimitAdapter` - to enforce a minimum interval between requests, and configures urllib3 Retry - with exponential backoff on rate limit (429) responses. - """ - - def __init__(self, rate_limit: float = 0.25): - """Initialize session with rate limiting and retry/backoff. - - Args: - rate_limit: Minimum interval in seconds between consecutive - requests (default 0.25s = 4 requests/sec). - """ - super().__init__() - retry = Retry( - total=6, - backoff_factor=1.0, - status_forcelist=[ - HTTPStatus.INTERNAL_SERVER_ERROR, - HTTPStatus.BAD_GATEWAY, - HTTPStatus.SERVICE_UNAVAILABLE, - HTTPStatus.GATEWAY_TIMEOUT, - HTTPStatus.TOO_MANY_REQUESTS, - ], - ) - adapter = RateLimitAdapter(rate_limit=rate_limit, max_retries=retry) - self.mount("https://", adapter) - self.mount("http://", adapter) - - class LyricsRequestHandler(RequestHandler): _log: Logger def create_session(self) -> TimeoutAndRetrySession: """Return a rate-limited session for lyrics HTTP requests.""" - return LyricsSession() + return TimeoutAndRetrySession() def status_to_error(self, code: int) -> type[requests.HTTPError] | None: if err := super().status_to_error(code): diff --git a/docs/changelog.rst b/docs/changelog.rst index 2ac3e0ea1..23812a7b7 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -96,6 +96,10 @@ For plugin developers Other changes ~~~~~~~~~~~~~ +- :doc:`plugins/lyrics`: Fold rate limiting and 429 retry from the + lyrics-specific ``LyricsSession`` into the shared + :class:`~beetsplug._utils.requests.TimeoutAndRetrySession` so all plugins + benefit. The standalone ``LyricsSession`` class has been removed. - :doc:`plugins/spotify`: ``spotifysync`` now batches its SQLite commit for a sync run, follows the standard beets write-before-store pattern, and logs audio-features API unavailability only once per run. From 7c3566e073a8f2f20a642391802c15abe63b5f9b Mon Sep 17 00:00:00 2001 From: Alok Saboo Date: Fri, 12 Jun 2026 10:30:04 -0400 Subject: [PATCH 2/2] fix mypy errors --- beetsplug/_utils/requests.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/beetsplug/_utils/requests.py b/beetsplug/_utils/requests.py index 2d801d3f0..33356ff8a 100644 --- a/beetsplug/_utils/requests.py +++ b/beetsplug/_utils/requests.py @@ -71,7 +71,7 @@ class TimeoutAndRetrySession(requests.Session, metaclass=SingletonMeta): * raises exceptions for HTTP error status codes """ - def __init__(self, rate_limit: float = 0.25, *args, **kwargs) -> None: + def __init__(self, *args, **kwargs) -> None: super().__init__(*args, **kwargs) self.headers["User-Agent"] = f"beets/{__version__} https://beets.io/" @@ -86,7 +86,7 @@ class TimeoutAndRetrySession(requests.Session, metaclass=SingletonMeta): HTTPStatus.TOO_MANY_REQUESTS, ], ) - adapter = RateLimitAdapter(rate_limit=rate_limit, max_retries=retry) + adapter = RateLimitAdapter(rate_limit=0.25, max_retries=retry) self.mount("https://", adapter) self.mount("http://", adapter)