/
/
/
1"""Test Bandcamp converters."""
2
3from unittest.mock import Mock
4
5import pytest
6from bandcamp_async_api.models import BCAlbum, BCArtist, BCTrack, FeedTrack
7from music_assistant_models.enums import ContentType
8
9from music_assistant.providers.bandcamp.converters import BandcampConverters, DiscographyItem
10
11
12@pytest.fixture
13def converters() -> BandcampConverters:
14 """Return a BandcampConverters instance."""
15 return BandcampConverters("bandcamp", "bandcamp_test")
16
17
18def test_track_from_search(converters: BandcampConverters) -> None:
19 """Test converting SearchResultTrack to MA Track."""
20 # Create a mock SearchResultTrack
21 search_result = Mock()
22 search_result.artist_id = 123
23 search_result.album_id = 456
24 search_result.id = 789
25 search_result.name = "Test Track"
26 search_result.artist_name = "Test Artist"
27 search_result.album_name = "Test Album"
28 search_result.url = "https://test.bandcamp.com/track/test-track"
29
30 result = converters.track_from_search(search_result)
31
32 assert result.item_id == "123-456-789"
33 assert result.name == "Test Track"
34 assert result.provider == "bandcamp_test"
35
36
37def test_album_from_search(converters: BandcampConverters) -> None:
38 """Test converting SearchResultAlbum to MA Album."""
39 # Create a mock SearchResultAlbum
40 search_result = Mock()
41 search_result.artist_id = 123
42 search_result.id = 456
43 search_result.name = "Test Album"
44 search_result.artist_name = "Test Artist"
45 search_result.image_url = "https://f4.bcbits.com/img/a1234567890_16.jpg"
46 search_result.url = "https://test.bandcamp.com/album/test-album"
47 search_result.artist_url = "https://test.bandcamp.com"
48
49 result = converters.album_from_search(search_result)
50
51 assert result.item_id == "123-456"
52 assert result.name == "Test Album"
53 assert result.provider == "bandcamp_test"
54
55
56def test_artist_from_search(converters: BandcampConverters) -> None:
57 """Test converting SearchResultArtist to MA Artist."""
58 # Create a mock SearchResultArtist
59 search_result = Mock()
60 search_result.id = 123
61 search_result.name = "Test Artist"
62 search_result.url = "https://test.bandcamp.com"
63 search_result.image_url = "https://f4.bcbits.com/img/a1234567890_16.jpg"
64 search_result.tags = ["rock", "indie"]
65
66 result = converters.artist_from_search(search_result)
67
68 assert result.item_id == "123"
69 assert result.name == "Test Artist"
70 assert result.provider == "bandcamp_test"
71
72
73def test_track_from_api(converters: BandcampConverters) -> None:
74 """Test converting API Track to MA Track."""
75 # Create mock API models
76 mock_artist = Mock()
77 mock_artist.id = 123
78 mock_artist.name = "Test Artist"
79 mock_artist.url = "https://test.bandcamp.com"
80
81 mock_track = Mock()
82 mock_track.id = 789
83 mock_track.title = "Test Track"
84 mock_track.artist = mock_artist
85 mock_track.url = "https://test.bandcamp.com/track/test-track"
86 mock_track.duration = 300
87 mock_track.lyrics = "Test lyrics"
88 mock_track.track_number = 1
89 mock_track.streaming_url = {"mp3-320": "https://example.com/track.mp3"}
90
91 result = converters.track_from_api(
92 track=mock_track,
93 album_id=456,
94 album_name="Test Album",
95 album_image_url="https://f4.bcbits.com/img/a1234567890_16.jpg",
96 )
97
98 assert result.item_id == "123-456-789"
99 assert result.name == "Test Track"
100 assert result.provider == "bandcamp_test"
101
102
103def test_artist_from_api(converters: BandcampConverters) -> None:
104 """Test converting API Artist to MA Artist."""
105 # Create mock API artist
106 mock_artist = Mock()
107 mock_artist.id = 123
108 mock_artist.name = "Test Artist"
109 mock_artist.url = "https://test.bandcamp.com"
110 mock_artist.image_url = "https://f4.bcbits.com/img/a1234567890_16.jpg"
111 mock_artist.bio = "Test bio"
112
113 result = converters.artist_from_api(mock_artist)
114
115 assert result.item_id == "123"
116 assert result.name == "Test Artist"
117 assert result.provider == "bandcamp_test"
118
119
120def test_album_from_api(converters: BandcampConverters) -> None:
121 """Test converting API Album to MA Album."""
122 # Create mock API models
123 mock_artist = Mock()
124 mock_artist.id = 123
125 mock_artist.name = "Test Artist"
126 mock_artist.url = "https://test.bandcamp.com"
127
128 mock_album = Mock()
129 mock_album.id = 456
130 mock_album.title = "Test Album"
131 mock_album.artist = mock_artist
132 mock_album.url = "https://test.bandcamp.com/album/test-album"
133 mock_album.art_url = "https://f4.bcbits.com/img/a1234567890_16.jpg"
134 mock_album.release_date = 1609459200
135 mock_album.about = "Test album description"
136
137 result = converters.album_from_api(mock_album)
138
139 assert result.item_id == "123-456"
140 assert result.name == "Test Album"
141 assert result.provider == "bandcamp_test"
142
143
144def test_track_from_api_without_album_info(converters: BandcampConverters) -> None:
145 """Test converting API Track without album info."""
146 # Create mock API models
147 mock_artist = BCArtist(id=123, name="Test Artist", url="https://test.bandcamp.com")
148 mock_track = BCTrack(
149 id=789,
150 title="Test Track",
151 artist=mock_artist,
152 url="https://test.bandcamp.com/track/test-track",
153 duration=300,
154 lyrics="Test lyrics",
155 track_number=1,
156 streaming_url={"mp3-320": "https://example.com/track.mp3"},
157 )
158
159 result = converters.track_from_api(track=mock_track)
160
161 assert result.item_id == "123-0-789"
162 assert result.album is None
163 assert result.metadata.lyrics == "Test lyrics"
164
165
166def test_track_from_api_with_album(converters: BandcampConverters) -> None:
167 """Test converting API Track with album information."""
168 # Create mock API models
169 mock_artist = BCArtist(id=123, name="Test Artist", url="https://test.bandcamp.com")
170 mock_album = BCAlbum(
171 id=456,
172 title="Test Album",
173 artist=mock_artist,
174 url="https://test.bandcamp.com/album/test-album",
175 art_url="https://f4.bcbits.com/img/a1234567890_16.jpg",
176 release_date=1609459200,
177 about="Test album description",
178 )
179 mock_track = BCTrack(
180 id=789,
181 title="Test Track",
182 artist=mock_artist,
183 album=mock_album,
184 url="https://test.bandcamp.com/track/test-track",
185 duration=300,
186 lyrics="Test lyrics",
187 track_number=1,
188 streaming_url={"mp3-320": "https://example.com/track.mp3"},
189 )
190
191 result = converters.track_from_api(track=mock_track)
192
193 assert result.item_id == "123-0-789"
194 assert result.album is not None
195 assert result.album.item_id == "123-456"
196 assert result.album.name == "Test Album"
197
198
199def _make_mock_track(streaming_url: dict[str, str]) -> Mock:
200 """Create a mock API track with the given streaming URL."""
201 mock_artist = Mock()
202 mock_artist.id = 123
203 mock_artist.name = "Test Artist"
204 mock_artist.url = "https://test.bandcamp.com"
205
206 mock_track = Mock()
207 mock_track.id = 789
208 mock_track.title = "Test Track"
209 mock_track.artist = mock_artist
210 mock_track.url = "https://test.bandcamp.com/track/test-track"
211 mock_track.duration = 300
212 mock_track.lyrics = None
213 mock_track.track_number = 1
214 mock_track.streaming_url = streaming_url
215 return mock_track
216
217
218def test_track_from_api_audio_format_mp3_320(converters: BandcampConverters) -> None:
219 """Test that mp3-320 streaming URL sets audio format correctly."""
220 mock_track = _make_mock_track({"mp3-320": "https://example.com/track.mp3"})
221 result = converters.track_from_api(track=mock_track, album_id=456)
222 mapping = next(iter(result.provider_mappings))
223 assert mapping.audio_format.content_type == ContentType.MP3
224 assert mapping.audio_format.bit_rate == 320
225
226
227def test_track_from_api_audio_format_mp3_v0(converters: BandcampConverters) -> None:
228 """Test that mp3-v0 streaming URL sets content type with no bitrate (VBR)."""
229 mock_track = _make_mock_track({"mp3-v0": "https://example.com/track.mp3"})
230 result = converters.track_from_api(track=mock_track, album_id=456)
231 mapping = next(iter(result.provider_mappings))
232 assert mapping.audio_format.content_type == ContentType.MP3
233 assert mapping.audio_format.bit_rate is None
234
235
236def test_track_from_api_audio_format_mp3_128(converters: BandcampConverters) -> None:
237 """Test that mp3-128 streaming URL sets audio format correctly."""
238 mock_track = _make_mock_track({"mp3-128": "https://example.com/track.mp3"})
239 result = converters.track_from_api(track=mock_track, album_id=456)
240 mapping = next(iter(result.provider_mappings))
241 assert mapping.audio_format.content_type == ContentType.MP3
242 assert mapping.audio_format.bit_rate == 128
243
244
245def test_track_from_api_audio_format_none_streaming_url(converters: BandcampConverters) -> None:
246 """Test that None streaming_url does not crash."""
247 mock_track = _make_mock_track({"mp3-128": "https://example.com/track.mp3"})
248 mock_track.streaming_url = None
249 result = converters.track_from_api(track=mock_track, album_id=456)
250 mapping = next(iter(result.provider_mappings))
251 assert mapping.audio_format.content_type == ContentType.MP3
252 assert mapping.audio_format.bit_rate is None
253
254
255def test_track_from_feed(converters: BandcampConverters) -> None:
256 """Test converting a FeedTrack to MA Track."""
257 track = FeedTrack(
258 track_id=789,
259 title="Feed Track",
260 band_id=123,
261 band_name="Test Artist",
262 album_id=456,
263 album_title="Test Album",
264 track_num=3,
265 duration=212.5,
266 streaming_url={"mp3-128": "https://example.com/feed.mp3"},
267 art_id=987,
268 track_url="https://test.bandcamp.com/track/feed-track",
269 )
270
271 result = converters.track_from_feed(track)
272
273 assert result.item_id == "123-456-789"
274 assert result.name == "Feed Track"
275 assert result.duration == 212
276 assert result.track_number == 3
277 assert result.album is not None
278 assert result.album.item_id == "123-456"
279 assert result.provider == "bandcamp_test"
280
281
282def test_track_from_feed_standalone(converters: BandcampConverters) -> None:
283 """Test a feed track without an album maps to album_id 0 and omits the album."""
284 track = FeedTrack(track_id=789, title="Single", band_id=123, band_name="Test Artist")
285
286 result = converters.track_from_feed(track)
287
288 assert result.item_id == "123-0-789"
289 assert result.album is None
290
291
292def test_streaming_url_priority_v0_over_320(converters: BandcampConverters) -> None:
293 """Test that mp3-v0 is preferred over mp3-320."""
294 url, bitrate, content_type = converters.streaming_url_from_api(
295 {
296 "mp3-320": "https://example.com/320.mp3",
297 "mp3-v0": "https://example.com/v0.mp3",
298 }
299 )
300 assert url == "https://example.com/v0.mp3"
301 assert bitrate is None
302 assert content_type == ContentType.MP3
303
304
305def test_streaming_url_priority_320_over_128(converters: BandcampConverters) -> None:
306 """Test that mp3-320 is preferred over mp3-128."""
307 url, bitrate, content_type = converters.streaming_url_from_api(
308 {
309 "mp3-128": "https://example.com/128.mp3",
310 "mp3-320": "https://example.com/320.mp3",
311 }
312 )
313 assert url == "https://example.com/320.mp3"
314 assert bitrate == 320
315 assert content_type == ContentType.MP3
316
317
318def test_streaming_url_priority_v0_over_320_over_128(converters: BandcampConverters) -> None:
319 """Test full priority chain when all three formats are present."""
320 url, bitrate, content_type = converters.streaming_url_from_api(
321 {
322 "mp3-128": "https://example.com/128.mp3",
323 "mp3-320": "https://example.com/320.mp3",
324 "mp3-v0": "https://example.com/v0.mp3",
325 }
326 )
327 assert url == "https://example.com/v0.mp3"
328 assert bitrate is None
329 assert content_type == ContentType.MP3
330
331
332def test_streaming_url_fallback_unknown_key(converters: BandcampConverters) -> None:
333 """Test that an unknown streaming key falls back with UNKNOWN content type."""
334 url, bitrate, content_type = converters.streaming_url_from_api(
335 {"ogg-vorbis": "https://example.com/track.ogg"}
336 )
337 assert url == "https://example.com/track.ogg"
338 assert bitrate is None
339 assert content_type == ContentType.UNKNOWN
340
341
342def test_streaming_url_empty_dict(converters: BandcampConverters) -> None:
343 """Test that empty dict returns None for URL and bitrate."""
344 url, bitrate, content_type = converters.streaming_url_from_api({})
345 assert url is None
346 assert bitrate is None
347 assert content_type == ContentType.MP3
348
349
350def test_album_from_discography_item(converters: BandcampConverters) -> None:
351 """Test converting a full discography item to MA Album."""
352 item: DiscographyItem = {
353 "item_id": 394626934,
354 "item_type": "album",
355 "artist_name": "Amanda Palmer & Friends",
356 "band_name": "Amanda Palmer",
357 "title": "Forty-Five Degrees",
358 "art_id": 3547137148,
359 "release_date": "21 Feb 2020 00:00:00 GMT",
360 "band_id": 3463798201,
361 }
362 result = converters.album_from_discography_item(item)
363
364 assert result.item_id == "3463798201-394626934"
365 assert result.name == "Forty-Five Degrees"
366 assert result.year == 2020
367 assert result.provider == "bandcamp_test"
368 artists = list(result.artists)
369 assert len(artists) == 1
370 assert artists[0].name == "Amanda Palmer & Friends"
371 assert artists[0].item_id == "3463798201"
372 assert result.metadata.images
373 assert any("a3547137148_0.jpg" in img.path for img in result.metadata.images)
374
375
376def test_album_from_discography_item_missing_art_id(converters: BandcampConverters) -> None:
377 """Test discography item with no art_id produces album without image."""
378 item: DiscographyItem = {
379 "item_id": 100,
380 "item_type": "album",
381 "band_name": "Test",
382 "title": "No Art",
383 "band_id": 200,
384 "release_date": "01 Jan 2021 00:00:00 GMT",
385 }
386 result = converters.album_from_discography_item(item)
387 assert result.item_id == "200-100"
388 assert result.year == 2021
389 assert not result.metadata.images
390
391
392def test_album_from_discography_item_missing_release_date(converters: BandcampConverters) -> None:
393 """Test discography item with no release_date sets year to None."""
394 item: DiscographyItem = {
395 "item_id": 100,
396 "item_type": "album",
397 "band_name": "Test",
398 "title": "No Date",
399 "band_id": 200,
400 "art_id": 999,
401 }
402 result = converters.album_from_discography_item(item)
403 assert result.year is None
404
405
406def test_album_from_discography_item_artist_name_fallback(
407 converters: BandcampConverters,
408) -> None:
409 """Test that band_name is used when artist_name is None."""
410 item: DiscographyItem = {
411 "item_id": 100,
412 "item_type": "album",
413 "artist_name": None,
414 "band_name": "Fallback Artist",
415 "title": "Test",
416 "band_id": 200,
417 "art_id": 999,
418 "release_date": "15 Mar 2022 00:00:00 GMT",
419 }
420 result = converters.album_from_discography_item(item)
421 artists = list(result.artists)
422 assert artists[0].name == "Fallback Artist"
423