/
/
/
1"""Tests for Zvuk Music parsers."""
2
3from __future__ import annotations
4
5from unittest.mock import Mock
6
7import pytest
8from music_assistant_models.enums import AlbumType, ImageType
9
10from music_assistant.providers.zvuk_music.parsers import (
11 parse_album,
12 parse_artist,
13 parse_playlist,
14 parse_track,
15)
16
17
18def _create_mock_image(template: str = "https://zvuk.com/image/{width}x{height}") -> Mock:
19 """Create a mock Zvuk Image object.
20
21 :param template: URL template with {width} and {height} placeholders.
22 :return: Mock Image object.
23 """
24 image = Mock()
25 image.get_url = Mock(
26 side_effect=lambda w, h: template.format(width=w, height=h) if template else None
27 )
28 return image
29
30
31def _create_mock_artist(
32 artist_id: int = 123,
33 title: str | None = "Test Artist",
34 image: Mock | None = None,
35) -> Mock:
36 """Create a mock Zvuk artist object.
37
38 :param artist_id: Artist ID.
39 :param title: Artist name.
40 :param image: Optional mock image object.
41 :return: Mock artist object.
42 """
43 artist = Mock()
44 artist.id = artist_id
45 artist.title = title
46 artist.image = image
47 return artist
48
49
50def _create_mock_release(
51 release_id: int = 456,
52 title: str | None = "Test Album",
53 artists: list[Mock] | None = None,
54 release_type: str | None = None,
55 date: str | None = None,
56 explicit: bool = False,
57 genres: list[Mock] | None = None,
58 image: Mock | None = None,
59) -> Mock:
60 """Create a mock Zvuk release object.
61
62 :param release_id: Release ID.
63 :param title: Album title.
64 :param artists: List of mock artist objects.
65 :param release_type: Release type (album, single, ep, compilation).
66 :param date: Release date in ISO format.
67 :param explicit: Whether the release is explicit.
68 :param genres: List of mock genre objects.
69 :param image: Optional mock image object.
70 :return: Mock release object.
71 """
72 release = Mock()
73 release.id = release_id
74 release.title = title
75 release.artists = artists or []
76 release.explicit = explicit
77 release.image = image
78 release.date = date
79
80 # Type handling
81 if release_type:
82 type_mock = Mock()
83 type_mock.value = release_type
84 release.type = type_mock
85 else:
86 release.type = None
87
88 # get_year method
89 if date:
90 release.get_year = Mock(return_value=int(date[:4]))
91 else:
92 release.get_year = Mock(return_value=None)
93
94 # Genres (only on full Release)
95 if genres is not None:
96 release.genres = genres
97 else:
98 # SimpleRelease doesn't have genres
99 del release.genres
100
101 return release
102
103
104def _create_mock_track(
105 track_id: int = 789,
106 title: str | None = "Test Track",
107 artists: list[Mock] | None = None,
108 release: Mock | None = None,
109 duration: int = 180,
110 position: int | None = None,
111 explicit: bool = False,
112) -> Mock:
113 """Create a mock Zvuk track object.
114
115 :param track_id: Track ID.
116 :param title: Track title.
117 :param artists: List of mock artist objects.
118 :param release: Mock release object.
119 :param duration: Track duration in seconds.
120 :param position: Track position in album.
121 :param explicit: Whether the track is explicit.
122 :return: Mock track object.
123 """
124 track = Mock()
125 track.id = track_id
126 track.title = title
127 track.artists = artists or []
128 track.release = release
129 track.duration = duration
130 track.explicit = explicit
131
132 # Position is only on full Track, not SimpleTrack
133 if position is not None:
134 track.position = position
135 else:
136 del track.position
137
138 return track
139
140
141def _create_mock_playlist(
142 playlist_id: int = 999,
143 title: str | None = "Test Playlist",
144 description: str | None = None,
145 user_id: int | None = None,
146 image: Mock | None = None,
147) -> Mock:
148 """Create a mock Zvuk playlist object.
149
150 :param playlist_id: Playlist ID.
151 :param title: Playlist title.
152 :param description: Playlist description.
153 :param user_id: User ID (owner).
154 :param image: Optional mock image object.
155 :return: Mock playlist object.
156 """
157 playlist = Mock()
158 playlist.id = playlist_id
159 playlist.title = title
160 playlist.description = description
161 playlist.image = image
162
163 # user_id is only on full Playlist, not SimplePlaylist
164 if user_id is not None:
165 playlist.user_id = user_id
166 else:
167 del playlist.user_id
168
169 return playlist
170
171
172@pytest.fixture
173def mock_provider() -> Mock:
174 """Create a mock ZvukMusicProvider."""
175 provider = Mock()
176 provider.instance_id = "zvuk_music_test"
177 provider.domain = "zvuk_music"
178 provider.client = Mock()
179 provider.client.user_id = 12345
180
181 def mock_get_item_mapping(
182 media_type: str, # noqa: ARG001
183 key: str,
184 name: str,
185 ) -> Mock:
186 mapping = Mock()
187 mapping.item_id = key
188 mapping.name = name
189 return mapping
190
191 provider.get_item_mapping = Mock(side_effect=mock_get_item_mapping)
192 return provider
193
194
195class TestParseArtist:
196 """Tests for parse_artist function."""
197
198 def test_parse_artist_basic(self, mock_provider: Mock) -> None:
199 """Test parsing a basic artist without image."""
200 artist_obj = _create_mock_artist(artist_id=123, title="Test Artist")
201
202 result = parse_artist(mock_provider, artist_obj)
203
204 assert result.item_id == "123"
205 assert result.name == "Test Artist"
206 assert result.provider == "zvuk_music_test"
207 assert len(result.provider_mappings) == 1
208
209 mapping = next(iter(result.provider_mappings))
210 assert mapping.item_id == "123"
211 assert mapping.provider_domain == "zvuk_music"
212 assert mapping.provider_instance == "zvuk_music_test"
213 assert mapping.url == "https://zvuk.com/artist/123"
214
215 def test_parse_artist_with_image(self, mock_provider: Mock) -> None:
216 """Test parsing an artist with image."""
217 image = _create_mock_image("https://zvuk.com/img/{width}x{height}.jpg")
218 artist_obj = _create_mock_artist(artist_id=456, title="Artist With Image", image=image)
219
220 result = parse_artist(mock_provider, artist_obj)
221
222 assert result.item_id == "456"
223 assert result.name == "Artist With Image"
224 assert result.metadata.images is not None
225 assert len(result.metadata.images) == 1
226 assert result.metadata.images[0].type == ImageType.THUMB
227 assert result.metadata.images[0].path == "https://zvuk.com/img/600x600.jpg"
228 assert result.metadata.images[0].remotely_accessible is True
229
230 def test_parse_artist_unknown_name(self, mock_provider: Mock) -> None:
231 """Test parsing an artist with missing title defaults to Unknown Artist."""
232 artist_obj = _create_mock_artist(artist_id=789, title=None)
233
234 result = parse_artist(mock_provider, artist_obj)
235
236 assert result.name == "Unknown Artist"
237
238
239class TestParseAlbum:
240 """Tests for parse_album function."""
241
242 def test_parse_album_basic(self, mock_provider: Mock) -> None:
243 """Test parsing a basic album."""
244 release_obj = _create_mock_release(release_id=456, title="Test Album")
245
246 result = parse_album(mock_provider, release_obj)
247
248 assert result.item_id == "456"
249 assert result.name == "Test Album"
250 assert result.provider == "zvuk_music_test"
251 assert result.album_type == AlbumType.ALBUM
252
253 mapping = next(iter(result.provider_mappings))
254 assert mapping.url == "https://zvuk.com/release/456"
255
256 def test_parse_album_type_single(self, mock_provider: Mock) -> None:
257 """Test parsing an album with type single."""
258 release_obj = _create_mock_release(
259 release_id=1, title="Single Track", release_type="single"
260 )
261
262 result = parse_album(mock_provider, release_obj)
263
264 assert result.album_type == AlbumType.SINGLE
265
266 def test_parse_album_type_ep(self, mock_provider: Mock) -> None:
267 """Test parsing an album with type EP."""
268 release_obj = _create_mock_release(release_id=2, title="EP Release", release_type="ep")
269
270 result = parse_album(mock_provider, release_obj)
271
272 assert result.album_type == AlbumType.EP
273
274 def test_parse_album_type_compilation(self, mock_provider: Mock) -> None:
275 """Test parsing an album with type compilation."""
276 release_obj = _create_mock_release(
277 release_id=3, title="Greatest Hits", release_type="compilation"
278 )
279
280 result = parse_album(mock_provider, release_obj)
281
282 assert result.album_type == AlbumType.COMPILATION
283
284 def test_parse_album_with_date(self, mock_provider: Mock) -> None:
285 """Test parsing an album with release date."""
286 release_obj = _create_mock_release(release_id=456, title="Album 2023", date="2023-06-15")
287
288 result = parse_album(mock_provider, release_obj)
289
290 assert result.year == 2023
291 assert result.metadata.release_date is not None
292 assert result.metadata.release_date.year == 2023
293 assert result.metadata.release_date.month == 6
294 assert result.metadata.release_date.day == 15
295
296 def test_parse_album_explicit(self, mock_provider: Mock) -> None:
297 """Test parsing an explicit album."""
298 release_obj = _create_mock_release(release_id=456, title="Explicit Album", explicit=True)
299
300 result = parse_album(mock_provider, release_obj)
301
302 assert result.metadata.explicit is True
303
304 def test_parse_album_with_artists(self, mock_provider: Mock) -> None:
305 """Test parsing an album with artists."""
306 artists = [
307 _create_mock_artist(artist_id=1, title="Artist One"),
308 _create_mock_artist(artist_id=2, title="Artist Two"),
309 ]
310 release_obj = _create_mock_release(
311 release_id=456, title="Collaboration Album", artists=artists
312 )
313
314 result = parse_album(mock_provider, release_obj)
315
316 assert len(result.artists) == 2
317 assert result.artists[0].name == "Artist One"
318 assert result.artists[1].name == "Artist Two"
319
320 def test_parse_album_with_genres(self, mock_provider: Mock) -> None:
321 """Test parsing an album with genres (full Release only)."""
322 genre1 = Mock()
323 genre1.name = "Rock"
324 genre2 = Mock()
325 genre2.name = "Alternative"
326 release_obj = _create_mock_release(
327 release_id=456, title="Rock Album", genres=[genre1, genre2]
328 )
329
330 result = parse_album(mock_provider, release_obj)
331
332 assert result.metadata.genres == {"Rock", "Alternative"}
333
334 def test_parse_album_with_image(self, mock_provider: Mock) -> None:
335 """Test parsing an album with cover image."""
336 image = _create_mock_image("https://zvuk.com/cover/{width}x{height}.jpg")
337 release_obj = _create_mock_release(release_id=456, title="Album With Cover", image=image)
338
339 result = parse_album(mock_provider, release_obj)
340
341 assert result.metadata.images is not None
342 assert len(result.metadata.images) == 1
343 assert result.metadata.images[0].path == "https://zvuk.com/cover/600x600.jpg"
344
345 def test_parse_album_with_version_in_title(self, mock_provider: Mock) -> None:
346 """Test parsing an album with version in title."""
347 release_obj = _create_mock_release(release_id=456, title="Album Name (Deluxe Edition)")
348
349 result = parse_album(mock_provider, release_obj)
350
351 assert result.name == "Album Name"
352 assert result.version == "Deluxe Edition"
353
354
355class TestParseTrack:
356 """Tests for parse_track function."""
357
358 def test_parse_track_basic(self, mock_provider: Mock) -> None:
359 """Test parsing a basic track."""
360 track_obj = _create_mock_track(track_id=789, title="Test Track", duration=180)
361
362 result = parse_track(mock_provider, track_obj)
363
364 assert result.item_id == "789"
365 assert result.name == "Test Track"
366 assert result.duration == 180
367 assert result.provider == "zvuk_music_test"
368
369 mapping = next(iter(result.provider_mappings))
370 assert mapping.url == "https://zvuk.com/track/789"
371
372 def test_parse_track_with_artists(self, mock_provider: Mock) -> None:
373 """Test parsing a track with artists."""
374 artists = [
375 _create_mock_artist(artist_id=1, title="Singer"),
376 _create_mock_artist(artist_id=2, title="Featuring Artist"),
377 ]
378 track_obj = _create_mock_track(track_id=789, title="Track", artists=artists)
379
380 result = parse_track(mock_provider, track_obj)
381
382 assert len(result.artists) == 2
383 assert result.artists[0].name == "Singer"
384 assert result.artists[1].name == "Featuring Artist"
385
386 def test_parse_track_with_release(self, mock_provider: Mock) -> None:
387 """Test parsing a track with album (release) information."""
388 release = Mock()
389 release.id = 456
390 release.title = "Test Album"
391 release.image = _create_mock_image("https://zvuk.com/cover/{width}x{height}.jpg")
392
393 track_obj = _create_mock_track(track_id=789, title="Track", release=release)
394
395 result = parse_track(mock_provider, track_obj)
396
397 assert result.album is not None
398 assert result.album.item_id == "456"
399 assert result.album.name == "Test Album"
400 mock_provider.get_item_mapping.assert_called_with(
401 media_type="album",
402 key="456",
403 name="Test Album",
404 )
405
406 def test_parse_track_with_release_image(self, mock_provider: Mock) -> None:
407 """Test parsing a track gets image from release."""
408 release = Mock()
409 release.id = 456
410 release.title = "Test Album"
411 release.image = _create_mock_image("https://zvuk.com/cover/{width}x{height}.jpg")
412
413 track_obj = _create_mock_track(track_id=789, title="Track", release=release)
414
415 result = parse_track(mock_provider, track_obj)
416
417 assert result.metadata.images is not None
418 assert len(result.metadata.images) == 1
419 assert result.metadata.images[0].path == "https://zvuk.com/cover/600x600.jpg"
420
421 def test_parse_track_explicit(self, mock_provider: Mock) -> None:
422 """Test parsing an explicit track."""
423 track_obj = _create_mock_track(track_id=789, title="Explicit Track", explicit=True)
424
425 result = parse_track(mock_provider, track_obj)
426
427 assert result.metadata.explicit is True
428
429 def test_parse_track_with_position(self, mock_provider: Mock) -> None:
430 """Test parsing a track with position (full Track only)."""
431 track_obj = _create_mock_track(track_id=789, title="Track 5", position=5)
432
433 result = parse_track(mock_provider, track_obj)
434
435 assert result.track_number == 5
436
437 def test_parse_track_with_version_in_title(self, mock_provider: Mock) -> None:
438 """Test parsing a track with version in title."""
439 track_obj = _create_mock_track(track_id=789, title="Song Name (Acoustic Version)")
440
441 result = parse_track(mock_provider, track_obj)
442
443 assert result.name == "Song Name"
444 assert result.version == "Acoustic Version"
445
446 def test_parse_track_unknown_name(self, mock_provider: Mock) -> None:
447 """Test parsing a track with missing title defaults to Unknown Track."""
448 track_obj = _create_mock_track(track_id=789, title=None)
449
450 result = parse_track(mock_provider, track_obj)
451
452 assert result.name == "Unknown Track"
453
454
455class TestParsePlaylist:
456 """Tests for parse_playlist function."""
457
458 def test_parse_playlist_basic(self, mock_provider: Mock) -> None:
459 """Test parsing a basic playlist."""
460 playlist_obj = _create_mock_playlist(playlist_id=999, title="Test Playlist")
461
462 result = parse_playlist(mock_provider, playlist_obj)
463
464 assert result.item_id == "999"
465 assert result.name == "Test Playlist"
466 assert result.provider == "zvuk_music_test"
467 assert result.owner == "Zvuk Music"
468 assert result.is_editable is False
469
470 mapping = next(iter(result.provider_mappings))
471 assert mapping.url == "https://zvuk.com/playlist/999"
472 assert mapping.is_unique is False
473
474 def test_parse_playlist_editable(self, mock_provider: Mock) -> None:
475 """Test parsing a user-owned playlist (is_editable=True)."""
476 playlist_obj = _create_mock_playlist(
477 playlist_id=999,
478 title="My Playlist",
479 user_id=12345, # Same as mock_provider.client.user_id
480 )
481
482 result = parse_playlist(mock_provider, playlist_obj)
483
484 assert result.is_editable is True
485 assert result.owner == "Me"
486
487 mapping = next(iter(result.provider_mappings))
488 assert mapping.is_unique is True
489
490 def test_parse_playlist_not_editable(self, mock_provider: Mock) -> None:
491 """Test parsing another user's playlist (is_editable=False)."""
492 playlist_obj = _create_mock_playlist(
493 playlist_id=999,
494 title="Their Playlist",
495 user_id=99999, # Different from mock_provider.client.user_id
496 )
497
498 result = parse_playlist(mock_provider, playlist_obj)
499
500 assert result.is_editable is False
501 assert result.owner == "Zvuk Music"
502
503 def test_parse_playlist_with_description(self, mock_provider: Mock) -> None:
504 """Test parsing a playlist with description."""
505 playlist_obj = _create_mock_playlist(
506 playlist_id=999,
507 title="Playlist",
508 description="A great collection of songs",
509 )
510
511 result = parse_playlist(mock_provider, playlist_obj)
512
513 assert result.metadata.description == "A great collection of songs"
514
515 def test_parse_playlist_with_image(self, mock_provider: Mock) -> None:
516 """Test parsing a playlist with cover image."""
517 image = _create_mock_image("https://zvuk.com/playlist/{width}x{height}.jpg")
518 playlist_obj = _create_mock_playlist(
519 playlist_id=999,
520 title="Playlist With Cover",
521 image=image,
522 )
523
524 result = parse_playlist(mock_provider, playlist_obj)
525
526 assert result.metadata.images is not None
527 assert len(result.metadata.images) == 1
528 assert result.metadata.images[0].path == "https://zvuk.com/playlist/600x600.jpg"
529
530 def test_parse_playlist_unknown_name(self, mock_provider: Mock) -> None:
531 """Test parsing a playlist with missing title defaults to Unknown Playlist."""
532 playlist_obj = _create_mock_playlist(playlist_id=999, title=None)
533
534 result = parse_playlist(mock_provider, playlist_obj)
535
536 assert result.name == "Unknown Playlist"
537