From: Marcel van der Veldt Date: Thu, 4 Dec 2025 17:40:08 +0000 (+0100) Subject: Fix provider filtering X-Git-Url: https://git.kitaultman.com/?a=commitdiff_plain;h=f6c5382d006286ed4c2dda9d6fa4c83398446231;p=music-assistant-server.git Fix provider filtering --- diff --git a/music_assistant/controllers/media/albums.py b/music_assistant/controllers/media/albums.py index a773fd4b..205e2b36 100644 --- a/music_assistant/controllers/media/albums.py +++ b/music_assistant/controllers/media/albums.py @@ -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, diff --git a/music_assistant/controllers/media/artists.py b/music_assistant/controllers/media/artists.py index 53c68df0..9a362101 100644 --- a/music_assistant/controllers/media/artists.py +++ b/music_assistant/controllers/media/artists.py @@ -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 [] diff --git a/music_assistant/controllers/media/audiobooks.py b/music_assistant/controllers/media/audiobooks.py index 2ff44baf..6ef0bb09 100644 --- a/music_assistant/controllers/media/audiobooks.py +++ b/music_assistant/controllers/media/audiobooks.py @@ -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, ) diff --git a/music_assistant/controllers/media/base.py b/music_assistant/controllers/media/base.py index ef6c1ef2..4adaeee3 100644 --- a/music_assistant/controllers/media/base.py +++ b/music_assistant/controllers/media/base.py @@ -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 diff --git a/music_assistant/controllers/media/playlists.py b/music_assistant/controllers/media/playlists.py index 541e5c43..592a965e 100644 --- a/music_assistant/controllers/media/playlists.py +++ b/music_assistant/controllers/media/playlists.py @@ -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) diff --git a/music_assistant/controllers/media/podcasts.py b/music_assistant/controllers/media/podcasts.py index 46a5d783..59183eb8 100644 --- a/music_assistant/controllers/media/podcasts.py +++ b/music_assistant/controllers/media/podcasts.py @@ -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, ) diff --git a/music_assistant/controllers/media/tracks.py b/music_assistant/controllers/media/tracks.py index e1c953a3..1872e8a3 100644 --- a/music_assistant/controllers/media/tracks.py +++ b/music_assistant/controllers/media/tracks.py @@ -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, diff --git a/music_assistant/providers/filesystem_local/__init__.py b/music_assistant/providers/filesystem_local/__init__.py index 269276e9..659a0729 100644 --- a/music_assistant/providers/filesystem_local/__init__.py +++ b/music_assistant/providers/filesystem_local/__init__.py @@ -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 diff --git a/music_assistant/providers/plex/__init__.py b/music_assistant/providers/plex/__init__.py index 3a9bc17c..dbbe6591 100644 --- a/music_assistant/providers/plex/__init__.py +++ b/music_assistant/providers/plex/__init__.py @@ -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])