-
Notifications
You must be signed in to change notification settings - Fork 1.9k
Centralise common autotagger search functionality #5982
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
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -40,12 +40,13 @@ | |||||||||||||||||||
| from beets import config | ||||||||||||||||||||
| from beets.autotag.distance import string_dist | ||||||||||||||||||||
| from beets.autotag.hooks import AlbumInfo, TrackInfo | ||||||||||||||||||||
| from beets.metadata_plugins import MetadataSourcePlugin | ||||||||||||||||||||
| from beets.metadata_plugins import IDResponse, SearchApiMetadataSourcePlugin | ||||||||||||||||||||
|
|
||||||||||||||||||||
| if TYPE_CHECKING: | ||||||||||||||||||||
| from collections.abc import Callable, Iterable, Sequence | ||||||||||||||||||||
| from collections.abc import Callable, Iterable, Iterator, Sequence | ||||||||||||||||||||
|
|
||||||||||||||||||||
| from beets.library import Item | ||||||||||||||||||||
| from beets.metadata_plugins import QueryType, SearchParams | ||||||||||||||||||||
|
|
||||||||||||||||||||
| USER_AGENT = f"beets/{beets.__version__} +https://beets.io/" | ||||||||||||||||||||
| API_KEY = "rAzVUQYRaoFjeBjyWuWZ" | ||||||||||||||||||||
|
|
@@ -121,7 +122,7 @@ def __init__( | |||||||||||||||||||
| super().__init__(**kwargs) | ||||||||||||||||||||
|
|
||||||||||||||||||||
|
|
||||||||||||||||||||
| class DiscogsPlugin(MetadataSourcePlugin): | ||||||||||||||||||||
| class DiscogsPlugin(SearchApiMetadataSourcePlugin[IDResponse]): | ||||||||||||||||||||
| def __init__(self): | ||||||||||||||||||||
| super().__init__() | ||||||||||||||||||||
| self.config.add( | ||||||||||||||||||||
|
|
@@ -211,11 +212,6 @@ def authenticate(self, c_key: str, c_secret: str) -> tuple[str, str]: | |||||||||||||||||||
|
|
||||||||||||||||||||
| return token, secret | ||||||||||||||||||||
|
|
||||||||||||||||||||
| def candidates( | ||||||||||||||||||||
| self, items: Sequence[Item], artist: str, album: str, va_likely: bool | ||||||||||||||||||||
| ) -> Iterable[AlbumInfo]: | ||||||||||||||||||||
| return self.get_albums(f"{artist} {album}" if va_likely else album) | ||||||||||||||||||||
|
|
||||||||||||||||||||
| def get_track_from_album( | ||||||||||||||||||||
| self, album_info: AlbumInfo, compare: Callable[[TrackInfo], float] | ||||||||||||||||||||
| ) -> TrackInfo | None: | ||||||||||||||||||||
|
|
@@ -232,21 +228,19 @@ def get_track_from_album( | |||||||||||||||||||
|
|
||||||||||||||||||||
| def item_candidates( | ||||||||||||||||||||
| self, item: Item, artist: str, title: str | ||||||||||||||||||||
| ) -> Iterable[TrackInfo]: | ||||||||||||||||||||
| ) -> Iterator[TrackInfo]: | ||||||||||||||||||||
| albums = self.candidates([item], artist, title, False) | ||||||||||||||||||||
|
|
||||||||||||||||||||
| def compare_func(track_info: TrackInfo) -> float: | ||||||||||||||||||||
| return string_dist(track_info.title, title) | ||||||||||||||||||||
|
|
||||||||||||||||||||
| tracks = (self.get_track_from_album(a, compare_func) for a in albums) | ||||||||||||||||||||
| return list(filter(None, tracks)) | ||||||||||||||||||||
| return filter(None, tracks) | ||||||||||||||||||||
|
|
||||||||||||||||||||
| def album_for_id(self, album_id: str) -> AlbumInfo | None: | ||||||||||||||||||||
| """Fetches an album by its Discogs ID and returns an AlbumInfo object | ||||||||||||||||||||
| or None if the album is not found. | ||||||||||||||||||||
| """ | ||||||||||||||||||||
| self._log.debug("Searching for release {}", album_id) | ||||||||||||||||||||
|
|
||||||||||||||||||||
| discogs_id = self._extract_id(album_id) | ||||||||||||||||||||
|
|
||||||||||||||||||||
| if not discogs_id: | ||||||||||||||||||||
|
|
@@ -280,29 +274,25 @@ def track_for_id(self, track_id: str) -> TrackInfo | None: | |||||||||||||||||||
|
|
||||||||||||||||||||
| return None | ||||||||||||||||||||
|
|
||||||||||||||||||||
| def get_albums(self, query: str) -> Iterable[AlbumInfo]: | ||||||||||||||||||||
| def get_search_filters( | ||||||||||||||||||||
| self, | ||||||||||||||||||||
| query_type: QueryType, | ||||||||||||||||||||
| items: Sequence[Item], | ||||||||||||||||||||
| artist: str, | ||||||||||||||||||||
| name: str, | ||||||||||||||||||||
| va_likely: bool, | ||||||||||||||||||||
| ) -> tuple[str, dict[str, str]]: | ||||||||||||||||||||
| if va_likely: | ||||||||||||||||||||
| artist = items[0].artist | ||||||||||||||||||||
|
Comment on lines
+285
to
+286
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. question (bug_risk): Possible unintended override of artist for VA releases. Using items[0].artist for VA releases could be inaccurate if items include multiple artists. Please verify that this logic aligns with the expected handling of VA releases. |
||||||||||||||||||||
|
|
||||||||||||||||||||
| return f"{artist} - {name}", {"type": "release"} | ||||||||||||||||||||
|
|
||||||||||||||||||||
| def get_search_response(self, params: SearchParams) -> Sequence[IDResponse]: | ||||||||||||||||||||
| """Returns a list of AlbumInfo objects for a discogs search query.""" | ||||||||||||||||||||
| # Strip non-word characters from query. Things like "!" and "-" can | ||||||||||||||||||||
| # cause a query to return no results, even if they match the artist or | ||||||||||||||||||||
| # album title. Use `re.UNICODE` flag to avoid stripping non-english | ||||||||||||||||||||
| # word characters. | ||||||||||||||||||||
| query = re.sub(r"(?u)\W+", " ", query) | ||||||||||||||||||||
| # Strip medium information from query, Things like "CD1" and "disk 1" | ||||||||||||||||||||
| # can also negate an otherwise positive result. | ||||||||||||||||||||
| query = re.sub(r"(?i)\b(CD|disc|vinyl)\s*\d+", "", query) | ||||||||||||||||||||
|
|
||||||||||||||||||||
| try: | ||||||||||||||||||||
| results = self.discogs_client.search(query, type="release") | ||||||||||||||||||||
| results.per_page = self.config["search_limit"].get() | ||||||||||||||||||||
| releases = results.page(1) | ||||||||||||||||||||
| except CONNECTION_ERRORS: | ||||||||||||||||||||
| self._log.debug( | ||||||||||||||||||||
| "Communication error while searching for {0!r}", | ||||||||||||||||||||
| query, | ||||||||||||||||||||
| exc_info=True, | ||||||||||||||||||||
| ) | ||||||||||||||||||||
| return [] | ||||||||||||||||||||
| return filter(None, map(self.get_album_info, releases)) | ||||||||||||||||||||
| limit = params.filters.pop("limit") | ||||||||||||||||||||
| results = self.discogs_client.search(params.query, **params.filters) | ||||||||||||||||||||
|
Comment on lines
+292
to
+293
|
||||||||||||||||||||
| limit = params.filters.pop("limit") | |
| results = self.discogs_client.search(params.query, **params.filters) | |
| limit = params.filters.get("limit") | |
| filters = {k: v for k, v in params.filters.items() if k != "limit"} | |
| results = self.discogs_client.search(params.query, **filters) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
suggestion: Popping 'limit' from filters mutates the input dictionary.
Copy the filters dictionary before using pop to prevent unintended side effects if the SearchParams object is reused.
| limit = params.filters.pop("limit") | |
| results = self.discogs_client.search(params.query, **params.filters) | |
| results.per_page = limit | |
| return [r.data for r in results.page(1)] | |
| filters = params.filters.copy() | |
| limit = filters.pop("limit") | |
| results = self.discogs_client.search(params.query, **filters) | |
| results.per_page = limit | |
| return [r.data for r in results.page(1)] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing error handling for HTTP errors and JSON parsing. Unlike the previous implementation (which returned an empty tuple on errors), this will now raise unhandled exceptions. The base class catches all exceptions in
_search_apibut this bypasses proper error logging. Consider usingraise_for_status()to let the base class handle errors appropriately, or handle Deezer API-specific errors (like the old code's check for 'error' in response data).