fix some more multi user issues
authorMarcel van der Veldt <m.vanderveldt@outlook.com>
Fri, 12 Dec 2025 01:39:19 +0000 (02:39 +0100)
committerMarcel van der Veldt <m.vanderveldt@outlook.com>
Fri, 12 Dec 2025 01:39:19 +0000 (02:39 +0100)
music_assistant/controllers/config.py
music_assistant/controllers/media/albums.py
music_assistant/controllers/media/artists.py
music_assistant/controllers/media/base.py
music_assistant/controllers/music.py
music_assistant/providers/airplay/player.py

index 6423c2059d81b1bb7606d30f3bacae97f0b02a37..35bfd82d9ffe48ac68d96001e0ebd3d690ad9f94 100644 (file)
@@ -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
index 02ce833274193f85f351ff4bc5165b7455567019..1a764788308db7541e98c8bc8c79dc05060c1f23 100644 (file)
@@ -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
             )
index dbc0a38a2d2151b75d05d2c767f1b92833dea002..4042d22b2ef7abb2e815e0fb41501b24690cd07f 100644 (file)
@@ -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
index bd1d1f0fe610b798510ff29dad82f0c19f0cf337..2e44ecc99bab1f3149f9f15c91749ffb15fc5637 100644 (file)
@@ -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(
index d31e4ba101b95a3882475f734f84cf554e16ed62..485e2afa692f35e4b638d3c6c59ee116a914d3aa 100644 (file)
@@ -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")
index 79fb43226fb8462de048f5dd6c9dd03f3769e3d1..62ceb4597c4d9ee40b0f5f8dc88965867e406295 100644 (file)
@@ -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 []: