From 02eaf3dd1a07b753175be029b391ba49ebe74f69 Mon Sep 17 00:00:00 2001 From: Jozef Kruszynski <60214390+jozefKruszynski@users.noreply.github.com> Date: Fri, 5 Apr 2024 01:24:33 +0200 Subject: [PATCH] Tidal: Fix retrieval of images for media items (#1204) * change when images are parsed * fix the image retrieval --------- Co-authored-by: Marcel van der Veldt --- .../server/providers/tidal/__init__.py | 196 +++++++----------- .../server/providers/tidal/helpers.py | 8 +- 2 files changed, 78 insertions(+), 126 deletions(-) diff --git a/music_assistant/server/providers/tidal/__init__.py b/music_assistant/server/providers/tidal/__init__.py index eea1e5de..7e267af7 100644 --- a/music_assistant/server/providers/tidal/__init__.py +++ b/music_assistant/server/providers/tidal/__init__.py @@ -3,6 +3,7 @@ from __future__ import annotations import asyncio +from contextlib import suppress from datetime import datetime, timedelta from typing import TYPE_CHECKING, Any, cast @@ -14,6 +15,7 @@ from tidalapi import Playlist as TidalPlaylist from tidalapi import Quality as TidalQuality from tidalapi import Session as TidalSession from tidalapi import Track as TidalTrack +from tidalapi import exceptions as tidal_exceptions from music_assistant.common.models.config_entries import ( ConfigEntry, @@ -90,6 +92,7 @@ CONF_EXPIRY_TIME = "expiry_time" CONF_QUALITY = "quality" BROWSE_URL = "https://tidal.com/browse" +RESOURCES_URL = "https://resources.tidal.com/images" async def setup( @@ -255,16 +258,16 @@ class TidalProvider(MusicProvider): parsed_results = SearchResults() if results["artists"]: for artist in results["artists"]: - parsed_results.artists.append(await self._parse_artist(artist_obj=artist)) + parsed_results.artists.append(self._parse_artist(artist)) if results["albums"]: for album in results["albums"]: - parsed_results.albums.append(await self._parse_album(album_obj=album)) + parsed_results.albums.append(self._parse_album(album)) if results["playlists"]: for playlist in results["playlists"]: - parsed_results.playlists.append(await self._parse_playlist(playlist_obj=playlist)) + parsed_results.playlists.append(self._parse_playlist(playlist)) if results["tracks"]: for track in results["tracks"]: - parsed_results.tracks.append(await self._parse_track(track_obj=track)) + parsed_results.tracks.append(self._parse_track(track)) return parsed_results async def get_library_artists(self) -> AsyncGenerator[Artist, None]: @@ -274,7 +277,7 @@ class TidalProvider(MusicProvider): async for artist in self._iter_items( get_library_artists, tidal_session, self._tidal_user_id, limit=DEFAULT_LIMIT ): - yield await self._parse_artist(artist_obj=artist) + yield self._parse_artist(artist) async def get_library_albums(self) -> AsyncGenerator[Album, None]: """Retrieve all library albums from Tidal.""" @@ -283,7 +286,7 @@ class TidalProvider(MusicProvider): async for album in self._iter_items( get_library_albums, tidal_session, self._tidal_user_id, limit=DEFAULT_LIMIT ): - yield await self._parse_album(album_obj=album) + yield self._parse_album(album) async def get_library_tracks(self) -> AsyncGenerator[Track, None]: """Retrieve library tracks from Tidal.""" @@ -292,7 +295,7 @@ class TidalProvider(MusicProvider): async for track in self._iter_items( get_library_tracks, tidal_session, self._tidal_user_id, limit=DEFAULT_LIMIT ): - yield await self._parse_track(track_obj=track) + yield self._parse_track(track) async def get_library_playlists(self) -> AsyncGenerator[Playlist, None]: """Retrieve all library playlists from the provider.""" @@ -301,7 +304,7 @@ class TidalProvider(MusicProvider): async for playlist in self._iter_items( get_library_playlists, tidal_session, self._tidal_user_id ): - yield await self._parse_playlist(playlist_obj=playlist) + yield self._parse_playlist(playlist) async def get_album_tracks(self, prov_album_id: str) -> list[AlbumTrack]: """Get album tracks for given album id.""" @@ -310,7 +313,7 @@ class TidalProvider(MusicProvider): return cast( list[AlbumTrack], [ - await self._parse_track( + self._parse_track( track_obj=track_obj, extra_init_kwargs={ "disc_number": track_obj.volume_num, @@ -326,7 +329,7 @@ class TidalProvider(MusicProvider): tidal_session = await self._get_tidal_session() async with self._throttler: return [ - await self._parse_album(album_obj=album) + self._parse_album(album) for album in await get_artist_albums(tidal_session, prov_artist_id) ] @@ -335,7 +338,7 @@ class TidalProvider(MusicProvider): tidal_session = await self._get_tidal_session() async with self._throttler: return [ - await self._parse_track(track_obj=track) + self._parse_track(track) for track in await get_artist_toptracks(tidal_session, prov_artist_id) ] @@ -350,7 +353,7 @@ class TidalProvider(MusicProvider): get_playlist_tracks, tidal_session, prov_playlist_id, limit=DEFAULT_LIMIT ): total_playlist_tracks += 1 - track = await self._parse_track( + track = self._parse_track( track_obj=track_obj, extra_init_kwargs={"position": total_playlist_tracks}, ) @@ -361,7 +364,7 @@ class TidalProvider(MusicProvider): tidal_session = await self._get_tidal_session() async with self._throttler: return [ - await self._parse_track(track_obj=track) + self._parse_track(track) for track in await get_similar_tracks(tidal_session, prov_track_id, limit) ] @@ -418,7 +421,7 @@ class TidalProvider(MusicProvider): title=name, description="", ) - return await self._parse_playlist(playlist_obj=playlist_obj) + return self._parse_playlist(playlist_obj) async def get_stream_details(self, item_id: str) -> StreamDetails: """Return the content details for the given track when it will be streamed.""" @@ -448,36 +451,36 @@ class TidalProvider(MusicProvider): """Get artist details for given artist id.""" tidal_session = await self._get_tidal_session() async with self._throttler: - return await self._parse_artist( + return self._parse_artist( artist_obj=await get_artist(tidal_session, prov_artist_id), - full_details=True, ) async def get_album(self, prov_album_id: str) -> Album: """Get album details for given album id.""" tidal_session = await self._get_tidal_session() async with self._throttler: - return await self._parse_album( + return self._parse_album( album_obj=await get_album(tidal_session, prov_album_id), - full_details=True, ) async def get_track(self, prov_track_id: str) -> Track: """Get track details for given track id.""" tidal_session = await self._get_tidal_session() async with self._throttler: - return await self._parse_track( - track_obj=await get_track(tidal_session, prov_track_id), - full_details=True, - ) + track_obj = await get_track(tidal_session, prov_track_id) + track = self._parse_track(track_obj) + # get some extra details for the full track info + with suppress(tidal_exceptions.MetadataNotAvailable): + lyrics: TidalLyrics = await asyncio.to_thread(track_obj.lyrics) + track.metadata.lyrics = lyrics.text + return track async def get_playlist(self, prov_playlist_id: str) -> Playlist: """Get playlist details for given playlist id.""" tidal_session = await self._get_tidal_session() async with self._throttler: - return await self._parse_playlist( - playlist_obj=await get_playlist(tidal_session, prov_playlist_id), - full_details=True, + return self._parse_playlist( + await get_playlist(tidal_session, prov_playlist_id), ) def get_item_mapping(self, media_type: MediaType, key: str, name: str) -> ItemMapping: @@ -542,7 +545,7 @@ class TidalProvider(MusicProvider): # Parsers - async def _parse_artist(self, artist_obj: TidalArtist, full_details: bool = False) -> Artist: + def _parse_artist(self, artist_obj: TidalArtist) -> Artist: """Parse tidal artist object to generic layout.""" artist_id = artist_obj.id artist = Artist( @@ -559,21 +562,19 @@ class TidalProvider(MusicProvider): }, ) # metadata - if full_details and artist_obj.name != "Various Artists": - try: - image_url = await self._get_image_url(artist_obj, size=750) - artist.metadata.images = [ - MediaItemImage( - type=ImageType.THUMB, - path=image_url, - ) - ] - except Exception: - self.logger.info(f"Artist {artist_obj.id} has no available picture") + if artist_obj.picture: + picture_id = artist_obj.picture.replace("-", "/") + image_url = f"{RESOURCES_URL}/{picture_id}/750x750.jpg" + artist.metadata.images = [ + MediaItemImage( + type=ImageType.THUMB, + path=image_url, + ) + ] return artist - async def _parse_album(self, album_obj: TidalAlbum, full_details: bool = False) -> Album: + def _parse_album(self, album_obj: TidalAlbum) -> Album: """Parse tidal album object to generic layout.""" name = album_obj.name version = album_obj.version or "" @@ -597,7 +598,7 @@ class TidalProvider(MusicProvider): }, ) for artist_obj in album_obj.artists: - album.artists.append(await self._parse_artist(artist_obj=artist_obj)) + album.artists.append(self._parse_artist(artist_obj)) if album_obj.type == "ALBUM": album.album_type = AlbumType.ALBUM elif album_obj.type == "COMPILATION": @@ -607,30 +608,28 @@ class TidalProvider(MusicProvider): elif album_obj.type == "SINGLE": album.album_type = AlbumType.SINGLE - # album.upc = album_obj.universal_product_number album.year = int(album_obj.year) # metadata + if album_obj.universal_product_number: + album.external_ids.add((ExternalID.BARCODE, album_obj.universal_product_number)) album.metadata.copyright = album_obj.copyright album.metadata.explicit = album_obj.explicit album.metadata.popularity = album_obj.popularity - if full_details: - try: - image_url = await self._get_image_url(album_obj, size=1280) - album.metadata.images = [ - MediaItemImage( - type=ImageType.THUMB, - path=image_url, - ) - ] - except Exception: - self.logger.info(f"Album {album_obj.id} has no available picture") + if album_obj.cover: + picture_id = album_obj.cover.replace("-", "/") + image_url = f"{RESOURCES_URL}/{picture_id}/750x750.jpg" + album.metadata.images = [ + MediaItemImage( + type=ImageType.THUMB, + path=image_url, + ) + ] return album - async def _parse_track( + def _parse_track( self, track_obj: TidalTrack, - full_details: bool = False, extra_init_kwargs: dict[str, Any] | None = None, ) -> Track | AlbumTrack | PlaylistTrack: """Parse tidal track object to generic layout.""" @@ -667,54 +666,34 @@ class TidalProvider(MusicProvider): ) if track_obj.isrc: track.external_ids.add((ExternalID.ISRC, track_obj.isrc)) - # Here we use an ItemMapping as Tidal return minimal data when getting an Album from a Track - track.album = self.get_item_mapping( - media_type=MediaType.ALBUM, - key=str(track_obj.album.id), - name=track_obj.album.name, - ) track.artists = [] for track_artist in track_obj.artists: - artist = await self._parse_artist(artist_obj=track_artist) + artist = self._parse_artist(track_artist) track.artists.append(artist) # metadata track.metadata.explicit = track_obj.explicit track.metadata.popularity = track_obj.popularity track.metadata.copyright = track_obj.copyright - if full_details: - image_url = None - try: - if lyrics_obj := await self._get_lyrics(track_obj): - track.metadata.lyrics = lyrics_obj.text - except Exception: - self.logger.info(f"Track {track_obj.id} has no available lyrics") - try: - image_url = await self._get_track_image_url(track_obj, width=1080, height=720) + if track_obj.album: + # Here we use an ItemMapping as Tidal returns + # minimal data when getting an Album from a Track + track.album = self.get_item_mapping( + media_type=MediaType.ALBUM, + key=str(track_obj.album.id), + name=track_obj.album.name, + ) + if track_obj.album.cover: + picture_id = track_obj.album.cover.replace("-", "/") + image_url = f"{RESOURCES_URL}/{picture_id}/750x750.jpg" track.metadata.images = [ MediaItemImage( type=ImageType.THUMB, path=image_url, ) ] - except Exception: - self.logger.info(f"Track {track_obj.id} has no available picture") - if image_url is None: - try: - image_url = await self._get_image_url(track_obj.album, size=1280) - track_obj.album.metadata.images = [ - MediaItemImage( - type=ImageType.THUMB, - path=image_url, - ) - ] - except Exception: - self.logger.info(f"Album {track_obj.album.id} has no available picture") - return track - async def _parse_playlist( - self, playlist_obj: TidalPlaylist, full_details: bool = False - ) -> Playlist: + def _parse_playlist(self, playlist_obj: TidalPlaylist) -> Playlist: """Parse tidal playlist object to generic layout.""" playlist_id = playlist_obj.id creator_id = playlist_obj.creator.id if playlist_obj.creator else None @@ -738,45 +717,18 @@ class TidalProvider(MusicProvider): # metadata playlist.metadata.checksum = str(playlist_obj.last_updated) playlist.metadata.popularity = playlist_obj.popularity - if full_details: - try: - image_url = await self._get_image_url(playlist_obj, size=1080) - playlist.metadata.images = [ - MediaItemImage( - type=ImageType.THUMB, - path=image_url, - ) - ] - except Exception: - self.logger.info(f"Playlist {playlist_obj.id} has no available picture") + if picture := (playlist_obj.square_picture or playlist_obj.picture): + picture_id = picture.replace("-", "/") + image_url = f"{RESOURCES_URL}/{picture_id}/750x750.jpg" + playlist.metadata.images = [ + MediaItemImage( + type=ImageType.THUMB, + path=image_url, + ) + ] return playlist - async def _get_image_url( - self, item: TidalArtist | TidalAlbum | TidalPlaylist, size: int = 0 - ) -> str: - def inner() -> str: - return item.image(size) - - return await asyncio.to_thread(inner) - - async def _get_track_image_url( - self, - item: TidalTrack, - width: int = 0, - height: int = 0, - ) -> str: - def inner() -> str: - return item.image(width, height) - - return await asyncio.to_thread(inner) - - async def _get_lyrics(self, item: TidalTrack) -> TidalLyrics: - def inner() -> TidalLyrics: - return item.lyrics - - return await asyncio.to_thread(inner) - async def _iter_items( self, func: Awaitable | Callable, *args, **kwargs ) -> AsyncGenerator[Any, None]: diff --git a/music_assistant/server/providers/tidal/helpers.py b/music_assistant/server/providers/tidal/helpers.py index cbc58f20..663d7d6b 100644 --- a/music_assistant/server/providers/tidal/helpers.py +++ b/music_assistant/server/providers/tidal/helpers.py @@ -106,6 +106,10 @@ async def get_artist_albums(session: TidalSession, prov_artist_id: str) -> list[ def inner() -> list[TidalAlbum]: try: artist_obj = TidalArtist(session, prov_artist_id) + except (ObjectNotFound, TooManyRequests) as err: + msg = f"Artist {prov_artist_id} not found" + raise MediaNotFoundError(msg) from err + else: all_albums = [] albums = artist_obj.get_albums(limit=DEFAULT_LIMIT) eps_singles = artist_obj.get_albums_ep_singles(limit=DEFAULT_LIMIT) @@ -113,10 +117,6 @@ async def get_artist_albums(session: TidalSession, prov_artist_id: str) -> list[ all_albums.extend(albums) all_albums.extend(eps_singles) all_albums.extend(compilations) - except (ObjectNotFound, TooManyRequests) as err: - msg = f"Artist {prov_artist_id} not found" - raise MediaNotFoundError(msg) from err - else: return all_albums return await asyncio.to_thread(inner) -- 2.34.1