Chore: Separate BrowseFolder from MediaItemType (#2194)
authorFabian Munkes <105975993+fmunkes@users.noreply.github.com>
Wed, 21 May 2025 00:00:05 +0000 (02:00 +0200)
committerGitHub <noreply@github.com>
Wed, 21 May 2025 00:00:05 +0000 (02:00 +0200)
music_assistant/controllers/music.py
music_assistant/controllers/player_queues.py
music_assistant/helpers/compare.py
music_assistant/models/music_provider.py
music_assistant/providers/_template_music_provider/__init__.py
music_assistant/providers/audiobookshelf/__init__.py
music_assistant/providers/filesystem_local/__init__.py
music_assistant/providers/radiobrowser/__init__.py
music_assistant/providers/siriusxm/__init__.py
music_assistant/providers/tidal/__init__.py

index 456d6863f0c5d384759f7941a8f9d3eeea1cec49..7e8134e150a7806c19f5432b8753da9477a1cc68 100644 (file)
@@ -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.
index 15846dd297e7506e9491b7742d74b2dc94ea071b..04a41655a08c02052568e301d77efed1a69c2f05 100644 (file)
@@ -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
index 4f2e91502eec586744ec3f6af0aa9b2e88e702d1..cd4714c11a6b8b26438c28ae844dae7f3c59e1b9 100644 (file)
@@ -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."""
index b2581b9b6bcdcc8ae44f7b8bf69de029667ebd84..b293c94b1d34d8c80b329a537655c4c2cc0d68dd 100644 (file)
@@ -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]:
         """
index adfbe52e17fd7fd0393652590a2b72ad29771bbe..97686bb13c71a6200b07f2142af665f5457aeb57 100644 (file)
@@ -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).
index 44642f014ad534f6e1cc5e5846e13abad2c4c38d..97e21b2ce7ef0cce8ad6a56bbd0fe0e100c39446 100644 (file)
@@ -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:
index 1595c26f58eda4d0037b84abe0c013e2ce50484c..039263a4e9a3f98d7c8824a8f52219d640066fa1 100644 (file)
@@ -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 = ""
index 78e2e3eff2eb55f8caafa0920523ff012e802f72..d95b6729d6a1d2f63757ebcd43fbcabece577748 100644 (file)
@@ -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).
index a2d98a16798b43217bab0be89715638af3d7d2a6..b172a4841ff08c81c0b21cee70c4e89f1c737d58 100644 (file)
@@ -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).
index 70912b190b5ea8081b6775c9a02a0de59c1de4bf..bb2e5de9b58d2cac817051f994e1cf3e29bf24ae 100644 (file)
@@ -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),