/
/
/
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 (minimal data)
257 if track_obj.albums and len(track_obj.albums) > 0:
258 album = track_obj.albums[0]
259 track.album = provider.get_item_mapping(
260 media_type="album",
261 key=str(album.id),
262 name=album.title or "Unknown Album",
263 )
264 # Get image from album if available
265 if album.cover_uri:
266 image_url = _get_image_url(album.cover_uri)
267 if image_url:
268 track.metadata.images = UniqueList(
269 [
270 MediaItemImage(
271 type=ImageType.THUMB,
272 path=image_url,
273 provider=provider.instance_id,
274 remotely_accessible=True,
275 )
276 ]
277 )
278
279 # Parse external IDs
280 if track_obj.real_id:
281 # real_id can be used as an identifier
282 pass
283
284 # Metadata
285 if track_obj.content_warning:
286 track.metadata.explicit = track_obj.content_warning == "explicit"
287
288 return track
289
290
291def parse_playlist(
292 provider: YandexMusicProvider, playlist_obj: YandexPlaylist, owner_name: str | None = None
293) -> Playlist:
294 """Parse Yandex playlist object to MA Playlist model.
295
296 :param provider: The Yandex Music provider instance.
297 :param playlist_obj: Yandex playlist object.
298 :param owner_name: Optional owner name override.
299 :return: Music Assistant Playlist model.
300 """
301 # Playlist ID in Yandex is a combination of owner uid and playlist kind
302 owner_id = str(playlist_obj.owner.uid) if playlist_obj.owner else str(provider.client.user_id)
303 playlist_kind = str(playlist_obj.kind)
304 playlist_id = f"{owner_id}:{playlist_kind}"
305
306 # Determine if editable (user owns the playlist)
307 is_editable = owner_id == str(provider.client.user_id)
308
309 # Get owner name
310 if owner_name is None:
311 if playlist_obj.owner and playlist_obj.owner.name:
312 owner_name = playlist_obj.owner.name
313 elif is_editable:
314 owner_name = "Me"
315 else:
316 owner_name = "Yandex Music"
317
318 playlist = Playlist(
319 item_id=playlist_id,
320 provider=provider.instance_id,
321 name=playlist_obj.title or "Unknown Playlist",
322 owner=owner_name,
323 provider_mappings={
324 ProviderMapping(
325 item_id=playlist_id,
326 provider_domain=provider.domain,
327 provider_instance=provider.instance_id,
328 url=f"https://music.yandex.ru/users/{owner_id}/playlists/{playlist_kind}",
329 is_unique=is_editable,
330 )
331 },
332 is_editable=is_editable,
333 )
334
335 # Metadata
336 if playlist_obj.description:
337 playlist.metadata.description = playlist_obj.description
338
339 # Add cover image
340 if playlist_obj.cover:
341 # Cover can be CoverImage or a string
342 cover = playlist_obj.cover
343 if hasattr(cover, "uri") and cover.uri:
344 image_url = _get_image_url(cover.uri)
345 if image_url:
346 playlist.metadata.images = UniqueList(
347 [
348 MediaItemImage(
349 type=ImageType.THUMB,
350 path=image_url,
351 provider=provider.instance_id,
352 remotely_accessible=True,
353 )
354 ]
355 )
356 elif playlist_obj.og_image:
357 image_url = _get_image_url(playlist_obj.og_image)
358 if image_url:
359 playlist.metadata.images = UniqueList(
360 [
361 MediaItemImage(
362 type=ImageType.THUMB,
363 path=image_url,
364 provider=provider.instance_id,
365 remotely_accessible=True,
366 )
367 ]
368 )
369
370 return playlist
371