/
/
/
1"""Parsers for Yandex Music API responses."""
2
3from __future__ import annotations
4
5from contextlib import suppress
6from datetime import datetime
7from typing import TYPE_CHECKING
8
9from music_assistant_models.enums import (
10 AlbumType,
11 ContentType,
12 ImageType,
13)
14from music_assistant_models.media_items import (
15 Album,
16 Artist,
17 AudioFormat,
18 MediaItemImage,
19 Playlist,
20 ProviderMapping,
21 Track,
22 UniqueList,
23)
24
25from music_assistant.helpers.util import parse_title_and_version
26
27from .constants import IMAGE_SIZE_LARGE
28
29if TYPE_CHECKING:
30 from yandex_music import Album as YandexAlbum
31 from yandex_music import Artist as YandexArtist
32 from yandex_music import Playlist as YandexPlaylist
33 from yandex_music import Track as YandexTrack
34
35 from .provider import YandexMusicProvider
36
37
38def _get_content_type(provider: YandexMusicProvider) -> ContentType:
39 """Get content type based on provider quality setting.
40
41 :param provider: The Yandex Music provider instance.
42 :return: ContentType.UNKNOWN as actual codec is determined at stream time.
43 """
44 # Actual codec is determined when getting stream details
45 # Suppress unused argument warning
46 _ = provider
47 return ContentType.UNKNOWN
48
49
50def _get_image_url(cover_uri: str | None, size: str = IMAGE_SIZE_LARGE) -> str | None:
51 """Convert Yandex cover URI to full URL.
52
53 :param cover_uri: Yandex cover URI template.
54 :param size: Image size (e.g., '1000x1000').
55 :return: Full image URL or None.
56 """
57 if not cover_uri:
58 return None
59 # Cover URIs come in format "avatars.yandex.net/get-music-content/xxx/yyy/%%"
60 # Replace %% with the desired size
61 return f"https://{cover_uri.replace('%%', size)}"
62
63
64def parse_artist(provider: YandexMusicProvider, artist_obj: YandexArtist) -> Artist:
65 """Parse Yandex artist object to MA Artist model.
66
67 :param provider: The Yandex Music provider instance.
68 :param artist_obj: Yandex artist object.
69 :return: Music Assistant Artist model.
70 """
71 artist_id = str(artist_obj.id)
72 artist = Artist(
73 item_id=artist_id,
74 provider=provider.instance_id,
75 name=artist_obj.name or "Unknown Artist",
76 provider_mappings={
77 ProviderMapping(
78 item_id=artist_id,
79 provider_domain=provider.domain,
80 provider_instance=provider.instance_id,
81 url=f"https://music.yandex.ru/artist/{artist_id}",
82 )
83 },
84 )
85
86 # Add image if available
87 if artist_obj.cover:
88 image_url = _get_image_url(artist_obj.cover.uri)
89 if image_url:
90 artist.metadata.images = UniqueList(
91 [
92 MediaItemImage(
93 type=ImageType.THUMB,
94 path=image_url,
95 provider=provider.instance_id,
96 remotely_accessible=True,
97 )
98 ]
99 )
100 elif artist_obj.og_image:
101 image_url = _get_image_url(artist_obj.og_image)
102 if image_url:
103 artist.metadata.images = UniqueList(
104 [
105 MediaItemImage(
106 type=ImageType.THUMB,
107 path=image_url,
108 provider=provider.instance_id,
109 remotely_accessible=True,
110 )
111 ]
112 )
113
114 return artist
115
116
117def parse_album(provider: YandexMusicProvider, album_obj: YandexAlbum) -> Album:
118 """Parse Yandex album object to MA Album model.
119
120 :param provider: The Yandex Music provider instance.
121 :param album_obj: Yandex album object.
122 :return: Music Assistant Album model.
123 """
124 name, version = parse_title_and_version(
125 album_obj.title or "Unknown Album",
126 album_obj.version or None,
127 )
128 album_id = str(album_obj.id)
129
130 # Determine availability
131 available = album_obj.available or False
132
133 album = Album(
134 item_id=album_id,
135 provider=provider.instance_id,
136 name=name,
137 version=version,
138 provider_mappings={
139 ProviderMapping(
140 item_id=album_id,
141 provider_domain=provider.domain,
142 provider_instance=provider.instance_id,
143 audio_format=AudioFormat(
144 content_type=_get_content_type(provider),
145 ),
146 url=f"https://music.yandex.ru/album/{album_id}",
147 available=available,
148 )
149 },
150 )
151
152 # Parse artists
153 various_artist_album = False
154 if album_obj.artists:
155 for artist in album_obj.artists:
156 if artist.name and artist.name.lower() in ("various artists", "ÑбоÑник"):
157 various_artist_album = True
158 album.artists.append(parse_artist(provider, artist))
159
160 # Determine album type
161 album_type_str = album_obj.type or "album"
162 if album_type_str == "compilation" or various_artist_album:
163 album.album_type = AlbumType.COMPILATION
164 elif album_type_str == "single":
165 album.album_type = AlbumType.SINGLE
166 else:
167 album.album_type = AlbumType.ALBUM
168
169 # Parse year
170 if album_obj.year:
171 album.year = album_obj.year
172 if album_obj.release_date:
173 with suppress(ValueError):
174 album.metadata.release_date = datetime.fromisoformat(album_obj.release_date)
175
176 # Parse metadata
177 if album_obj.genre:
178 album.metadata.genres = {album_obj.genre}
179
180 # Add cover image
181 if album_obj.cover_uri:
182 image_url = _get_image_url(album_obj.cover_uri)
183 if image_url:
184 album.metadata.images = UniqueList(
185 [
186 MediaItemImage(
187 type=ImageType.THUMB,
188 path=image_url,
189 provider=provider.instance_id,
190 remotely_accessible=True,
191 )
192 ]
193 )
194 elif album_obj.og_image:
195 image_url = _get_image_url(album_obj.og_image)
196 if image_url:
197 album.metadata.images = UniqueList(
198 [
199 MediaItemImage(
200 type=ImageType.THUMB,
201 path=image_url,
202 provider=provider.instance_id,
203 remotely_accessible=True,
204 )
205 ]
206 )
207
208 return album
209
210
211def parse_track(provider: YandexMusicProvider, track_obj: YandexTrack) -> Track:
212 """Parse Yandex track object to MA Track model.
213
214 :param provider: The Yandex Music provider instance.
215 :param track_obj: Yandex track object.
216 :return: Music Assistant Track model.
217 """
218 name, version = parse_title_and_version(
219 track_obj.title or "Unknown Track",
220 track_obj.version or None,
221 )
222 track_id = str(track_obj.id)
223
224 # Determine availability
225 available = track_obj.available or False
226
227 # Duration is in milliseconds in Yandex API
228 duration = (track_obj.duration_ms or 0) // 1000
229
230 track = Track(
231 item_id=track_id,
232 provider=provider.instance_id,
233 name=name,
234 version=version,
235 duration=duration,
236 provider_mappings={
237 ProviderMapping(
238 item_id=track_id,
239 provider_domain=provider.domain,
240 provider_instance=provider.instance_id,
241 audio_format=AudioFormat(
242 content_type=_get_content_type(provider),
243 ),
244 url=f"https://music.yandex.ru/track/{track_id}",
245 available=available,
246 )
247 },
248 )
249
250 # Parse artists
251 if track_obj.artists:
252 track.artists = UniqueList()
253 for artist in track_obj.artists:
254 track.artists.append(parse_artist(provider, artist))
255
256 # Parse album (full data so album gets cover art in the library)
257 if track_obj.albums and len(track_obj.albums) > 0:
258 album_obj = track_obj.albums[0]
259 track.album = parse_album(provider, album_obj)
260 # Also set track image from album cover if available
261 if album_obj.cover_uri:
262 image_url = _get_image_url(album_obj.cover_uri)
263 if image_url:
264 track.metadata.images = UniqueList(
265 [
266 MediaItemImage(
267 type=ImageType.THUMB,
268 path=image_url,
269 provider=provider.instance_id,
270 remotely_accessible=True,
271 )
272 ]
273 )
274
275 # Parse external IDs
276 if track_obj.real_id:
277 # real_id can be used as an identifier
278 pass
279
280 # Metadata
281 if track_obj.content_warning:
282 track.metadata.explicit = track_obj.content_warning == "explicit"
283
284 return track
285
286
287def parse_playlist(
288 provider: YandexMusicProvider, playlist_obj: YandexPlaylist, owner_name: str | None = None
289) -> Playlist:
290 """Parse Yandex playlist object to MA Playlist model.
291
292 :param provider: The Yandex Music provider instance.
293 :param playlist_obj: Yandex playlist object.
294 :param owner_name: Optional owner name override.
295 :return: Music Assistant Playlist model.
296 """
297 # Playlist ID in Yandex is a combination of owner uid and playlist kind
298 owner_id = str(playlist_obj.owner.uid) if playlist_obj.owner else str(provider.client.user_id)
299 playlist_kind = str(playlist_obj.kind)
300 playlist_id = f"{owner_id}:{playlist_kind}"
301
302 # Determine if editable (user owns the playlist)
303 is_editable = owner_id == str(provider.client.user_id)
304
305 # Get owner name
306 if owner_name is None:
307 if playlist_obj.owner and playlist_obj.owner.name:
308 owner_name = playlist_obj.owner.name
309 elif is_editable:
310 owner_name = "Me"
311 else:
312 owner_name = "Yandex Music"
313
314 playlist = Playlist(
315 item_id=playlist_id,
316 provider=provider.instance_id,
317 name=playlist_obj.title or "Unknown Playlist",
318 owner=owner_name,
319 provider_mappings={
320 ProviderMapping(
321 item_id=playlist_id,
322 provider_domain=provider.domain,
323 provider_instance=provider.instance_id,
324 url=f"https://music.yandex.ru/users/{owner_id}/playlists/{playlist_kind}",
325 is_unique=is_editable,
326 )
327 },
328 is_editable=is_editable,
329 )
330
331 # Metadata
332 if playlist_obj.description:
333 playlist.metadata.description = playlist_obj.description
334
335 # Add cover image
336 if playlist_obj.cover:
337 # Cover can be CoverImage or a string
338 cover = playlist_obj.cover
339 if hasattr(cover, "uri") and cover.uri:
340 image_url = _get_image_url(cover.uri)
341 if image_url:
342 playlist.metadata.images = UniqueList(
343 [
344 MediaItemImage(
345 type=ImageType.THUMB,
346 path=image_url,
347 provider=provider.instance_id,
348 remotely_accessible=True,
349 )
350 ]
351 )
352 elif playlist_obj.og_image:
353 image_url = _get_image_url(playlist_obj.og_image)
354 if image_url:
355 playlist.metadata.images = UniqueList(
356 [
357 MediaItemImage(
358 type=ImageType.THUMB,
359 path=image_url,
360 provider=provider.instance_id,
361 remotely_accessible=True,
362 )
363 ]
364 )
365
366 return playlist
367