/
/
/
1"""MusicBrainz data models."""
2
3from __future__ import annotations
4
5from dataclasses import dataclass, field
6from typing import TYPE_CHECKING, Any, cast
7
8from mashumaro import DataClassDictMixin
9
10
11def replace_hyphens(
12 data: dict[str, Any] | list[dict[str, Any]] | Any,
13) -> dict[str, Any] | list[dict[str, Any]] | Any:
14 """Change all hyphened keys to underscores."""
15 if isinstance(data, dict):
16 return {key.replace("-", "_"): replace_hyphens(value) for key, value in data.items()}
17
18 if isinstance(data, list):
19 return [replace_hyphens(x) for x in data]
20
21 return data
22
23
24@dataclass
25class MusicBrainzTag(DataClassDictMixin):
26 """Model for a (basic) Tag object as received from the MusicBrainz API."""
27
28 count: int
29 name: str
30
31
32@dataclass
33class MusicBrainzAlias(DataClassDictMixin):
34 """Model for a (basic) Alias object from MusicBrainz."""
35
36 name: str
37 sort_name: str
38
39 # optional fields
40 locale: str | None = None
41 type: str | None = None
42 primary: bool | None = None
43 begin_date: str | None = None
44 end_date: str | None = None
45
46
47@dataclass
48class MusicBrainzLifeSpan(DataClassDictMixin):
49 """Model for a LifeSpan object from MusicBrainz."""
50
51 begin: str | None = None
52 end: str | None = None
53 ended: bool = False
54
55
56@dataclass
57class MusicBrainzUrl(DataClassDictMixin):
58 """Model for a Url object embedded in a MusicBrainz relation."""
59
60 resource: str
61
62
63@dataclass
64class MusicBrainzRelation(DataClassDictMixin):
65 """Model for a Relation object from MusicBrainz."""
66
67 type: str
68
69 # optional - only populated on url-rels (work-rels and friends have other targets)
70 url: MusicBrainzUrl | None = None
71
72
73@dataclass
74class MusicBrainzArtist(DataClassDictMixin):
75 """Model for a (basic) Artist object from MusicBrainz."""
76
77 id: str
78 name: str
79 sort_name: str
80
81 # optional fields
82 type: str | None = None
83 aliases: list[MusicBrainzAlias] | None = None
84 tags: list[MusicBrainzTag] | None = None
85 relations: list[MusicBrainzRelation] | None = None
86 life_span: MusicBrainzLifeSpan | None = None
87
88 @classmethod
89 def from_raw(cls, data: Any) -> MusicBrainzArtist:
90 """Instantiate object from raw api data."""
91 alt_data = replace_hyphens(data)
92 if TYPE_CHECKING:
93 alt_data = cast("dict[str, Any]", alt_data)
94 return MusicBrainzArtist.from_dict(alt_data)
95
96
97@dataclass
98class MusicBrainzArtistCredit(DataClassDictMixin):
99 """Model for a (basic) ArtistCredit object from MusicBrainz."""
100
101 name: str
102 artist: MusicBrainzArtist
103
104
105@dataclass
106class MusicBrainzReleaseGroup(DataClassDictMixin):
107 """Model for a (basic) ReleaseGroup object from MusicBrainz."""
108
109 id: str
110 title: str
111
112 # optional fields
113 primary_type: str | None = None
114 primary_type_id: str | None = None
115 secondary_types: list[str] | None = None
116 secondary_type_ids: list[str] | None = None
117 artist_credit: list[MusicBrainzArtistCredit] | None = None
118 barcode: str | None = None
119 first_release_date: str | None = None
120
121 @classmethod
122 def from_raw(cls, data: Any) -> MusicBrainzReleaseGroup:
123 """Instantiate object from raw api data."""
124 alt_data = replace_hyphens(data)
125 if TYPE_CHECKING:
126 alt_data = cast("dict[str, Any]", alt_data)
127 return MusicBrainzReleaseGroup.from_dict(alt_data)
128
129
130@dataclass
131class MusicBrainzTrack(DataClassDictMixin):
132 """Model for a (basic) Track object from MusicBrainz."""
133
134 id: str
135 number: str
136 title: str
137 length: int | None = None
138
139 @classmethod
140 def from_raw(cls, data: Any) -> MusicBrainzTrack:
141 """Instantiate object from raw api data."""
142 alt_data = replace_hyphens(data)
143 if TYPE_CHECKING:
144 alt_data = cast("dict[str, Any]", alt_data)
145 return MusicBrainzTrack.from_dict(alt_data)
146
147
148@dataclass
149class MusicBrainzMedia(DataClassDictMixin):
150 """Model for a (basic) Media object from MusicBrainz."""
151
152 format: str
153 track: list[MusicBrainzTrack]
154 position: int = 0
155 track_count: int = 0
156 track_offset: int = 0
157
158
159@dataclass
160class MusicBrainzRelease(DataClassDictMixin):
161 """Model for a (basic) Release object from MusicBrainz."""
162
163 id: str
164 status_id: str
165 count: int
166 title: str
167 status: str
168 artist_credit: list[MusicBrainzArtistCredit]
169 release_group: MusicBrainzReleaseGroup
170 track_count: int = 0
171
172 # optional fields
173 media: list[MusicBrainzMedia] = field(default_factory=list)
174 date: str | None = None
175 country: str | None = None
176 disambiguation: str | None = None # version
177 # TODO (if needed): release-events
178
179 @classmethod
180 def from_raw(cls, data: Any) -> MusicBrainzRelease:
181 """Instantiate object from raw api data."""
182 alt_data = replace_hyphens(data)
183 if TYPE_CHECKING:
184 alt_data = cast("dict[str, Any]", alt_data)
185 return MusicBrainzRelease.from_dict(alt_data)
186
187
188@dataclass
189class MusicBrainzBarcodeRelease(DataClassDictMixin):
190 """
191 Slim release identity from a barcode search result.
192
193 A barcode search only needs each hit's release and release-group id, so this
194 deliberately ignores the summary fields a search response carries (media without a
195 tracklist, artist credits, ...) that the full release model cannot parse.
196 """
197
198 id: str
199 release_group: MusicBrainzReleaseGroup
200
201 @classmethod
202 def from_raw(cls, data: Any) -> MusicBrainzBarcodeRelease:
203 """Instantiate object from raw api data."""
204 alt_data = replace_hyphens(data)
205 if TYPE_CHECKING:
206 alt_data = cast("dict[str, Any]", alt_data)
207 return MusicBrainzBarcodeRelease.from_dict(alt_data)
208
209
210@dataclass
211class MusicBrainzRecording(DataClassDictMixin):
212 """Model for a (basic) Recording object as received from the MusicBrainz API."""
213
214 id: str
215 title: str
216 artist_credit: list[MusicBrainzArtistCredit] = field(default_factory=list)
217 # optional fields
218 length: int | None = None
219 first_release_date: str | None = None
220 isrcs: list[str] | None = None
221 tags: list[MusicBrainzTag] | None = None
222 disambiguation: str | None = None # version (e.g. live, karaoke etc.)
223
224 @classmethod
225 def from_raw(cls, data: Any) -> MusicBrainzRecording:
226 """Instantiate object from raw api data."""
227 alt_data = replace_hyphens(data)
228 if TYPE_CHECKING:
229 alt_data = cast("dict[str, Any]", alt_data)
230 return MusicBrainzRecording.from_dict(alt_data)
231