From: Marcel van der Veldt Date: Sun, 7 Dec 2025 23:44:40 +0000 (+0100) Subject: Fixes for multiple instances of the same provider (#2765) X-Git-Url: https://git.kitaultman.com/?a=commitdiff_plain;h=a541661a9f2b56eaa30fa4089e43061546535650;p=music-assistant-server.git Fixes for multiple instances of the same provider (#2765) --- diff --git a/music_assistant/controllers/media/base.py b/music_assistant/controllers/media/base.py index e19902e1..6c07919a 100644 --- a/music_assistant/controllers/media/base.py +++ b/music_assistant/controllers/media/base.py @@ -7,7 +7,7 @@ import logging from abc import ABCMeta, abstractmethod from collections.abc import Iterable from contextlib import suppress -from typing import TYPE_CHECKING, Any, TypeVar, cast +from typing import TYPE_CHECKING, Any, TypeVar, cast, final from music_assistant_models.enums import EventType, ExternalID, MediaType, ProviderFeature from music_assistant_models.errors import ( @@ -116,6 +116,7 @@ class MediaControllerBase[ItemCls: "MediaItemType"](metaclass=ABCMeta): ) self._db_add_lock = asyncio.Lock() + @final async def add_item_to_library( self, item: ItemCls, @@ -129,6 +130,7 @@ class MediaControllerBase[ItemCls: "MediaItemType"](metaclass=ABCMeta): await self._update_library_item(library_id, item, overwrite=overwrite_existing) else: # actually add a new item in the library db + self.mass.music.match_provider_instances(item) async with self._db_add_lock: library_id = await self._add_library_item(item) new_item = True @@ -141,6 +143,7 @@ class MediaControllerBase[ItemCls: "MediaItemType"](metaclass=ABCMeta): ) return library_item + @final async def _get_library_item_by_match(self, item: ItemCls | ItemMapping) -> int | None: if item.provider == "library": return int(item.item_id) @@ -169,10 +172,12 @@ class MediaControllerBase[ItemCls: "MediaItemType"](metaclass=ABCMeta): return int(db_item.item_id) return None + @final async def update_item_in_library( self, item_id: str | int, update: ItemCls, overwrite: bool = False ) -> ItemCls: """Update existing library record in the library database.""" + self.mass.music.match_provider_instances(update) await self._update_library_item(item_id, update, overwrite=overwrite) # return the updated object library_item = await self.get_library_item(item_id) @@ -376,6 +381,7 @@ class MediaControllerBase[ItemCls: "MediaItemType"](metaclass=ABCMeta): return item return None + @final async def get_library_item_by_prov_mappings( self, provider_mappings: Iterable[ProviderMapping], @@ -397,6 +403,7 @@ class MediaControllerBase[ItemCls: "MediaItemType"](metaclass=ABCMeta): return item return None + @final async def get_library_item_by_external_id( self, external_id: str, external_id_type: ExternalID | None = None ) -> ItemCls | None: @@ -413,6 +420,7 @@ class MediaControllerBase[ItemCls: "MediaItemType"](metaclass=ABCMeta): return item return None + @final async def get_library_item_by_external_ids( self, external_ids: set[tuple[ExternalID, str]] ) -> ItemCls | None: @@ -422,6 +430,7 @@ class MediaControllerBase[ItemCls: "MediaItemType"](metaclass=ABCMeta): return match return None + @final async def get_library_items_by_prov_id( self, provider_domain: str | None = None, @@ -461,6 +470,7 @@ class MediaControllerBase[ItemCls: "MediaItemType"](metaclass=ABCMeta): extra_query_params=query_params, ) + @final async def iter_library_items_by_prov_id( self, provider_instance_id_or_domain: str, @@ -482,6 +492,7 @@ class MediaControllerBase[ItemCls: "MediaItemType"](metaclass=ABCMeta): break offset += limit + @final async def set_favorite(self, item_id: str | int, favorite: bool) -> None: """Set the favorite bool on a database item.""" db_id = int(item_id) # ensure integer @@ -494,6 +505,7 @@ class MediaControllerBase[ItemCls: "MediaItemType"](metaclass=ABCMeta): self.mass.signal_event(EventType.MEDIA_ITEM_UPDATED, library_item.uri, library_item) @guard_single_request # type: ignore[type-var] # TODO: fix typing for MediaControllerBase + @final async def get_provider_item( self, item_id: str, @@ -551,6 +563,7 @@ class MediaControllerBase[ItemCls: "MediaItemType"](metaclass=ABCMeta): ) raise MediaNotFoundError(msg) + @final async def add_provider_mapping( self, item_id: str | int, provider_mapping: ProviderMapping ) -> None: @@ -563,6 +576,7 @@ class MediaControllerBase[ItemCls: "MediaItemType"](metaclass=ABCMeta): library_item.provider_mappings.add(provider_mapping) await self.set_provider_mappings(db_id, library_item.provider_mappings) + @final async def remove_provider_mapping( self, item_id: str | int, provider_instance_id: str, provider_item_id: str ) -> None: @@ -610,6 +624,7 @@ class MediaControllerBase[ItemCls: "MediaItemType"](metaclass=ABCMeta): with suppress(AssertionError): await self.remove_item_from_library(db_id) + @final async def remove_provider_mappings(self, item_id: str | int, provider_instance_id: str) -> None: """Remove all provider mappings from an item.""" db_id = int(item_id) # ensure integer @@ -645,6 +660,7 @@ class MediaControllerBase[ItemCls: "MediaItemType"](metaclass=ABCMeta): with suppress(AssertionError): await self.remove_item_from_library(db_id) + @final async def set_provider_mappings( self, item_id: str | int, @@ -708,6 +724,7 @@ class MediaControllerBase[ItemCls: "MediaItemType"](metaclass=ABCMeta): ) -> list[Track]: """Get the list of base tracks from the controller used to calculate the dynamic radio.""" + @final async def _get_library_items_by_query( self, favorite: bool | None = None, @@ -755,6 +772,7 @@ class MediaControllerBase[ItemCls: "MediaItemType"](metaclass=ABCMeta): ) ] + @final def _preprocess_search(self, search: str | None, query_params: dict[str, Any]) -> str | None: """Preprocess search string and add to query params.""" if search: @@ -762,11 +780,13 @@ class MediaControllerBase[ItemCls: "MediaItemType"](metaclass=ABCMeta): query_params["search"] = f"%{search}%" return search + @final @staticmethod def _clean_query_parts(query_parts: list[str]) -> list[str]: """Clean the query parts list by removing duplicate where statements.""" return [x[5:] if x.lower().startswith("where ") else x for x in query_parts] + @final def _apply_random_subquery( self, query_parts: list[str], @@ -808,6 +828,7 @@ class MediaControllerBase[ItemCls: "MediaItemType"](metaclass=ABCMeta): query_parts.append(f"{self.db_table}.item_id in ({sub_query})") join_parts.clear() + @final def _apply_filters( self, query_parts: list[str], @@ -840,6 +861,7 @@ class MediaControllerBase[ItemCls: "MediaItemType"](metaclass=ABCMeta): f"AND ({' OR '.join(provider_conditions)})" ) + @final def _build_final_query( self, query_parts: list[str], @@ -867,6 +889,7 @@ class MediaControllerBase[ItemCls: "MediaItemType"](metaclass=ABCMeta): return sql_query + @final @staticmethod def _parse_db_row(db_row: Mapping[str, Any]) -> dict[str, Any]: """Parse raw db Mapping into a dict.""" @@ -903,6 +926,7 @@ class MediaControllerBase[ItemCls: "MediaItemType"](metaclass=ABCMeta): db_row_dict["metadata"]["images"] = [album_thumb] return db_row_dict + @final def _ensure_provider_filter( self, provider: str | list[str] | None, @@ -934,6 +958,7 @@ class MediaControllerBase[ItemCls: "MediaItemType"](metaclass=ABCMeta): final_provider_filter = [provider] if isinstance(provider, str) else provider return final_provider_filter + @final def _select_provider_id(self, library_item: ItemCls) -> tuple[str, str]: """Select the correct provider id to use for fetching the item.""" user = get_current_user() diff --git a/music_assistant/controllers/music.py b/music_assistant/controllers/music.py index fcb8bf9a..a26430e5 100644 --- a/music_assistant/controllers/music.py +++ b/music_assistant/controllers/music.py @@ -35,6 +35,7 @@ from music_assistant_models.media_items import ( BrowseFolder, ItemMapping, MediaItemType, + ProviderMapping, RecommendationFolder, SearchResults, Track, @@ -94,7 +95,7 @@ CONF_RESET_DB = "reset_db" DEFAULT_SYNC_INTERVAL = 12 * 60 # default sync interval in minutes CONF_SYNC_INTERVAL = "sync_interval" CONF_DELETED_PROVIDERS = "deleted_providers" -DB_SCHEMA_VERSION: Final[int] = 22 +DB_SCHEMA_VERSION: Final[int] = 23 CACHE_CATEGORY_LAST_SYNC: Final[int] = 9 CACHE_CATEGORY_SEARCH_RESULTS: Final[int] = 10 @@ -1352,6 +1353,16 @@ class MusicController(CoreController): return self.genres raise NotImplementedError + def get_provider_instances( + self, domain: str, return_unavailable: bool = False + ) -> list[MusicProvider]: + """Return all provider instances for a given domain.""" + return [ + prov + for prov in self.providers + if prov.domain == domain and (return_unavailable or prov.available) + ] + def get_unique_providers(self) -> set[str]: """ Return all unique MusicProvider (instance or domain) ids. @@ -1488,6 +1499,43 @@ class MusicController(CoreController): if sync_task.provider_instance == provider_instance_id: sync_task.task.cancel() + def match_provider_instances( + self, + item: MediaItemType, + ) -> None: + """Match all provider instances for the given item.""" + for provider_mapping in list(item.provider_mappings): + if provider_mapping.is_unique: + # unique mapping, no need to map + continue + if not (provider := self.mass.get_provider(item.provider)): + continue + if not provider.is_streaming_provider: + continue + provider_instances = self.get_provider_instances( + provider.domain, return_unavailable=True + ) + if len(provider_instances) <= 1: + # only a single instance, no need to map + continue + for prov_instance in provider_instances: + if prov_instance.instance_id == provider.instance_id: + continue + # create additional mapping for other provider instances of the same provider + item.provider_mappings.add( + ProviderMapping( + item_id=provider_mapping.item_id, + provider_domain=provider.domain, + provider_instance=prov_instance.instance_id, + available=provider_mapping.available, + is_unique=provider_mapping.is_unique, + audio_format=provider_mapping.audio_format, + url=provider_mapping.url, + details=provider_mapping.details, + in_library=provider_mapping.in_library, + ) + ) + async def _get_default_recommendations(self) -> list[RecommendationFolder]: """Return default recommendations.""" return [ @@ -1824,7 +1872,7 @@ class MusicController(CoreController): else: self.logger.debug("Compacting database done") - async def __migrate_database(self, prev_version: int) -> None: + async def __migrate_database(self, prev_version: int) -> None: # noqa: PLR0915 """Perform a database migration.""" self.logger.info( "Migrating database from version %s to %s", prev_version, DB_SCHEMA_VERSION @@ -1956,6 +2004,16 @@ class MusicController(CoreController): # If we can't create the index due to duplicate entries, log and continue self.logger.warning("Could not create unique index on playlog: %s", err) + if prev_version <= 23: + # add is_unique column to provider_mappings table + try: + await self._database.execute( + f"ALTER TABLE {DB_TABLE_PROVIDER_MAPPINGS} ADD COLUMN is_unique BOOLEAN" + ) + except Exception as err: + if "duplicate column" not in str(err): + raise + # save changes await self._database.commit() @@ -2150,6 +2208,7 @@ class MusicController(CoreController): [provider_item_id] TEXT NOT NULL, [available] BOOLEAN NOT NULL DEFAULT 1, [in_library] BOOLEAN NOT NULL DEFAULT 0, + [is_unique] BOOLEAN, [url] text, [audio_format] json, [details] TEXT, diff --git a/music_assistant/models/music_provider.py b/music_assistant/models/music_provider.py index 4c241517..fdc45506 100644 --- a/music_assistant/models/music_provider.py +++ b/music_assistant/models/music_provider.py @@ -1131,9 +1131,12 @@ class MusicProvider(Provider): """Check if provider mapping(s) are consistent between library and provider items.""" for provider_mapping in provider_item.provider_mappings: if provider_mapping.item_id != provider_item.item_id: + # this should never happen, but guard against it raise MusicAssistantError("Inconsistent provider mapping item_id found") if provider_mapping.provider_instance != self.instance_id: + # this should never happen, but guard against it raise MusicAssistantError("Inconsistent provider mapping instance_id found") + # check if the provider mapping matches the library item provider_mapping.in_library = in_library library_mapping = next( ( @@ -1147,6 +1150,34 @@ class MusicProvider(Provider): if not library_mapping: return False if provider_mapping.in_library != library_mapping.in_library: + # in-library status doesn't match return False + if provider_mapping.is_unique != library_mapping.is_unique: + # unique status doesn't match + return False + # check if the library item has all provider instances mappings + is_unique = provider_mapping.is_unique or (not self.is_streaming_provider) + if not is_unique: + # for streaming providers we need to make sure all provider instances + # for this domain are represented in the provider mappings + prov_instances = self.mass.music.get_provider_instances( + domain=provider_mapping.provider_domain, + return_unavailable=True, + ) + if len(prov_instances) > 1: + # multiple provider instances for this domain exist + # make sure the library item has all provider mappings + for prov_instance in prov_instances: + if not any( + x.provider_instance == prov_instance.instance_id + and x.item_id == provider_mapping.item_id + for x in library_item.provider_mappings + ): + # missing provider mapping for another instance + # the rest of the core logic will take care of adding it + # just return False here to trigger that logic + return False + + # final check: availability return provider_mapping.available == library_mapping.available return False diff --git a/music_assistant/providers/apple_music/__init__.py b/music_assistant/providers/apple_music/__init__.py index f3192218..ac352a93 100644 --- a/music_assistant/providers/apple_music/__init__.py +++ b/music_assistant/providers/apple_music/__init__.py @@ -922,6 +922,7 @@ class AppleMusicProvider(MusicProvider): provider_domain=self.domain, provider_instance=self.instance_id, url=attributes.get("url"), + is_unique=is_editable, # user-owned playlists are unique ) }, is_editable=is_editable, diff --git a/music_assistant/providers/deezer/__init__.py b/music_assistant/providers/deezer/__init__.py index a8cd37f4..f3831cf1 100644 --- a/music_assistant/providers/deezer/__init__.py +++ b/music_assistant/providers/deezer/__init__.py @@ -657,6 +657,7 @@ class DeezerProvider(MusicProvider): provider_domain=self.domain, provider_instance=self.instance_id, url=getattr(playlist, "link", None), + is_unique=is_editable, # user-owned playlists are unique ) }, metadata=MediaItemMetadata( diff --git a/music_assistant/providers/qobuz/__init__.py b/music_assistant/providers/qobuz/__init__.py index c701f82c..8644cf3c 100644 --- a/music_assistant/providers/qobuz/__init__.py +++ b/music_assistant/providers/qobuz/__init__.py @@ -737,6 +737,7 @@ class QobuzProvider(MusicProvider): provider_domain=self.domain, provider_instance=self.instance_id, url=f"https://open.qobuz.com/playlist/{playlist_obj['id']}", + is_unique=is_editable, # user-owned playlists are unique ) }, is_editable=is_editable, diff --git a/music_assistant/providers/sendspin/provider.py b/music_assistant/providers/sendspin/provider.py index cf6b4bc9..2f1ae4c5 100644 --- a/music_assistant/providers/sendspin/provider.py +++ b/music_assistant/providers/sendspin/provider.py @@ -315,6 +315,7 @@ class SendspinProvider(PlayerProvider): await self.mass.webserver.auth.update_user_filters( user, player_filter=new_filter, provider_filter=None ) + user.player_filter = new_filter return { "local_ws_url": f"ws://{self.mass.streams.publish_ip}:8927/sendspin", diff --git a/music_assistant/providers/spotify/parsers.py b/music_assistant/providers/spotify/parsers.py index 626813d9..9aac57ca 100644 --- a/music_assistant/providers/spotify/parsers.py +++ b/music_assistant/providers/spotify/parsers.py @@ -215,6 +215,7 @@ def parse_playlist(playlist_obj: dict[str, Any], provider: SpotifyProvider) -> P provider_domain=provider.domain, provider_instance=provider.instance_id, url=playlist_obj["external_urls"]["spotify"], + is_unique=is_editable, # user-owned playlists are unique ) }, is_editable=is_editable, diff --git a/music_assistant/providers/spotify/provider.py b/music_assistant/providers/spotify/provider.py index 631f4935..c2d0db52 100644 --- a/music_assistant/providers/spotify/provider.py +++ b/music_assistant/providers/spotify/provider.py @@ -854,6 +854,7 @@ class SpotifyProvider(MusicProvider): provider_domain=self.domain, provider_instance=self.instance_id, url="https://open.spotify.com/collection/tracks", + is_unique=True, # liked songs is user-specific ) }, ) diff --git a/music_assistant/providers/tidal/parsers.py b/music_assistant/providers/tidal/parsers.py index 142fb6c4..5911ae83 100644 --- a/music_assistant/providers/tidal/parsers.py +++ b/music_assistant/providers/tidal/parsers.py @@ -272,6 +272,7 @@ def parse_playlist( provider_domain=provider.domain, provider_instance=provider.instance_id, url=f"{BROWSE_URL}/{url_path}/{raw_id}", + is_unique=is_editable, # user-owned playlists are unique ) }, is_editable=is_editable, diff --git a/music_assistant/providers/ytmusic/__init__.py b/music_assistant/providers/ytmusic/__init__.py index ab68bc17..9cc5981a 100644 --- a/music_assistant/providers/ytmusic/__init__.py +++ b/music_assistant/providers/ytmusic/__init__.py @@ -845,6 +845,7 @@ class YoutubeMusicProvider(MusicProvider): provider_domain=self.domain, provider_instance=self.instance_id, url=f"{YTM_DOMAIN}/playlist?list={playlist_id}", + is_unique=is_editable, # user-owned playlists are unique ) }, is_editable=is_editable, diff --git a/pyproject.toml b/pyproject.toml index 46527658..d60e8c20 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,7 +27,7 @@ dependencies = [ "get-mac==0.9.2", "mashumaro==3.17", "music-assistant-frontend==2.17.32", - "music-assistant-models==1.1.79", + "music-assistant-models==1.1.81", "mutagen==1.47.0", "orjson==3.11.4", "pillow==11.3.0", diff --git a/requirements_all.txt b/requirements_all.txt index 582aa3fb..5c2e2a4f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -40,7 +40,7 @@ librosa==0.11.0 lyricsgenius==3.7.2 mashumaro==3.17 music-assistant-frontend==2.17.32 -music-assistant-models==1.1.79 +music-assistant-models==1.1.81 mutagen==1.47.0 niconico.py-ma==2.1.0.post1 orjson==3.11.4 diff --git a/tests/providers/jellyfin/__snapshots__/test_parsers.ambr b/tests/providers/jellyfin/__snapshots__/test_parsers.ambr index 795059ba..200f9e56 100644 --- a/tests/providers/jellyfin/__snapshots__/test_parsers.ambr +++ b/tests/providers/jellyfin/__snapshots__/test_parsers.ambr @@ -79,6 +79,7 @@ 'available': True, 'details': None, 'in_library': None, + 'is_unique': None, 'item_id': '70b7288088b42d318f75dbcc41fd0091', 'provider_domain': 'jellyfin', 'provider_instance': 'xx-instance-id-xx', @@ -172,6 +173,7 @@ 'available': True, 'details': None, 'in_library': None, + 'is_unique': None, 'item_id': '32ed6a0091733dcff57eae67010f3d4b', 'provider_domain': 'jellyfin', 'provider_instance': 'xx-instance-id-xx', @@ -251,6 +253,7 @@ 'available': True, 'details': None, 'in_library': None, + 'is_unique': None, 'item_id': '7c8d0bd55291c7fc0451d17ebef30017', 'provider_domain': 'jellyfin', 'provider_instance': 'xx-instance-id-xx', @@ -334,6 +337,7 @@ 'available': True, 'details': None, 'in_library': None, + 'is_unique': None, 'item_id': 'dd954bbf54398e247d803186d3585b79', 'provider_domain': 'jellyfin', 'provider_instance': 'xx-instance-id-xx', @@ -435,6 +439,7 @@ 'available': True, 'details': None, 'in_library': None, + 'is_unique': None, 'item_id': 'da9c458e425584680765ddc3a89cbc0c', 'provider_domain': 'jellyfin', 'provider_instance': 'xx-instance-id-xx', @@ -531,6 +536,7 @@ 'available': True, 'details': None, 'in_library': None, + 'is_unique': None, 'item_id': 'b5319fb11cde39fca2023184fcfa9862', 'provider_domain': 'jellyfin', 'provider_instance': 'xx-instance-id-xx', @@ -619,6 +625,7 @@ 'available': True, 'details': None, 'in_library': None, + 'is_unique': None, 'item_id': '54918f75ee8f6c8b8dc5efd680644f29', 'provider_domain': 'jellyfin', 'provider_instance': 'xx-instance-id-xx', @@ -740,6 +747,7 @@ 'available': True, 'details': None, 'in_library': None, + 'is_unique': None, 'item_id': 'fb12a77f49616a9fc56a6fab2b94174c', 'provider_domain': 'jellyfin', 'provider_instance': 'xx-instance-id-xx', diff --git a/tests/providers/nicovideo/__snapshots__/test_converters.ambr b/tests/providers/nicovideo/__snapshots__/test_converters.ambr index 27e0fa24..1fa43e2a 100644 --- a/tests/providers/nicovideo/__snapshots__/test_converters.ambr +++ b/tests/providers/nicovideo/__snapshots__/test_converters.ambr @@ -49,6 +49,7 @@ 'available': True, 'details': None, 'in_library': None, + 'is_unique': None, 'item_id': '68461151', 'provider_domain': 'nicovideo', 'provider_instance': 'nicovideo_test', @@ -111,6 +112,7 @@ 'available': True, 'details': None, 'in_library': None, + 'is_unique': None, 'item_id': '527007', 'provider_domain': 'nicovideo', 'provider_instance': 'nicovideo_test', @@ -175,6 +177,7 @@ 'available': True, 'details': None, 'in_library': None, + 'is_unique': None, 'item_id': '68461151', 'provider_domain': 'nicovideo', 'provider_instance': 'nicovideo_test', @@ -237,6 +240,7 @@ 'available': True, 'details': None, 'in_library': None, + 'is_unique': None, 'item_id': '527007', 'provider_domain': 'nicovideo', 'provider_instance': 'nicovideo_test', @@ -311,6 +315,7 @@ 'available': True, 'details': None, 'in_library': None, + 'is_unique': None, 'item_id': '68461151', 'provider_domain': 'nicovideo', 'provider_instance': 'nicovideo_test', @@ -389,6 +394,7 @@ 'available': True, 'details': None, 'in_library': None, + 'is_unique': None, 'item_id': 'sm45285955', 'provider_domain': 'nicovideo', 'provider_instance': 'nicovideo_test', @@ -454,6 +460,7 @@ 'available': True, 'details': None, 'in_library': None, + 'is_unique': None, 'item_id': '68461151', 'provider_domain': 'nicovideo', 'provider_instance': 'nicovideo_test', @@ -516,6 +523,7 @@ 'available': True, 'details': None, 'in_library': None, + 'is_unique': None, 'item_id': '527007', 'provider_domain': 'nicovideo', 'provider_instance': 'nicovideo_test', @@ -588,6 +596,7 @@ 'available': True, 'details': None, 'in_library': None, + 'is_unique': None, 'item_id': '4', 'provider_domain': 'nicovideo', 'provider_instance': 'nicovideo_test', @@ -659,6 +668,7 @@ 'available': True, 'details': None, 'in_library': None, + 'is_unique': None, 'item_id': '68461151', 'provider_domain': 'nicovideo', 'provider_instance': 'nicovideo_test', @@ -733,6 +743,7 @@ 'available': True, 'details': None, 'in_library': None, + 'is_unique': None, 'item_id': '68461151', 'provider_domain': 'nicovideo', 'provider_instance': 'nicovideo_test', @@ -811,6 +822,7 @@ 'available': True, 'details': None, 'in_library': None, + 'is_unique': None, 'item_id': 'sm45285955', 'provider_domain': 'nicovideo', 'provider_instance': 'nicovideo_test', @@ -886,6 +898,7 @@ 'available': True, 'details': None, 'in_library': None, + 'is_unique': None, 'item_id': '68461151', 'provider_domain': 'nicovideo', 'provider_instance': 'nicovideo_test', @@ -964,6 +977,7 @@ 'available': True, 'details': None, 'in_library': None, + 'is_unique': None, 'item_id': 'sm45285955', 'provider_domain': 'nicovideo', 'provider_instance': 'nicovideo_test', @@ -1038,6 +1052,7 @@ 'available': True, 'details': None, 'in_library': None, + 'is_unique': None, 'item_id': '78597499', 'provider_domain': 'nicovideo', 'provider_instance': 'nicovideo_test', @@ -1111,6 +1126,7 @@ 'available': True, 'details': None, 'in_library': None, + 'is_unique': None, 'item_id': '78597499', 'provider_domain': 'nicovideo', 'provider_instance': 'nicovideo_test', @@ -1185,6 +1201,7 @@ 'available': True, 'details': None, 'in_library': None, + 'is_unique': None, 'item_id': '78597499', 'provider_domain': 'nicovideo', 'provider_instance': 'nicovideo_test', @@ -1258,6 +1275,7 @@ 'available': True, 'details': None, 'in_library': None, + 'is_unique': None, 'item_id': '68461151', 'provider_domain': 'nicovideo', 'provider_instance': 'nicovideo_test', @@ -1336,6 +1354,7 @@ 'available': True, 'details': None, 'in_library': None, + 'is_unique': None, 'item_id': 'sm45285955', 'provider_domain': 'nicovideo', 'provider_instance': 'nicovideo_test', @@ -1412,6 +1431,7 @@ 'available': True, 'details': None, 'in_library': None, + 'is_unique': None, 'item_id': '78597499', 'provider_domain': 'nicovideo', 'provider_instance': 'nicovideo_test', @@ -1474,6 +1494,7 @@ 'available': True, 'details': None, 'in_library': None, + 'is_unique': None, 'item_id': '68461151', 'provider_domain': 'nicovideo', 'provider_instance': 'nicovideo_test', @@ -1536,6 +1557,7 @@ 'available': True, 'details': None, 'in_library': None, + 'is_unique': None, 'item_id': '527007', 'provider_domain': 'nicovideo', 'provider_instance': 'nicovideo_test', @@ -1611,6 +1633,7 @@ 'available': True, 'details': None, 'in_library': None, + 'is_unique': None, 'item_id': '68461151', 'provider_domain': 'nicovideo', 'provider_instance': 'nicovideo_test', @@ -1689,6 +1712,7 @@ 'available': True, 'details': None, 'in_library': None, + 'is_unique': None, 'item_id': 'sm45285955', 'provider_domain': 'nicovideo', 'provider_instance': 'nicovideo_test', @@ -1764,6 +1788,7 @@ 'available': True, 'details': None, 'in_library': None, + 'is_unique': None, 'item_id': '68461151', 'provider_domain': 'nicovideo', 'provider_instance': 'nicovideo_test', @@ -1842,6 +1867,7 @@ 'available': True, 'details': None, 'in_library': None, + 'is_unique': None, 'item_id': 'sm45285955', 'provider_domain': 'nicovideo', 'provider_instance': 'nicovideo_test', @@ -1955,6 +1981,7 @@ 'available': True, 'details': None, 'in_library': None, + 'is_unique': None, 'item_id': '68461151', 'provider_domain': 'nicovideo', 'provider_instance': 'nicovideo_test', @@ -2033,6 +2060,7 @@ 'available': True, 'details': None, 'in_library': None, + 'is_unique': None, 'item_id': 'sm45285955', 'provider_domain': 'nicovideo', 'provider_instance': 'nicovideo_test', @@ -2108,6 +2136,7 @@ 'available': True, 'details': None, 'in_library': None, + 'is_unique': None, 'item_id': '68461151', 'provider_domain': 'nicovideo', 'provider_instance': 'nicovideo_test', @@ -2186,6 +2215,7 @@ 'available': True, 'details': None, 'in_library': None, + 'is_unique': None, 'item_id': 'sm45285955', 'provider_domain': 'nicovideo', 'provider_instance': 'nicovideo_test', @@ -2262,6 +2292,7 @@ 'available': True, 'details': None, 'in_library': None, + 'is_unique': None, 'item_id': '68461151', 'provider_domain': 'nicovideo', 'provider_instance': 'nicovideo_test', @@ -2324,6 +2355,7 @@ 'available': True, 'details': None, 'in_library': None, + 'is_unique': None, 'item_id': '527007', 'provider_domain': 'nicovideo', 'provider_instance': 'nicovideo_test', @@ -2395,6 +2427,7 @@ 'available': True, 'details': None, 'in_library': None, + 'is_unique': None, 'item_id': '68461151', 'provider_domain': 'nicovideo', 'provider_instance': 'nicovideo_test', @@ -2483,6 +2516,7 @@ 'available': True, 'details': None, 'in_library': None, + 'is_unique': None, 'item_id': 'sm45285955', 'provider_domain': 'nicovideo', 'provider_instance': 'nicovideo_test', diff --git a/tests/providers/opensubsonic/__snapshots__/test_parsers.ambr b/tests/providers/opensubsonic/__snapshots__/test_parsers.ambr index 248f2bf8..d712db7e 100644 --- a/tests/providers/opensubsonic/__snapshots__/test_parsers.ambr +++ b/tests/providers/opensubsonic/__snapshots__/test_parsers.ambr @@ -49,6 +49,7 @@ 'available': True, 'details': None, 'in_library': None, + 'is_unique': None, 'item_id': 'fake_artist_unknown', 'provider_domain': 'opensubsonic', 'provider_instance': 'xx-instance-id-xx', @@ -120,6 +121,7 @@ 'available': True, 'details': None, 'in_library': None, + 'is_unique': None, 'item_id': 'ad0f112b6dcf83de5e9cae85d07f0d35', 'provider_domain': 'opensubsonic', 'provider_instance': 'xx-instance-id-xx', @@ -183,6 +185,7 @@ 'available': True, 'details': None, 'in_library': None, + 'is_unique': None, 'item_id': 'fake_artist_unknown', 'provider_domain': 'opensubsonic', 'provider_instance': 'xx-instance-id-xx', @@ -260,6 +263,7 @@ 'available': True, 'details': None, 'in_library': None, + 'is_unique': None, 'item_id': 'ad0f112b6dcf83de5e9cae85d07f0d35', 'provider_domain': 'opensubsonic', 'provider_instance': 'xx-instance-id-xx', @@ -382,6 +386,7 @@ 'available': True, 'details': None, 'in_library': None, + 'is_unique': None, 'item_id': 'ad0f112b6dcf83de5e9cae85d07f0d35', 'provider_domain': 'opensubsonic', 'provider_instance': 'xx-instance-id-xx', @@ -510,6 +515,7 @@ 'available': True, 'details': None, 'in_library': None, + 'is_unique': None, 'item_id': 'ad0f112b6dcf83de5e9cae85d07f0d35', 'provider_domain': 'opensubsonic', 'provider_instance': 'xx-instance-id-xx', @@ -587,6 +593,7 @@ 'available': True, 'details': None, 'in_library': None, + 'is_unique': None, 'item_id': '37ec820ca7193e17040c98f7da7c4b51', 'provider_domain': 'opensubsonic', 'provider_instance': 'xx-instance-id-xx', @@ -669,6 +676,7 @@ 'available': True, 'details': None, 'in_library': None, + 'is_unique': None, 'item_id': '37ec820ca7193e17040c98f7da7c4b51', 'provider_domain': 'opensubsonic', 'provider_instance': 'xx-instance-id-xx', @@ -745,6 +753,7 @@ 'available': True, 'details': None, 'in_library': None, + 'is_unique': None, 'item_id': '37ec820ca7193e17040c98f7da7c4b51', 'provider_domain': 'opensubsonic', 'provider_instance': 'xx-instance-id-xx', @@ -827,6 +836,7 @@ 'available': True, 'details': None, 'in_library': None, + 'is_unique': None, 'item_id': '37ec820ca7193e17040c98f7da7c4b51', 'provider_domain': 'opensubsonic', 'provider_instance': 'xx-instance-id-xx', @@ -893,6 +903,7 @@ 'available': True, 'details': None, 'in_library': None, + 'is_unique': None, 'item_id': '100000002', 'provider_domain': 'opensubsonic', 'provider_instance': 'xx-instance-id-xx', @@ -965,6 +976,7 @@ 'available': True, 'details': None, 'in_library': None, + 'is_unique': None, 'item_id': '100000002', 'provider_domain': 'opensubsonic', 'provider_instance': 'xx-instance-id-xx', @@ -1070,6 +1082,7 @@ 'available': True, 'details': None, 'in_library': None, + 'is_unique': None, 'item_id': 'pd-5', 'provider_domain': 'opensubsonic', 'provider_instance': 'xx-instance-id-xx', @@ -1099,6 +1112,7 @@ 'available': True, 'details': None, 'in_library': None, + 'is_unique': None, 'item_id': 'pd-5$!$pe-1860', 'provider_domain': 'opensubsonic', 'provider_instance': 'xx-instance-id-xx', @@ -1205,6 +1219,7 @@ 'available': True, 'details': None, 'in_library': None, + 'is_unique': None, 'item_id': 'pd-5', 'provider_domain': 'opensubsonic', 'provider_instance': 'xx-instance-id-xx', @@ -1234,6 +1249,7 @@ 'available': True, 'details': None, 'in_library': None, + 'is_unique': None, 'item_id': 'pd-5$!$pe-1860', 'provider_domain': 'opensubsonic', 'provider_instance': 'xx-instance-id-xx', @@ -1340,6 +1356,7 @@ 'available': True, 'details': None, 'in_library': None, + 'is_unique': None, 'item_id': 'pd-5', 'provider_domain': 'opensubsonic', 'provider_instance': 'xx-instance-id-xx', @@ -1369,6 +1386,7 @@ 'available': True, 'details': None, 'in_library': None, + 'is_unique': None, 'item_id': 'pd-5$!$pe-1860', 'provider_domain': 'opensubsonic', 'provider_instance': 'xx-instance-id-xx', @@ -1438,6 +1456,7 @@ 'available': True, 'details': None, 'in_library': None, + 'is_unique': None, 'item_id': 'Mi8xNzQzNzg5NTk5MzM1LTE3NDM3ODk1OTkzMzUubTN1', 'provider_domain': 'opensubsonic', 'provider_instance': 'xx-instance-id-xx', @@ -1504,6 +1523,7 @@ 'available': True, 'details': None, 'in_library': None, + 'is_unique': None, 'item_id': 'pd-5', 'provider_domain': 'opensubsonic', 'provider_instance': 'xx-instance-id-xx', @@ -1572,6 +1592,7 @@ 'available': True, 'details': None, 'in_library': None, + 'is_unique': None, 'item_id': 'pd-5', 'provider_domain': 'opensubsonic', 'provider_instance': 'xx-instance-id-xx', @@ -1650,6 +1671,7 @@ 'available': True, 'details': None, 'in_library': None, + 'is_unique': None, 'item_id': 'fake_artist_unknown', 'provider_domain': 'opensubsonic', 'provider_instance': 'xx-instance-id-xx', @@ -1721,6 +1743,7 @@ 'available': True, 'details': None, 'in_library': None, + 'is_unique': None, 'item_id': '082f435a363c32c57d5edb6a678a28d4', 'provider_domain': 'opensubsonic', 'provider_instance': 'xx-instance-id-xx', @@ -1851,6 +1874,7 @@ 'available': True, 'details': None, 'in_library': None, + 'is_unique': None, 'item_id': 'fake_artist_unknown', 'provider_domain': 'opensubsonic', 'provider_instance': 'xx-instance-id-xx', @@ -1922,6 +1946,7 @@ 'available': True, 'details': None, 'in_library': None, + 'is_unique': None, 'item_id': '082f435a363c32c57d5edb6a678a28d4', 'provider_domain': 'opensubsonic', 'provider_instance': 'xx-instance-id-xx', @@ -1999,6 +2024,7 @@ 'available': True, 'details': None, 'in_library': None, + 'is_unique': None, 'item_id': 'MA-NAVIDROME-The New Deal', 'provider_domain': 'opensubsonic', 'provider_instance': 'xx-instance-id-xx', @@ -2070,6 +2096,7 @@ 'available': True, 'details': None, 'in_library': None, + 'is_unique': None, 'item_id': '082f435a363c32c57d5edb6a678a28d4', 'provider_domain': 'opensubsonic', 'provider_instance': 'xx-instance-id-xx', @@ -2200,6 +2227,7 @@ 'available': True, 'details': None, 'in_library': None, + 'is_unique': None, 'item_id': 'MA-NAVIDROME-The New Deal', 'provider_domain': 'opensubsonic', 'provider_instance': 'xx-instance-id-xx', @@ -2271,6 +2299,7 @@ 'available': True, 'details': None, 'in_library': None, + 'is_unique': None, 'item_id': '082f435a363c32c57d5edb6a678a28d4', 'provider_domain': 'opensubsonic', 'provider_instance': 'xx-instance-id-xx', @@ -2407,6 +2436,7 @@ 'available': True, 'details': None, 'in_library': None, + 'is_unique': None, 'item_id': '082f435a363c32c57d5edb6a678a28d4', 'provider_domain': 'opensubsonic', 'provider_instance': 'xx-instance-id-xx', @@ -2596,6 +2626,7 @@ 'available': True, 'details': None, 'in_library': None, + 'is_unique': None, 'item_id': '082f435a363c32c57d5edb6a678a28d4', 'provider_domain': 'opensubsonic', 'provider_instance': 'xx-instance-id-xx', diff --git a/tests/providers/tidal/__snapshots__/test_parsers.ambr b/tests/providers/tidal/__snapshots__/test_parsers.ambr index 6a5b1079..34992564 100644 --- a/tests/providers/tidal/__snapshots__/test_parsers.ambr +++ b/tests/providers/tidal/__snapshots__/test_parsers.ambr @@ -56,6 +56,7 @@ 'available': True, 'details': None, 'in_library': None, + 'is_unique': None, 'item_id': '12345', 'provider_domain': 'tidal', 'provider_instance': 'tidal_instance', @@ -124,6 +125,7 @@ 'available': True, 'details': None, 'in_library': None, + 'is_unique': None, 'item_id': '67890', 'provider_domain': 'tidal', 'provider_instance': 'tidal_instance', @@ -191,6 +193,7 @@ 'available': True, 'details': None, 'in_library': None, + 'is_unique': None, 'item_id': '12345', 'provider_domain': 'tidal', 'provider_instance': 'tidal_instance', @@ -259,6 +262,7 @@ 'available': True, 'details': None, 'in_library': None, + 'is_unique': False, 'item_id': 'mix_mix_123', 'provider_domain': 'tidal', 'provider_instance': 'tidal_instance', @@ -327,6 +331,7 @@ 'available': True, 'details': None, 'in_library': None, + 'is_unique': False, 'item_id': 'aabbcc-1122-3344-5566', 'provider_domain': 'tidal', 'provider_instance': 'tidal_instance', @@ -410,6 +415,7 @@ 'available': True, 'details': None, 'in_library': None, + 'is_unique': None, 'item_id': '12345', 'provider_domain': 'tidal', 'provider_instance': 'tidal_instance', @@ -481,6 +487,7 @@ 'available': True, 'details': None, 'in_library': None, + 'is_unique': None, 'item_id': '112233', 'provider_domain': 'tidal', 'provider_instance': 'tidal_instance',