From f70acb1116684408603fb731cab9576b6d89a817 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Thu, 21 Apr 2022 17:27:50 +0200 Subject: [PATCH] Fix sort order of MediaQuality (#260) * Fix sort order of MediaQuality breaking change to make it more future proof full db resync needed * Migrate database schema * make media quality optional * add url to provider mappings * make track matching a bit more strict --- .../controllers/metadata/__init__.py | 38 ++++++++------ music_assistant/controllers/music/__init__.py | 6 ++- music_assistant/controllers/stream.py | 2 +- music_assistant/helpers/audio.py | 6 +-- music_assistant/helpers/cache.py | 7 +-- music_assistant/helpers/compare.py | 4 +- music_assistant/helpers/database.py | 44 +++++++++++++++++ music_assistant/mass.py | 1 + music_assistant/models/media_controller.py | 10 ++++ music_assistant/models/media_items.py | 41 ++++++---------- music_assistant/providers/filesystem.py | 49 ++++++++++++++----- music_assistant/providers/qobuz.py | 36 ++++++++------ music_assistant/providers/spotify/__init__.py | 24 ++++----- 13 files changed, 177 insertions(+), 91 deletions(-) diff --git a/music_assistant/controllers/metadata/__init__.py b/music_assistant/controllers/metadata/__init__.py index 5ad7100a..397570b2 100755 --- a/music_assistant/controllers/metadata/__init__.py +++ b/music_assistant/controllers/metadata/__init__.py @@ -41,23 +41,12 @@ class MetaDataController: async def get_artist_metadata(self, mb_artist_id: str, cur_metadata: dict) -> dict: """Get/update rich metadata for an artist by providing the musicbrainz artist id.""" metadata = cur_metadata - if "fanart" in metadata: + if "fanart" not in metadata: # no need to query (other) metadata providers if we already have a result - return metadata - self.logger.info( - "Fetching metadata for MusicBrainz Artist %s on Fanrt.tv", mb_artist_id - ) - cache_key = f"fanarttv.artist_metadata.{mb_artist_id}" - res = await cached( - self.cache, cache_key, self.fanarttv.get_artist_images, mb_artist_id - ) - if res: - metadata = merge_dict(metadata, res) - self.logger.debug( - "Found metadata for MusicBrainz Artist %s on Fanart.tv: %s", - mb_artist_id, - ", ".join(res.keys()), + metadata = merge_dict( + metadata, await self._get_fanarttv_metadata(mb_artist_id) ) + return metadata async def get_thumbnail(self, url, size) -> bytes: @@ -71,3 +60,22 @@ class MetaDataController: TABLE_THUMBS, {**match, "img": thumbnail} ) return thumbnail + + async def _get_fanarttv_metadata(self, mb_artist_id: str) -> dict: + """Get metadata from fanarttv for artist.""" + metadata = {} + self.logger.info( + "Fetching metadata for MusicBrainz Artist %s on Fanrt.tv", mb_artist_id + ) + cache_key = f"fanarttv.artist_metadata.{mb_artist_id}" + res = await cached( + self.cache, cache_key, self.fanarttv.get_artist_images, mb_artist_id + ) + if res: + metadata = res + self.logger.debug( + "Found metadata for MusicBrainz Artist %s on Fanart.tv: %s", + mb_artist_id, + ", ".join(res.keys()), + ) + return metadata diff --git a/music_assistant/controllers/music/__init__.py b/music_assistant/controllers/music/__init__.py index acc4be91..4d6e3a9b 100755 --- a/music_assistant/controllers/music/__init__.py +++ b/music_assistant/controllers/music/__init__.py @@ -262,8 +262,9 @@ class MusicController: "media_type": media_type.value, "prov_item_id": prov_id.item_id, "provider": prov_id.provider, - "quality": prov_id.quality.value, + "quality": prov_id.quality.value if prov_id.quality else None, "details": prov_id.details, + "url": prov_id.url, }, ) @@ -500,8 +501,9 @@ class MusicController: media_type TEXT NOT NULL, prov_item_id TEXT NOT NULL, provider TEXT NOT NULL, - quality INTEGER NOT NULL, + quality INTEGER NULL, details TEXT NULL, + url TEXT NULL, UNIQUE(item_id, media_type, prov_item_id, provider) );""" ) diff --git a/music_assistant/controllers/stream.py b/music_assistant/controllers/stream.py index 373a465b..4f9c8f0c 100644 --- a/music_assistant/controllers/stream.py +++ b/music_assistant/controllers/stream.py @@ -389,7 +389,7 @@ class StreamController: # get streamdetails try: streamdetails = await get_stream_details( - self.mass, queue_track, queue.queue_id, lazy=track_count == 1 + self.mass, queue_track, queue.queue_id ) except MediaNotFoundError as err: self.logger.warning( diff --git a/music_assistant/helpers/audio.py b/music_assistant/helpers/audio.py index 7776535e..68010f31 100644 --- a/music_assistant/helpers/audio.py +++ b/music_assistant/helpers/audio.py @@ -175,7 +175,7 @@ async def analyze_audio(mass: MusicAssistant, streamdetails: StreamDetails) -> N async def get_stream_details( - mass: MusicAssistant, queue_item: QueueItem, queue_id: str = "", lazy: bool = True + mass: MusicAssistant, queue_item: QueueItem, queue_id: str = "" ) -> StreamDetails: """ Get streamdetails for the given QueueItem. @@ -196,9 +196,7 @@ async def get_stream_details( ) else: # always request the full db track as there might be other qualities available - full_item = await mass.music.get_item_by_uri( - queue_item.uri, force_refresh=not lazy, lazy=lazy - ) + full_item = await mass.music.get_item_by_uri(queue_item.uri) # sort by quality and check track availability for prov_media in sorted( full_item.provider_ids, key=lambda x: x.quality, reverse=True diff --git a/music_assistant/helpers/cache.py b/music_assistant/helpers/cache.py index ae430e2c..57f7dd7b 100644 --- a/music_assistant/helpers/cache.py +++ b/music_assistant/helpers/cache.py @@ -61,8 +61,10 @@ class Cache: data = await asyncio.get_running_loop().run_in_executor( None, pickle.loads, db_row["data"] ) - except Exception: # pylint: disable=broad-except - self.logger.warning("Error parsing cache data for %s", cache_key) + except Exception as exc: # pylint: disable=broad-except + self.logger.exception( + "Error parsing cache data for %s", cache_key, exc_info=exc + ) else: # also store in memory cache for faster access if cache_key not in self._mem_cache: @@ -72,7 +74,6 @@ class Cache: db_row["expires"], ) return data - self.logger.debug("no cache data for %s", cache_key) return default async def set(self, cache_key, data, checksum="", expiration=(86400 * 30)): diff --git a/music_assistant/helpers/compare.py b/music_assistant/helpers/compare.py index 55a7ff29..553b5ccb 100644 --- a/music_assistant/helpers/compare.py +++ b/music_assistant/helpers/compare.py @@ -104,8 +104,8 @@ def compare_track(left_track: "Track", right_track: "Track"): # album match OR near exact duration match if ( compare_album(left_track.album, right_track.album) - and abs(left_track.duration - right_track.duration) < 5 - ) or abs(left_track.duration - right_track.duration) < 1: + and left_track.duration == right_track.duration + ) or abs(left_track.duration - right_track.duration) <= 2: # 100% match, all criteria passed return True return False diff --git a/music_assistant/helpers/database.py b/music_assistant/helpers/database.py index c0ba1830..bc38af9c 100755 --- a/music_assistant/helpers/database.py +++ b/music_assistant/helpers/database.py @@ -11,6 +11,8 @@ from music_assistant.helpers.typing import MusicAssistant # pylint: disable=invalid-name +SCHEMA_VERSION = 1 + class Database: """Class that holds the (logic to the) database.""" @@ -21,6 +23,17 @@ class Database: self.mass = mass self.logger = mass.logger.getChild("db") + async def setup(self) -> None: + """Perform async initialization.""" + async with self.get_db() as _db: + await _db.execute( + """CREATE TABLE IF NOT EXISTS settings( + key TEXT PRIMARY KEY, + value TEXT + );""" + ) + await self._migrate() + @asynccontextmanager async def get_db(self, db: Optional[Db] = None) -> Db: """Context manager helper to get the active db connection.""" @@ -113,3 +126,34 @@ class Database: sql_query = f"DELETE FROM {table}" sql_query += " WHERE " + " AND ".join((f"{x} = :{x}" for x in match)) await _db.execute(sql_query) + + async def _migrate(self): + """Perform database migration actions if needed.""" + prev_version = await self.get_row("settings", {"key": "version"}) + if prev_version: + prev_version = int(prev_version["value"]) + else: + prev_version = 0 + if SCHEMA_VERSION != prev_version: + self.logger.info( + "Performing database migration from %s to %s", + prev_version, + SCHEMA_VERSION, + ) + + # schema version 1: too many breaking changes, simply drop the media tables for now + async with self.get_db() as _db: + await _db.execute("DROP TABLE IF EXISTS artists") + await _db.execute("DROP TABLE IF EXISTS albums") + await _db.execute("DROP TABLE IF EXISTS tracks") + await _db.execute("DROP TABLE IF EXISTS playlists") + await _db.execute("DROP TABLE IF EXISTS radios") + await _db.execute("DROP TABLE IF EXISTS playlist_tracks") + await _db.execute("DROP TABLE IF EXISTS album_tracks") + await _db.execute("DROP TABLE IF EXISTS provider_mappings") + await _db.execute("DROP TABLE IF EXISTS cache") + + # store current schema version + await self.insert_or_replace( + "settings", {"key": "version", "value": str(SCHEMA_VERSION)} + ) diff --git a/music_assistant/mass.py b/music_assistant/mass.py index 83a2ce09..044522a8 100644 --- a/music_assistant/mass.py +++ b/music_assistant/mass.py @@ -73,6 +73,7 @@ class MusicAssistant: connector=aiohttp.TCPConnector(ssl=False), ) # setup core controllers + await self.database.setup() await self.cache.setup() await self.music.setup() await self.metadata.setup() diff --git a/music_assistant/models/media_controller.py b/music_assistant/models/media_controller.py index 08675cf3..e2f895f9 100644 --- a/music_assistant/models/media_controller.py +++ b/music_assistant/models/media_controller.py @@ -2,6 +2,7 @@ from __future__ import annotations from abc import ABCMeta, abstractmethod +from time import time from typing import Generic, List, Optional, Tuple, TypeVar from music_assistant.helpers.cache import cached @@ -12,6 +13,9 @@ from .media_items import MediaItemType, MediaType ItemCls = TypeVar("ItemCls", bound="MediaControllerBase") +REFRESH_INTERVAL = 60 * 60 * 24 * 30 +REFRESH_KEY = "last_refresh" + class MediaControllerBase(Generic[ItemCls], metaclass=ABCMeta): """Base model for controller managing a MediaType.""" @@ -52,12 +56,18 @@ class MediaControllerBase(Generic[ItemCls], metaclass=ABCMeta): ) -> ItemCls: """Return (full) details for a single media item.""" db_item = await self.get_db_item_by_prov_id(provider_id, provider_item_id) + if ( + db_item + and (time() - db_item.metadata.get(REFRESH_KEY, 0)) > REFRESH_INTERVAL + ): + force_refresh = True if db_item and force_refresh: provider_id, provider_item_id = await self.get_provider_id(db_item) elif db_item: return db_item if not details: details = await self.get_provider_item(provider_item_id, provider_id) + details.metadata[REFRESH_KEY] = int(time()) if not lazy: return await self.add(details) self.mass.add_job(self.add(details), f"Add {details.uri} to database") diff --git a/music_assistant/models/media_items.py b/music_assistant/models/media_items.py index 65e6734a..91234e17 100755 --- a/music_assistant/models/media_items.py +++ b/music_assistant/models/media_items.py @@ -10,6 +10,8 @@ from mashumaro import DataClassDictMixin from music_assistant.helpers.json import json from music_assistant.helpers.util import create_sort_name +MetadataTypes = Union[int, bool, str, List[str]] + class MediaType(Enum): """Enum for MediaType.""" @@ -21,29 +23,19 @@ class MediaType(Enum): RADIO = "radio" UNKNOWN = "unknown" - @classmethod - def _missing_(cls: "MediaType", value: str): - """Set default enum member if an unknown value is provided.""" - return cls.UNKNOWN - class MediaQuality(IntEnum): """Enum for Media Quality.""" - LOSSY_MP3 = 0 - LOSSY_OGG = 1 - LOSSY_AAC = 2 - FLAC_LOSSLESS = 6 # 44.1/48khz 16 bits - FLAC_LOSSLESS_HI_RES_1 = 7 # 44.1/48khz 24 bits HI-RES - FLAC_LOSSLESS_HI_RES_2 = 8 # 88.2/96khz 24 bits HI-RES - FLAC_LOSSLESS_HI_RES_3 = 9 # 176/192khz 24 bits HI-RES - FLAC_LOSSLESS_HI_RES_4 = 10 # above 192khz 24 bits HI-RES - UNKNOWN = 99 - - @classmethod - def _missing_(cls: "MediaQuality", value: str): - """Set default enum member if an unknown value is provided.""" - return cls.UNKNOWN + UNKNOWN = 0 + LOSSY_MP3 = 1 + LOSSY_OGG = 2 + LOSSY_AAC = 3 + FLAC_LOSSLESS = 10 # 44.1/48khz 16 bits + FLAC_LOSSLESS_HI_RES_1 = 20 # 44.1/48khz 24 bits HI-RES + FLAC_LOSSLESS_HI_RES_2 = 21 # 88.2/96khz 24 bits HI-RES + FLAC_LOSSLESS_HI_RES_3 = 22 # 176/192khz 24 bits HI-RES + FLAC_LOSSLESS_HI_RES_4 = 23 # above 192khz 24 bits HI-RES @dataclass @@ -52,9 +44,10 @@ class MediaItemProviderId(DataClassDictMixin): provider: str item_id: str - quality: MediaQuality = MediaQuality.UNKNOWN - details: str = None available: bool = True + quality: Optional[MediaQuality] = None + details: Optional[str] = None + url: Optional[str] = None def __hash__(self): """Return custom hash.""" @@ -69,7 +62,7 @@ class MediaItem(DataClassDictMixin): provider: str name: str sort_name: Optional[str] = None - metadata: Dict[str, Any] = field(default_factory=dict) + metadata: Dict[str, MetadataTypes] = field(default_factory=dict) provider_ids: List[MediaItemProviderId] = field(default_factory=list) in_library: bool = False media_type: MediaType = MediaType.UNKNOWN @@ -81,10 +74,6 @@ class MediaItem(DataClassDictMixin): self.uri = create_uri(self.media_type, self.provider, self.item_id) if not self.sort_name: self.sort_name = create_sort_name(self.name) - if not self.provider_ids: - self.provider_ids.append( - MediaItemProviderId(provider=self.provider, item_id=self.item_id) - ) @classmethod def from_db_row(cls, db_row: Mapping): diff --git a/music_assistant/providers/filesystem.py b/music_assistant/providers/filesystem.py index 4b050534..5823ff94 100644 --- a/music_assistant/providers/filesystem.py +++ b/music_assistant/providers/filesystem.py @@ -88,7 +88,7 @@ class FileSystemProvider(MusicProvider): :param limit: Number of items to return in the search (per type). """ result = [] - for track in await self.get_library_tracks(): + for track in await self.get_library_tracks(True): for search_part in search_query.split(" - "): if media_types is None or MediaType.TRACK in media_types: if compare_strings(track.name, search_part): @@ -103,31 +103,47 @@ class FileSystemProvider(MusicProvider): result.append(track.album.artist) return result - async def get_library_artists(self) -> List[Artist]: + async def get_library_artists(self, allow_cache=False) -> List[Artist]: """Retrieve all library artists.""" + # pylint: disable = arguments-differ + cache_key = f"{self.id}.library_artists" + if allow_cache: + if cache_result := await self.mass.cache.get(cache_key): + return cache_result result = [] prev_ids = set() - for track in await self.get_library_tracks(): + for track in await self.get_library_tracks(allow_cache): if track.album is not None and track.album.artist is not None: if track.album.artist.item_id not in prev_ids: result.append(track.album.artist) prev_ids.add(track.album.artist.item_id) + await self.mass.cache.set(cache_key, result) return result - async def get_library_albums(self) -> List[Album]: + async def get_library_albums(self, allow_cache=False) -> List[Album]: """Get album folders recursively.""" + # pylint: disable = arguments-differ + cache_key = f"{self.id}.library_albums" + if allow_cache: + if cache_result := await self.mass.cache.get(cache_key): + return cache_result result = [] prev_ids = set() - for track in await self.get_library_tracks(): + for track in await self.get_library_tracks(allow_cache): if track.album is not None: if track.album.item_id not in prev_ids: result.append(track.album) prev_ids.add(track.album.item_id) + await self.mass.cache.set(cache_key, result) return result - async def get_library_tracks(self) -> List[Track]: + async def get_library_tracks(self, allow_cache=False) -> List[Track]: """Get all tracks recursively.""" - # TODO: apply caching for very large libraries ? + # pylint: disable = arguments-differ + cache_key = f"{self.id}.library_tracks" + if allow_cache: + if cache_result := await self.mass.cache.get(cache_key): + return cache_result result = [] for _root, _dirs, _files in os.walk(self._music_dir): for file in _files: @@ -135,12 +151,18 @@ class FileSystemProvider(MusicProvider): if TinyTag.is_supported(filename): if track := await self._parse_track(filename): result.append(track) + await self.mass.cache.set(cache_key, result) return result - async def get_library_playlists(self) -> List[Playlist]: + async def get_library_playlists(self, allow_cache=False) -> List[Playlist]: """Retrieve playlists from disk.""" + # pylint: disable = arguments-differ if not self._playlists_dir: return [] + cache_key = f"{self.id}.library_playlists" + if allow_cache: + if cache_result := await self.mass.cache.get(cache_key): + return cache_result result = [] for filename in os.listdir(self._playlists_dir): filepath = os.path.join(self._playlists_dir, filename) @@ -152,6 +174,7 @@ class FileSystemProvider(MusicProvider): playlist = await self.get_playlist(filepath) if playlist: result.append(playlist) + await self.mass.cache.set(cache_key, result) return result async def get_artist(self, prov_artist_id: str) -> Artist: @@ -159,7 +182,7 @@ class FileSystemProvider(MusicProvider): return next( ( track.album.artist - for track in await self.get_library_tracks() + for track in await self.get_library_tracks(True) if track.album is not None and track.album.artist is not None and track.album.artist.item_id == prov_artist_id @@ -172,7 +195,7 @@ class FileSystemProvider(MusicProvider): return next( ( track.album - for track in await self.get_library_tracks() + for track in await self.get_library_tracks(True) if track.album is not None and track.album.item_id == prov_album_id ), None, @@ -215,7 +238,7 @@ class FileSystemProvider(MusicProvider): """Get album tracks for given album id.""" return [ track - for track in await self.get_library_tracks() + for track in await self.get_library_tracks(True) if track.album is not None and track.album.item_id == prov_album_id ] @@ -244,7 +267,7 @@ class FileSystemProvider(MusicProvider): """Get a list of albums for the given artist.""" return [ track.album - for track in await self.get_library_tracks() + for track in await self.get_library_tracks(True) if track.album is not None and track.album.artist is not None and track.album.artist.item_id == prov_artist_id @@ -254,7 +277,7 @@ class FileSystemProvider(MusicProvider): """Get a list of all tracks as we have no clue about preference.""" return [ track - for track in await self.get_library_tracks() + for track in await self.get_library_tracks(True) if track.artists is not None and prov_artist_id in [x.item_id for x in track.provider_ids] ] diff --git a/music_assistant/providers/qobuz.py b/music_assistant/providers/qobuz.py index 29927424..014b0b43 100644 --- a/music_assistant/providers/qobuz.py +++ b/music_assistant/providers/qobuz.py @@ -401,19 +401,23 @@ class QobuzProvider(MusicProvider): } await self._get_data("/track/reportStreamingEnd", params) - async def _parse_artist(self, artist_obj): + async def _parse_artist(self, artist_obj: dict): """Parse qobuz artist object to generic layout.""" artist = Artist( item_id=str(artist_obj["id"]), provider=self.id, name=artist_obj["name"] ) artist.provider_ids.append( - MediaItemProviderId(provider=self.id, item_id=str(artist_obj["id"])) + MediaItemProviderId( + provider=self.id, + item_id=str(artist_obj["id"]), + url=artist_obj.get( + "url", f'https://open.qobuz.com/artist/{artist_obj["id"]}' + ), + ) ) artist.metadata["image"] = self.__get_image(artist_obj) if artist_obj.get("biography"): artist.metadata["biography"] = artist_obj["biography"].get("content", "") - if artist_obj.get("url"): - artist.metadata["qobuz_url"] = artist_obj["url"] return artist async def _parse_album(self, album_obj: dict, artist_obj: dict = None): @@ -444,6 +448,9 @@ class QobuzProvider(MusicProvider): provider=self.id, item_id=str(album_obj["id"]), quality=quality, + url=album_obj.get( + "url", f'https://open.qobuz.com/album/{album_obj["id"]}' + ), details=f'{album_obj["maximum_sampling_rate"]}kHz {album_obj["maximum_bit_depth"]}bit', available=album_obj["streamable"] and album_obj["displayable"], ) @@ -483,15 +490,11 @@ class QobuzProvider(MusicProvider): album.year = datetime.datetime.fromtimestamp(album_obj["released_at"]).year if album_obj.get("copyright"): album.metadata["copyright"] = album_obj["copyright"] - if album_obj.get("hires"): - album.metadata["hires"] = "true" - if album_obj.get("url"): - album.metadata["qobuz_url"] = album_obj["url"] if album_obj.get("description"): album.metadata["description"] = album_obj["description"] return album - async def _parse_track(self, track_obj): + async def _parse_track(self, track_obj: dict): """Parse qobuz track object to generic layout.""" name, version = parse_title_and_version( track_obj["title"], track_obj.get("version") @@ -535,8 +538,6 @@ class QobuzProvider(MusicProvider): track.album = album if track_obj.get("hires"): track.metadata["hires"] = "true" - if track_obj.get("url"): - track.metadata["qobuz_url"] = track_obj["url"] if track_obj.get("isrc"): track.isrc = track_obj["isrc"] if track_obj.get("performers"): @@ -568,6 +569,9 @@ class QobuzProvider(MusicProvider): provider=self.id, item_id=str(track_obj["id"]), quality=quality, + url=track_obj.get( + "url", f'https://open.qobuz.com/track/{track_obj["id"]}' + ), details=f'{track_obj["maximum_sampling_rate"]}kHz {track_obj["maximum_bit_depth"]}bit', available=track_obj["streamable"] and track_obj["displayable"], ) @@ -583,15 +587,19 @@ class QobuzProvider(MusicProvider): owner=playlist_obj["owner"]["name"], ) playlist.provider_ids.append( - MediaItemProviderId(provider=self.id, item_id=str(playlist_obj["id"])) + MediaItemProviderId( + provider=self.id, + item_id=str(playlist_obj["id"]), + url=playlist_obj.get( + "url", f'https://open.qobuz.com/playlist/{playlist_obj["id"]}' + ), + ) ) playlist.is_editable = ( playlist_obj["owner"]["id"] == self.__user_auth_info["user"]["id"] or playlist_obj["is_collaborative"] ) playlist.metadata["image"] = self.__get_image(playlist_obj) - if playlist_obj.get("url"): - playlist.metadata["qobuz_url"] = playlist_obj["url"] playlist.checksum = playlist_obj["updated_at"] return playlist diff --git a/music_assistant/providers/spotify/__init__.py b/music_assistant/providers/spotify/__init__.py index e1d93cec..0efd6a01 100644 --- a/music_assistant/providers/spotify/__init__.py +++ b/music_assistant/providers/spotify/__init__.py @@ -280,7 +280,11 @@ class SpotifyProvider(MusicProvider): item_id=artist_obj["id"], provider=self.id, name=artist_obj["name"] ) artist.provider_ids.append( - MediaItemProviderId(provider=self.id, item_id=artist_obj["id"]) + MediaItemProviderId( + provider=self.id, + item_id=artist_obj["id"], + url=artist_obj["external_urls"]["spotify"], + ) ) if "genres" in artist_obj: artist.metadata["genres"] = artist_obj["genres"] @@ -290,11 +294,9 @@ class SpotifyProvider(MusicProvider): if "2a96cbd8b46e442fc41c2b86b821562f" not in img_url: artist.metadata["image"] = img_url break - if artist_obj.get("external_urls"): - artist.metadata["spotify_url"] = artist_obj["external_urls"]["spotify"] return artist - async def _parse_album(self, album_obj): + async def _parse_album(self, album_obj: dict): """Parse spotify album object to generic layout.""" name, version = parse_title_and_version(album_obj["name"]) album = Album( @@ -322,8 +324,6 @@ class SpotifyProvider(MusicProvider): album.year = int(album_obj["release_date"].split("-")[0]) if album_obj.get("copyrights"): album.metadata["copyright"] = album_obj["copyrights"][0]["text"] - if album_obj.get("external_urls"): - album.metadata["spotify_url"] = album_obj["external_urls"]["spotify"] if album_obj.get("explicit"): album.metadata["explicit"] = str(album_obj["explicit"]).lower() album.provider_ids.append( @@ -331,6 +331,7 @@ class SpotifyProvider(MusicProvider): provider=self.id, item_id=album_obj["id"], quality=MediaQuality.LOSSY_OGG, + url=album_obj["external_urls"]["spotify"], ) ) return album @@ -367,8 +368,6 @@ class SpotifyProvider(MusicProvider): track.metadata["copyright"] = track_obj["copyright"] if track_obj.get("explicit"): track.metadata["explicit"] = True - if track_obj.get("external_urls"): - track.metadata["spotify_url"] = track_obj["external_urls"]["spotify"] if track_obj.get("popularity"): track.metadata["popularity"] = track_obj["popularity"] track.provider_ids.append( @@ -376,6 +375,7 @@ class SpotifyProvider(MusicProvider): provider=self.id, item_id=track_obj["id"], quality=MediaQuality.LOSSY_OGG, + url=track_obj["external_urls"]["spotify"], available=not track_obj["is_local"] and track_obj["is_playable"], ) ) @@ -390,7 +390,11 @@ class SpotifyProvider(MusicProvider): owner=playlist_obj["owner"]["display_name"], ) playlist.provider_ids.append( - MediaItemProviderId(provider=self.id, item_id=playlist_obj["id"]) + MediaItemProviderId( + provider=self.id, + item_id=playlist_obj["id"], + url=playlist_obj["external_urls"]["spotify"], + ) ) playlist.is_editable = ( playlist_obj["owner"]["id"] == self._sp_user["id"] @@ -398,8 +402,6 @@ class SpotifyProvider(MusicProvider): ) if playlist_obj.get("images"): playlist.metadata["image"] = playlist_obj["images"][0]["url"] - if playlist_obj.get("external_urls"): - playlist.metadata["spotify_url"] = playlist_obj["external_urls"]["spotify"] playlist.checksum = playlist_obj["snapshot_id"] return playlist -- 2.34.1