if album.artist:
album.artist = await self.mass.music.artists.get(
album.artist.item_id,
- album.artist.provider,
+ provider_instance=album.artist.provider,
lazy=True,
- details=album.artist,
+ details=None if isinstance(album.artist, ItemMapping) else album.artist,
add_to_db=add_to_db,
)
return album
"""Add album to local db and return the database item."""
# grab additional metadata
await self.mass.metadata.get_album_metadata(item)
- existing = await self.get_db_item_by_prov_id(item.item_id, item.provider)
+ existing = await self.get_db_item_by_prov_id(item.item_id, provider_instance=item.provider)
if existing:
db_item = await self.update_db_item(existing.item_id, item)
else:
prov_mapping.item_id, prov_mapping.provider_instance
):
await self.mass.music.tracks.get(
- track.item_id, track.provider, details=track, add_to_db=True
+ track.item_id, provider_instance=track.provider, details=track, add_to_db=True
)
self.mass.signal_event(
EventType.MEDIA_ITEM_UPDATED if existing else EventType.MEDIA_ITEM_ADDED,
return ItemMapping.from_item(artist)
if db_artist := await self.mass.music.artists.get_db_item_by_prov_id(
- artist.item_id, provider_domain=artist.provider
+ artist.item_id, provider_instance=artist.provider
):
return ItemMapping.from_item(db_artist)
provider_domain or provider_instance
), "provider_domain or provider_instance must be supplied"
if not add_to_db and "database" in (provider_domain, provider_instance):
- return await self.get_provider_item(item_id, provider_instance or provider_domain)
+ return await self.get_db_item(item_id)
if details and details.provider == "database":
details = None
db_item = await self.get_db_item_by_prov_id(
return db_item
if not details and provider_instance:
# no details provider nor in db, fetch them from the provider
- details = await self.get_provider_item(item_id, provider_instance)
+ details = await self.get_provider_item(
+ item_id, provider_instance, force_refresh=force_refresh
+ )
if not details and provider_domain:
# check providers for given provider domain one by one
for prov in self.mass.music.providers:
continue
if prov.domain == provider_domain:
try:
- details = await self.get_provider_item(item_id, prov.domain)
+ details = await self.get_provider_item(
+ item_id, prov.domain, force_refresh=force_refresh
+ )
except MediaNotFoundError:
pass
else:
self.mass.signal_event(EventType.MEDIA_ITEM_UPDATED, db_item.uri, db_item)
async def get_provider_item(
- self,
- item_id: str,
- provider_domain_or_instance_id: str,
+ self, item_id: str, provider_domain_or_instance_id: str, force_refresh: bool = False
) -> ItemCls:
"""Return item details for the given provider item id."""
+ cache_key = (
+ f"provider_item.{self.media_type.value}.{provider_domain_or_instance_id}.{item_id}"
+ )
if provider_domain_or_instance_id == "database":
- item = await self.get_db_item(item_id)
- else:
- provider = self.mass.get_provider(provider_domain_or_instance_id)
- item = (await provider.get_item(self.media_type, item_id)) if provider else None
- if not item:
- raise MediaNotFoundError(
- f"{self.media_type.value}://{item_id} not found on provider {provider_domain_or_instance_id}" # noqa: E501
- )
- return item
+ return await self.get_db_item(item_id)
+ if not force_refresh and (cache := await self.mass.cache.get(cache_key)):
+ return self.item_cls.from_dict(cache)
+ if provider := self.mass.get_provider(provider_domain_or_instance_id):
+ item = await provider.get_item(self.media_type, item_id)
+ await self.mass.cache.set(cache_key, item.to_dict(), 3600)
+ return item
+ raise MediaNotFoundError(
+ f"{self.media_type.value}://{item_id} not found on provider {provider_domain_or_instance_id}" # noqa: E501
+ )
async def remove_prov_mapping(self, item_id: int, provider_instance: str) -> None:
"""Remove provider id(s) from item."""
"""Add playlist to local db and return the new database item."""
item.metadata.last_refresh = int(time())
await self.mass.metadata.get_playlist_metadata(item)
- existing = await self.get_db_item_by_prov_id(item.item_id, item.provider)
+ existing = await self.get_db_item_by_prov_id(item.item_id, provider_instance=item.provider)
if existing:
db_item = await self.update_db_item(existing.item_id, item)
else:
"""Add radio to local db and return the new database item."""
item.metadata.last_refresh = int(time())
await self.mass.metadata.get_radio_metadata(item)
- existing = await self.get_db_item_by_prov_id(item.item_id, item.provider)
+ existing = await self.get_db_item_by_prov_id(item.item_id, provider_instance=item.provider)
if existing:
db_item = await self.update_db_item(existing.item_id, item)
else:
elif track.album:
track.album = await self.mass.music.albums.get(
track.album.item_id,
- track.album.provider,
+ provider_instance=track.album.provider,
lazy=True,
- details=track.album,
+ details=None if isinstance(track.album, ItemMapping) else track.album,
add_to_db=add_to_db,
)
except MediaNotFoundError:
for artist in track.artists:
full_artists.append(
await self.mass.music.artists.get(
- artist.item_id, artist.provider, lazy=True, details=artist, add_to_db=add_to_db
+ artist.item_id,
+ provider_instance=artist.provider,
+ lazy=True,
+ details=None if isinstance(artist, ItemMapping) else artist,
+ add_to_db=add_to_db,
)
)
track.artists = full_artists
assert item.artists
# grab additional metadata
await self.mass.metadata.get_track_metadata(item)
- existing = await self.get_db_item_by_prov_id(item.item_id, item.provider)
+ existing = await self.get_db_item_by_prov_id(item.item_id, provider_instance=item.provider)
if existing:
db_item = await self.update_db_item(existing.item_id, item)
else:
track = await self.get(item_id, provider_domain or provider_instance, add_to_db=False)
return await asyncio.gather(
*[
- self.mass.music.albums.get(album.item_id, album.provider, add_to_db=False)
+ self.mass.music.albums.get(
+ album.item_id, provider_instance=album.provider, add_to_db=False
+ )
for album in track.albums
]
)
:param limit: number of items to return in the search (per type).
"""
# include results from all (unique) music providers
- provider_domains = {item.domain for item in self.providers}
results_per_provider: list[SearchResults] = await asyncio.gather(
*[
self.search_provider(
search_query,
media_types,
- provider_domain=provider_domain,
+ provider_instance=provider_instance,
limit=limit,
)
- for provider_domain in provider_domains
+ for provider_instance in self.get_unique_providers()
]
)
# return result from all providers while keeping index
for prov in self.providers:
if prov.instance_id == provider_domain_or_instance_id:
provider_instance = prov.instance_id
- provider_domain = None
+ provider_domain = prov.domain
break
else:
provider_instance = None
self.add_to_library(
media_type=item.media_type,
item_id=item.item_id,
- provider_domain=item.provider,
+ provider_instance=item.provider,
)
)
)
media_type=item.media_type,
item_id=item.item_id,
provider_domain=item.provider,
+ provider_instance=item.provider,
)
)
)
return await self.get_item(
media_item.media_type,
media_item.item_id,
- provider_domain=media_item.provider,
+ provider_instance=media_item.provider,
force_refresh=True,
lazy=False,
add_to_db=True,
"""
for media_item in items:
self.mass.create_task(
- self.add_to_library(media_item.media_type, media_item.item_id, media_item.provider)
+ self.add_to_library(
+ media_item.media_type,
+ media_item.item_id,
+ provider_instance=media_item.provider,
+ )
)
async def library_remove_items(self, items: list[MediaItemType]) -> None:
for media_item in items:
self.mass.create_task(
self.remove_from_library(
- media_item.media_type, media_item.item_id, media_item.provider
+ media_item.media_type,
+ media_item.item_id,
+ provider_instance=media_item.provider,
)
)
instances = set()
domains = set()
for provider in self.providers:
- if provider.domain not in domains or provider.is_file():
+ if provider.domain not in domains or provider.is_unique:
instances.add(provider.instance_id)
domains.add(provider.domain)
return instances
Music Provider implementations should inherit from this base model.
"""
+ @property
+ def is_unique(self) -> bool:
+ """
+ Return True if the (non user related) data in this provider instance is unique.
+
+ For example on a global streaming provider (like Spotify),
+ the data on all instances is the same.
+ For a file provider each instance has other items.
+ Setting this to False will only query one instance of the provider for search and lookups.
+ Setting this to True will query all instances of this provider for search and lookups.
+ """
+ return False
+
async def search(
self,
search_query: str,
)
if not db_item:
# dump the item in the db, rich metadata is lazy loaded later
- db_item = await controller.add_db_item(prov_item)
+ db_item = await controller.get(prov_item)
elif (
db_item.metadata.checksum and prov_item.metadata.checksum
) and db_item.metadata.checksum != prov_item.metadata.checksum:
# item checksum changed
db_item = await controller.update_db_item(db_item.item_id, prov_item)
- # preload album/playlist tracks
- if prov_item.media_type == (MediaType.ALBUM, MediaType.PLAYLIST):
+ # add album tracks to the db too
+ if prov_item.media_type == MediaType.ALBUM:
+ prov_item: Album # noqa: PLW2901
for track in controller.tracks(prov_item.item_id, prov_item.provider):
- await self.mass.music.tracks.add_db_item(track)
+ track: Track # noqa: PLW2901
+ track.album = db_item
+ await self.mass.music.tracks.get(
+ track.item_id,
+ provider_instance=self.instance_id,
+ lazy=False,
+ details=track,
+ add_to_db=True,
+ )
+ # preload playlist tracks listing, do not load them in the db
+ # because that would make the sync very slow and has not much benefit
+ if prov_item.media_type == MediaType.PLAYLIST:
+ async for track in controller.tracks(
+ prov_item.item_id, provider_instance=self.instance_id
+ ):
+ pass
cur_db_ids.add(db_item.item_id)
if not db_item.in_library:
await controller.set_db_library(db_item.item_id, True)
# only mark the item as not in library and leave the metadata in db
await controller.set_db_library(db_item.item_id, False)
- def is_file(self) -> bool:
- """Return if this is a FileSystem based provider."""
- # override this is needed
- return self.domain.startswith("filesystem")
-
# DO NOT OVERRIDE BELOW
def library_supported(self, media_type: MediaType) -> bool:
# DEFAULT/GENERIC IMPLEMENTATION BELOW
# should normally not be needed to override
+ @property
+ def is_unique(self) -> bool:
+ """
+ Return True if the (non user related) data in this provider instance is unique.
+
+ For example on a global streaming provider (like Spotify),
+ the data on all instances is the same.
+ For a file provider each instance has other items.
+ Setting this to False will only query one instance of the provider for search and lookups.
+ Setting this to True will query all instances of this provider for search and lookups.
+ """
+ return True
+
async def search(
self, search_query: str, media_types=list[MediaType] | None, limit: int = 5 # noqa: ARG002
) -> SearchResults:
subitems.append(
BrowseFolder(
item_id=item.path,
- provider=self.domain,
+ provider=self.instance_id,
path=f"{self.instance_id}://{item.path}",
name=item.name,
)
return BrowseFolder(
item_id=item_path,
- provider=self.domain,
+ provider=self.instance_id,
path=path,
name=item_path or self.name,
# make sure to sort the resulting listing
file_item = await self.resolve(prov_playlist_id)
playlist = Playlist(
file_item.path,
- provider=self.domain,
+ provider=self.instance_id,
name=file_item.name.replace(f".{file_item.ext}", ""),
)
playlist.is_editable = file_item.ext != "pls" # can only edit m3u playlists
name, version = parse_title_and_version(tags.title, tags.version)
track = Track(
item_id=file_item.path,
- provider=self.domain,
+ provider=self.instance_id,
name=name,
version=version,
)
artist = Artist(
artist_path,
- self.domain,
+ self.instance_id,
name,
provider_mappings={
- ProviderMapping(artist_path, self.domain, self.instance_id, url=artist_path)
+ ProviderMapping(artist_path, self.instance_id, self.instance_id, url=artist_path)
},
musicbrainz_id=VARIOUS_ARTISTS_ID if compare_strings(name, VARIOUS_ARTISTS) else None,
)
album = Album(
album_path,
- self.domain,
+ self.instance_id,
name,
artists=artists,
provider_mappings={
- ProviderMapping(album_path, self.domain, self.instance_id, url=album_path)
+ ProviderMapping(album_path, self.instance_id, self.instance_id, url=album_path)
},
)
self._plex_server.library.section, self.config.get_value(CONF_LIBRARY_NAME)
)
- async def resolve_image(self, path: str) -> str | bytes | AsyncGenerator[bytes, None]:
- """Return the full image URL including the auth token."""
- return self._plex_server.url(path, True)
-
@property
def supported_features(self) -> tuple[ProviderFeature, ...]:
"""Return a list of supported features."""
ProviderFeature.ARTIST_ALBUMS,
)
+ @property
+ def is_unique(self) -> bool:
+ """
+ Return True if the (non user related) data in this provider instance is unique.
+
+ For example on a global streaming provider (like Spotify),
+ the data on all instances is the same.
+ For a file provider each instance has other items.
+ Setting this to False will only query one instance of the provider for search and lookups.
+ Setting this to True will query all instances of this provider for search and lookups.
+ """
+ return True
+
+ async def resolve_image(self, path: str) -> str | bytes | AsyncGenerator[bytes, None]:
+ """Return the full image URL including the auth token."""
+ return self._plex_server.url(path, True)
+
async def _run_async(self, call: Callable, *args, **kwargs):
return await self.mass.create_task(call, *args, **kwargs)
album_id = plex_album.key
album = Album(
item_id=album_id,
- provider=self.instance_id,
+ provider=self.domain,
name=plex_album.title,
)
if plex_album.year:
artist_id = plex_artist.key
if not artist_id:
raise InvalidDataError("Artist does not have a valid ID")
- artist = Artist(item_id=artist_id, name=plex_artist.title, provider=self.instance_id)
+ artist = Artist(item_id=artist_id, name=plex_artist.title, provider=self.domain)
if plex_artist.summary:
artist.metadata.description = plex_artist.summary
if thumb := plex_artist.firstAttr("thumb", "parentThumb", "grandparentThumb"):
async def _parse_playlist(self, plex_playlist: PlexPlaylist) -> Playlist:
"""Parse a Plex Playlist response to a Playlist object."""
playlist = Playlist(
- item_id=plex_playlist.key, provider=self.instance_id, name=plex_playlist.title
+ item_id=plex_playlist.key, provider=self.domain, name=plex_playlist.title
)
if plex_playlist.summary:
playlist.metadata.description = plex_playlist.summary