Fix sort order of MediaQuality (#260)
authorMarcel van der Veldt <m.vanderveldt@outlook.com>
Thu, 21 Apr 2022 15:27:50 +0000 (17:27 +0200)
committerGitHub <noreply@github.com>
Thu, 21 Apr 2022 15:27:50 +0000 (17:27 +0200)
* 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

13 files changed:
music_assistant/controllers/metadata/__init__.py
music_assistant/controllers/music/__init__.py
music_assistant/controllers/stream.py
music_assistant/helpers/audio.py
music_assistant/helpers/cache.py
music_assistant/helpers/compare.py
music_assistant/helpers/database.py
music_assistant/mass.py
music_assistant/models/media_controller.py
music_assistant/models/media_items.py
music_assistant/providers/filesystem.py
music_assistant/providers/qobuz.py
music_assistant/providers/spotify/__init__.py

index 5ad7100ad475102f0d60de782d7226078bc2bcc5..397570b22797e8ccaee5d21f9f730d931e4c2985 100755 (executable)
@@ -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
index acc4be91121c6072c96941942e1cd072ce494e99..4d6e3a9beda73809150c3cc063efe4324d374691 100755 (executable)
@@ -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)
                         );"""
             )
index 373a465b6ffe01dc371f5dec9161867f8105083d..4f9c8f0c04c1821acb2ae24240f007c4b219e07b 100644 (file)
@@ -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(
index 7776535eccdaf48ffed10a6f9eb6f35dbc7c2eb3..68010f316d1844b2ad993b5f811c1e5e1664945f 100644 (file)
@@ -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
index ae430e2ccdb20feacf995bbaa3fd9b306d3c576b..57f7dd7b8047ba9b8ddda36ce2a878257cbbcc06 100644 (file)
@@ -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)):
index 55a7ff29172228865e0a9367f3954e2507fd2d06..553b5ccb20a8643c29b9e7dc7eddf786246145c8 100644 (file)
@@ -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
index c0ba18308ef798bd7daeccf3842c1cbb5250c14d..bc38af9c6efa4777bd599c688287c436f5af0255 100755 (executable)
@@ -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)}
+            )
index 83a2ce098b6d2e18f49fa2063bb8f5d8a7888535..044522a82cdffe452fb94787c37f9a758043c3a5 100644 (file)
@@ -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()
index 08675cf3222a8f5ff1d2a8cea8874ec150fef4dc..e2f895f9db6b87bedf111e23ee16d902b6e3fcb7 100644 (file)
@@ -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")
index 65e6734a610b829fa249df9b3f9321c30bddff9c..91234e17981056f2b1198a1ca594688036f21f0b 100755 (executable)
@@ -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):
index 4b050534db3b9286a5663fdc1934cae67fa0a408..5823ff94f3489a7dbd7e325488f78fc8bef0202b 100644 (file)
@@ -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]
         ]
index 29927424221621d468ec42582453faeb57a0c7a6..014b0b43a86dc1b015c8fdf15668f87a758b808d 100644 (file)
@@ -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
 
index e1d93cec47ece3556c90f551e5d70adc36f706d1..0efd6a01d80b9a59c2f34651dc27a18918907b6f 100644 (file)
@@ -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