music-assistant-server

11.1 KBPY
parsers.py
11.1 KB355 lines • python
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