Skip to content

Commit ff9f93e

Browse files
authored
Type musicbrainz (#6329)
## Add full type coverage to the MusicBrainz plugin This PR introduces complete static typing for `beetsplug/musicbrainz.py`, `beetsplug/mbpseudo.py`, and `beetsplug/_utils/musicbrainz.py`, and enforces it via `mypy` strict mode. ### Key changes **New `TypedDict` schema in `beetsplug/_utils/musicbrainz.py`** A comprehensive set of `TypedDict` classes now models the full MusicBrainz API response surface — `Release`, `Recording`, `Track`, `ReleaseGroup`, `Artist`, `ArtistCredit`, `Work`, and all relation types. Public API methods (`get_release`, `get_recording`, etc.) now return these typed shapes instead of the opaque `JSONDict`. **Key normalization: dash-to-underscore** The internal `_normalize_data` method (previously `_group_relations`) now also converts all hyphenated keys (e.g. `artist-credit`, `release-group`, `sort-name`) to underscored equivalents (e.g. `artist_credit`, `release_group`, `sort_name`) at parse time. This makes the data structure Python-idiomatic and is what allows the `TypedDict` definitions to use clean attribute names. All downstream field accesses, test fixtures, and JSON test resources are updated accordingly. **`mypy` strict mode enabled** for the three affected modules via `setup.cfg`. ### Impact - No behaviour change for end users — this is purely an internal refactor. - Code reading the MusicBrainz response now has IDE completion and type-checking support. - The normalization boundary is clearly established at `_normalize_data`, so callers never see raw hyphenated keys. - Test fixtures and resource JSON files are updated to match the new normalized shape.
2 parents c6409d2 + aab4dff commit ff9f93e

14 files changed

Lines changed: 1269 additions & 752 deletions

beetsplug/_utils/musicbrainz.py

Lines changed: 448 additions & 32 deletions
Large diffs are not rendered by default.

beetsplug/listenbrainz.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -299,7 +299,7 @@ def get_track_info(self, tracks):
299299
identifier, includes=["releases", "artist-credits"]
300300
)
301301
title = recording.get("title")
302-
artist_credit = recording.get("artist-credit", [])
302+
artist_credit = recording.get("artist_credit", [])
303303
if artist_credit:
304304
artist = artist_credit[0].get("artist", {}).get("name")
305305
else:

beetsplug/mbcollection.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ def id(self) -> str:
101101
@property
102102
def release_count(self) -> int:
103103
"""Total number of releases recorded in the collection."""
104-
return self.data["release-count"]
104+
return self.data["release_count"]
105105

106106
@property
107107
def releases_url(self) -> str:
@@ -173,7 +173,7 @@ def collection(self) -> MBCollection:
173173
# Get all release collection IDs, avoiding event collections
174174
if not (
175175
collection_by_id := {
176-
c["id"]: c for c in collections if c["entity-type"] == "release"
176+
c["id"]: c for c in collections if c["entity_type"] == "release"
177177
}
178178
):
179179
raise ui.UserError("No release collection found.")

beetsplug/mbpseudo.py

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,12 @@
4141
from beets.autotag.distance import Distance
4242
from beets.autotag.hooks import AlbumMatch
4343
from beets.library import Item
44-
from beetsplug._typing import JSONDict
44+
45+
from ._utils.musicbrainz import (
46+
Release,
47+
ReleaseRelation,
48+
ReleaseRelationRelease,
49+
)
4550

4651
_STATUS_PSEUDO = "Pseudo-Release"
4752

@@ -133,7 +138,7 @@ def candidates(
133138
yield album_info
134139

135140
@override
136-
def album_info(self, release: JSONDict) -> AlbumInfo:
141+
def album_info(self, release: Release) -> AlbumInfo:
137142
official_release = super().album_info(release)
138143

139144
if release.get("status") == _STATUS_PSEUDO:
@@ -161,30 +166,32 @@ def album_info(self, release: JSONDict) -> AlbumInfo:
161166
else:
162167
return official_release
163168

164-
def _intercept_mb_release(self, data: JSONDict) -> list[str]:
169+
def _intercept_mb_release(self, data: Release) -> list[str]:
165170
album_id = data["id"] if "id" in data else None
166171
if self._has_desired_script(data) or not isinstance(album_id, str):
167172
return []
168173

169174
return [
170175
pr_id
171-
for rel in data.get("release-relations", [])
176+
for rel in data.get("release_relations", [])
172177
if (pr_id := self._wanted_pseudo_release_id(album_id, rel))
173178
is not None
174179
]
175180

176-
def _has_desired_script(self, release: JSONDict) -> bool:
181+
def _has_desired_script(
182+
self, release: Release | ReleaseRelationRelease
183+
) -> bool:
177184
if len(self._scripts) == 0:
178185
return False
179-
elif script := release.get("text-representation", {}).get("script"):
186+
elif script := release.get("text_representation", {}).get("script"):
180187
return script in self._scripts
181188
else:
182189
return False
183190

184191
def _wanted_pseudo_release_id(
185192
self,
186193
album_id: str,
187-
relation: JSONDict,
194+
relation: ReleaseRelation,
188195
) -> str | None:
189196
if (
190197
len(self._scripts) == 0
@@ -206,19 +213,17 @@ def _wanted_pseudo_release_id(
206213
return None
207214

208215
def _replace_artist_with_alias(
209-
self,
210-
raw_pseudo_release: JSONDict,
211-
pseudo_release: AlbumInfo,
216+
self, raw_pseudo_release: Release, pseudo_release: AlbumInfo
212217
):
213218
"""Use the pseudo-release's language to search for artist
214219
alias if the user hasn't configured import languages."""
215220

216221
if len(config["import"]["languages"].as_str_seq()) > 0:
217222
return
218223

219-
lang = raw_pseudo_release.get("text-representation", {}).get("language")
220-
artist_credits = raw_pseudo_release.get("release-group", {}).get(
221-
"artist-credit", []
224+
lang = raw_pseudo_release.get("text_representation", {}).get("language")
225+
artist_credits = raw_pseudo_release.get("release_group", {}).get(
226+
"artist_credit", []
222227
)
223228
aliases = [
224229
artist_credit.get("artist", {}).get("aliases", [])

0 commit comments

Comments
 (0)