/
/
/
1"""Test Tidal Library Manager."""
2
3from collections.abc import AsyncGenerator
4from typing import Any
5from unittest.mock import AsyncMock, MagicMock, Mock, patch
6
7import pytest
8from music_assistant_models.enums import MediaType
9from music_assistant_models.media_items import ItemMapping
10
11from music_assistant.providers.tidal.library import TidalLibraryManager
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.api = AsyncMock()
22 provider.api.get.return_value = {"items": []}
23 provider.api.paginate = MagicMock()
24
25 # Configure async iterator for paginate
26 async def async_iter(*_args: Any, **_kwargs: Any) -> AsyncGenerator[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@pytest.fixture
49def library_manager(provider_mock: Mock) -> TidalLibraryManager:
50 """Return a TidalLibraryManager instance."""
51 return TidalLibraryManager(provider_mock)
52
53
54@patch("music_assistant.providers.tidal.library.parse_artist")
55async def test_get_artists(
56 mock_parse_artist: Mock, library_manager: TidalLibraryManager, provider_mock: Mock
57) -> None:
58 """Test get_artists."""
59 provider_mock.api.paginate.return_value = [
60 {"created": "2024-01-01T00:00:00", "item": {"id": 1, "name": "Test Artist"}}
61 ]
62 mock_parse_artist.return_value = Mock(item_id="1")
63
64 artists = [a async for a in library_manager.get_artists()]
65
66 assert len(artists) == 1
67 assert artists[0].item_id == "1"
68 provider_mock.api.paginate.assert_called_with(
69 "users/12345/favorites/artists",
70 )
71 mock_parse_artist.assert_called_once()
72
73
74@patch("music_assistant.providers.tidal.library.parse_album")
75async def test_get_albums(
76 mock_parse_album: Mock, library_manager: TidalLibraryManager, provider_mock: Mock
77) -> None:
78 """Test get_albums."""
79 provider_mock.api.paginate.return_value = [
80 {"created": "2024-01-01T00:00:00", "item": {"id": 1, "title": "Test Album"}}
81 ]
82 mock_parse_album.return_value = Mock(item_id="1")
83
84 albums = [a async for a in library_manager.get_albums()]
85
86 assert len(albums) == 1
87 assert albums[0].item_id == "1"
88 provider_mock.api.paginate.assert_called_with(
89 "users/12345/favorites/albums",
90 )
91 mock_parse_album.assert_called_once()
92
93
94@patch("music_assistant.providers.tidal.library.parse_track")
95async def test_get_tracks(
96 mock_parse_track: Mock, library_manager: TidalLibraryManager, provider_mock: Mock
97) -> None:
98 """Test get_tracks."""
99 provider_mock.api.paginate.return_value = [
100 {"created": "2024-01-01T00:00:00", "item": {"id": 1, "title": "Test Track"}}
101 ]
102 mock_parse_track.return_value = Mock(item_id="1")
103
104 tracks = [t async for t in library_manager.get_tracks()]
105
106 assert len(tracks) == 1
107 assert tracks[0].item_id == "1"
108 provider_mock.api.paginate.assert_called_with(
109 "users/12345/favorites/tracks",
110 )
111 mock_parse_track.assert_called_once()
112
113
114@patch("music_assistant.providers.tidal.library.parse_playlist")
115async def test_get_playlists(
116 mock_parse_playlist: Mock, library_manager: TidalLibraryManager, provider_mock: Mock
117) -> None:
118 """Test get_playlists."""
119 # Mock mixes response
120 mixes_response = [{"id": "mix_1", "title": "Mix 1"}]
121 # Mock playlists response
122 playlists_response = [
123 {
124 "created": "2024-01-01T00:00:00",
125 "playlist": {"uuid": "pl_1", "title": "Playlist 1"},
126 }
127 ]
128
129 # Configure paginate side effect
130 async def paginate_side_effect(endpoint: str, **_kwargs: Any) -> AsyncGenerator[dict[str, Any]]:
131 if "mixes" in endpoint:
132 for item in mixes_response:
133 yield item
134 else:
135 # The ignore[assignment] is needed because of the different return types
136 for item in playlists_response: # type: ignore[assignment]
137 yield item
138
139 provider_mock.api.paginate.side_effect = paginate_side_effect
140
141 # Setup mock return values
142 mock_parse_playlist.side_effect = [
143 Mock(item_id="mix_1"),
144 Mock(item_id="pl_1"),
145 ]
146
147 playlists = [p async for p in library_manager.get_playlists()]
148
149 assert len(playlists) == 3
150 assert playlists[0].item_id == "mix_1"
151 assert playlists[1].item_id == "pl_1"
152 assert playlists[2].item_id == "favorite_tracks"
153 assert mock_parse_playlist.call_count == 2
154
155
156async def test_add_item_artist(library_manager: TidalLibraryManager, provider_mock: Mock) -> None:
157 """Test add_item for artist."""
158 item = Mock(item_id="123", media_type=MediaType.ARTIST)
159 await library_manager.add_item(item)
160
161 provider_mock.api.post.assert_called_with(
162 "users/12345/favorites/artists",
163 data={"artistId": "123"},
164 as_form=True,
165 )
166
167
168async def test_add_item_album(library_manager: TidalLibraryManager, provider_mock: Mock) -> None:
169 """Test add_item for album."""
170 item = Mock(item_id="123", media_type=MediaType.ALBUM)
171 await library_manager.add_item(item)
172
173 provider_mock.api.post.assert_called_with(
174 "users/12345/favorites/albums",
175 data={"albumId": "123"},
176 as_form=True,
177 )
178
179
180async def test_add_item_track(library_manager: TidalLibraryManager, provider_mock: Mock) -> None:
181 """Test add_item for track."""
182 item = Mock(item_id="123", media_type=MediaType.TRACK)
183 await library_manager.add_item(item)
184
185 provider_mock.api.post.assert_called_with(
186 "users/12345/favorites/tracks",
187 data={"trackId": "123"},
188 as_form=True,
189 )
190
191
192async def test_add_item_playlist(library_manager: TidalLibraryManager, provider_mock: Mock) -> None:
193 """Test add_item for playlist."""
194 item = Mock(item_id="123", media_type=MediaType.PLAYLIST)
195 await library_manager.add_item(item)
196
197 provider_mock.api.post.assert_called_with(
198 "users/12345/favorites/playlists",
199 data={"uuids": "123"},
200 as_form=True,
201 )
202
203
204async def test_remove_item_artist(
205 library_manager: TidalLibraryManager, provider_mock: Mock
206) -> None:
207 """Test remove_item for artist."""
208 await library_manager.remove_item("123", MediaType.ARTIST)
209
210 provider_mock.api.delete.assert_called_with("users/12345/favorites/artists/123")
211
212
213async def test_remove_item_album(library_manager: TidalLibraryManager, provider_mock: Mock) -> None:
214 """Test remove_item for album."""
215 await library_manager.remove_item("123", MediaType.ALBUM)
216
217 provider_mock.api.delete.assert_called_with("users/12345/favorites/albums/123")
218
219
220async def test_remove_item_track(library_manager: TidalLibraryManager, provider_mock: Mock) -> None:
221 """Test remove_item for track."""
222 await library_manager.remove_item("123", MediaType.TRACK)
223
224 provider_mock.api.delete.assert_called_with("users/12345/favorites/tracks/123")
225
226
227async def test_remove_item_playlist(
228 library_manager: TidalLibraryManager, provider_mock: Mock
229) -> None:
230 """Test remove_item for playlist."""
231 await library_manager.remove_item("123", MediaType.PLAYLIST)
232
233 provider_mock.api.delete.assert_called_with("users/12345/favorites/playlists/123")
234
235
236async def test_get_playlists_includes_favorite_tracks(
237 library_manager: TidalLibraryManager, provider_mock: Mock
238) -> None:
239 """Test that get_playlists yields the favorite tracks playlist as the first item."""
240 provider_mock.api.paginate.return_value = []
241
242 playlists = [p async for p in library_manager.get_playlists()]
243
244 assert len(playlists) >= 1
245 assert playlists[0].item_id == "favorite_tracks"
246 assert playlists[0].name == "Favorite Tracks"
247 assert not playlists[0].is_editable
248