TimeoutAndRetrySession: fold in RateLimitAdapter and 429 retry (#6731)

Follow up to https://github.com/beetbox/beets/pull/6729

Move shared rate-limiting and 429 retry logic from the lyrics-specific
`LyricsSession` into the base `TimeoutAndRetrySession` so all plugins
benefit. Simplify `lyrics.py` by removing the now-redundant
`LyricsSession` subclass.
This commit is contained in:
Šarūnas Nejus
2026-06-13 01:17:44 +01:00
committed by GitHub
3 changed files with 7 additions and 39 deletions

View File

@@ -78,15 +78,15 @@ class TimeoutAndRetrySession(requests.Session, metaclass=SingletonMeta):
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=0.25, max_retries=retry)
self.mount("https://", adapter)
self.mount("http://", adapter)

View File

@@ -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):

View File

@@ -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.