if not self.onboard_done:
# mark onboard as complete as soon as the first provider is added
await self.set_onboard_complete()
+ if manifest.type == ProviderType.MUSIC:
+ # correct any multi-instance provider mappings
+ self.mass.create_task(self.mass.music.correct_multi_instance_provider_mappings())
return config
from music_assistant.constants import DB_TABLE_ALBUM_ARTISTS, DB_TABLE_ALBUM_TRACKS, DB_TABLE_ALBUMS
from music_assistant.controllers.media.base import MediaControllerBase
+from music_assistant.controllers.webserver.helpers.auth_middleware import get_current_user
from music_assistant.helpers.compare import (
compare_album,
compare_artists,
item_id, provider_instance_id_or_domain
)
if not library_album:
- return await self._get_provider_album_tracks(item_id, provider_instance_id_or_domain)
+ album_tracks = await self._get_provider_album_tracks(
+ item_id, provider_instance_id_or_domain
+ )
+ if album_tracks and not album_tracks[0].image:
+ # set album image from provider album if not present on tracks
+ prov_album = await self.get_provider_item(item_id, provider_instance_id_or_domain)
+ if prov_album.image:
+ for track in album_tracks:
+ if not track.image:
+ track.metadata.add_image(prov_album.image)
+ return album_tracks
db_items = await self.get_library_album_tracks(library_album.item_id)
result: list[Track] = list(db_items)
unique_ids.update({f"{x.name.lower()}.{x.version.lower()}" for x in db_items})
for db_item in db_items:
unique_ids.update(x.item_id for x in db_item.provider_mappings)
+ user = get_current_user()
+ user_provider_filter = user.provider_filter if user and user.provider_filter else None
for provider_mapping in library_album.provider_mappings:
+ if (
+ user_provider_filter
+ and provider_mapping.provider_instance not in user_provider_filter
+ ):
+ continue
provider_tracks = await self._get_provider_album_tracks(
provider_mapping.item_id, provider_mapping.provider_instance
)
# return all (unique) items from all providers
# initialize unique_ids with db_items to prevent duplicates
unique_ids: set[str] = {f"{item.name}.{item.version}" for item in db_items}
+ unique_providers = self.mass.music.get_unique_providers()
for provider_mapping in library_artist.provider_mappings:
+ if provider_mapping.provider_instance not in unique_providers:
+ continue
provider_tracks = await self.get_provider_artist_toptracks(
provider_mapping.item_id, provider_mapping.provider_instance
)
# return all (unique) items from all providers
# initialize unique_ids with db_items to prevent duplicates
unique_ids: set[str] = {f"{item.name}.{item.version}" for item in db_items}
+ unique_providers = self.mass.music.get_unique_providers()
for provider_mapping in library_artist.provider_mappings:
+ if provider_mapping.provider_instance not in unique_providers:
+ continue
provider_albums = await self.get_provider_artist_albums(
provider_mapping.item_id, provider_mapping.provider_instance
)
"""Return all tracks for an artist in the library/db."""
subquery = f"SELECT track_id FROM {DB_TABLE_TRACK_ARTISTS} WHERE artist_id = {item_id}"
query = f"tracks.item_id in ({subquery})"
- return await self.mass.music.tracks._get_library_items_by_query(extra_query_parts=[query])
+ return await self.mass.music.tracks.library_items(extra_query=query)
async def get_provider_artist_albums(
self,
"""Return all in-library albums for an artist."""
subquery = f"SELECT album_id FROM {DB_TABLE_ALBUM_ARTISTS} WHERE artist_id = {item_id}"
query = f"albums.item_id in ({subquery})"
- return await self.mass.music.albums._get_library_items_by_query(extra_query_parts=[query])
+ return await self.mass.music.albums.library_items(extra_query=query)
async def _add_library_item(
self, item: Artist | ItemMapping, overwrite_existing: bool = False
)
# build and execute final query
sql_query = self._build_final_query(query_parts, join_parts, order_by)
+
return [
cast("ItemCls", self.item_cls.from_dict(self._parse_db_row(db_row)))
for db_row in await self.mass.music.database.get_rows_from_query(
import logging
import os
import shutil
+import time
from collections.abc import Sequence
from contextlib import suppress
from copy import deepcopy
CACHE_CATEGORY_LAST_SYNC: Final[int] = 9
CACHE_CATEGORY_SEARCH_RESULTS: Final[int] = 10
+LAST_PROVIDER_INSTANCE_SCAN: Final[str] = "last_provider_instance_scan"
+PROVIDER_INSTANCE_SCAN_INTERVAL: Final[int] = 30 * 24 * 60 * 60 # one month in seconds
class MusicController(CoreController):
self.domain, CONF_DELETED_PROVIDERS, []
):
await self.cleanup_provider(removed_provider)
+ # schedule cleanup task for matching provider instances
+ last_scan = cast("int", self.config.get_value(LAST_PROVIDER_INSTANCE_SCAN, 0))
+ if time.time() - last_scan > PROVIDER_INSTANCE_SCAN_INTERVAL:
+ self.mass.call_later(60, self.correct_multi_instance_provider_mappings)
async def close(self) -> None:
"""Cleanup on exit."""
"""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)
+ # don't use self.providers here as that applies user filters
+ for prov in self.mass.providers
+ if isinstance(prov, MusicProvider)
+ and prov.domain == domain
+ and (return_unavailable or prov.available)
]
def get_unique_providers(self) -> set[str]:
def match_provider_instances(
self,
item: MediaItemType,
- ) -> None:
+ ) -> bool:
"""Match all provider instances for the given item."""
+ mappings_added = False
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)):
+ if not (provider := self.mass.get_provider(provider_mapping.provider_instance)):
continue
if not provider.is_streaming_provider:
continue
in_library=None,
)
)
+ mappings_added = True
+ return mappings_added
@api_command("music/add_provider_mapping")
async def add_provider_mapping(
"""
)
await self.database.commit()
+
+ async def correct_multi_instance_provider_mappings(self) -> None:
+ """Correct provider mappings for multi-instance providers."""
+ self.logger.debug("Correcting provider mappings for multi-instance providers...")
+ multi_instance_providers: set[str] = set()
+ for provider in self.providers:
+ if len(self.get_provider_instances(provider.domain)) > 1:
+ multi_instance_providers.add(provider.instance_id)
+ if not multi_instance_providers:
+ return # no multi-instance providers found, nothing to do
+
+ for ctrl in (
+ self.albums,
+ self.artists,
+ self.tracks,
+ self.playlists,
+ self.radio,
+ self.audiobooks,
+ self.podcasts,
+ ):
+ async for db_item in ctrl.iter_library_items(provider=list(multi_instance_providers)):
+ if self.match_provider_instances(db_item):
+ await ctrl.update_item_in_library(db_item.item_id, db_item)
+ self.mass.config.set_raw_core_config_value(
+ self.domain, LAST_PROVIDER_INSTANCE_SCAN, int(time.time())
+ )
+ self.logger.debug("Provider mappings correction done")
if child_player.player_id in player_ids_to_remove:
if stream_session:
await stream_session.remove_client(child_player)
- self._attr_group_members.remove(child_player.player_id)
+ if child_player.player_id in self._attr_group_members:
+ self._attr_group_members.remove(child_player.player_id)
# handle additions
for player_id in player_ids_to_add or []: