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