Chore: some code cleanup and fixes for browsing
authorMarcel van der Veldt <m.vanderveldt@outlook.com>
Thu, 13 Feb 2025 16:11:23 +0000 (17:11 +0100)
committerMarcel van der Veldt <m.vanderveldt@outlook.com>
Thu, 13 Feb 2025 16:11:23 +0000 (17:11 +0100)
music_assistant/controllers/media/base.py
music_assistant/controllers/music.py
music_assistant/controllers/player_queues.py
music_assistant/helpers/api.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/siriusxm/__init__.py

index f9b0a5e3e4e941a9927c05c09c7be4ed7900bb2d..aef30608430c93829a379fbe0220df31a5005aad 100644 (file)
@@ -21,6 +21,7 @@ from music_assistant_models.media_items import (
     Album,
     ItemMapping,
     MediaItemType,
+    MediaItemTypeOrItemMapping,
     ProviderMapping,
     SearchResults,
     Track,
@@ -35,7 +36,7 @@ if TYPE_CHECKING:
 
     from music_assistant import MusicAssistant
 
-MediaItemTypeBound = MediaItemType | ItemMapping
+MediaItemTypeBound = MediaItemTypeOrItemMapping
 ItemCls = TypeVar("ItemCls", bound="MediaItemType")
 LibraryUpdate = TypeVar("LibraryUpdate", bound="MediaItemTypeBound")
 
index eb17600542e6788a3a6dd8f02e0018d3925d5342..50cef52851c415d288195c723236542ae7fd3f7a 100644 (file)
@@ -31,6 +31,7 @@ from music_assistant_models.media_items import (
     BrowseFolder,
     ItemMapping,
     MediaItemType,
+    MediaItemTypeOrItemMapping,
     SearchResults,
 )
 from music_assistant_models.provider import SyncTask
@@ -541,6 +542,13 @@ class MusicController(CoreController):
         if media_type == MediaType.PODCAST_EPISODE:
             # special case for podcast episodes
             return await self.podcasts.episode(item_id, provider_instance_id_or_domain)
+        if media_type == MediaType.FOLDER:
+            # special case for folders
+            return BrowseFolder(
+                item_id=item_id,
+                provider=provider_instance_id_or_domain,
+                name=item_id,
+            )
         ctrl = self.get_controller(media_type)
         return await ctrl.get(
             item_id=item_id,
@@ -563,7 +571,7 @@ class MusicController(CoreController):
     @api_command("music/favorites/add_item")
     async def add_item_to_favorites(
         self,
-        item: str | MediaItemType,
+        item: str | MediaItemTypeOrItemMapping,
     ) -> None:
         """Add an item to the favorites."""
         if isinstance(item, str):
@@ -794,7 +802,7 @@ class MusicController(CoreController):
     @api_command("music/mark_played")
     async def mark_item_played(
         self,
-        media_item: MediaItemType | ItemMapping,
+        media_item: MediaItemTypeOrItemMapping,
         fully_played: bool = True,
         seconds_played: int | None = None,
     ) -> None:
@@ -863,7 +871,7 @@ class MusicController(CoreController):
     @api_command("music/mark_unplayed")
     async def mark_item_unplayed(
         self,
-        media_item: MediaItemType | ItemMapping,
+        media_item: MediaItemTypeOrItemMapping,
     ) -> None:
         """Mark item as unplayed in playlog."""
         # update generic playlog table
index cc572a23755e0ceca90306972355b2573b4e20c5..6fed50e057e792ec75cf059a613cde709b98c03d 100644 (file)
@@ -41,7 +41,10 @@ from music_assistant_models.errors import (
     UnsupportedFeaturedException,
 )
 from music_assistant_models.media_items import (
+    BrowseFolder,
+    ItemMapping,
     MediaItemType,
+    MediaItemTypeOrItemMapping,
     PlayableMediaItemType,
     Playlist,
     PodcastEpisode,
@@ -1415,9 +1418,12 @@ class PlayerQueuesController(CoreController):
         )
 
     async def _resolve_media_items(
-        self, media_item: MediaItemType, start_item: str | None = None
+        self, media_item: MediaItemTypeOrItemMapping, start_item: str | None = None
     ) -> list[MediaItemType]:
         """Resolve/unwrap media items to enqueue."""
+        # resolve Itemmapping to full media item
+        if isinstance(media_item, ItemMapping):
+            media_item = await self.mass.music.get_item_by_uri(media_item.uri)
         if media_item.media_type == MediaType.PLAYLIST:
             self.mass.create_task(self.mass.music.mark_item_played(media_item))
             return await self.get_playlist_tracks(media_item, start_item)
@@ -1438,6 +1444,8 @@ class PlayerQueuesController(CoreController):
             return await self.get_next_podcast_episodes(media_item, start_item)
         if media_item.media_type == MediaType.PODCAST_EPISODE:
             return await self.get_next_podcast_episodes(None, media_item)
+        if media_item.media_type == MediaType.FOLDER:
+            return await self._get_folder_tracks(media_item)
         # all other: single track or radio item
         return [media_item]
 
@@ -1524,6 +1532,21 @@ class PlayerQueuesController(CoreController):
             )
         return queue_tracks
 
+    async def _get_folder_tracks(self, folder: BrowseFolder) -> list[Track]:
+        """Fetch (playable) tracks for given browse folder."""
+        self.logger.info(
+            "Fetching tracks to play for folder %s",
+            folder.name,
+        )
+        tracks: list[Track] = []
+        for item in await self.mass.music.browse(folder.path):
+            if not item.is_playable:
+                continue
+            # recursively fetch tracks from all media types
+            tracks += await self._resolve_media_items(item)
+
+        return tracks
+
     def _update_queue_from_player(
         self,
         player: Player,
index ddc7f0cd42072955231603c08067015ddbf6b20c..95bd75cf22c5fd47982b168a41ec1e67eb0f7803 100644 (file)
@@ -11,6 +11,8 @@ from enum import Enum
 from types import NoneType, UnionType
 from typing import Any, TypeVar, Union, get_args, get_origin, get_type_hints
 
+from mashumaro.exceptions import MissingField
+
 LOGGER = logging.getLogger(__name__)
 
 _F = TypeVar("_F", bound=Callable[..., Any])
@@ -88,7 +90,11 @@ def parse_utc_timestamp(datetime_string: str) -> datetime:
 def parse_value(name: str, value: Any, value_type: Any, default: Any = MISSING) -> Any:
     """Try to parse a value from raw (json) data and type annotations."""
     if isinstance(value, dict) and hasattr(value_type, "from_dict"):
-        if "media_type" in value and value["media_type"] != value_type.media_type:
+        if (
+            "media_type" in value
+            and value_type.__name__ != "ItemMapping"
+            and value["media_type"] != value_type.media_type
+        ):
             msg = "Invalid MediaType"
             raise ValueError(msg)
         return value_type.from_dict(value)
@@ -122,7 +128,7 @@ def parse_value(name: str, value: Any, value_type: Any, default: Any = MISSING)
             # try them all until one succeeds
             try:
                 return parse_value(name, value, sub_arg_type)
-            except (KeyError, TypeError, ValueError):
+            except (KeyError, TypeError, ValueError, MissingField):
                 pass
         # if we get to this point, all possibilities failed
         # find out if we should raise or log this
index 76ee93159518c0dcf7a288ed08b6923f23ad427e..b3e6d0f9cf58b137f44b37b434b5206fa262181e 100644 (file)
@@ -14,7 +14,7 @@ from music_assistant_models.media_items import (
     ItemMapping,
     MediaItem,
     MediaItemMetadata,
-    MediaItemType,
+    MediaItemTypeOrItemMapping,
     Playlist,
     Podcast,
     Radio,
@@ -30,8 +30,8 @@ IGNORE_VERSIONS = (
 
 
 def compare_media_item(
-    base_item: MediaItemType | ItemMapping,
-    compare_item: MediaItemType | ItemMapping,
+    base_item: MediaItemTypeOrItemMapping,
+    compare_item: MediaItemTypeOrItemMapping,
     strict: bool = True,
 ) -> bool | None:
     """Compare two media items and return True if they match."""
index 8fca1d1fcfba373d1b0c68218319d6063de7ce63..0d1eedc5d694141b7399f687a910e76e2dd2e4d5 100644 (file)
@@ -13,8 +13,8 @@ from music_assistant_models.media_items import (
     Artist,
     Audiobook,
     BrowseFolder,
-    ItemMapping,
     MediaItemType,
+    MediaItemTypeOrItemMapping,
     Playlist,
     Podcast,
     PodcastEpisode,
@@ -82,45 +82,38 @@ class MusicProvider(Provider):
 
     async def get_library_artists(self) -> AsyncGenerator[Artist, None]:
         """Retrieve library artists from the provider."""
-        if ProviderFeature.LIBRARY_ARTISTS in self.supported_features:
-            raise NotImplementedError
-        yield  # type: ignore
+        yield
+        raise NotImplementedError
 
     async def get_library_albums(self) -> AsyncGenerator[Album, None]:
         """Retrieve library albums from the provider."""
-        if ProviderFeature.LIBRARY_ALBUMS in self.supported_features:
-            raise NotImplementedError
-        yield  # type: ignore
+        yield
+        raise NotImplementedError
 
     async def get_library_tracks(self) -> AsyncGenerator[Track, None]:
         """Retrieve library tracks from the provider."""
-        if ProviderFeature.LIBRARY_TRACKS in self.supported_features:
-            raise NotImplementedError
-        yield  # type: ignore
+        yield
+        raise NotImplementedError
 
     async def get_library_playlists(self) -> AsyncGenerator[Playlist, None]:
         """Retrieve library/subscribed playlists from the provider."""
-        if ProviderFeature.LIBRARY_PLAYLISTS in self.supported_features:
-            raise NotImplementedError
-        yield  # type: ignore
+        yield
+        raise NotImplementedError
 
     async def get_library_radios(self) -> AsyncGenerator[Radio, None]:
         """Retrieve library/subscribed radio stations from the provider."""
-        if ProviderFeature.LIBRARY_RADIOS in self.supported_features:
-            raise NotImplementedError
-        yield  # type: ignore
+        yield
+        raise NotImplementedError
 
     async def get_library_audiobooks(self) -> AsyncGenerator[Audiobook, None]:
         """Retrieve library/subscribed audiobooks from the provider."""
-        if ProviderFeature.LIBRARY_AUDIOBOOKS in self.supported_features:
-            raise NotImplementedError
-        yield  # type: ignore
+        yield
+        raise NotImplementedError
 
     async def get_library_podcasts(self) -> AsyncGenerator[Podcast, None]:
         """Retrieve library/subscribed podcasts from the provider."""
-        if ProviderFeature.LIBRARY_AUDIOBOOKS in self.supported_features:
-            raise NotImplementedError
-        yield  # type: ignore
+        yield
+        raise NotImplementedError
 
     async def get_artist(self, prov_artist_id: str) -> Artist:
         """Get full artist details by id."""
@@ -404,7 +397,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[MediaItemType | ItemMapping]:  # noqa: PLR0915
+    async def browse(self, path: str) -> Sequence[MediaItemTypeOrItemMapping]:  # noqa: PLR0911, PLR0915
         """Browse this provider's items.
 
         :param path: The path to browse, (e.g. provider_id://artists).
@@ -416,14 +409,14 @@ class MusicProvider(Provider):
         subpath = path.split("://", 1)[1]
         # this reference implementation can be overridden with a provider specific approach
         if subpath == "artists":
-            library_items = await self.mass.cache.get(
+            library_item_ids = await self.mass.cache.get(
                 "artist",
                 category=CacheCategory.LIBRARY_ITEMS,
                 base_key=self.instance_id,
             )
-            if library_items is None:
-                return await self.mass.music.artists.library_items(provider=self.instance_id)
-            library_items = cast(list[int], library_items)
+            if not library_item_ids:
+                return [x async for x in self.get_library_artists()]
+            library_items = cast(list[int], library_item_ids)
             query = "artists.item_id in :ids"
             query_params = {"ids": library_items}
             return await self.mass.music.artists.library_items(
@@ -432,86 +425,86 @@ class MusicProvider(Provider):
                 extra_query_params=query_params,
             )
         if subpath == "albums":
-            library_items = await self.mass.cache.get(
+            library_item_ids = await self.mass.cache.get(
                 "album",
                 category=CacheCategory.LIBRARY_ITEMS,
                 base_key=self.instance_id,
             )
-            if library_items is None:
-                return await self.mass.music.albums.library_items(provider=self.instance_id)
-            library_items = cast(list[int], library_items)
+            if not library_item_ids:
+                return [x async for x in self.get_library_albums()]
+            library_item_ids = cast(list[int], library_item_ids)
             query = "albums.item_id in :ids"
-            query_params = {"ids": library_items}
+            query_params = {"ids": library_item_ids}
             return await self.mass.music.albums.library_items(
                 extra_query=query, extra_query_params=query_params
             )
         if subpath == "tracks":
-            library_items = await self.mass.cache.get(
+            library_item_ids = await self.mass.cache.get(
                 "track",
                 category=CacheCategory.LIBRARY_ITEMS,
                 base_key=self.instance_id,
             )
-            if library_items is None:
-                return await self.mass.music.tracks.library_items(provider=self.instance_id)
-            library_items = cast(list[int], library_items)
+            if not library_item_ids:
+                return [x async for x in self.get_library_tracks()]
+            library_item_ids = cast(list[int], library_item_ids)
             query = "tracks.item_id in :ids"
             query_params = {"ids": library_items}
             return await self.mass.music.tracks.library_items(
                 extra_query=query, extra_query_params=query_params
             )
         if subpath == "radios":
-            library_items = await self.mass.cache.get(
+            library_item_ids = await self.mass.cache.get(
                 "radio",
                 category=CacheCategory.LIBRARY_ITEMS,
                 base_key=self.instance_id,
             )
-            if library_items is None:
-                return await self.mass.music.radio.library_items(provider=self.instance_id)
-            library_items = cast(list[int], library_items)
+            if not library_item_ids:
+                return [x async for x in self.get_library_radios()]
+            library_item_ids = cast(list[int], library_item_ids)
             query = "radios.item_id in :ids"
-            query_params = {"ids": library_items}
+            query_params = {"ids": library_item_ids}
             return await self.mass.music.radio.library_items(
                 extra_query=query, extra_query_params=query_params
             )
         if subpath == "playlists":
-            library_items = await self.mass.cache.get(
+            library_item_ids = await self.mass.cache.get(
                 "playlist",
                 category=CacheCategory.LIBRARY_ITEMS,
                 base_key=self.instance_id,
             )
-            if library_items is None:
-                return await self.mass.music.playlists.library_items(provider=self.instance_id)
-            library_items = cast(list[int], library_items)
+            if not library_item_ids:
+                return [x async for x in self.get_library_playlists()]
+            library_item_ids = cast(list[int], library_item_ids)
             query = "playlists.item_id in :ids"
-            query_params = {"ids": library_items}
+            query_params = {"ids": library_item_ids}
             return await self.mass.music.playlists.library_items(
                 extra_query=query, extra_query_params=query_params
             )
         if subpath == "audiobooks":
-            library_items = await self.mass.cache.get(
+            library_item_ids = await self.mass.cache.get(
                 "audiobook",
                 category=CacheCategory.LIBRARY_ITEMS,
                 base_key=self.instance_id,
             )
-            if library_items is None:
-                return await self.mass.music.audiobooks.library_items(provider=self.instance_id)
-            library_items = cast(list[int], library_items)
+            if not library_item_ids:
+                return [x async for x in self.get_library_audiobooks()]
+            library_item_ids = cast(list[int], library_item_ids)
             query = "audiobooks.item_id in :ids"
-            query_params = {"ids": library_items}
+            query_params = {"ids": library_item_ids}
             return await self.mass.music.audiobooks.library_items(
                 extra_query=query, extra_query_params=query_params
             )
         if subpath == "podcasts":
-            library_items = await self.mass.cache.get(
+            library_item_ids = await self.mass.cache.get(
                 "podcast",
                 category=CacheCategory.LIBRARY_ITEMS,
                 base_key=self.instance_id,
             )
-            if library_items is None:
-                return await self.mass.music.podcasts.library_items(provider=self.instance_id)
-            library_items = cast(list[int], library_items)
+            if not library_item_ids:
+                return [x async for x in self.get_library_podcasts()]
+            library_item_ids = cast(list[int], library_item_ids)
             query = "podcasts.item_id in :ids"
-            query_params = {"ids": library_items}
+            query_params = {"ids": library_item_ids}
             return await self.mass.music.podcasts.library_items(
                 extra_query=query, extra_query_params=query_params
             )
@@ -526,20 +519,22 @@ class MusicProvider(Provider):
             items.append(
                 BrowseFolder(
                     item_id="artists",
-                    provider=self.domain,
+                    provider=self.instance_id,
                     path=path + "artists",
                     name="",
                     label="artists",
+                    is_playable=True,
                 )
             )
         if ProviderFeature.LIBRARY_ALBUMS in self.supported_features:
             items.append(
                 BrowseFolder(
                     item_id="albums",
-                    provider=self.domain,
+                    provider=self.instance_id,
                     path=path + "albums",
                     name="",
                     label="albums",
+                    is_playable=True,
                 )
             )
         if ProviderFeature.LIBRARY_TRACKS in self.supported_features:
@@ -550,23 +545,25 @@ class MusicProvider(Provider):
                     path=path + "tracks",
                     name="",
                     label="tracks",
+                    is_playable=True,
                 )
             )
         if ProviderFeature.LIBRARY_PLAYLISTS in self.supported_features:
             items.append(
                 BrowseFolder(
                     item_id="playlists",
-                    provider=self.domain,
+                    provider=self.instance_id,
                     path=path + "playlists",
                     name="",
                     label="playlists",
+                    is_playable=True,
                 )
             )
         if ProviderFeature.LIBRARY_RADIOS in self.supported_features:
             items.append(
                 BrowseFolder(
                     item_id="radios",
-                    provider=self.domain,
+                    provider=self.instance_id,
                     path=path + "radios",
                     name="",
                     label="radios",
@@ -576,7 +573,7 @@ class MusicProvider(Provider):
             items.append(
                 BrowseFolder(
                     item_id="audiobooks",
-                    provider=self.domain,
+                    provider=self.instance_id,
                     path=path + "audiobooks",
                     name="",
                     label="audiobooks",
@@ -586,12 +583,15 @@ class MusicProvider(Provider):
             items.append(
                 BrowseFolder(
                     item_id="podcasts",
-                    provider=self.domain,
+                    provider=self.instance_id,
                     path=path + "podcasts",
                     name="",
                     label="podcasts",
                 )
             )
+        if len(items) == 1:
+            # only one level, return the items directly
+            return await self.browse(items[0].path)
         return items
 
     async def recommendations(self) -> list[MediaItemType]:
index 9f6fdfc3d874b567b22ba9f6bad226147538fca1..7bb596e37cc6225866d1b9c51080cfb808663cea 100644 (file)
@@ -45,8 +45,8 @@ from music_assistant_models.media_items import (
     Album,
     Artist,
     AudioFormat,
-    ItemMapping,
     MediaItemType,
+    MediaItemTypeOrItemMapping,
     Playlist,
     ProviderMapping,
     Radio,
@@ -482,7 +482,7 @@ class MyDemoMusicprovider(MusicProvider):
         # to false in a MediaItemImage object.
         return path
 
-    async def browse(self, path: str) -> Sequence[MediaItemType | ItemMapping]:
+    async def browse(self, path: str) -> Sequence[MediaItemTypeOrItemMapping]:
         """Browse this provider's items.
 
         :param path: The path to browse, (e.g. provider_id://artists).
index 2c5791487a8e017f74ff663ca325981c0785d044..31cc4dc990ea993fbd9c663d993499580a4ca5aa 100644 (file)
@@ -26,7 +26,7 @@ from music_assistant_models.media_items import (
     ItemMapping,
     MediaItemChapter,
     MediaItemImage,
-    MediaItemType,
+    MediaItemTypeOrItemMapping,
     Podcast,
     PodcastEpisode,
     ProviderMapping,
@@ -563,13 +563,13 @@ class Audiobookshelf(MusicProvider):
 
     async def _browse_root(
         self, library_list: list[LibraryWithItemIDs], item_path: str
-    ) -> Sequence[MediaItemType | ItemMapping]:
+    ) -> Sequence[MediaItemTypeOrItemMapping]:
         """Browse root folder in browse view.
 
         Helper functions. Shows the library name, ABS supports multiple libraries
         of both podcasts and audiobooks.
         """
-        items: list[MediaItemType | ItemMapping] = []
+        items: list[MediaItemTypeOrItemMapping] = []
         for library in library_list:
             items.append(
                 BrowseFolder(
@@ -586,7 +586,7 @@ class Audiobookshelf(MusicProvider):
         library_id: str,
         library_list: list[LibraryWithItemIDs],
         media_type: MediaType,
-    ) -> Sequence[MediaItemType | ItemMapping]:
+    ) -> Sequence[MediaItemTypeOrItemMapping]:
         """Browse lib folder in browse view.
 
         Helper functions. Shows the items which are part of an ABS library.
@@ -598,7 +598,7 @@ class Audiobookshelf(MusicProvider):
         if library is None:
             raise MediaNotFoundError("Lib missing.")
 
-        items: list[MediaItemType | ItemMapping] = []
+        items: list[MediaItemTypeOrItemMapping] = []
         if media_type in [MediaType.PODCAST, MediaType.AUDIOBOOK]:
             for item_id in library.item_ids:
                 mass_item = await self.mass.music.get_library_item_by_prov_id(
@@ -612,7 +612,7 @@ class Audiobookshelf(MusicProvider):
             raise RuntimeError(f"Media type must not be {media_type}")
         return items
 
-    async def browse(self, path: str) -> Sequence[MediaItemType | ItemMapping]:
+    async def browse(self, path: str) -> Sequence[MediaItemTypeOrItemMapping]:
         """Browse features shows libraries names."""
         item_path = path.split("://", 1)[1]
         if not item_path:  # root
index 93920f874c634ceea641eaad1dfdbf05c223420e..e1f1c8bd3e8adccbb6bde140d9260fe938c12738 100644 (file)
@@ -35,7 +35,7 @@ from music_assistant_models.media_items import (
     ItemMapping,
     MediaItemChapter,
     MediaItemImage,
-    MediaItemType,
+    MediaItemTypeOrItemMapping,
     Playlist,
     Podcast,
     PodcastEpisode,
@@ -249,16 +249,17 @@ class LocalFileSystemProvider(MusicProvider):
             )
         return result
 
-    async def browse(self, path: str) -> Sequence[MediaItemType | ItemMapping]:
+    async def browse(self, path: str) -> Sequence[MediaItemTypeOrItemMapping]:
         """Browse this provider's items.
 
         :param path: The path to browse, (e.g. provid://artists).
         """
-        if self.media_content_type in ("audiobooks", "podcasts"):
-            # for audiobooks and podcasts just use the default implementation
-            # so we dont have to deal with multi-part audiobooks and podcast episodes
-            return await super().browse(path)
-        items: list[MediaItemType | ItemMapping] = []
+        # for audiobooks and podcasts we just return all library items
+        if self.media_content_type == "podcasts":
+            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] = []
         item_path = path.split("://", 1)[1]
         if not item_path:
             item_path = ""
@@ -275,6 +276,8 @@ class LocalFileSystemProvider(MusicProvider):
                         provider=self.instance_id,
                         path=f"{self.instance_id}://{item.relative_path}",
                         name=item.filename,
+                        # mark folder as playable, assuming it contains tracks underneath
+                        is_playable=True,
                     )
                 )
             elif item.ext in TRACK_EXTENSIONS:
index 0649cabadad9acc38ec8b37f6fa23bf7b172acb2..466eef2519aadfdb113a7dcd79da0252c3ca7099 100644 (file)
@@ -18,10 +18,9 @@ from music_assistant_models.enums import (
 from music_assistant_models.errors import LoginFailed, MediaNotFoundError
 from music_assistant_models.media_items import (
     AudioFormat,
-    ItemMapping,
     MediaItemImage,
     MediaItemLink,
-    MediaItemType,
+    MediaItemTypeOrItemMapping,
     ProviderMapping,
     Radio,
 )
@@ -243,7 +242,7 @@ class SiriusXMProvider(MusicProvider):
 
         return self._current_stream_details
 
-    async def browse(self, path: str) -> Sequence[MediaItemType | ItemMapping]:
+    async def browse(self, path: str) -> Sequence[MediaItemTypeOrItemMapping]:
         """Browse this provider's items.
 
         :param path: The path to browse, (e.g. provider_id://artists).