/
/
/
1"""Test Tidal Media Manager."""
2
3from typing import Any
4from unittest.mock import AsyncMock, MagicMock, Mock, patch
5
6import pytest
7from music_assistant_models.enums import MediaType
8from music_assistant_models.errors import RetriesExhausted
9from music_assistant_models.media_items import ItemMapping
10
11from music_assistant.providers.tidal.media import TidalMediaManager
12
13
14@pytest.fixture
15def provider_mock() -> Mock:
16 """Return a mock provider."""
17 provider = Mock()
18 provider.domain = "tidal"
19 provider.instance_id = "tidal_instance"
20 provider.auth.user_id = "12345"
21 provider.auth.country_code = "US"
22 provider.api = AsyncMock()
23 provider.api.get.return_value = {}
24 provider.api.paginate = MagicMock()
25
26 async def async_iter(*_args: Any, **_kwargs: Any) -> Any:
27 for item in provider.api.paginate.return_value:
28 yield item
29
30 provider.api.paginate.side_effect = async_iter
31 provider.api.paginate.return_value = []
32
33 provider.logger = Mock()
34
35 def get_item_mapping(media_type: MediaType, key: str, name: str) -> ItemMapping:
36 return ItemMapping(
37 media_type=media_type,
38 item_id=key,
39 provider=provider.instance_id,
40 name=name,
41 )
42
43 provider.get_item_mapping.side_effect = get_item_mapping
44
45 return provider
46
47
48@patch("music_assistant.providers.tidal.media.parse_artist")
49@patch("music_assistant.providers.tidal.media.parse_album")
50@patch("music_assistant.providers.tidal.media.parse_track")
51@patch("music_assistant.providers.tidal.media.parse_playlist")
52async def test_search(
53 mock_parse_playlist: Mock,
54 mock_parse_track: Mock,
55 mock_parse_album: Mock,
56 mock_parse_artist: Mock,
57 media_manager: TidalMediaManager,
58 provider_mock: Mock,
59) -> None:
60 """Test search."""
61 provider_mock.api.get.return_value = {
62 "artists": {"items": [{"id": 1}]},
63 "albums": {"items": [{"id": 1}]},
64 "tracks": {"items": [{"id": 1}]},
65 "playlists": {"items": [{"uuid": "1"}]},
66 }
67
68 mock_parse_artist.return_value = Mock(item_id="1", media_type=MediaType.ARTIST)
69 mock_parse_album.return_value = Mock(item_id="1", media_type=MediaType.ALBUM)
70 mock_parse_track.return_value = Mock(item_id="1", media_type=MediaType.TRACK)
71 mock_parse_playlist.return_value = Mock(item_id="1", media_type=MediaType.PLAYLIST)
72
73 results = await media_manager.search(
74 "query", [MediaType.ARTIST, MediaType.ALBUM, MediaType.TRACK, MediaType.PLAYLIST]
75 )
76
77 assert len(results.artists) == 1
78 assert len(results.albums) == 1
79 assert len(results.tracks) == 1
80 assert len(results.playlists) == 1
81
82 mock_parse_artist.assert_called()
83 mock_parse_album.assert_called()
84 mock_parse_track.assert_called()
85 mock_parse_playlist.assert_called()
86
87 provider_mock.api.get.assert_called_with(
88 "search",
89 params={
90 "query": "query",
91 "types": "artists,albums,tracks,playlists",
92 "limit": 5,
93 },
94 )
95
96
97@patch("music_assistant.providers.tidal.media.parse_artist")
98async def test_get_artist(
99 mock_parse_artist: Mock, media_manager: TidalMediaManager, provider_mock: Mock
100) -> None:
101 """Test get_artist."""
102 provider_mock.api.get.return_value = {"id": 1, "name": "Test Artist"}
103 mock_parse_artist.return_value = Mock(item_id="1")
104
105 artist = await media_manager.get_artist("1")
106
107 assert artist.item_id == "1"
108 provider_mock.api.get.assert_called_with("artists/1")
109 mock_parse_artist.assert_called_once()
110
111
112@patch("music_assistant.providers.tidal.media.parse_album")
113async def test_get_album(
114 mock_parse_album: Mock, media_manager: TidalMediaManager, provider_mock: Mock
115) -> None:
116 """Test get_album."""
117 provider_mock.api.get.return_value = {"id": 1, "title": "Test Album"}
118 mock_parse_album.return_value = Mock(item_id="1")
119
120 album = await media_manager.get_album("1")
121
122 assert album.item_id == "1"
123 provider_mock.api.get.assert_called_with("albums/1")
124 mock_parse_album.assert_called_once()
125
126
127@patch("music_assistant.providers.tidal.media.parse_track")
128async def test_get_track(
129 mock_parse_track: Mock, media_manager: TidalMediaManager, provider_mock: Mock
130) -> None:
131 """Test get_track."""
132 provider_mock.api.get.side_effect = [
133 {"id": 1, "title": "Test Track"}, # Track data
134 {"lyrics": "Test Lyrics"}, # Lyrics data
135 ]
136 mock_parse_track.return_value = Mock(item_id="1")
137
138 track = await media_manager.get_track("1")
139
140 assert track.item_id == "1"
141 assert provider_mock.api.get.call_count == 2
142 provider_mock.api.get.assert_any_call("tracks/1")
143 provider_mock.api.get.assert_any_call("tracks/1/lyrics")
144 mock_parse_track.assert_called_once()
145
146
147@patch("music_assistant.providers.tidal.media.parse_track")
148async def test_get_track_tolerates_lyrics_failure(
149 mock_parse_track: Mock, media_manager: TidalMediaManager, provider_mock: Mock
150) -> None:
151 """Test get_track still returns the track when the lyrics lookup fails."""
152 provider_mock.api.get.side_effect = [
153 {"id": 1, "title": "Test Track"}, # Track data
154 RetriesExhausted("lyrics lookup failed"), # Lyrics data
155 ]
156 mock_parse_track.return_value = Mock(item_id="1")
157
158 track = await media_manager.get_track("1")
159
160 assert track.item_id == "1"
161 mock_parse_track.assert_called_once_with(
162 provider_mock, {"id": 1, "title": "Test Track"}, lyrics=None
163 )
164
165
166@patch("music_assistant.providers.tidal.media.parse_playlist")
167async def test_get_playlist(
168 mock_parse_playlist: Mock, media_manager: TidalMediaManager, provider_mock: Mock
169) -> None:
170 """Test get_playlist."""
171 provider_mock.api.get.return_value = {"uuid": "1", "title": "Test Playlist"}
172 mock_parse_playlist.return_value = Mock(item_id="1")
173
174 playlist = await media_manager.get_playlist("1")
175
176 assert playlist.item_id == "1"
177 provider_mock.api.get.assert_called_with("playlists/1")
178 mock_parse_playlist.assert_called_once()
179
180
181@patch("music_assistant.providers.tidal.media.parse_track")
182async def test_get_album_tracks(
183 mock_parse_track: Mock, media_manager: TidalMediaManager, provider_mock: Mock
184) -> None:
185 """Test get_album_tracks."""
186 provider_mock.api.get.return_value = {"items": [{"id": 1}]}
187 mock_parse_track.return_value = Mock(item_id="1")
188
189 tracks = await media_manager.get_album_tracks("1")
190
191 assert len(tracks) == 1
192 assert tracks[0].item_id == "1"
193 provider_mock.api.get.assert_called_with(
194 "albums/1/tracks",
195 params={"limit": 250},
196 )
197
198
199@patch("music_assistant.providers.tidal.media.parse_album")
200async def test_get_artist_albums(
201 mock_parse_album: Mock, media_manager: TidalMediaManager, provider_mock: Mock
202) -> None:
203 """Test get_artist_albums."""
204 provider_mock.api.get.return_value = {"items": [{"id": 1}]}
205 mock_parse_album.return_value = Mock(item_id="1")
206
207 albums = await media_manager.get_artist_albums("1")
208
209 assert len(albums) == 1
210 assert albums[0].item_id == "1"
211 provider_mock.api.get.assert_called_with(
212 "artists/1/albums",
213 params={"limit": 250},
214 )
215
216
217@patch("music_assistant.providers.tidal.media.parse_track")
218async def test_get_artist_toptracks(
219 mock_parse_track: Mock, media_manager: TidalMediaManager, provider_mock: Mock
220) -> None:
221 """Test get_artist_toptracks."""
222 provider_mock.api.get.return_value = {"items": [{"id": 1}]}
223 mock_parse_track.return_value = Mock(item_id="1")
224
225 tracks = await media_manager.get_artist_toptracks("1")
226
227 assert len(tracks) == 1
228 assert tracks[0].item_id == "1"
229 provider_mock.api.get.assert_called_with(
230 "artists/1/toptracks",
231 params={"limit": 10, "offset": 0},
232 )
233
234
235@patch("music_assistant.providers.tidal.media.parse_track")
236async def test_get_playlist_tracks(
237 mock_parse_track: Mock, media_manager: TidalMediaManager, provider_mock: Mock
238) -> None:
239 """Test get_playlist_tracks."""
240 provider_mock.api.get.return_value = {"items": [{"id": 1}]}
241 mock_parse_track.return_value = Mock(item_id="1")
242
243 tracks = await media_manager.get_playlist_tracks("1")
244
245 assert len(tracks) == 1
246 assert tracks[0].item_id == "1"
247 provider_mock.api.get.assert_called_with(
248 "playlists/1/tracks",
249 params={"limit": 200, "offset": 0},
250 )
251
252
253async def test_get_playlist_favorite_tracks(
254 media_manager: TidalMediaManager, provider_mock: Mock
255) -> None:
256 """Test get_playlist returns the virtual favorite tracks playlist without an API call."""
257 playlist = await media_manager.get_playlist("favorite_tracks")
258
259 assert playlist.item_id == "favorite_tracks"
260 assert playlist.name == "Favorite Tracks"
261 assert not playlist.is_editable
262 provider_mock.api.get.assert_not_called()
263
264
265@patch("music_assistant.providers.tidal.media.parse_track")
266async def test_get_playlist_tracks_favorite_tracks(
267 mock_parse_track: Mock, media_manager: TidalMediaManager, provider_mock: Mock
268) -> None:
269 """Test get_playlist_tracks returns favorite tracks ordered by date descending."""
270 provider_mock.api.get.return_value = {"items": [{"item": {"id": 1}}]}
271 mock_parse_track.return_value = Mock(item_id="1")
272
273 tracks = await media_manager.get_playlist_tracks("favorite_tracks", page=0)
274
275 assert len(tracks) == 1
276 assert tracks[0].item_id == "1"
277 provider_mock.api.get.assert_called_with(
278 "users/12345/favorites/tracks",
279 params={"limit": 200, "offset": 0, "order": "DATE", "orderDirection": "DESC"},
280 )
281