music-assistant-server

9.5 KBPY
test_parsers.py
9.5 KB248 lines • python
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