From 1e18b4d75c99ce8bbced549247cdc4d2c771c500 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0ar=C5=ABnas=20Nejus?= Date: Sun, 28 Sep 2025 13:17:43 +0100 Subject: [PATCH] Define MusicBrainzAPI class with rate limiting --- beetsplug/musicbrainz.py | 21 +++++++++++++++++++++ poetry.lock | 35 ++++++++++++++++++++++++++++++++++- pyproject.toml | 1 + 3 files changed, 56 insertions(+), 1 deletion(-) diff --git a/beetsplug/musicbrainz.py b/beetsplug/musicbrainz.py index 8e259e94b..9a1bef80f 100644 --- a/beetsplug/musicbrainz.py +++ b/beetsplug/musicbrainz.py @@ -26,6 +26,7 @@ from urllib.parse import urljoin import musicbrainzngs from confuse.exceptions import NotFoundError +from requests_ratelimiter import LimiterMixin import beets import beets.autotag.hooks @@ -33,6 +34,8 @@ from beets import config, plugins, util from beets.metadata_plugins import MetadataSourcePlugin from beets.util.id_extractors import extract_release_id +from ._utils.requests import TimeoutSession + if TYPE_CHECKING: from typing import Literal @@ -55,6 +58,11 @@ FIELDS_TO_MB_KEYS = { "year": "date", } + +class LimiterTimeoutSession(LimiterMixin, TimeoutSession): + pass + + musicbrainzngs.set_useragent("beets", beets.__version__, "https://beets.io/") @@ -118,6 +126,19 @@ BROWSE_CHUNKSIZE = 100 BROWSE_MAXTRACKS = 500 +class MusicBrainzAPI: + api_url = "https://musicbrainz.org/ws/2/" + + @cached_property + def session(self) -> LimiterTimeoutSession: + return LimiterTimeoutSession(per_second=1) + + def _get(self, entity: str, **kwargs) -> JSONDict: + return self.session.get( + f"{self.api_url}/{entity}", params={**kwargs, "fmt": "json"} + ).json() + + def _preferred_alias(aliases: list[JSONDict]): """Given an list of alias structures for an artist credit, select and return the user's preferred alias alias or None if no matching diff --git a/poetry.lock b/poetry.lock index 615598d67..cb87e5f49 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2392,6 +2392,21 @@ docs = ["sphinx", "sphinx_rtd_theme"] fuzzer = ["atheris", "hypothesis"] test = ["coverage[toml] (>=5.2)", "hypothesis", "pytest (>=6.0)", "pytest-benchmark", "pytest-cov", "pytest-timeout"] +[[package]] +name = "pyrate-limiter" +version = "2.10.0" +description = "Python Rate-Limiter using Leaky-Bucket Algorithm" +optional = false +python-versions = ">=3.7,<4.0" +files = [ + {file = "pyrate_limiter-2.10.0-py3-none-any.whl", hash = "sha256:a99e52159f5ed5eb58118bed8c645e30818e7c0e0d127a0585c8277c776b0f7f"}, + {file = "pyrate_limiter-2.10.0.tar.gz", hash = "sha256:98cc52cdbe058458e945ae87d4fd5a73186497ffa545ee6e98372f8599a5bd34"}, +] + +[package.extras] +all = ["filelock (>=3.0)", "redis (>=3.3,<4.0)", "redis-py-cluster (>=2.1.3,<3.0.0)"] +docs = ["furo (>=2022.3.4,<2023.0.0)", "myst-parser (>=0.17)", "sphinx (>=4.3.0,<5.0.0)", "sphinx-autodoc-typehints (>=1.17,<2.0)", "sphinx-copybutton (>=0.5)", "sphinxcontrib-apidoc (>=0.3,<0.4)"] + [[package]] name = "pytest" version = "8.4.1" @@ -2883,6 +2898,24 @@ requests = ">=2.0.0" [package.extras] rsa = ["oauthlib[signedtoken] (>=3.0.0)"] +[[package]] +name = "requests-ratelimiter" +version = "0.7.0" +description = "Rate-limiting for the requests library" +optional = false +python-versions = "<4.0,>=3.7" +files = [ + {file = "requests_ratelimiter-0.7.0-py3-none-any.whl", hash = "sha256:1a7ef2faaa790272722db8539728690046237766fcc479f85b9591e5356a8185"}, + {file = "requests_ratelimiter-0.7.0.tar.gz", hash = "sha256:a070c8a359a6f3a001b0ccb08f17228b7ae0a6e21d8df5b6f6bd58389cddde45"}, +] + +[package.dependencies] +pyrate-limiter = "<3.0" +requests = ">=2.20" + +[package.extras] +docs = ["furo (>=2023.3,<2024.0)", "myst-parser (>=1.0)", "sphinx (>=5.2,<6.0)", "sphinx-autodoc-typehints (>=1.22,<2.0)", "sphinx-copybutton (>=0.5)"] + [[package]] name = "resampy" version = "0.4.3" @@ -3683,4 +3716,4 @@ web = ["flask", "flask-cors"] [metadata] lock-version = "2.0" python-versions = ">=3.9,<4" -content-hash = "aedfeb1ac78ae0120855c6a7d6f35963c63cc50a8750142c95dd07ffd213683f" +content-hash = "2a77524f06c0a8feecc47a0ac64faf7e2f170d1c99329130bfc5af48691178b2" diff --git a/pyproject.toml b/pyproject.toml index b546b4dc2..703d3d613 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -52,6 +52,7 @@ musicbrainzngs = ">=0.4" numpy = ">=1.24.4" platformdirs = ">=3.5.0" pyyaml = "*" +requests-ratelimiter = ">=0.7.0" typing_extensions = "*" unidecode = ">=1.3.6"