From f0a6696472c52bba24d371d6bde2c7835a83cf1e Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Fri, 12 Dec 2025 02:39:19 +0100 Subject: [PATCH] fix some more multi user issues --- music_assistant/controllers/config.py | 3 ++ music_assistant/controllers/media/albums.py | 20 +++++++- music_assistant/controllers/media/artists.py | 10 +++- music_assistant/controllers/media/base.py | 1 + music_assistant/controllers/music.py | 48 ++++++++++++++++++-- music_assistant/providers/airplay/player.py | 3 +- 6 files changed, 77 insertions(+), 8 deletions(-) diff --git a/music_assistant/controllers/config.py b/music_assistant/controllers/config.py index 6423c205..35bfd82d 100644 --- a/music_assistant/controllers/config.py +++ b/music_assistant/controllers/config.py @@ -1493,4 +1493,7 @@ class ConfigController: 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 diff --git a/music_assistant/controllers/media/albums.py b/music_assistant/controllers/media/albums.py index 02ce8332..1a764788 100644 --- a/music_assistant/controllers/media/albums.py +++ b/music_assistant/controllers/media/albums.py @@ -20,6 +20,7 @@ from music_assistant_models.media_items import ( 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, @@ -250,7 +251,17 @@ class AlbumsController(MediaControllerBase[Album]): 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) @@ -265,7 +276,14 @@ class AlbumsController(MediaControllerBase[Album]): 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 ) diff --git a/music_assistant/controllers/media/artists.py b/music_assistant/controllers/media/artists.py index dbc0a38a..4042d22b 100644 --- a/music_assistant/controllers/media/artists.py +++ b/music_assistant/controllers/media/artists.py @@ -126,7 +126,10 @@ class ArtistsController(MediaControllerBase[Artist]): # 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 ) @@ -165,7 +168,10 @@ class ArtistsController(MediaControllerBase[Artist]): # 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 ) @@ -244,7 +250,7 @@ class ArtistsController(MediaControllerBase[Artist]): """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, @@ -280,7 +286,7 @@ class ArtistsController(MediaControllerBase[Artist]): """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 diff --git a/music_assistant/controllers/media/base.py b/music_assistant/controllers/media/base.py index bd1d1f0f..2e44ecc9 100644 --- a/music_assistant/controllers/media/base.py +++ b/music_assistant/controllers/media/base.py @@ -782,6 +782,7 @@ class MediaControllerBase[ItemCls: "MediaItemType"](metaclass=ABCMeta): ) # 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( diff --git a/music_assistant/controllers/music.py b/music_assistant/controllers/music.py index d31e4ba1..485e2afa 100644 --- a/music_assistant/controllers/music.py +++ b/music_assistant/controllers/music.py @@ -6,6 +6,7 @@ import asyncio import logging import os import shutil +import time from collections.abc import Sequence from contextlib import suppress from copy import deepcopy @@ -99,6 +100,8 @@ DB_SCHEMA_VERSION: Final[int] = 23 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): @@ -176,6 +179,10 @@ 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.""" @@ -1359,8 +1366,11 @@ class MusicController(CoreController): """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]: @@ -1502,13 +1512,14 @@ class MusicController(CoreController): 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 @@ -1541,6 +1552,8 @@ class MusicController(CoreController): in_library=None, ) ) + mappings_added = True + return mappings_added @api_command("music/add_provider_mapping") async def add_provider_mapping( @@ -2430,3 +2443,30 @@ class MusicController(CoreController): """ ) 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") diff --git a/music_assistant/providers/airplay/player.py b/music_assistant/providers/airplay/player.py index 79fb4322..62ceb459 100644 --- a/music_assistant/providers/airplay/player.py +++ b/music_assistant/providers/airplay/player.py @@ -497,7 +497,8 @@ class AirPlayPlayer(Player): 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 []: -- 2.34.1