/
/
/
1"""Test we can parse Yandex Music API objects into Music Assistant models."""
2
3from __future__ import annotations
4
5import json
6import pathlib
7from typing import TYPE_CHECKING, Any, cast
8
9import pytest
10from yandex_music import Album as YandexAlbum
11from yandex_music import Artist as YandexArtist
12from yandex_music import Playlist as YandexPlaylist
13from yandex_music import Track as YandexTrack
14
15from music_assistant.providers.yandex_music.parsers import (
16 parse_album,
17 parse_artist,
18 parse_playlist,
19 parse_track,
20)
21from music_assistant.providers.yandex_music.provider import YandexMusicProvider
22
23from .conftest import DE_JSON_CLIENT
24
25if TYPE_CHECKING:
26 from syrupy.assertion import SnapshotAssertion
27
28 from .conftest import ProviderStub
29
30FIXTURES_DIR = pathlib.Path(__file__).parent / "fixtures"
31ARTIST_FIXTURES = list(FIXTURES_DIR.glob("artists/*.json"))
32ALBUM_FIXTURES = list(FIXTURES_DIR.glob("albums/*.json"))
33TRACK_FIXTURES = list(FIXTURES_DIR.glob("tracks/*.json"))
34PLAYLIST_FIXTURES = list(FIXTURES_DIR.glob("playlists/*.json"))
35
36
37def _load_json(path: pathlib.Path) -> dict[str, Any]:
38 """Load JSON fixture."""
39 with open(path) as f:
40 return cast("dict[str, Any]", json.load(f))
41
42
43def _artist_from_fixture(path: pathlib.Path) -> YandexArtist | None:
44 """Deserialize Yandex Artist from fixture JSON."""
45 data = _load_json(path)
46 return YandexArtist.de_json(data, DE_JSON_CLIENT)
47
48
49def _album_from_fixture(path: pathlib.Path) -> YandexAlbum | None:
50 """Deserialize Yandex Album from fixture JSON."""
51 data = _load_json(path)
52 return YandexAlbum.de_json(data, DE_JSON_CLIENT)
53
54
55def _track_from_fixture(path: pathlib.Path) -> YandexTrack | None:
56 """Deserialize Yandex Track from fixture JSON."""
57 data = _load_json(path)
58 return YandexTrack.de_json(data, DE_JSON_CLIENT)
59
60
61def _playlist_from_fixture(path: pathlib.Path) -> YandexPlaylist | None:
62 """Deserialize Yandex Playlist from fixture JSON."""
63 data = _load_json(path)
64 return YandexPlaylist.de_json(data, DE_JSON_CLIENT)
65
66
67# provider_stub fixture is provided by conftest.py
68
69
70@pytest.mark.parametrize("example", ARTIST_FIXTURES, ids=lambda val: val.stem)
71def test_parse_artist(example: pathlib.Path, provider_stub: ProviderStub) -> None:
72 """Test we can parse artists from fixture JSON."""
73 artist_obj = _artist_from_fixture(example)
74 assert artist_obj is not None
75 result = parse_artist(cast("YandexMusicProvider", provider_stub), artist_obj)
76 assert result.item_id == str(artist_obj.id)
77 assert result.name == (artist_obj.name or "Unknown Artist")
78 assert result.provider == provider_stub.instance_id
79 assert len(result.provider_mappings) == 1
80 mapping = next(iter(result.provider_mappings))
81 assert f"music.yandex.ru/artist/{artist_obj.id}" in (mapping.url or "")
82
83
84def test_parse_artist_with_cover(provider_stub: ProviderStub) -> None:
85 """Test parsing artist with cover image."""
86 path = FIXTURES_DIR / "artists" / "with_cover.json"
87 artist_obj = _artist_from_fixture(path)
88 assert artist_obj is not None
89 result = parse_artist(cast("YandexMusicProvider", provider_stub), artist_obj)
90 assert result.item_id == "200"
91 assert result.name == "Artist With Cover"
92 if artist_obj.cover and artist_obj.cover.uri:
93 assert result.metadata.images is not None
94 assert len(result.metadata.images) == 1
95 assert "avatars.yandex.net" in (result.metadata.images[0].path or "")
96
97
98@pytest.mark.parametrize("example", ALBUM_FIXTURES, ids=lambda val: val.stem)
99def test_parse_album(example: pathlib.Path, provider_stub: ProviderStub) -> None:
100 """Test we can parse albums from fixture JSON."""
101 album_obj = _album_from_fixture(example)
102 assert album_obj is not None
103 result = parse_album(cast("YandexMusicProvider", provider_stub), album_obj)
104 assert result.item_id == str(album_obj.id)
105 assert result.name
106 assert result.provider == provider_stub.instance_id
107 mapping = next(iter(result.provider_mappings))
108 assert f"music.yandex.ru/album/{album_obj.id}" in (mapping.url or "")
109 if album_obj.year:
110 assert result.year == album_obj.year
111
112
113@pytest.mark.parametrize("example", TRACK_FIXTURES, ids=lambda val: val.stem)
114def test_parse_track(example: pathlib.Path, provider_stub: ProviderStub) -> None:
115 """Test we can parse tracks from fixture JSON."""
116 track_obj = _track_from_fixture(example)
117 assert track_obj is not None
118 result = parse_track(cast("YandexMusicProvider", provider_stub), track_obj)
119 assert result.item_id == str(track_obj.id)
120 assert result.name
121 assert result.duration == (track_obj.duration_ms or 0) // 1000
122 mapping = next(iter(result.provider_mappings))
123 assert f"music.yandex.ru/track/{track_obj.id}" in (mapping.url or "")
124
125
126def test_parse_track_with_artist_and_album(provider_stub: ProviderStub) -> None:
127 """Test parsing track with artist and album."""
128 path = FIXTURES_DIR / "tracks" / "with_artist_and_album.json"
129 track_obj = _track_from_fixture(path)
130 assert track_obj is not None
131 result = parse_track(cast("YandexMusicProvider", provider_stub), track_obj)
132 assert result.item_id == "500"
133 if track_obj.artists:
134 assert len(result.artists) >= 1
135 assert result.artists[0].name == "Track Artist"
136 if track_obj.albums:
137 assert result.album is not None
138 assert result.album.item_id == "20"
139 assert result.album.name == "Track Album"
140
141
142@pytest.mark.parametrize("example", PLAYLIST_FIXTURES, ids=lambda val: val.stem)
143def test_parse_playlist(example: pathlib.Path, provider_stub: ProviderStub) -> None:
144 """Test we can parse playlists from fixture JSON."""
145 playlist_obj = _playlist_from_fixture(example)
146 assert playlist_obj is not None
147 result = parse_playlist(cast("YandexMusicProvider", provider_stub), playlist_obj)
148 owner_id = (
149 str(playlist_obj.owner.uid) if playlist_obj.owner else str(provider_stub.client.user_id)
150 )
151 kind = str(playlist_obj.kind)
152 assert result.item_id == f"{owner_id}:{kind}"
153 assert result.name == (playlist_obj.title or "Unknown Playlist")
154 mapping = next(iter(result.provider_mappings))
155 assert f"music.yandex.ru/users/{owner_id}/playlists/{kind}" in (mapping.url or "")
156
157
158def test_parse_playlist_editable(provider_stub: ProviderStub) -> None:
159 """Test parsing own playlist (editable)."""
160 path = FIXTURES_DIR / "playlists" / "minimal.json"
161 playlist_obj = _playlist_from_fixture(path)
162 assert playlist_obj is not None
163 result = parse_playlist(cast("YandexMusicProvider", provider_stub), playlist_obj)
164 assert result.owner == "Me"
165 assert result.is_editable is True
166
167
168def test_parse_playlist_other_user(provider_stub: ProviderStub) -> None:
169 """Test parsing playlist owned by another user."""
170 path = FIXTURES_DIR / "playlists" / "other_user.json"
171 playlist_obj = _playlist_from_fixture(path)
172 assert playlist_obj is not None
173 result = parse_playlist(cast("YandexMusicProvider", provider_stub), playlist_obj)
174 assert result.item_id == "99999:1"
175 assert result.name == "Shared Playlist"
176 assert result.owner == "Other User"
177 assert result.is_editable is False
178 assert result.metadata.description == "A shared playlist"
179
180
181# --- Snapshot tests ---
182
183
184def _sort_for_snapshot(parsed: dict[str, Any]) -> dict[str, Any]:
185 """Sort lists in parsed dict for deterministic snapshot comparison."""
186 if parsed.get("external_ids"):
187 parsed["external_ids"] = sorted(parsed["external_ids"])
188 if "metadata" in parsed and isinstance(parsed["metadata"], dict):
189 if parsed["metadata"].get("genres"):
190 parsed["metadata"]["genres"] = sorted(parsed["metadata"]["genres"])
191 return parsed
192
193
194@pytest.mark.parametrize("example", ARTIST_FIXTURES, ids=lambda val: val.stem)
195def test_parse_artist_snapshot(
196 example: pathlib.Path,
197 provider_stub: ProviderStub,
198 snapshot: SnapshotAssertion,
199) -> None:
200 """Snapshot test for artist parsing."""
201 artist_obj = _artist_from_fixture(example)
202 assert artist_obj is not None
203 result = parse_artist(cast("YandexMusicProvider", provider_stub), artist_obj)
204 parsed = _sort_for_snapshot(result.to_dict())
205 assert snapshot == parsed
206
207
208@pytest.mark.parametrize("example", ALBUM_FIXTURES, ids=lambda val: val.stem)
209def test_parse_album_snapshot(
210 example: pathlib.Path,
211 provider_stub: ProviderStub,
212 snapshot: SnapshotAssertion,
213) -> None:
214 """Snapshot test for album parsing."""
215 album_obj = _album_from_fixture(example)
216 assert album_obj is not None
217 result = parse_album(cast("YandexMusicProvider", provider_stub), album_obj)
218 parsed = _sort_for_snapshot(result.to_dict())
219 assert snapshot == parsed
220
221
222@pytest.mark.parametrize("example", TRACK_FIXTURES, ids=lambda val: val.stem)
223def test_parse_track_snapshot(
224 example: pathlib.Path,
225 provider_stub: ProviderStub,
226 snapshot: SnapshotAssertion,
227) -> None:
228 """Snapshot test for track parsing."""
229 track_obj = _track_from_fixture(example)
230 assert track_obj is not None
231 result = parse_track(cast("YandexMusicProvider", provider_stub), track_obj)
232 parsed = _sort_for_snapshot(result.to_dict())
233 assert snapshot == parsed
234
235
236@pytest.mark.parametrize("example", PLAYLIST_FIXTURES, ids=lambda val: val.stem)
237def test_parse_playlist_snapshot(
238 example: pathlib.Path,
239 provider_stub: ProviderStub,
240 snapshot: SnapshotAssertion,
241) -> None:
242 """Snapshot test for playlist parsing."""
243 playlist_obj = _playlist_from_fixture(example)
244 assert playlist_obj is not None
245 result = parse_playlist(cast("YandexMusicProvider", provider_stub), playlist_obj)
246 parsed = _sort_for_snapshot(result.to_dict())
247 assert snapshot == parsed
248