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