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 (
)
self._db_add_lock = asyncio.Lock()
+ @final
async def add_item_to_library(
self,
item: ItemCls,
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
)
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)
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)
return item
return None
+ @final
async def get_library_item_by_prov_mappings(
self,
provider_mappings: Iterable[ProviderMapping],
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:
return item
return None
+ @final
async def get_library_item_by_external_ids(
self, external_ids: set[tuple[ExternalID, str]]
) -> ItemCls | None:
return match
return None
+ @final
async def get_library_items_by_prov_id(
self,
provider_domain: str | None = None,
extra_query_params=query_params,
)
+ @final
async def iter_library_items_by_prov_id(
self,
provider_instance_id_or_domain: str,
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
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,
)
raise MediaNotFoundError(msg)
+ @final
async def add_provider_mapping(
self, item_id: str | int, provider_mapping: ProviderMapping
) -> None:
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:
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
with suppress(AssertionError):
await self.remove_item_from_library(db_id)
+ @final
async def set_provider_mappings(
self,
item_id: str | int,
) -> 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,
)
]
+ @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:
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],
query_parts.append(f"{self.db_table}.item_id in ({sub_query})")
join_parts.clear()
+ @final
def _apply_filters(
self,
query_parts: list[str],
f"AND ({' OR '.join(provider_conditions)})"
)
+ @final
def _build_final_query(
self,
query_parts: list[str],
return sql_query
+ @final
@staticmethod
def _parse_db_row(db_row: Mapping[str, Any]) -> dict[str, Any]:
"""Parse raw db Mapping into a dict."""
db_row_dict["metadata"]["images"] = [album_thumb]
return db_row_dict
+ @final
def _ensure_provider_filter(
self,
provider: str | list[str] | None,
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()
BrowseFolder,
ItemMapping,
MediaItemType,
+ ProviderMapping,
RecommendationFolder,
SearchResults,
Track,
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
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.
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 [
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
# 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()
[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,
"""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(
(
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
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,
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(
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,
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",
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,
provider_domain=self.domain,
provider_instance=self.instance_id,
url="https://open.spotify.com/collection/tracks",
+ is_unique=True, # liked songs is user-specific
)
},
)
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,
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,
"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",
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
'available': True,
'details': None,
'in_library': None,
+ 'is_unique': None,
'item_id': '70b7288088b42d318f75dbcc41fd0091',
'provider_domain': 'jellyfin',
'provider_instance': 'xx-instance-id-xx',
'available': True,
'details': None,
'in_library': None,
+ 'is_unique': None,
'item_id': '32ed6a0091733dcff57eae67010f3d4b',
'provider_domain': 'jellyfin',
'provider_instance': 'xx-instance-id-xx',
'available': True,
'details': None,
'in_library': None,
+ 'is_unique': None,
'item_id': '7c8d0bd55291c7fc0451d17ebef30017',
'provider_domain': 'jellyfin',
'provider_instance': 'xx-instance-id-xx',
'available': True,
'details': None,
'in_library': None,
+ 'is_unique': None,
'item_id': 'dd954bbf54398e247d803186d3585b79',
'provider_domain': 'jellyfin',
'provider_instance': 'xx-instance-id-xx',
'available': True,
'details': None,
'in_library': None,
+ 'is_unique': None,
'item_id': 'da9c458e425584680765ddc3a89cbc0c',
'provider_domain': 'jellyfin',
'provider_instance': 'xx-instance-id-xx',
'available': True,
'details': None,
'in_library': None,
+ 'is_unique': None,
'item_id': 'b5319fb11cde39fca2023184fcfa9862',
'provider_domain': 'jellyfin',
'provider_instance': 'xx-instance-id-xx',
'available': True,
'details': None,
'in_library': None,
+ 'is_unique': None,
'item_id': '54918f75ee8f6c8b8dc5efd680644f29',
'provider_domain': 'jellyfin',
'provider_instance': 'xx-instance-id-xx',
'available': True,
'details': None,
'in_library': None,
+ 'is_unique': None,
'item_id': 'fb12a77f49616a9fc56a6fab2b94174c',
'provider_domain': 'jellyfin',
'provider_instance': 'xx-instance-id-xx',
'available': True,
'details': None,
'in_library': None,
+ 'is_unique': None,
'item_id': '68461151',
'provider_domain': 'nicovideo',
'provider_instance': 'nicovideo_test',
'available': True,
'details': None,
'in_library': None,
+ 'is_unique': None,
'item_id': '527007',
'provider_domain': 'nicovideo',
'provider_instance': 'nicovideo_test',
'available': True,
'details': None,
'in_library': None,
+ 'is_unique': None,
'item_id': '68461151',
'provider_domain': 'nicovideo',
'provider_instance': 'nicovideo_test',
'available': True,
'details': None,
'in_library': None,
+ 'is_unique': None,
'item_id': '527007',
'provider_domain': 'nicovideo',
'provider_instance': 'nicovideo_test',
'available': True,
'details': None,
'in_library': None,
+ 'is_unique': None,
'item_id': '68461151',
'provider_domain': 'nicovideo',
'provider_instance': 'nicovideo_test',
'available': True,
'details': None,
'in_library': None,
+ 'is_unique': None,
'item_id': 'sm45285955',
'provider_domain': 'nicovideo',
'provider_instance': 'nicovideo_test',
'available': True,
'details': None,
'in_library': None,
+ 'is_unique': None,
'item_id': '68461151',
'provider_domain': 'nicovideo',
'provider_instance': 'nicovideo_test',
'available': True,
'details': None,
'in_library': None,
+ 'is_unique': None,
'item_id': '527007',
'provider_domain': 'nicovideo',
'provider_instance': 'nicovideo_test',
'available': True,
'details': None,
'in_library': None,
+ 'is_unique': None,
'item_id': '4',
'provider_domain': 'nicovideo',
'provider_instance': 'nicovideo_test',
'available': True,
'details': None,
'in_library': None,
+ 'is_unique': None,
'item_id': '68461151',
'provider_domain': 'nicovideo',
'provider_instance': 'nicovideo_test',
'available': True,
'details': None,
'in_library': None,
+ 'is_unique': None,
'item_id': '68461151',
'provider_domain': 'nicovideo',
'provider_instance': 'nicovideo_test',
'available': True,
'details': None,
'in_library': None,
+ 'is_unique': None,
'item_id': 'sm45285955',
'provider_domain': 'nicovideo',
'provider_instance': 'nicovideo_test',
'available': True,
'details': None,
'in_library': None,
+ 'is_unique': None,
'item_id': '68461151',
'provider_domain': 'nicovideo',
'provider_instance': 'nicovideo_test',
'available': True,
'details': None,
'in_library': None,
+ 'is_unique': None,
'item_id': 'sm45285955',
'provider_domain': 'nicovideo',
'provider_instance': 'nicovideo_test',
'available': True,
'details': None,
'in_library': None,
+ 'is_unique': None,
'item_id': '78597499',
'provider_domain': 'nicovideo',
'provider_instance': 'nicovideo_test',
'available': True,
'details': None,
'in_library': None,
+ 'is_unique': None,
'item_id': '78597499',
'provider_domain': 'nicovideo',
'provider_instance': 'nicovideo_test',
'available': True,
'details': None,
'in_library': None,
+ 'is_unique': None,
'item_id': '78597499',
'provider_domain': 'nicovideo',
'provider_instance': 'nicovideo_test',
'available': True,
'details': None,
'in_library': None,
+ 'is_unique': None,
'item_id': '68461151',
'provider_domain': 'nicovideo',
'provider_instance': 'nicovideo_test',
'available': True,
'details': None,
'in_library': None,
+ 'is_unique': None,
'item_id': 'sm45285955',
'provider_domain': 'nicovideo',
'provider_instance': 'nicovideo_test',
'available': True,
'details': None,
'in_library': None,
+ 'is_unique': None,
'item_id': '78597499',
'provider_domain': 'nicovideo',
'provider_instance': 'nicovideo_test',
'available': True,
'details': None,
'in_library': None,
+ 'is_unique': None,
'item_id': '68461151',
'provider_domain': 'nicovideo',
'provider_instance': 'nicovideo_test',
'available': True,
'details': None,
'in_library': None,
+ 'is_unique': None,
'item_id': '527007',
'provider_domain': 'nicovideo',
'provider_instance': 'nicovideo_test',
'available': True,
'details': None,
'in_library': None,
+ 'is_unique': None,
'item_id': '68461151',
'provider_domain': 'nicovideo',
'provider_instance': 'nicovideo_test',
'available': True,
'details': None,
'in_library': None,
+ 'is_unique': None,
'item_id': 'sm45285955',
'provider_domain': 'nicovideo',
'provider_instance': 'nicovideo_test',
'available': True,
'details': None,
'in_library': None,
+ 'is_unique': None,
'item_id': '68461151',
'provider_domain': 'nicovideo',
'provider_instance': 'nicovideo_test',
'available': True,
'details': None,
'in_library': None,
+ 'is_unique': None,
'item_id': 'sm45285955',
'provider_domain': 'nicovideo',
'provider_instance': 'nicovideo_test',
'available': True,
'details': None,
'in_library': None,
+ 'is_unique': None,
'item_id': '68461151',
'provider_domain': 'nicovideo',
'provider_instance': 'nicovideo_test',
'available': True,
'details': None,
'in_library': None,
+ 'is_unique': None,
'item_id': 'sm45285955',
'provider_domain': 'nicovideo',
'provider_instance': 'nicovideo_test',
'available': True,
'details': None,
'in_library': None,
+ 'is_unique': None,
'item_id': '68461151',
'provider_domain': 'nicovideo',
'provider_instance': 'nicovideo_test',
'available': True,
'details': None,
'in_library': None,
+ 'is_unique': None,
'item_id': 'sm45285955',
'provider_domain': 'nicovideo',
'provider_instance': 'nicovideo_test',
'available': True,
'details': None,
'in_library': None,
+ 'is_unique': None,
'item_id': '68461151',
'provider_domain': 'nicovideo',
'provider_instance': 'nicovideo_test',
'available': True,
'details': None,
'in_library': None,
+ 'is_unique': None,
'item_id': '527007',
'provider_domain': 'nicovideo',
'provider_instance': 'nicovideo_test',
'available': True,
'details': None,
'in_library': None,
+ 'is_unique': None,
'item_id': '68461151',
'provider_domain': 'nicovideo',
'provider_instance': 'nicovideo_test',
'available': True,
'details': None,
'in_library': None,
+ 'is_unique': None,
'item_id': 'sm45285955',
'provider_domain': 'nicovideo',
'provider_instance': 'nicovideo_test',
'available': True,
'details': None,
'in_library': None,
+ 'is_unique': None,
'item_id': 'fake_artist_unknown',
'provider_domain': 'opensubsonic',
'provider_instance': 'xx-instance-id-xx',
'available': True,
'details': None,
'in_library': None,
+ 'is_unique': None,
'item_id': 'ad0f112b6dcf83de5e9cae85d07f0d35',
'provider_domain': 'opensubsonic',
'provider_instance': 'xx-instance-id-xx',
'available': True,
'details': None,
'in_library': None,
+ 'is_unique': None,
'item_id': 'fake_artist_unknown',
'provider_domain': 'opensubsonic',
'provider_instance': 'xx-instance-id-xx',
'available': True,
'details': None,
'in_library': None,
+ 'is_unique': None,
'item_id': 'ad0f112b6dcf83de5e9cae85d07f0d35',
'provider_domain': 'opensubsonic',
'provider_instance': 'xx-instance-id-xx',
'available': True,
'details': None,
'in_library': None,
+ 'is_unique': None,
'item_id': 'ad0f112b6dcf83de5e9cae85d07f0d35',
'provider_domain': 'opensubsonic',
'provider_instance': 'xx-instance-id-xx',
'available': True,
'details': None,
'in_library': None,
+ 'is_unique': None,
'item_id': 'ad0f112b6dcf83de5e9cae85d07f0d35',
'provider_domain': 'opensubsonic',
'provider_instance': 'xx-instance-id-xx',
'available': True,
'details': None,
'in_library': None,
+ 'is_unique': None,
'item_id': '37ec820ca7193e17040c98f7da7c4b51',
'provider_domain': 'opensubsonic',
'provider_instance': 'xx-instance-id-xx',
'available': True,
'details': None,
'in_library': None,
+ 'is_unique': None,
'item_id': '37ec820ca7193e17040c98f7da7c4b51',
'provider_domain': 'opensubsonic',
'provider_instance': 'xx-instance-id-xx',
'available': True,
'details': None,
'in_library': None,
+ 'is_unique': None,
'item_id': '37ec820ca7193e17040c98f7da7c4b51',
'provider_domain': 'opensubsonic',
'provider_instance': 'xx-instance-id-xx',
'available': True,
'details': None,
'in_library': None,
+ 'is_unique': None,
'item_id': '37ec820ca7193e17040c98f7da7c4b51',
'provider_domain': 'opensubsonic',
'provider_instance': 'xx-instance-id-xx',
'available': True,
'details': None,
'in_library': None,
+ 'is_unique': None,
'item_id': '100000002',
'provider_domain': 'opensubsonic',
'provider_instance': 'xx-instance-id-xx',
'available': True,
'details': None,
'in_library': None,
+ 'is_unique': None,
'item_id': '100000002',
'provider_domain': 'opensubsonic',
'provider_instance': 'xx-instance-id-xx',
'available': True,
'details': None,
'in_library': None,
+ 'is_unique': None,
'item_id': 'pd-5',
'provider_domain': 'opensubsonic',
'provider_instance': 'xx-instance-id-xx',
'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',
'available': True,
'details': None,
'in_library': None,
+ 'is_unique': None,
'item_id': 'pd-5',
'provider_domain': 'opensubsonic',
'provider_instance': 'xx-instance-id-xx',
'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',
'available': True,
'details': None,
'in_library': None,
+ 'is_unique': None,
'item_id': 'pd-5',
'provider_domain': 'opensubsonic',
'provider_instance': 'xx-instance-id-xx',
'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',
'available': True,
'details': None,
'in_library': None,
+ 'is_unique': None,
'item_id': 'Mi8xNzQzNzg5NTk5MzM1LTE3NDM3ODk1OTkzMzUubTN1',
'provider_domain': 'opensubsonic',
'provider_instance': 'xx-instance-id-xx',
'available': True,
'details': None,
'in_library': None,
+ 'is_unique': None,
'item_id': 'pd-5',
'provider_domain': 'opensubsonic',
'provider_instance': 'xx-instance-id-xx',
'available': True,
'details': None,
'in_library': None,
+ 'is_unique': None,
'item_id': 'pd-5',
'provider_domain': 'opensubsonic',
'provider_instance': 'xx-instance-id-xx',
'available': True,
'details': None,
'in_library': None,
+ 'is_unique': None,
'item_id': 'fake_artist_unknown',
'provider_domain': 'opensubsonic',
'provider_instance': 'xx-instance-id-xx',
'available': True,
'details': None,
'in_library': None,
+ 'is_unique': None,
'item_id': '082f435a363c32c57d5edb6a678a28d4',
'provider_domain': 'opensubsonic',
'provider_instance': 'xx-instance-id-xx',
'available': True,
'details': None,
'in_library': None,
+ 'is_unique': None,
'item_id': 'fake_artist_unknown',
'provider_domain': 'opensubsonic',
'provider_instance': 'xx-instance-id-xx',
'available': True,
'details': None,
'in_library': None,
+ 'is_unique': None,
'item_id': '082f435a363c32c57d5edb6a678a28d4',
'provider_domain': 'opensubsonic',
'provider_instance': 'xx-instance-id-xx',
'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',
'available': True,
'details': None,
'in_library': None,
+ 'is_unique': None,
'item_id': '082f435a363c32c57d5edb6a678a28d4',
'provider_domain': 'opensubsonic',
'provider_instance': 'xx-instance-id-xx',
'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',
'available': True,
'details': None,
'in_library': None,
+ 'is_unique': None,
'item_id': '082f435a363c32c57d5edb6a678a28d4',
'provider_domain': 'opensubsonic',
'provider_instance': 'xx-instance-id-xx',
'available': True,
'details': None,
'in_library': None,
+ 'is_unique': None,
'item_id': '082f435a363c32c57d5edb6a678a28d4',
'provider_domain': 'opensubsonic',
'provider_instance': 'xx-instance-id-xx',
'available': True,
'details': None,
'in_library': None,
+ 'is_unique': None,
'item_id': '082f435a363c32c57d5edb6a678a28d4',
'provider_domain': 'opensubsonic',
'provider_instance': 'xx-instance-id-xx',
'available': True,
'details': None,
'in_library': None,
+ 'is_unique': None,
'item_id': '12345',
'provider_domain': 'tidal',
'provider_instance': 'tidal_instance',
'available': True,
'details': None,
'in_library': None,
+ 'is_unique': None,
'item_id': '67890',
'provider_domain': 'tidal',
'provider_instance': 'tidal_instance',
'available': True,
'details': None,
'in_library': None,
+ 'is_unique': None,
'item_id': '12345',
'provider_domain': 'tidal',
'provider_instance': 'tidal_instance',
'available': True,
'details': None,
'in_library': None,
+ 'is_unique': False,
'item_id': 'mix_mix_123',
'provider_domain': 'tidal',
'provider_instance': 'tidal_instance',
'available': True,
'details': None,
'in_library': None,
+ 'is_unique': False,
'item_id': 'aabbcc-1122-3344-5566',
'provider_domain': 'tidal',
'provider_instance': 'tidal_instance',
'available': True,
'details': None,
'in_library': None,
+ 'is_unique': None,
'item_id': '12345',
'provider_domain': 'tidal',
'provider_instance': 'tidal_instance',
'available': True,
'details': None,
'in_library': None,
+ 'is_unique': None,
'item_id': '112233',
'provider_domain': 'tidal',
'provider_instance': 'tidal_instance',