-
Notifications
You must be signed in to change notification settings - Fork 1.9k
Make musicbrainz plugin talk to musicbrainz directly #6052
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
snejus
wants to merge
13
commits into
master
Choose a base branch
from
use-musicbrainz-api-directly
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
13 commits
Select commit
Hold shift + click to select a range
af7dbb8
Move TimeoutSession under beetsplug._utils
snejus 1e18b4d
Define MusicBrainzAPI class with rate limiting
snejus ee4e6e5
Add missing blame ignore revs from musicbrainz plugin
snejus 1912cad
Move pseudo release lookup under the plugin
snejus 15d72ed
musicbrainz: lookup release directly
snejus 2ef1722
musicbrainz: lookup recordings directly
snejus 51810bf
musicbrainz: search directly
snejus 82a77f0
musicbrainz: browse directly
snejus 87a074e
musicbrainz: access the custom server directly, if configured
snejus d8f201e
musicbrainz: remove error handling
snejus 69dc06d
Make musicbrainzngs dependency optional and requests required
snejus 71b20ae
Refactor HTTP request handling with RequestHandler base class
snejus 9aee0b1
Add Usage block to RequestHandler
snejus File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,160 @@ | ||
| from __future__ import annotations | ||
|
|
||
| import atexit | ||
| import threading | ||
| from contextlib import contextmanager | ||
| from functools import cached_property | ||
| from http import HTTPStatus | ||
| from typing import TYPE_CHECKING, Any, ClassVar, Generic, Protocol, TypeVar | ||
|
|
||
| import requests | ||
|
|
||
| from beets import __version__ | ||
|
|
||
| if TYPE_CHECKING: | ||
| from collections.abc import Iterator | ||
|
|
||
|
|
||
| class BeetsHTTPError(requests.exceptions.HTTPError): | ||
| STATUS: ClassVar[HTTPStatus] | ||
|
|
||
| def __init__(self, *args, **kwargs) -> None: | ||
| super().__init__( | ||
| f"HTTP Error: {self.STATUS.value} {self.STATUS.phrase}", | ||
| *args, | ||
| **kwargs, | ||
| ) | ||
|
|
||
|
|
||
| class HTTPNotFoundError(BeetsHTTPError): | ||
| STATUS = HTTPStatus.NOT_FOUND | ||
|
|
||
|
|
||
| class Closeable(Protocol): | ||
| """Protocol for objects that have a close method.""" | ||
|
|
||
| def close(self) -> None: ... | ||
|
|
||
|
|
||
| C = TypeVar("C", bound=Closeable) | ||
|
|
||
|
|
||
| class SingletonMeta(type, Generic[C]): | ||
| """Metaclass ensuring a single shared instance per class. | ||
|
|
||
| Creates one instance per class type on first instantiation, reusing it | ||
| for all subsequent calls. Automatically registers cleanup on program exit | ||
| for proper resource management. | ||
| """ | ||
|
|
||
| _instances: ClassVar[dict[type[Any], Any]] = {} | ||
| _lock: ClassVar[threading.Lock] = threading.Lock() | ||
|
|
||
| def __call__(cls, *args: Any, **kwargs: Any) -> C: | ||
| if cls not in cls._instances: | ||
| with cls._lock: | ||
| if cls not in SingletonMeta._instances: | ||
| instance = super().__call__(*args, **kwargs) | ||
| SingletonMeta._instances[cls] = instance | ||
| atexit.register(instance.close) | ||
| return SingletonMeta._instances[cls] | ||
|
|
||
|
|
||
| class TimeoutSession(requests.Session, metaclass=SingletonMeta): | ||
| """HTTP session with automatic timeout and status checking. | ||
|
|
||
| Extends requests.Session to provide sensible defaults for beets HTTP | ||
| requests: automatic timeout enforcement, status code validation, and | ||
| proper user agent identification. | ||
| """ | ||
|
|
||
| def __init__(self, *args, **kwargs) -> None: | ||
| super().__init__(*args, **kwargs) | ||
| self.headers["User-Agent"] = f"beets/{__version__} https://beets.io/" | ||
|
|
||
| def request(self, *args, **kwargs): | ||
| """Execute HTTP request with automatic timeout and status validation. | ||
|
|
||
| Ensures all requests have a timeout (defaults to 10 seconds) and raises | ||
| an exception for HTTP error status codes. | ||
| """ | ||
| kwargs.setdefault("timeout", 10) | ||
| r = super().request(*args, **kwargs) | ||
| r.raise_for_status() | ||
|
|
||
| return r | ||
|
|
||
|
|
||
| class RequestHandler: | ||
| """Manages HTTP requests with custom error handling and session management. | ||
snejus marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| Provides a reusable interface for making HTTP requests with automatic | ||
| conversion of standard HTTP errors to beets-specific exceptions. Supports | ||
| custom session types and error mappings that can be overridden by | ||
| subclasses. | ||
|
|
||
| Usage: | ||
| Subclass and override :class:`RequestHandler.session_type`, | ||
| :class:`RequestHandler.explicit_http_errors` or | ||
| :class:`RequestHandler.status_to_error()` to customize behavior. | ||
|
|
||
| Use :class:`RequestHandler.get()` or | ||
| :class:`RequestHandler.fetch_json()` for common operations, or | ||
| :class:`RequestHandler.request()` for full control over HTTP methods. | ||
|
|
||
| Feel free to define common methods that are used in multiple plugins. | ||
| """ | ||
|
|
||
| session_type: ClassVar[type[TimeoutSession]] = TimeoutSession | ||
| explicit_http_errors: ClassVar[list[type[BeetsHTTPError]]] = [ | ||
| HTTPNotFoundError | ||
| ] | ||
|
|
||
| @cached_property | ||
| def session(self) -> Any: | ||
| """Lazily initialize and cache the HTTP session.""" | ||
| return self.session_type() | ||
|
|
||
| def status_to_error( | ||
| self, code: int | ||
| ) -> type[requests.exceptions.HTTPError] | None: | ||
| """Map HTTP status codes to beets-specific exception types. | ||
|
|
||
| Searches the configured explicit HTTP errors for a matching status code. | ||
| Returns None if no specific error type is registered for the given code. | ||
| """ | ||
| return next( | ||
| (e for e in self.explicit_http_errors if e.STATUS == code), None | ||
| ) | ||
|
|
||
| @contextmanager | ||
| def handle_http_error(self) -> Iterator[None]: | ||
| """Convert standard HTTP errors to beets-specific exceptions. | ||
|
|
||
| Wraps operations that may raise HTTPError, automatically translating | ||
| recognized status codes into their corresponding beets exception types. | ||
| Unrecognized errors are re-raised unchanged. | ||
| """ | ||
| try: | ||
| yield | ||
| except requests.exceptions.HTTPError as e: | ||
| if beets_error := self.status_to_error(e.response.status_code): | ||
| raise beets_error(response=e.response) from e | ||
| raise | ||
|
|
||
| def request(self, method: str, *args, **kwargs) -> requests.Response: | ||
| """Perform HTTP request using the session with automatic error handling. | ||
|
|
||
| Delegates to the underlying session method while converting recognized | ||
| HTTP errors to beets-specific exceptions through the error handler. | ||
| """ | ||
| with self.handle_http_error(): | ||
| return getattr(self.session, method)(*args, **kwargs) | ||
|
|
||
| def get(self, *args, **kwargs) -> requests.Response: | ||
| """Perform HTTP GET request with automatic error handling.""" | ||
| return self.request("get", *args, **kwargs) | ||
|
|
||
| def fetch_json(self, *args, **kwargs): | ||
| """Fetch and parse JSON data from an HTTP endpoint.""" | ||
| return self.get(*args, **kwargs).json() | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.