music-assistant-server

9.8 KBPY
parsers.py
9.8 KB312 lines • python
1"""Parsers for Zvuk 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, ZVUK_BASE_URL
28
29if TYPE_CHECKING:
30    from zvuk_music import Artist as ZvukArtist
31    from zvuk_music import Playlist as ZvukPlaylist
32    from zvuk_music import Release as ZvukRelease
33    from zvuk_music import Track as ZvukTrack
34    from zvuk_music.models.artist import SimpleArtist as ZvukSimpleArtist
35    from zvuk_music.models.common import Image as ZvukImage
36    from zvuk_music.models.playlist import SimplePlaylist as ZvukSimplePlaylist
37    from zvuk_music.models.release import SimpleRelease as ZvukSimpleRelease
38    from zvuk_music.models.track import SimpleTrack as ZvukSimpleTrack
39
40    from .provider import ZvukMusicProvider
41
42
43def _get_image_url(image: ZvukImage | None, size: int = IMAGE_SIZE_LARGE) -> str | None:
44    """Convert Zvuk Image to full URL.
45
46    :param image: Zvuk Image object.
47    :param size: Image size in pixels.
48    :return: Full image URL or None.
49    """
50    if not image:
51        return None
52    url = image.get_url(size, size)
53    return url if url else None
54
55
56def parse_artist(provider: ZvukMusicProvider, artist_obj: ZvukArtist | ZvukSimpleArtist) -> Artist:
57    """Parse Zvuk artist object to MA Artist model.
58
59    :param provider: The Zvuk Music provider instance.
60    :param artist_obj: Zvuk artist or SimpleArtist object.
61    :return: Music Assistant Artist model.
62    """
63    artist_id = str(artist_obj.id)
64    artist = Artist(
65        item_id=artist_id,
66        provider=provider.instance_id,
67        name=artist_obj.title or "Unknown Artist",
68        provider_mappings={
69            ProviderMapping(
70                item_id=artist_id,
71                provider_domain=provider.domain,
72                provider_instance=provider.instance_id,
73                url=f"{ZVUK_BASE_URL}/artist/{artist_id}",
74            )
75        },
76    )
77
78    if artist_obj.image:
79        image_url = _get_image_url(artist_obj.image)
80        if image_url:
81            artist.metadata.images = UniqueList(
82                [
83                    MediaItemImage(
84                        type=ImageType.THUMB,
85                        path=image_url,
86                        provider=provider.instance_id,
87                        remotely_accessible=True,
88                    )
89                ]
90            )
91
92    return artist
93
94
95def parse_album(provider: ZvukMusicProvider, release_obj: ZvukRelease | ZvukSimpleRelease) -> Album:
96    """Parse Zvuk release object to MA Album model.
97
98    :param provider: The Zvuk Music provider instance.
99    :param release_obj: Zvuk release or SimpleRelease object.
100    :return: Music Assistant Album model.
101    """
102    name, version = parse_title_and_version(
103        release_obj.title or "Unknown Album",
104    )
105    album_id = str(release_obj.id)
106
107    album = Album(
108        item_id=album_id,
109        provider=provider.instance_id,
110        name=name,
111        version=version,
112        provider_mappings={
113            ProviderMapping(
114                item_id=album_id,
115                provider_domain=provider.domain,
116                provider_instance=provider.instance_id,
117                audio_format=AudioFormat(
118                    content_type=ContentType.UNKNOWN,
119                ),
120                url=f"{ZVUK_BASE_URL}/release/{album_id}",
121            )
122        },
123    )
124
125    # Parse artists
126    if release_obj.artists:
127        for artist in release_obj.artists:
128            album.artists.append(parse_artist(provider, artist))
129
130    # Determine album type from ReleaseType
131    if release_obj.type:
132        release_type_value = (
133            release_obj.type.value if hasattr(release_obj.type, "value") else str(release_obj.type)
134        )
135        if release_type_value == "compilation":
136            album.album_type = AlbumType.COMPILATION
137        elif release_type_value == "single":
138            album.album_type = AlbumType.SINGLE
139        elif release_type_value == "ep":
140            album.album_type = AlbumType.EP
141        else:
142            album.album_type = AlbumType.ALBUM
143    else:
144        album.album_type = AlbumType.ALBUM
145
146    # Parse date
147    if release_obj.date:
148        # get_year() is available on both Release and SimpleRelease
149        year = release_obj.get_year()
150        if year:
151            album.year = year
152        with suppress(ValueError):
153            album.metadata.release_date = datetime.fromisoformat(release_obj.date)
154
155    # Parse genres (only available on full Release, not SimpleRelease)
156    if hasattr(release_obj, "genres") and release_obj.genres:
157        album.metadata.genres = {genre.name for genre in release_obj.genres if genre.name}
158
159    # Parse explicit flag
160    if release_obj.explicit:
161        album.metadata.explicit = True
162
163    # Add cover image
164    if release_obj.image:
165        image_url = _get_image_url(release_obj.image)
166        if image_url:
167            album.metadata.images = UniqueList(
168                [
169                    MediaItemImage(
170                        type=ImageType.THUMB,
171                        path=image_url,
172                        provider=provider.instance_id,
173                        remotely_accessible=True,
174                    )
175                ]
176            )
177
178    return album
179
180
181def parse_track(provider: ZvukMusicProvider, track_obj: ZvukTrack | ZvukSimpleTrack) -> Track:
182    """Parse Zvuk track object to MA Track model.
183
184    :param provider: The Zvuk Music provider instance.
185    :param track_obj: Zvuk track or SimpleTrack object.
186    :return: Music Assistant Track model.
187    """
188    name, version = parse_title_and_version(
189        track_obj.title or "Unknown Track",
190    )
191    track_id = str(track_obj.id)
192
193    # Duration is already in seconds in Zvuk API
194    duration = track_obj.duration or 0
195
196    track = Track(
197        item_id=track_id,
198        provider=provider.instance_id,
199        name=name,
200        version=version,
201        duration=duration,
202        provider_mappings={
203            ProviderMapping(
204                item_id=track_id,
205                provider_domain=provider.domain,
206                provider_instance=provider.instance_id,
207                audio_format=AudioFormat(
208                    content_type=ContentType.UNKNOWN,
209                ),
210                url=f"{ZVUK_BASE_URL}/track/{track_id}",
211            )
212        },
213    )
214
215    # Parse artists
216    if track_obj.artists:
217        track.artists = UniqueList()
218        for artist in track_obj.artists:
219            track.artists.append(parse_artist(provider, artist))
220
221    # Parse album from release (available on both Track and SimpleTrack)
222    if track_obj.release:
223        track.album = provider.get_item_mapping(
224            media_type="album",
225            key=str(track_obj.release.id),
226            name=track_obj.release.title or "Unknown Album",
227        )
228        # Get image from release
229        if track_obj.release.image:
230            image_url = _get_image_url(track_obj.release.image)
231            if image_url:
232                track.metadata.images = UniqueList(
233                    [
234                        MediaItemImage(
235                            type=ImageType.THUMB,
236                            path=image_url,
237                            provider=provider.instance_id,
238                            remotely_accessible=True,
239                        )
240                    ]
241                )
242
243    # Track number (position in release, only on full Track)
244    if hasattr(track_obj, "position") and track_obj.position is not None:
245        track.track_number = track_obj.position
246
247    # Explicit flag (boolean on both Track and SimpleTrack)
248    if track_obj.explicit:
249        track.metadata.explicit = True
250
251    return track
252
253
254def parse_playlist(
255    provider: ZvukMusicProvider, playlist_obj: ZvukPlaylist | ZvukSimplePlaylist
256) -> Playlist:
257    """Parse Zvuk playlist object to MA Playlist model.
258
259    :param provider: The Zvuk Music provider instance.
260    :param playlist_obj: Zvuk playlist or SimplePlaylist object.
261    :return: Music Assistant Playlist model.
262    """
263    playlist_id = str(playlist_obj.id)
264
265    # Determine if editable (user owns the playlist)
266    # user_id is only available on full Playlist, not SimplePlaylist
267    is_editable = False
268    owner_name = "Zvuk Music"
269    user_id = getattr(playlist_obj, "user_id", None)
270    if user_id and provider.client.user_id:
271        is_editable = str(user_id) == str(provider.client.user_id)
272        if is_editable:
273            owner_name = "Me"
274
275    playlist = Playlist(
276        item_id=playlist_id,
277        provider=provider.instance_id,
278        name=playlist_obj.title or "Unknown Playlist",
279        owner=owner_name,
280        provider_mappings={
281            ProviderMapping(
282                item_id=playlist_id,
283                provider_domain=provider.domain,
284                provider_instance=provider.instance_id,
285                url=f"{ZVUK_BASE_URL}/playlist/{playlist_id}",
286                is_unique=is_editable,
287            )
288        },
289        is_editable=is_editable,
290    )
291
292    # Metadata
293    if playlist_obj.description:
294        playlist.metadata.description = playlist_obj.description
295
296    # Add cover image
297    if playlist_obj.image:
298        image_url = _get_image_url(playlist_obj.image)
299        if image_url:
300            playlist.metadata.images = UniqueList(
301                [
302                    MediaItemImage(
303                        type=ImageType.THUMB,
304                        path=image_url,
305                        provider=provider.instance_id,
306                        remotely_accessible=True,
307                    )
308                ]
309            )
310
311    return playlist
312