/
/
/
1"""Test we can parse Open Subsonic models into Music Assistant models."""
2
3import logging
4import pathlib
5from typing import TYPE_CHECKING
6
7import aiofiles
8import pytest
9from libopensonic.media import (
10 AlbumID3,
11 AlbumInfo,
12 ArtistID3,
13 ArtistInfo2,
14 Child,
15 Lyrics,
16 Playlist,
17 PodcastChannel,
18 PodcastEpisode,
19 StructuredLyrics,
20)
21from libopensonic.media.media_types import InternetRadioStation
22
23from music_assistant.providers.opensubsonic.parsers import (
24 parse_album,
25 parse_artist,
26 parse_epsiode,
27 parse_playlist,
28 parse_podcast,
29 parse_radio,
30 parse_structured_lyrics,
31 parse_track,
32)
33
34if TYPE_CHECKING:
35 from syrupy.assertion import SnapshotAssertion
36
37FIXTURES_DIR = pathlib.Path(__file__).parent / "fixtures"
38ARTIST_FIXTURES = list(FIXTURES_DIR.glob("artists/*.artist.json"))
39ALBUM_FIXTURES = list(FIXTURES_DIR.glob("albums/*.album.json"))
40PLAYLIST_FIXTURES = list(FIXTURES_DIR.glob("playlists/*.playlist.json"))
41PODCAST_FIXTURES = list(FIXTURES_DIR.glob("podcasts/*.podcast.json"))
42EPISODE_FIXTURES = list(FIXTURES_DIR.glob("episodes/*.episode.json"))
43TRACK_FIXTURES = list(FIXTURES_DIR.glob("tracks/*.track.json"))
44LYRICS_FIXTURES = list(FIXTURES_DIR.glob("lyrics/*.lyrics.json"))
45STRUCTURED_LYRICS_FIXTURES = list(FIXTURES_DIR.glob("structured-lyrics/*.structured-lyrics.json"))
46
47_LOGGER = logging.getLogger(__name__)
48
49
50@pytest.mark.parametrize("example", ARTIST_FIXTURES, ids=lambda val: str(val.stem))
51async def test_parse_artists(example: pathlib.Path, snapshot: SnapshotAssertion) -> None:
52 """Test we can parse artists."""
53 async with aiofiles.open(example, encoding="utf-8") as fp:
54 artist = ArtistID3.from_json(await fp.read())
55
56 parsed = parse_artist("xx-instance-id-xx", artist).to_dict()
57 # sort external Ids to ensure they are always in the same order for snapshot testing
58 parsed["external_ids"].sort()
59 assert snapshot == parsed
60
61 # Find the corresponding info file
62 example_info = example.with_suffix("").with_suffix(".info.json")
63 async with aiofiles.open(example_info, encoding="utf-8") as fp:
64 artist_info = ArtistInfo2.from_json(await fp.read())
65
66 parsed = parse_artist("xx-instance-id-xx", artist, artist_info).to_dict()
67 # sort external Ids to ensure they are always in the same order for snapshot testing
68 parsed["external_ids"].sort()
69 assert snapshot == parsed
70
71
72@pytest.mark.parametrize("example", ALBUM_FIXTURES, ids=lambda val: str(val.stem))
73async def test_parse_albums(example: pathlib.Path, snapshot: SnapshotAssertion) -> None:
74 """Test we can parse albums."""
75 async with aiofiles.open(example, encoding="utf-8") as fp:
76 album = AlbumID3.from_json(await fp.read())
77
78 parsed = parse_album(_LOGGER, "xx-instance-id-xx", album).to_dict()
79 # sort external Ids and genres to ensure they are always in the same order for snapshot testing
80 parsed["external_ids"].sort()
81 parsed["metadata"]["genres"].sort()
82 assert snapshot == parsed
83
84 # Find the corresponding info file
85 example_info = example.with_suffix("").with_suffix(".info.json")
86 async with aiofiles.open(example_info, encoding="utf-8") as fp:
87 album_info = AlbumInfo.from_json(await fp.read())
88
89 parsed = parse_album(_LOGGER, "xx-instance-id-xx", album, album_info).to_dict()
90 # sort external Ids and genres to ensure they are always in the same order for snapshot testing
91 parsed["external_ids"].sort()
92 parsed["metadata"]["genres"].sort()
93 assert snapshot == parsed
94
95
96@pytest.mark.parametrize("example", PLAYLIST_FIXTURES, ids=lambda val: str(val.stem))
97async def test_parse_playlist(example: pathlib.Path, snapshot: SnapshotAssertion) -> None:
98 """Test we can parse Playlists."""
99 async with aiofiles.open(example, encoding="utf-8") as fp:
100 playlist = Playlist.from_json(await fp.read())
101
102 parsed = parse_playlist("xx-instance-id-xx", playlist).to_dict()
103 # sort external Ids to ensure they are always in the same order for snapshot testing
104 parsed["external_ids"].sort()
105 assert snapshot == parsed
106
107
108@pytest.mark.parametrize("example", PODCAST_FIXTURES, ids=lambda val: str(val.stem))
109async def test_parse_podcast(example: pathlib.Path, snapshot: SnapshotAssertion) -> None:
110 """Test we can parse Podcasts."""
111 async with aiofiles.open(example, encoding="utf-8") as fp:
112 podcast = PodcastChannel.from_json(await fp.read())
113
114 parsed = parse_podcast("xx-instance-id-xx", podcast).to_dict()
115 # sort external Ids to ensure they are always in the same order for snapshot testing
116 parsed["external_ids"].sort()
117 assert snapshot == parsed
118
119
120@pytest.mark.parametrize("example", EPISODE_FIXTURES, ids=lambda val: str(val.stem))
121async def test_parse_episode(example: pathlib.Path, snapshot: SnapshotAssertion) -> None:
122 """Test we can parse Podcast Episodes."""
123 async with aiofiles.open(example, encoding="utf-8") as fp:
124 episode = PodcastEpisode.from_json(await fp.read())
125
126 example_channel = example.with_suffix("").with_suffix(".podcast.json")
127 async with aiofiles.open(example_channel, encoding="utf-8") as fp:
128 channel = PodcastChannel.from_json(await fp.read())
129
130 parsed = parse_epsiode("xx-instance-id-xx", episode, channel).to_dict()
131 # sort external Ids to ensure they are always in the same order for snapshot testing
132 parsed["external_ids"].sort()
133 assert snapshot == parsed
134
135
136def test_parse_radio() -> None:
137 """Test we can parse an internet radio station into a Music Assistant radio item."""
138 station = InternetRadioStation(
139 id="station-1",
140 name="Sample Station",
141 stream_url="https://example.com/stream",
142 home_page_url="https://example.com",
143 cover_art="/cover.jpg",
144 )
145
146 parsed = parse_radio("xx-instance-id-xx", station)
147 images = parsed.metadata.images
148 assert parsed.item_id == "station-1"
149 assert parsed.name == "Sample Station"
150 assert parsed.provider == "xx-instance-id-xx"
151 assert parsed.uri == "https://example.com/stream"
152 assert images is not None
153 assert images[0].path == "/cover.jpg"
154 assert parsed.provider_mappings
155
156
157@pytest.mark.parametrize("example", TRACK_FIXTURES, ids=lambda val: str(val.stem))
158async def test_parse_track(example: pathlib.Path, snapshot: SnapshotAssertion) -> None:
159 """Test we can parse Tracks."""
160 async with aiofiles.open(example, encoding="utf-8") as fp:
161 song = Child.from_json(await fp.read())
162
163 parsed = parse_track(_LOGGER, "xx-instance-id-xx", song).to_dict()
164 # sort external Ids, genres, and performers to ensure they are always in the same
165 # order for snapshot testing
166 parsed["external_ids"].sort()
167 parsed["metadata"]["genres"].sort()
168 parsed["metadata"]["performers"].sort()
169 assert snapshot == parsed
170
171 example_album = example.with_suffix("").with_suffix(".album.json")
172 async with aiofiles.open(example_album, encoding="utf-8") as fp:
173 album = AlbumID3.from_json(await fp.read())
174
175 parsed = parse_track(
176 _LOGGER, "xx-instance-id-xx", song, parse_album(_LOGGER, "xx-instance-id-xx", album)
177 ).to_dict()
178 # sort external Ids, genres, and performers to ensure they are always in the same
179 # order for snapshot testing
180 parsed["external_ids"].sort()
181 parsed["metadata"]["genres"].sort()
182 parsed["metadata"]["performers"].sort()
183 if parsed.get("album"):
184 parsed["album"]["external_ids"].sort()
185 parsed["album"]["metadata"]["genres"].sort()
186 assert snapshot == parsed
187
188
189@pytest.mark.parametrize("example", LYRICS_FIXTURES, ids=lambda val: str(val.stem))
190async def test_lyrics(example: pathlib.Path, snapshot: SnapshotAssertion) -> None:
191 """Test that we can handle unstructured lyrics."""
192 async with aiofiles.open(example, encoding="utf-8") as fp:
193 lyrics = Lyrics.from_json(await fp.read())
194
195 example_track = example.with_suffix("").with_suffix(".track.json")
196 async with aiofiles.open(example_track, encoding="utf-8") as fp:
197 track = Child.from_json(await fp.read())
198
199 parsed = parse_track(_LOGGER, "xx-instance-id-xx", track, None, (lyrics.value, False)).to_dict()
200 parsed["external_ids"].sort()
201 parsed["metadata"]["genres"].sort()
202 parsed["metadata"]["performers"].sort()
203 assert snapshot == parsed
204
205
206@pytest.mark.parametrize("example", STRUCTURED_LYRICS_FIXTURES, ids=lambda val: str(val.stem))
207async def test_structured_lyrics(example: pathlib.Path, snapshot: SnapshotAssertion) -> None:
208 """Test that we can handle structured lyrics."""
209 async with aiofiles.open(example, encoding="utf-8") as fp:
210 lyrics = StructuredLyrics.from_json(await fp.read())
211
212 parsed, _ = parse_structured_lyrics(lyrics)
213 assert snapshot == parsed
214