/
/
/
1"""Additional tests for Tidal Media Manager - Mix operations and similar tracks."""
2
3from unittest.mock import AsyncMock, Mock, patch
4
5import pytest
6from music_assistant_models.enums import MediaType
7from music_assistant_models.errors import MediaNotFoundError
8from music_assistant_models.media_items import ItemMapping
9
10from music_assistant.providers.tidal.media import TidalMediaManager
11
12
13@pytest.fixture
14def provider_mock() -> Mock:
15 """Return a mock provider."""
16 provider = Mock()
17 provider.domain = "tidal"
18 provider.instance_id = "tidal_instance"
19 provider.auth.user_id = "12345"
20 provider.auth.country_code = "US"
21 provider.api = AsyncMock()
22 provider.api.get_data.return_value = {}
23 provider.logger = Mock()
24
25 def get_item_mapping(media_type: MediaType, key: str, name: str) -> ItemMapping:
26 return ItemMapping(
27 media_type=media_type,
28 item_id=key,
29 provider=provider.instance_id,
30 name=name,
31 )
32
33 provider.get_item_mapping.side_effect = get_item_mapping
34
35 return provider
36
37
38@pytest.fixture
39def media_manager(provider_mock: Mock) -> TidalMediaManager:
40 """Return a TidalMediaManager instance."""
41 return TidalMediaManager(provider_mock)
42
43
44@patch("music_assistant.providers.tidal.media.parse_playlist")
45async def test_get_playlist_mix(
46 mock_parse_playlist: Mock, media_manager: TidalMediaManager, provider_mock: Mock
47) -> None:
48 """Test get_playlist with mix ID."""
49 provider_mock.api.get_data.return_value = {
50 "title": "My Mix",
51 "rows": [
52 {"modules": [{"mix": {"images": {"MEDIUM": {"url": "http://example.com/mix.jpg"}}}}]},
53 ],
54 "lastUpdated": "2023-01-01",
55 }
56 mock_parse_playlist.return_value = Mock(item_id="mix_123")
57
58 playlist = await media_manager.get_playlist("mix_123")
59
60 assert playlist.item_id == "mix_123"
61 provider_mock.api.get_data.assert_called_with(
62 "pages/mix",
63 params={"mixId": "123", "deviceType": "BROWSER"},
64 )
65 mock_parse_playlist.assert_called_once()
66 # Verify is_mix=True was passed
67 assert mock_parse_playlist.call_args[1]["is_mix"] is True
68
69
70@patch("music_assistant.providers.tidal.media.parse_playlist")
71async def test_get_playlist_fallback_to_mix(
72 mock_parse_playlist: Mock, media_manager: TidalMediaManager, provider_mock: Mock
73) -> None:
74 """Test get_playlist falls back to mix lookup on MediaNotFoundError."""
75 # First call raises error, second succeeds
76 provider_mock.api.get_data.side_effect = [
77 MediaNotFoundError("Playlist not found"),
78 {
79 "title": "My Mix",
80 "rows": [{"modules": [{"mix": {"images": {}}}]}],
81 },
82 ]
83 mock_parse_playlist.return_value = Mock(item_id="123")
84
85 playlist = await media_manager.get_playlist("123")
86
87 assert playlist.item_id == "123"
88 assert provider_mock.api.get_data.call_count == 2
89 # First call as playlist
90 provider_mock.api.get_data.assert_any_call("playlists/123")
91 # Second call as mix
92 provider_mock.api.get_data.assert_any_call(
93 "pages/mix",
94 params={"mixId": "123", "deviceType": "BROWSER"},
95 )
96
97
98@patch("music_assistant.providers.tidal.media.parse_track")
99async def test_get_similar_tracks(
100 mock_parse_track: Mock, media_manager: TidalMediaManager, provider_mock: Mock
101) -> None:
102 """Test get_similar_tracks."""
103 provider_mock.api.get_data.return_value = {"items": [{"id": 1}, {"id": 2}, {"id": 3}]}
104 mock_parse_track.return_value = Mock(item_id="1")
105
106 tracks = await media_manager.get_similar_tracks("123", limit=25)
107
108 assert len(tracks) == 3
109 provider_mock.api.get_data.assert_called_with(
110 "tracks/123/radio",
111 params={"limit": 25},
112 )
113
114
115@patch("music_assistant.providers.tidal.media.parse_track")
116async def test_get_playlist_tracks_mix(
117 mock_parse_track: Mock, media_manager: TidalMediaManager, provider_mock: Mock
118) -> None:
119 """Test get_playlist_tracks with mix ID."""
120 provider_mock.api.get_data.return_value = {
121 "rows": [
122 {}, # First row is mix info
123 { # Second row has tracks
124 "modules": [{"pagedList": {"items": [{"id": 1}, {"id": 2}]}}]
125 },
126 ]
127 }
128
129 # Mock track with position attribute
130 def create_track(item_id: int, position: int) -> Mock:
131 track = Mock(item_id=str(item_id))
132 track.position = position
133 return track
134
135 mock_parse_track.side_effect = [
136 create_track(1, 1),
137 create_track(2, 2),
138 ]
139
140 tracks = await media_manager.get_playlist_tracks("mix_123")
141
142 assert len(tracks) == 2
143 assert tracks[0].position == 1
144 assert tracks[1].position == 2
145 provider_mock.api.get_data.assert_called_with(
146 "pages/mix",
147 params={"mixId": "123", "deviceType": "BROWSER"},
148 )
149
150
151async def test_get_mix_details_no_rows(
152 media_manager: TidalMediaManager, provider_mock: Mock
153) -> None:
154 """Test _get_mix_details raises error when no rows."""
155 provider_mock.api.get_data.return_value = {"rows": []}
156
157 with pytest.raises(MediaNotFoundError, match="Mix 123 has no tracks"):
158 await media_manager.get_playlist_tracks("mix_123")
159
160
161async def test_search_empty_results(media_manager: TidalMediaManager, provider_mock: Mock) -> None:
162 """Test search with empty results."""
163 provider_mock.api.get_data.return_value = {}
164
165 results = await media_manager.search("query", [MediaType.ARTIST])
166
167 assert len(results.artists) == 0
168 assert len(results.albums) == 0
169 assert len(results.tracks) == 0
170 assert len(results.playlists) == 0
171