/
/
/
1"""Converters for Bandcamp API models to Music Assistant models."""
2
3from datetime import datetime
4
5from bandcamp_async_api.models import BCAlbum as APIAlbum
6from bandcamp_async_api.models import BCArtist as APIArtist
7from bandcamp_async_api.models import BCTrack as APITrack
8from bandcamp_async_api.models import (
9 SearchResultAlbum,
10 SearchResultArtist,
11 SearchResultTrack,
12)
13from music_assistant_models.enums import ImageType, LinkType, MediaType
14from music_assistant_models.media_items import Album as MAAlbum
15from music_assistant_models.media_items import Artist as MAArtist
16from music_assistant_models.media_items import (
17 ItemMapping,
18 MediaItemImage,
19 MediaItemLink,
20 ProviderMapping,
21 UniqueList,
22)
23from music_assistant_models.media_items import Track as MATrack
24
25
26class BandcampConverters:
27 """Converters for Bandcamp API models to Music Assistant models."""
28
29 def __init__(self, domain: str, instance_id: str):
30 """Initialize converters with provider information."""
31 self.domain = domain
32 self.instance_id = instance_id
33
34 def streaming_url_from_api(
35 self, streaming_info: dict[str, str]
36 ) -> tuple[str | None, int | None]:
37 """Parse streaming URL info."""
38 # Extract streaming URL with priority: mp3-v0 > mp3-128
39 bitrate = None
40 streaming_url = None
41 if "mp3-v0" in streaming_info:
42 streaming_url = streaming_info["mp3-v0"]
43 bitrate = None # VBR
44 elif "mp3-320" in streaming_info:
45 streaming_url = streaming_info["mp3-320"]
46 bitrate = 320
47 elif "mp3-128" in streaming_info:
48 streaming_url = streaming_info["mp3-128"]
49 bitrate = 128
50 elif streaming_info:
51 # Fallback to first available URL
52 streaming_url = next(iter(streaming_info.values()))
53 return streaming_url, bitrate
54
55 def track_from_search(self, item: SearchResultTrack) -> MATrack:
56 """Create a Track from new API SearchResultTrack."""
57 track_id = f"{item.artist_id}-{item.album_id or 0}-{item.id}"
58 return MATrack(
59 item_id=track_id,
60 provider=self.instance_id,
61 name=item.name,
62 artists=UniqueList(
63 [
64 ItemMapping(
65 media_type=MediaType.ARTIST,
66 item_id=str(item.artist_id),
67 provider=self.instance_id,
68 name=item.artist_name,
69 )
70 ]
71 ),
72 album=(
73 ItemMapping(
74 media_type=MediaType.ALBUM,
75 item_id=f"{item.artist_id}-{item.album_id or 0}",
76 provider=self.instance_id,
77 name=item.album_name,
78 )
79 if item.album_id
80 else None
81 ),
82 provider_mappings={
83 ProviderMapping(
84 item_id=track_id,
85 provider_domain=self.domain,
86 provider_instance=self.instance_id,
87 url=item.url,
88 )
89 },
90 )
91
92 def album_from_search(self, item: SearchResultAlbum) -> MAAlbum:
93 """Create an Album from new API SearchResultAlbum."""
94 album_id = f"{item.artist_id}-{item.id}"
95 output = MAAlbum(
96 item_id=album_id,
97 provider=self.instance_id,
98 name=item.name,
99 uri=item.url,
100 artists=UniqueList(
101 [
102 ItemMapping(
103 media_type=MediaType.ARTIST,
104 item_id=str(item.artist_id),
105 provider=self.instance_id,
106 name=item.artist_name,
107 uri=item.artist_url,
108 )
109 ]
110 ),
111 provider_mappings={
112 ProviderMapping(
113 item_id=album_id,
114 provider_domain=self.domain,
115 provider_instance=self.instance_id,
116 url=item.url,
117 )
118 },
119 )
120 output.metadata.add_image(
121 MediaItemImage(
122 type=ImageType.THUMB,
123 path=item.image_url,
124 provider=self.instance_id,
125 remotely_accessible=True,
126 )
127 )
128 return output
129
130 def artist_from_search(self, item: SearchResultArtist) -> MAArtist:
131 """Create an Artist from new API SearchResultArtist."""
132 output = MAArtist(
133 item_id=str(item.id),
134 provider=self.instance_id,
135 name=item.name,
136 uri=item.url,
137 provider_mappings={
138 ProviderMapping(
139 item_id=str(item.id),
140 provider_domain=self.domain,
141 provider_instance=self.instance_id,
142 url=item.url,
143 )
144 },
145 )
146 output.metadata.genres = item.tags
147 if item.url:
148 output.metadata.description = item.url
149 output.metadata.add_image(
150 MediaItemImage(
151 type=ImageType.THUMB,
152 path=item.image_url,
153 provider=self.instance_id,
154 remotely_accessible=True,
155 )
156 )
157 return output
158
159 def track_from_api(
160 self,
161 track: APITrack,
162 album_id: str | int | None = None,
163 album_name: str = "",
164 album_image_url: str = "",
165 ) -> MATrack:
166 """Convert a Track object from the API to MA Track format."""
167 album_id = album_id or 0
168 output = MATrack(
169 item_id=f"{track.artist.id}-{album_id}-{track.id}",
170 provider=self.instance_id,
171 name=track.title,
172 artists=UniqueList(
173 [
174 ItemMapping(
175 media_type=MediaType.ARTIST,
176 item_id=str(track.artist.id),
177 provider=self.instance_id,
178 name=track.artist.name,
179 )
180 ]
181 ),
182 disc_number=0,
183 duration=track.duration,
184 provider_mappings={
185 ProviderMapping(
186 item_id=f"{track.artist.id}-{album_id}-{track.id}",
187 provider_domain=self.domain,
188 provider_instance=self.instance_id,
189 url=track.url,
190 )
191 },
192 )
193 if output.track_number is not None:
194 output.track_number = track.track_number
195
196 if album_id:
197 output.album = ItemMapping(
198 media_type=MediaType.ALBUM,
199 item_id=f"{track.artist.id}-{album_id}",
200 provider=self.instance_id,
201 name=album_name,
202 )
203 elif hasattr(track, "album") and track.album:
204 # If the track has an album attribute, use that information
205 output.album = ItemMapping(
206 media_type=MediaType.ALBUM,
207 item_id=f"{track.artist.id}-{track.album.id}",
208 provider=self.instance_id,
209 name=track.album.title,
210 )
211
212 streaming_url, _ = self.streaming_url_from_api(track.streaming_url)
213 if streaming_url:
214 output.metadata.links = {
215 MediaItemLink(
216 type=LinkType.UNKNOWN,
217 url=streaming_url,
218 )
219 }
220 output.metadata.lyrics = track.lyrics
221 if album_image_url:
222 output.metadata.add_image(
223 MediaItemImage(
224 type=ImageType.THUMB,
225 path=album_image_url,
226 provider=self.instance_id,
227 remotely_accessible=True,
228 )
229 )
230 return output
231
232 def artist_from_api(self, artist: APIArtist) -> MAArtist:
233 """Convert an API Artist object to MA Artist format."""
234 output = MAArtist(
235 item_id=str(artist.id),
236 uri=artist.url,
237 provider=self.instance_id,
238 name=artist.name,
239 provider_mappings={
240 ProviderMapping(
241 item_id=str(artist.id),
242 provider_domain=self.domain,
243 provider_instance=self.instance_id,
244 url=artist.url,
245 )
246 },
247 )
248 output.metadata.description = f"{artist.url}\n{artist.bio or ''}".strip()
249 output.metadata.add_image(
250 MediaItemImage(
251 type=ImageType.THUMB,
252 path=artist.image_url,
253 provider=self.instance_id,
254 remotely_accessible=True,
255 )
256 )
257 return output
258
259 def album_from_api(self, album: APIAlbum) -> MAAlbum:
260 """Convert an API Album object to MA Album format."""
261 album_id = f"{album.artist.id}-{album.id}"
262 output = MAAlbum(
263 item_id=album_id,
264 provider=self.instance_id,
265 name=album.title,
266 artists=UniqueList(
267 [
268 ItemMapping(
269 media_type=MediaType.ARTIST,
270 item_id=str(album.artist.id),
271 provider=self.instance_id,
272 name=album.artist.name,
273 image=MediaItemImage(
274 path=album.art_url,
275 type=ImageType.THUMB,
276 provider=self.instance_id,
277 remotely_accessible=True,
278 ),
279 )
280 ]
281 ),
282 provider_mappings={
283 ProviderMapping(
284 item_id=album_id,
285 provider_domain=self.domain,
286 provider_instance=self.instance_id,
287 url=album.url,
288 )
289 },
290 year=datetime.fromtimestamp(album.release_date).year,
291 )
292 output.metadata.add_image(
293 MediaItemImage(
294 type=ImageType.THUMB,
295 path=album.art_url,
296 provider=self.instance_id,
297 remotely_accessible=True,
298 )
299 )
300 output.metadata.description = f"{album.url}\n{album.about or ''}".strip()
301 return output
302