Fix provider filtering
authorMarcel van der Veldt <m.vanderveldt@outlook.com>
Thu, 4 Dec 2025 17:40:08 +0000 (18:40 +0100)
committerMarcel van der Veldt <m.vanderveldt@outlook.com>
Thu, 4 Dec 2025 17:40:08 +0000 (18:40 +0100)
music_assistant/controllers/media/albums.py
music_assistant/controllers/media/artists.py
music_assistant/controllers/media/audiobooks.py
music_assistant/controllers/media/base.py
music_assistant/controllers/media/playlists.py
music_assistant/controllers/media/podcasts.py
music_assistant/controllers/media/tracks.py
music_assistant/providers/filesystem_local/__init__.py
music_assistant/providers/plex/__init__.py

index a773fd4b815c1ab87bb17cce994721f56b1c210b..205e2b36cf9e10384483cdce6cd2ecb1d99cd389 100644 (file)
@@ -164,7 +164,7 @@ class AlbumsController(MediaControllerBase[Album]):
             limit=limit,
             offset=offset,
             order_by=order_by,
-            provider=provider,
+            provider_filter=self._ensure_provider_filter(provider),
             extra_query_parts=extra_query_parts,
             extra_query_params=extra_query_params,
             extra_join_parts=extra_join_parts,
@@ -191,7 +191,7 @@ class AlbumsController(MediaControllerBase[Album]):
                 search=None,
                 limit=remaining_limit,
                 order_by=order_by,
-                provider=provider,
+                provider_filter=self._ensure_provider_filter(provider),
                 extra_query_parts=extra_query_parts,
                 extra_query_params=extra_query_params,
                 extra_join_parts=extra_join_parts,
index 53c68df0da1d1f1621b9c38b2a6aac246de3daf6..9a362101f669b8078bc6d31e58aaa176a720165a 100644 (file)
@@ -100,7 +100,7 @@ class ArtistsController(MediaControllerBase[Artist]):
             limit=limit,
             offset=offset,
             order_by=order_by,
-            provider=provider,
+            provider_filter=self._ensure_provider_filter(provider),
             extra_query_parts=extra_query_parts,
             extra_query_params=extra_query_params,
         )
@@ -231,7 +231,7 @@ class ArtistsController(MediaControllerBase[Artist]):
             )
             query = f"tracks.item_id in ({subquery})"
             return await self.mass.music.tracks._get_library_items_by_query(
-                extra_query_parts=[query], provider=provider_instance_id_or_domain
+                extra_query_parts=[query], provider_filter=[provider_instance_id_or_domain]
             )
         return []
 
@@ -267,7 +267,7 @@ class ArtistsController(MediaControllerBase[Artist]):
             )
             query = f"albums.item_id in ({subquery})"
             return await self.mass.music.albums._get_library_items_by_query(
-                extra_query_parts=[query], provider=provider_instance_id_or_domain
+                extra_query_parts=[query], provider_filter=[provider_instance_id_or_domain]
             )
         return []
 
index 2ff44baf2b86478aba93e29ccac906b5d82616dd..6ef0bb09f94f2a4c183a3cf554e6f8f198e9ff8a 100644 (file)
@@ -89,7 +89,7 @@ class AudiobooksController(MediaControllerBase[Audiobook]):
             limit=limit,
             offset=offset,
             order_by=order_by,
-            provider=provider,
+            provider_filter=self._ensure_provider_filter(provider),
             extra_query_parts=extra_query_parts,
             extra_query_params=extra_query_params,
         )
@@ -104,7 +104,7 @@ class AudiobooksController(MediaControllerBase[Audiobook]):
                 search=None,
                 limit=limit,
                 order_by=order_by,
-                provider=provider,
+                provider_filter=self._ensure_provider_filter(provider),
                 extra_query_parts=extra_query_parts,
                 extra_query_params=extra_query_params,
             )
index ef6c1ef2e6daaca8828bbb920c247cc8ee7bb2ce..4adaeee30ea0073ea82d709cc0a3b5eb98db1b8c 100644 (file)
@@ -10,7 +10,11 @@ from contextlib import suppress
 from typing import TYPE_CHECKING, Any, TypeVar, cast
 
 from music_assistant_models.enums import EventType, ExternalID, MediaType, ProviderFeature
-from music_assistant_models.errors import MediaNotFoundError, ProviderUnavailableError
+from music_assistant_models.errors import (
+    InsufficientPermissions,
+    MediaNotFoundError,
+    ProviderUnavailableError,
+)
 from music_assistant_models.media_items import ItemMapping, MediaItemType, ProviderMapping, Track
 
 from music_assistant.constants import DB_TABLE_PLAYLOG, DB_TABLE_PROVIDER_MAPPINGS, MASS_LOGGER_NAME
@@ -242,7 +246,7 @@ class MediaControllerBase[ItemCls: "MediaItemType"](metaclass=ABCMeta):
             limit=limit,
             offset=offset,
             order_by=order_by,
-            provider=provider,
+            provider_filter=self._ensure_provider_filter(provider),
             extra_query_parts=[extra_query] if extra_query else None,
             extra_query_params=extra_query_params,
         )
@@ -705,7 +709,7 @@ class MediaControllerBase[ItemCls: "MediaItemType"](metaclass=ABCMeta):
         limit: int = 500,
         offset: int = 0,
         order_by: str | None = None,
-        provider: str | list[str] | None = None,
+        provider_filter: list[str] | None = None,
         extra_query_parts: list[str] | None = None,
         extra_query_params: dict[str, Any] | None = None,
         extra_join_parts: list[str] | None = None,
@@ -714,18 +718,28 @@ class MediaControllerBase[ItemCls: "MediaItemType"](metaclass=ABCMeta):
         query_params = extra_query_params or {}
         query_parts: list[str] = extra_query_parts or []
         join_parts: list[str] = extra_join_parts or []
-
         search = self._preprocess_search(search, query_params)
-
         # create special performant random query
         if order_by and order_by.startswith("random"):
             self._apply_random_subquery(
-                query_parts, query_params, join_parts, favorite, search, provider, limit
+                query_parts=query_parts,
+                query_params=query_params,
+                join_parts=join_parts,
+                favorite=favorite,
+                search=search,
+                provider_filter=provider_filter,
+                limit=limit,
             )
         else:
             # apply filters
-            self._apply_filters(query_parts, query_params, join_parts, favorite, search, provider)
-
+            self._apply_filters(
+                query_parts=query_parts,
+                query_params=query_params,
+                join_parts=join_parts,
+                favorite=favorite,
+                search=search,
+                provider_filter=provider_filter,
+            )
         # build and execute final query
         sql_query = self._build_final_query(query_parts, join_parts, order_by)
         return [
@@ -754,7 +768,7 @@ class MediaControllerBase[ItemCls: "MediaItemType"](metaclass=ABCMeta):
         join_parts: list[str],
         favorite: bool | None,
         search: str | None,
-        provider: str | list[str] | None,
+        provider_filter: list[str] | None,
         limit: int,
     ) -> None:
         """Build a fast random subquery with all filters applied."""
@@ -763,7 +777,12 @@ class MediaControllerBase[ItemCls: "MediaItemType"](metaclass=ABCMeta):
 
         # Apply all filters to the subquery
         self._apply_filters(
-            sub_query_parts, query_params, sub_join_parts, favorite, search, provider
+            query_parts=sub_query_parts,
+            query_params=query_params,
+            join_parts=sub_join_parts,
+            favorite=favorite,
+            search=search,
+            provider_filter=provider_filter,
         )
 
         # Build the subquery
@@ -790,55 +809,20 @@ class MediaControllerBase[ItemCls: "MediaItemType"](metaclass=ABCMeta):
         join_parts: list[str],
         favorite: bool | None,
         search: str | None,
-        provider: str | list[str] | None,
+        provider_filter: list[str] | None,
     ) -> None:
-        """Apply search, favorite, and provider filters.
-
-        If the current user has a provider_filter set, it will be applied automatically.
-        If an explicit provider filter is provided, it will be validated against the user's
-        allowed providers.
-        """
+        """Apply search, favorite, and provider filters."""
         # handle search
         if search:
             query_parts.append(f"{self.db_table}.search_name LIKE :search")
-
         # handle favorite filter
         if favorite is not None:
             query_parts.append(f"{self.db_table}.favorite = :favorite")
             query_params["favorite"] = favorite
-
-        # Apply user provider filter
-        user = get_current_user()
-        user_provider_filter = user.provider_filter if user and user.provider_filter else None
-
-        # Determine final provider filter
-        final_provider_filter: list[str] | None = None
-
-        if user_provider_filter:
-            # User has a provider filter set
-            if provider:
-                # Explicit provider filter provided - validate against user's allowed providers
-                requested_providers = [provider] if isinstance(provider, str) else provider
-                # Only include providers that are in both the user's filter and the requested list
-                final_provider_filter = [
-                    p for p in requested_providers if p in user_provider_filter
-                ]
-                if not final_provider_filter:
-                    # No overlap - user requested providers they don't have access to
-                    # Return empty results by adding impossible condition
-                    query_parts.append("1 = 0")
-                    return
-            else:
-                # No explicit filter - use user's provider filter
-                final_provider_filter = user_provider_filter
-        elif provider:
-            # No user filter, but explicit provider filter provided
-            final_provider_filter = [provider] if isinstance(provider, str) else provider
-
-        # Apply the final provider filter
-        if final_provider_filter:
+        # Apply the provider filter
+        if provider_filter:
             provider_conditions = []
-            for prov in final_provider_filter:
+            for prov in provider_filter:
                 provider_conditions.append(
                     f"provider_mappings.provider_instance = '{prov}' "
                     f"OR provider_mappings.provider_domain = '{prov}'"
@@ -912,3 +896,31 @@ class MediaControllerBase[ItemCls: "MediaItemType"](metaclass=ABCMeta):
                 else:
                     db_row_dict["metadata"]["images"] = [album_thumb]
         return db_row_dict
+
+    def _ensure_provider_filter(
+        self,
+        provider: str | list[str] | None,
+    ) -> list[str] | None:
+        """Ensure the provider filter respects the current user's provider filter."""
+        # Apply user provider filter if needed
+        user = get_current_user()
+        user_provider_filter = user.provider_filter if user and user.provider_filter else None
+        final_provider_filter: list[str] | None = None
+        if user_provider_filter:
+            # User has a provider filter set
+            if provider:
+                # Explicit provider filter provided - validate against user's allowed providers
+                requested_providers = [provider] if isinstance(provider, str) else provider
+                # Only include providers that are in both the user's filter and the requested list
+                final_provider_filter = [
+                    p for p in requested_providers if p in user_provider_filter
+                ]
+                if not final_provider_filter:
+                    # No overlap - user requested providers they don't have access to
+                    raise InsufficientPermissions(
+                        "User does not have permission to access the requested provider(s)."
+                    )
+            else:
+                # No explicit filter - use user's provider filter
+                final_provider_filter = user_provider_filter
+        return final_provider_filter
index 541e5c436ecce6aef8cd3fa91ed1e289411a3049..592a965ee11f38165fbcca7207b89ebcdde95612 100644 (file)
@@ -123,6 +123,9 @@ class PlaylistController(MediaControllerBase[Playlist]):
             raise InvalidDataError(msg)
         # create playlist on the provider
         playlist = await provider.create_playlist(name)
+        for prov_mapping in playlist.provider_mappings:
+            # when manually creating a playlist, it's always in the library
+            prov_mapping.in_library = True
         # add the new playlist to the library
         return await self.add_item_to_library(playlist, False)
 
index 46a5d7830cdcf3e47721403d242135a3911aa73c..59183eb8d0555331d4a062935339ab64b388bf75 100644 (file)
@@ -72,7 +72,7 @@ class PodcastsController(MediaControllerBase[Podcast]):
             limit=limit,
             offset=offset,
             order_by=order_by,
-            provider=provider,
+            provider_filter=self._ensure_provider_filter(provider),
             extra_query_parts=extra_query_parts,
             extra_query_params=extra_query_params,
         )
@@ -87,7 +87,7 @@ class PodcastsController(MediaControllerBase[Podcast]):
                 search=None,
                 limit=limit,
                 order_by=order_by,
-                provider=provider,
+                provider_filter=self._ensure_provider_filter(provider),
                 extra_query_parts=extra_query_parts,
                 extra_query_params=extra_query_params,
             )
index e1c953a380d808b5d04365266bb5e5fb1448583c..1872e8a3baddf99619a4df42800a2d1123643a8e 100644 (file)
@@ -203,7 +203,7 @@ class TracksController(MediaControllerBase[Track]):
             limit=limit,
             offset=offset,
             order_by=order_by,
-            provider=provider,
+            provider_filter=self._ensure_provider_filter(provider),
             extra_query_parts=extra_query_parts,
             extra_query_params=extra_query_params,
             extra_join_parts=extra_join_parts,
@@ -223,7 +223,7 @@ class TracksController(MediaControllerBase[Track]):
                 search=None,
                 limit=limit,
                 order_by=order_by,
-                provider=provider,
+                provider_filter=self._ensure_provider_filter(provider),
                 extra_query_parts=extra_query_parts,
                 extra_query_params=extra_query_params,
                 extra_join_parts=extra_join_parts,
index 269276e90d627e616bf57967456300dcc4a9aab7..659a0729bc37eeaf1a107e0c86eed26fe7e5664b 100644 (file)
@@ -231,38 +231,38 @@ class LocalFileSystemProvider(MusicProvider):
         # so instead we just query the db...
         if media_types is None or MediaType.TRACK in media_types:
             result.tracks = await self.mass.music.tracks._get_library_items_by_query(
-                search=search_query, provider=self.instance_id, limit=limit
+                search=search_query, provider_filter=[self.instance_id], limit=limit
             )
 
         if media_types is None or MediaType.ALBUM in media_types:
             result.albums = await self.mass.music.albums._get_library_items_by_query(
                 search=search_query,
-                provider=self.instance_id,
+                provider_filter=[self.instance_id],
                 limit=limit,
             )
 
         if media_types is None or MediaType.ARTIST in media_types:
             result.artists = await self.mass.music.artists._get_library_items_by_query(
                 search=search_query,
-                provider=self.instance_id,
+                provider_filter=[self.instance_id],
                 limit=limit,
             )
         if media_types is None or MediaType.PLAYLIST in media_types:
             result.playlists = await self.mass.music.playlists._get_library_items_by_query(
                 search=search_query,
-                provider=self.instance_id,
+                provider_filter=[self.instance_id],
                 limit=limit,
             )
         if media_types is None or MediaType.AUDIOBOOK in media_types:
             result.audiobooks = await self.mass.music.audiobooks._get_library_items_by_query(
                 search=search_query,
-                provider=self.instance_id,
+                provider_filter=[self.instance_id],
                 limit=limit,
             )
         if media_types is None or MediaType.PODCAST in media_types:
             result.podcasts = await self.mass.music.podcasts._get_library_items_by_query(
                 search=search_query,
-                provider=self.instance_id,
+                provider_filter=[self.instance_id],
                 limit=limit,
             )
         return result
index 3a9bc17c5e7da556090d30448059bef8335733ba..dbbe659171c2fc4a2463f26474c2316aff9e2aa4 100644 (file)
@@ -551,7 +551,7 @@ class PlexProvider(MusicProvider):
 
     async def _get_or_create_artist_by_name(self, artist_name: str) -> Artist | ItemMapping:
         if library_items := await self.mass.music.artists._get_library_items_by_query(
-            search=artist_name, provider=self.lookup_key
+            search=artist_name, provider_filter=[self.lookup_key]
         ):
             return ItemMapping.from_item(library_items[0])