From fa01eddb7d6e85fde5b399d8d2477a02b459003a Mon Sep 17 00:00:00 2001 From: Fabian Munkes <105975993+fmunkes@users.noreply.github.com> Date: Wed, 21 May 2025 02:00:05 +0200 Subject: [PATCH] Chore: Separate BrowseFolder from MediaItemType (#2194) --- music_assistant/controllers/music.py | 19 ++++--- music_assistant/controllers/player_queues.py | 5 +- music_assistant/helpers/compare.py | 6 +-- music_assistant/models/music_provider.py | 26 +++++----- .../_template_music_provider/__init__.py | 5 +- .../providers/audiobookshelf/__init__.py | 50 ++++++++----------- .../providers/filesystem_local/__init__.py | 6 +-- .../providers/radiobrowser/__init__.py | 2 +- .../providers/siriusxm/__init__.py | 6 ++- music_assistant/providers/tidal/__init__.py | 4 +- 10 files changed, 60 insertions(+), 69 deletions(-) diff --git a/music_assistant/controllers/music.py b/music_assistant/controllers/music.py index 456d6863..7e8134e1 100644 --- a/music_assistant/controllers/music.py +++ b/music_assistant/controllers/music.py @@ -33,7 +33,6 @@ from music_assistant_models.media_items import ( BrowseFolder, ItemMapping, MediaItemType, - MediaItemTypeOrItemMapping, RecommendationFolder, SearchResults, ) @@ -434,11 +433,11 @@ class MusicController(CoreController): return result @api_command("music/browse") - async def browse(self, path: str | None = None) -> list[MediaItemType]: + async def browse(self, path: str | None = None) -> Sequence[MediaItemType | BrowseFolder]: """Browse Music providers.""" if not path or path == "root": # root level; folder per provider - root_items: list[MediaItemType] = [] + root_items: list[BrowseFolder] = [] for prov in self.providers: if ProviderFeature.BROWSE not in prov.supported_features: continue @@ -454,7 +453,7 @@ class MusicController(CoreController): return root_items # provider level - prepend_items: list[MediaItemType] = [] + prepend_items: list[BrowseFolder] = [] provider_instance, sub_path = path.split("://", 1) prov = self.mass.get_provider(provider_instance) # handle regular provider listing, always add back folder first @@ -542,7 +541,7 @@ class MusicController(CoreController): return result @api_command("music/item_by_uri") - async def get_item_by_uri(self, uri: str) -> MediaItemType: + async def get_item_by_uri(self, uri: str) -> MediaItemType | BrowseFolder: """Fetch MediaItem by uri.""" media_type, provider_instance_id_or_domain, item_id = await parse_uri(uri) return await self.get_item( @@ -574,7 +573,7 @@ class MusicController(CoreController): media_type: MediaType, item_id: str, provider_instance_id_or_domain: str, - ) -> MediaItemType: + ) -> MediaItemType | BrowseFolder: """Get single music item by id and media type.""" if provider_instance_id_or_domain == "database": # backwards compatibility - to remove when 2.0 stable is released @@ -615,7 +614,7 @@ class MusicController(CoreController): @api_command("music/favorites/add_item") async def add_item_to_favorites( self, - item: str | MediaItemTypeOrItemMapping, + item: str | MediaItemType | ItemMapping, ) -> None: """Add an item to the favorites.""" if isinstance(item, str): @@ -1286,10 +1285,10 @@ class MusicController(CoreController): def _sort_search_result( self, search_query: str, - items: Sequence[MediaItemTypeOrItemMapping], - ) -> UniqueList[MediaItemTypeOrItemMapping]: + items: Sequence[MediaItemType | ItemMapping], + ) -> UniqueList[MediaItemType | ItemMapping]: """Sort search results on priority/preference.""" - scored_items: list[tuple[int, MediaItemTypeOrItemMapping]] = [] + scored_items: list[tuple[int, MediaItemType | ItemMapping]] = [] # search results are already sorted by (streaming) providers on relevance # but we prefer exact name matches and library items so we simply put those # on top of the list. diff --git a/music_assistant/controllers/player_queues.py b/music_assistant/controllers/player_queues.py index 15846dd2..04a41655 100644 --- a/music_assistant/controllers/player_queues.py +++ b/music_assistant/controllers/player_queues.py @@ -46,7 +46,6 @@ from music_assistant_models.media_items import ( BrowseFolder, ItemMapping, MediaItemType, - MediaItemTypeOrItemMapping, PlayableMediaItemType, Playlist, PodcastEpisode, @@ -369,7 +368,7 @@ class PlayerQueuesController(CoreController): async def play_media( self, queue_id: str, - media: MediaItemTypeOrItemMapping | list[MediaItemTypeOrItemMapping] | str | list[str], + media: MediaItemType | ItemMapping | list[MediaItemType | ItemMapping] | str | list[str], option: QueueOption | None = None, radio_mode: bool = False, start_item: PlayableMediaItemType | str | None = None, @@ -1590,7 +1589,7 @@ class PlayerQueuesController(CoreController): ) async def _resolve_media_items( - self, media_item: MediaItemTypeOrItemMapping, start_item: str | None = None + self, media_item: MediaItemType | ItemMapping | BrowseFolder, start_item: str | None = None ) -> list[MediaItemType]: """Resolve/unwrap media items to enqueue.""" # resolve Itemmapping to full media item diff --git a/music_assistant/helpers/compare.py b/music_assistant/helpers/compare.py index 4f2e9150..cd4714c1 100644 --- a/music_assistant/helpers/compare.py +++ b/music_assistant/helpers/compare.py @@ -14,7 +14,7 @@ from music_assistant_models.media_items import ( ItemMapping, MediaItem, MediaItemMetadata, - MediaItemTypeOrItemMapping, + MediaItemType, Playlist, Podcast, Radio, @@ -30,8 +30,8 @@ IGNORE_VERSIONS = ( def compare_media_item( - base_item: MediaItemTypeOrItemMapping, - compare_item: MediaItemTypeOrItemMapping, + base_item: MediaItemType | ItemMapping, + compare_item: MediaItemType | ItemMapping, strict: bool = True, ) -> bool | None: """Compare two media items and return True if they match.""" diff --git a/music_assistant/models/music_provider.py b/music_assistant/models/music_provider.py index b2581b9b..b293c94b 100644 --- a/music_assistant/models/music_provider.py +++ b/music_assistant/models/music_provider.py @@ -17,8 +17,8 @@ from music_assistant_models.media_items import ( Artist, Audiobook, BrowseFolder, + ItemMapping, MediaItemType, - MediaItemTypeOrItemMapping, Playlist, Podcast, PodcastEpisode, @@ -412,7 +412,7 @@ class MusicProvider(Provider): return await self.get_podcast_episode(prov_item_id) return await self.get_track(prov_item_id) - async def browse(self, path: str) -> Sequence[MediaItemTypeOrItemMapping]: # noqa: PLR0911, PLR0915 + async def browse(self, path: str) -> Sequence[MediaItemType | ItemMapping | BrowseFolder]: # noqa: PLR0911, PLR0915 """Browse this provider's items. :param path: The path to browse, (e.g. provider_id://artists). @@ -529,9 +529,9 @@ class MusicProvider(Provider): raise KeyError(msg) # no subpath: return main listing - items: list[MediaItemType] = [] + folders: list[BrowseFolder] = [] if ProviderFeature.LIBRARY_ARTISTS in self.supported_features: - items.append( + folders.append( BrowseFolder( item_id="artists", provider=self.instance_id, @@ -542,7 +542,7 @@ class MusicProvider(Provider): ) ) if ProviderFeature.LIBRARY_ALBUMS in self.supported_features: - items.append( + folders.append( BrowseFolder( item_id="albums", provider=self.instance_id, @@ -553,7 +553,7 @@ class MusicProvider(Provider): ) ) if ProviderFeature.LIBRARY_TRACKS in self.supported_features: - items.append( + folders.append( BrowseFolder( item_id="tracks", provider=self.domain, @@ -564,7 +564,7 @@ class MusicProvider(Provider): ) ) if ProviderFeature.LIBRARY_PLAYLISTS in self.supported_features: - items.append( + folders.append( BrowseFolder( item_id="playlists", provider=self.instance_id, @@ -575,7 +575,7 @@ class MusicProvider(Provider): ) ) if ProviderFeature.LIBRARY_RADIOS in self.supported_features: - items.append( + folders.append( BrowseFolder( item_id="radios", provider=self.instance_id, @@ -585,7 +585,7 @@ class MusicProvider(Provider): ) ) if ProviderFeature.LIBRARY_AUDIOBOOKS in self.supported_features: - items.append( + folders.append( BrowseFolder( item_id="audiobooks", provider=self.instance_id, @@ -595,7 +595,7 @@ class MusicProvider(Provider): ) ) if ProviderFeature.LIBRARY_PODCASTS in self.supported_features: - items.append( + folders.append( BrowseFolder( item_id="podcasts", provider=self.instance_id, @@ -604,10 +604,10 @@ class MusicProvider(Provider): translation_key="podcasts", ) ) - if len(items) == 1: + if len(folders) == 1: # only one level, return the items directly - return await self.browse(items[0].path) - return items + return await self.browse(folders[0].path) + return folders async def recommendations(self) -> list[RecommendationFolder]: """ diff --git a/music_assistant/providers/_template_music_provider/__init__.py b/music_assistant/providers/_template_music_provider/__init__.py index adfbe52e..97686bb1 100644 --- a/music_assistant/providers/_template_music_provider/__init__.py +++ b/music_assistant/providers/_template_music_provider/__init__.py @@ -45,8 +45,9 @@ from music_assistant_models.media_items import ( Album, Artist, AudioFormat, + BrowseFolder, + ItemMapping, MediaItemType, - MediaItemTypeOrItemMapping, Playlist, ProviderMapping, Radio, @@ -490,7 +491,7 @@ class MyDemoMusicprovider(MusicProvider): # to false in a MediaItemImage object. return path - async def browse(self, path: str) -> Sequence[MediaItemTypeOrItemMapping]: + async def browse(self, path: str) -> Sequence[MediaItemType | ItemMapping | BrowseFolder]: """Browse this provider's items. :param path: The path to browse, (e.g. provider_id://artists). diff --git a/music_assistant/providers/audiobookshelf/__init__.py b/music_assistant/providers/audiobookshelf/__init__.py index 44642f01..97e21b2c 100644 --- a/music_assistant/providers/audiobookshelf/__init__.py +++ b/music_assistant/providers/audiobookshelf/__init__.py @@ -50,8 +50,8 @@ from music_assistant_models.media_items import ( Audiobook, AudioFormat, BrowseFolder, + ItemMapping, MediaItemType, - MediaItemTypeOrItemMapping, PodcastEpisode, UniqueList, ) @@ -580,7 +580,7 @@ class Audiobookshelf(MusicProvider): # have multiple libraries. Instead we collect per ShelfId, and make sure, that we always get # roughly the same amount of items per row, no matter the amount of libraries # List of list (one list per lib) here, such that we can pick the items per lib later. - items_by_shelf_id: dict[AbsShelfId, list[list[MediaItemType]]] = {} + items_by_shelf_id: dict[AbsShelfId, list[list[MediaItemType | BrowseFolder]]] = {} all_libraries = {**self.libraries.audiobooks, **self.libraries.podcasts} max_items_per_row = 20 @@ -630,7 +630,7 @@ class Audiobookshelf(MusicProvider): # If there is only a single audiobook library, we add the folders # from _browse_lib_audiobooks, i.e. Authors, Narrators etc. # Podcast libs do not have filter folders, so always the root folders. - browse_items: list[MediaItemTypeOrItemMapping] = [] + browse_items: list[MediaItemType | BrowseFolder] = [] if len(self.libraries.audiobooks) <= 1: browse_names = [ x.name for x in self.libraries.audiobooks.values() @@ -674,7 +674,7 @@ class Audiobookshelf(MusicProvider): self, shelves: list[ShelfBook | ShelfPodcast | ShelfAuthors | ShelfEpisode | ShelfSeries], library_id: str, - items_by_shelf_id: dict[AbsShelfId, list[list[MediaItemType]]], + items_by_shelf_id: dict[AbsShelfId, list[list[MediaItemType | BrowseFolder]]], ) -> None: for shelf in shelves: media_type: MediaType @@ -691,7 +691,7 @@ class Audiobookshelf(MusicProvider): # this would be authors, currently continue - items: list[MediaItemType] = [] + items: list[MediaItemType | BrowseFolder] = [] # Recently added is the _only_ case, where we get a full podcast # We have a podcast object with only the episodes matching the # shelf.id_ otherwise. @@ -866,7 +866,7 @@ class Audiobookshelf(MusicProvider): is_finished=fully_played, ) - async def browse(self, path: str) -> Sequence[MediaItemTypeOrItemMapping]: + async def browse(self, path: str) -> Sequence[MediaItemType | ItemMapping | BrowseFolder]: """Browse for audiobookshelf. Generates this view: @@ -941,9 +941,7 @@ class Audiobookshelf(MusicProvider): return await self._browse_series_books(series_id=series_id) return [] - def _browse_root( - self, append_mediatype_suffix: bool = True - ) -> Sequence[MediaItemTypeOrItemMapping]: + def _browse_root(self, append_mediatype_suffix: bool = True) -> Sequence[BrowseFolder]: items = [] def _get_folder(path: str, lib_id: str, lib_name: str) -> BrowseFolder: @@ -974,7 +972,7 @@ class Audiobookshelf(MusicProvider): items.append(_get_folder(path, lib_id, name)) return items - async def _browse_lib_podcasts(self, library_id: str) -> list[MediaItemTypeOrItemMapping]: + async def _browse_lib_podcasts(self, library_id: str) -> list[MediaItemType]: """No sub categories for podcasts.""" if len(self.libraries.podcasts[library_id].item_ids) == 0: self._log_no_helper_item_ids() @@ -989,7 +987,7 @@ class Audiobookshelf(MusicProvider): items.append(mass_item) return sorted(items, key=lambda x: x.name) - def _browse_lib_audiobooks(self, current_path: str) -> Sequence[MediaItemTypeOrItemMapping]: + def _browse_lib_audiobooks(self, current_path: str) -> Sequence[BrowseFolder]: items = [] for item_name in AbsBrowseItemsBook: path = current_path + "/" + ABS_BROWSE_ITEMS_TO_PATH[item_name] @@ -1003,9 +1001,7 @@ class Audiobookshelf(MusicProvider): ) return items - async def _browse_authors( - self, current_path: str, library_id: str - ) -> Sequence[MediaItemTypeOrItemMapping]: + async def _browse_authors(self, current_path: str, library_id: str) -> Sequence[BrowseFolder]: abs_authors = await self._client.get_library_authors(library_id=library_id) items = [] for author in abs_authors: @@ -1021,9 +1017,7 @@ class Audiobookshelf(MusicProvider): return sorted(items, key=lambda x: x.name) - async def _browse_narrators( - self, current_path: str, library_id: str - ) -> Sequence[MediaItemTypeOrItemMapping]: + async def _browse_narrators(self, current_path: str, library_id: str) -> Sequence[BrowseFolder]: abs_narrators = await self._client.get_library_narrators(library_id=library_id) items = [] for narrator in abs_narrators: @@ -1039,9 +1033,7 @@ class Audiobookshelf(MusicProvider): return sorted(items, key=lambda x: x.name) - async def _browse_series( - self, current_path: str, library_id: str - ) -> Sequence[MediaItemTypeOrItemMapping]: + async def _browse_series(self, current_path: str, library_id: str) -> Sequence[BrowseFolder]: items = [] async for response in self._client.get_library_series(library_id=library_id): if not response.results: @@ -1061,7 +1053,7 @@ class Audiobookshelf(MusicProvider): async def _browse_collections( self, current_path: str, library_id: str - ) -> Sequence[MediaItemTypeOrItemMapping]: + ) -> Sequence[BrowseFolder]: items = [] async for response in self._client.get_library_collections(library_id=library_id): if not response.results: @@ -1078,7 +1070,7 @@ class Audiobookshelf(MusicProvider): ) return sorted(items, key=lambda x: x.name) - async def _browse_books(self, library_id: str) -> Sequence[MediaItemTypeOrItemMapping]: + async def _browse_books(self, library_id: str) -> Sequence[MediaItemType]: if len(self.libraries.audiobooks[library_id].item_ids) == 0: self._log_no_helper_item_ids() items = [] @@ -1094,8 +1086,8 @@ class Audiobookshelf(MusicProvider): async def _browse_author_books( self, current_path: str, author_id: str - ) -> Sequence[MediaItemTypeOrItemMapping]: - items: list[MediaItemTypeOrItemMapping] = [] + ) -> Sequence[MediaItemType | BrowseFolder]: + items: list[MediaItemType | BrowseFolder] = [] abs_author = await self._client.get_author( author_id=author_id, include_items=True, include_series=True @@ -1131,8 +1123,8 @@ class Audiobookshelf(MusicProvider): async def _browse_narrator_books( self, library_id: str, narrator_filter_str: str - ) -> Sequence[MediaItemTypeOrItemMapping]: - items: list[MediaItemTypeOrItemMapping] = [] + ) -> Sequence[MediaItemType]: + items: list[MediaItemType] = [] async for response in self._client.get_library_items( library_id=library_id, filter_str=f"narrators.{narrator_filter_str}" ): @@ -1149,7 +1141,7 @@ class Audiobookshelf(MusicProvider): return sorted(items, key=lambda x: x.name) - async def _browse_series_books(self, series_id: str) -> Sequence[MediaItemTypeOrItemMapping]: + async def _browse_series_books(self, series_id: str) -> Sequence[MediaItemType]: items = [] abs_series = await self._client.get_series(series_id=series_id, include_progress=True) @@ -1168,9 +1160,7 @@ class Audiobookshelf(MusicProvider): return items - async def _browse_collection_books( - self, collection_id: str - ) -> Sequence[MediaItemTypeOrItemMapping]: + async def _browse_collection_books(self, collection_id: str) -> Sequence[MediaItemType]: items = [] abs_collection = await self._client.get_collection(collection_id=collection_id) for book in abs_collection.books: diff --git a/music_assistant/providers/filesystem_local/__init__.py b/music_assistant/providers/filesystem_local/__init__.py index 1595c26f..039263a4 100644 --- a/music_assistant/providers/filesystem_local/__init__.py +++ b/music_assistant/providers/filesystem_local/__init__.py @@ -35,7 +35,7 @@ from music_assistant_models.media_items import ( ItemMapping, MediaItemChapter, MediaItemImage, - MediaItemTypeOrItemMapping, + MediaItemType, Playlist, Podcast, PodcastEpisode, @@ -259,7 +259,7 @@ class LocalFileSystemProvider(MusicProvider): ) return result - async def browse(self, path: str) -> Sequence[MediaItemTypeOrItemMapping]: + async def browse(self, path: str) -> Sequence[MediaItemType | ItemMapping | BrowseFolder]: """Browse this provider's items. :param path: The path to browse, (e.g. provid://artists). @@ -269,7 +269,7 @@ class LocalFileSystemProvider(MusicProvider): return await self.mass.music.podcasts.library_items(provider=self.instance_id) if self.media_content_type == "audiobooks": return await self.mass.music.audiobooks.library_items(provider=self.instance_id) - items: list[MediaItemTypeOrItemMapping] = [] + items: list[MediaItemType | ItemMapping | BrowseFolder] = [] item_path = path.split("://", 1)[1] if not item_path: item_path = "" diff --git a/music_assistant/providers/radiobrowser/__init__.py b/music_assistant/providers/radiobrowser/__init__.py index 78e2e3ef..d95b6729 100644 --- a/music_assistant/providers/radiobrowser/__init__.py +++ b/music_assistant/providers/radiobrowser/__init__.py @@ -135,7 +135,7 @@ class RadioBrowserProvider(MusicProvider): return result - async def browse(self, path: str) -> Sequence[MediaItemType]: + async def browse(self, path: str) -> Sequence[MediaItemType | BrowseFolder]: """Browse this provider's items. :param path: The path to browse, (e.g. provid://artists). diff --git a/music_assistant/providers/siriusxm/__init__.py b/music_assistant/providers/siriusxm/__init__.py index a2d98a16..b172a484 100644 --- a/music_assistant/providers/siriusxm/__init__.py +++ b/music_assistant/providers/siriusxm/__init__.py @@ -18,9 +18,11 @@ from music_assistant_models.enums import ( from music_assistant_models.errors import LoginFailed, MediaNotFoundError from music_assistant_models.media_items import ( AudioFormat, + BrowseFolder, + ItemMapping, MediaItemImage, MediaItemLink, - MediaItemTypeOrItemMapping, + MediaItemType, ProviderMapping, Radio, ) @@ -242,7 +244,7 @@ class SiriusXMProvider(MusicProvider): return self._current_stream_details - async def browse(self, path: str) -> Sequence[MediaItemTypeOrItemMapping]: + async def browse(self, path: str) -> Sequence[MediaItemType | ItemMapping | BrowseFolder]: """Browse this provider's items. :param path: The path to browse, (e.g. provider_id://artists). diff --git a/music_assistant/providers/tidal/__init__.py b/music_assistant/providers/tidal/__init__.py index 70912b19..bb2e5de9 100644 --- a/music_assistant/providers/tidal/__init__.py +++ b/music_assistant/providers/tidal/__init__.py @@ -37,10 +37,10 @@ from music_assistant_models.media_items import ( Album, Artist, AudioFormat, + BrowseFolder, ItemMapping, MediaItemImage, MediaItemType, - MediaItemTypeOrItemMapping, Playlist, ProviderMapping, RecommendationFolder, @@ -1169,7 +1169,7 @@ class TidalProvider(MusicProvider): item_id=item_id, name=module_title, provider=self.lookup_key, - items=UniqueList[MediaItemTypeOrItemMapping](unique_items), + items=UniqueList[MediaItemType | ItemMapping | BrowseFolder](unique_items), subtitle=f"From {page_name} • {len(unique_items)} items", translation_key=item_id, icon=get_icon_for_type(content_type), -- 2.34.1