Several small bugfixes (#1336)
authorMarcel van der Veldt <m.vanderveldt@outlook.com>
Mon, 10 Jun 2024 00:23:14 +0000 (02:23 +0200)
committerGitHub <noreply@github.com>
Mon, 10 Jun 2024 00:23:14 +0000 (02:23 +0200)
* Fix enum in config entry

* Fix server-side paging for unknown length lists

Dont assume list length from limit

* Fix smb mount with spaces

* Fix paged_items for playlist tracks as well

* add some comments

* More fixes for paged listings

* fix tunein listing

* Update __init__.py

17 files changed:
music_assistant/common/models/config_entries.py
music_assistant/common/models/media_items.py
music_assistant/server/controllers/media/artists.py
music_assistant/server/controllers/media/playlists.py
music_assistant/server/controllers/music.py
music_assistant/server/controllers/player_queues.py
music_assistant/server/models/music_provider.py
music_assistant/server/providers/builtin/__init__.py
music_assistant/server/providers/deezer/__init__.py
music_assistant/server/providers/filesystem_local/base.py
music_assistant/server/providers/filesystem_smb/__init__.py
music_assistant/server/providers/jellyfin/__init__.py
music_assistant/server/providers/opensubsonic/sonic_provider.py
music_assistant/server/providers/plex/__init__.py
music_assistant/server/providers/radiobrowser/__init__.py
music_assistant/server/providers/soundcloud/__init__.py
music_assistant/server/providers/tunein/__init__.py

index 818ee838cc607445b4fed193d156288b98e5664c..c080f5e526ca7767f3b722652a6f77537b96f973 100644 (file)
@@ -5,6 +5,7 @@ from __future__ import annotations
 import logging
 from collections.abc import Iterable
 from dataclasses import dataclass
+from enum import Enum
 from types import NoneType
 from typing import Any
 
@@ -194,6 +195,9 @@ class Config(DataClassDictMixin):
         """Parse Config from the raw values (as stored in persistent storage)."""
         conf = cls.from_dict({**raw, "values": {}})
         for entry in config_entries:
+            # unpack Enum value in default_value
+            if isinstance(entry.default_value, Enum):
+                entry.default_value = entry.default_value.value
             # create a copy of the entry
             conf.values[entry.key] = ConfigEntry.from_dict(entry.to_dict())
             conf.values[entry.key].parse_value(
index 4fa8dd0a202153ff907cd12fc1392787953064b3..5af397d72ebf10119b31bf2e5ec695c277885929 100644 (file)
@@ -568,8 +568,8 @@ class PagedItems(Generic[_T]):
         items: list[_T],
         limit: int,
         offset: int,
+        total: int | None,
         count: int | None = None,
-        total: int | None = None,
     ):
         """Initialize PagedItems."""
         self.items = items
@@ -577,9 +577,6 @@ class PagedItems(Generic[_T]):
         self.limit = limit
         self.offset = offset
         self.total = total
-        if total is None and count != limit:
-            # total is important so always calculate it from count if omitted
-            self.total = offset + count
 
     def to_dict(self, *args, **kwargs) -> dict[str, Any]:
         """Return PagedItems as serializable dict."""
index a039618290790956c522e7c3c4808859157775a4..551d79337b1bbbf9058b2146e5d93d3a8e2372d0 100644 (file)
@@ -9,7 +9,11 @@ from typing import TYPE_CHECKING, Any
 
 from music_assistant.common.helpers.json import serialize_to_json
 from music_assistant.common.models.enums import ProviderFeature
-from music_assistant.common.models.errors import MediaNotFoundError, UnsupportedFeaturedException
+from music_assistant.common.models.errors import (
+    MediaNotFoundError,
+    ProviderUnavailableError,
+    UnsupportedFeaturedException,
+)
 from music_assistant.common.models.media_items import (
     Album,
     AlbumType,
@@ -441,9 +445,10 @@ class ArtistsController(MediaControllerBase[Artist]):
         if len(ref_tracks) < 10:
             # fetch reference tracks from provider(s) attached to the artist
             for provider_mapping in db_artist.provider_mappings:
-                ref_tracks += await self.mass.music.artists.tracks(
-                    provider_mapping.item_id, provider_mapping.provider_instance
-                )
+                with contextlib.suppress(ProviderUnavailableError, MediaNotFoundError):
+                    ref_tracks += await self.mass.music.artists.tracks(
+                        provider_mapping.item_id, provider_mapping.provider_instance
+                    )
         for ref_track in ref_tracks:
             for search_str in (
                 f"{db_artist.name} - {ref_track.name}",
@@ -476,9 +481,10 @@ class ArtistsController(MediaControllerBase[Artist]):
         if len(ref_albums) < 10:
             # fetch reference albums from provider(s) attached to the artist
             for provider_mapping in db_artist.provider_mappings:
-                ref_albums += await self.mass.music.artists.albums(
-                    provider_mapping.item_id, provider_mapping.provider_instance
-                )
+                with contextlib.suppress(ProviderUnavailableError, MediaNotFoundError):
+                    ref_albums += await self.mass.music.artists.albums(
+                        provider_mapping.item_id, provider_mapping.provider_instance
+                    )
         for ref_album in ref_albums:
             if ref_album.album_type == AlbumType.COMPILATION:
                 continue
index b4d3261d1a23a7d90bdd6b9839f3b94898faa20f..728d4a5522309de02e1a74144eda2f2a32da4503 100644 (file)
@@ -58,11 +58,12 @@ class PlaylistController(MediaControllerBase[Playlist]):
             force_refresh=force_refresh,
             lazy=not force_refresh,
         )
-        prov = next(x for x in playlist.provider_mappings)
+        prov_map = next(x for x in playlist.provider_mappings)
+        cache_checksum = playlist.metadata.cache_checksum
         tracks = await self._get_provider_playlist_tracks(
-            prov.item_id,
-            prov.provider_instance,
-            cache_checksum=playlist.metadata.cache_checksum,
+            prov_map.item_id,
+            prov_map.provider_instance,
+            cache_checksum=cache_checksum,
             offset=offset,
             limit=limit,
         )
@@ -84,7 +85,9 @@ class PlaylistController(MediaControllerBase[Playlist]):
                     final_tracks.append(track)
         else:
             final_tracks = tracks
-        return PagedItems(items=final_tracks, limit=limit, offset=offset)
+        # we set total to None as we have no idea how many tracks there are
+        # the frontend can figure this out and stop paging when it gets an empty list
+        return PagedItems(items=final_tracks, limit=limit, offset=offset, total=None)
 
     async def create_playlist(
         self, name: str, provider_instance_or_domain: str | None = None
@@ -290,7 +293,7 @@ class PlaylistController(MediaControllerBase[Playlist]):
             playlist.name,
         )
         while True:
-            paged_items = await self.mass.music.playlists.tracks(
+            paged_items = await self.tracks(
                 item_id=playlist.item_id,
                 provider_instance_id_or_domain=playlist.provider,
                 offset=offset,
@@ -298,7 +301,9 @@ class PlaylistController(MediaControllerBase[Playlist]):
                 prefer_library_items=prefer_library_items,
             )
             result += paged_items.items
-            if paged_items.count != limit:
+            if paged_items.total is not None and len(result) >= paged_items.total:
+                break
+            if paged_items.count == 0:
                 break
             offset += paged_items.count
         return result
index a236c9d151a277b03041c8522fa820f92d4480d8..41b0acf4b9dc7262dcf457e80dc3aac5f9ed363b 100644 (file)
@@ -335,7 +335,7 @@ class MusicController(CoreController):
                         name=prov.name,
                     )
                 )
-            return PagedItems(items=root_items, limit=limit, offset=offset)
+            return PagedItems(items=root_items, limit=limit, offset=offset, total=len(root_items))
 
         # provider level
         prepend_items: list[MediaItemType] = []
@@ -347,7 +347,9 @@ class MusicController(CoreController):
                 BrowseFolder(item_id="root", provider="library", path="root", name="..")
             )
             if not prov:
-                return PagedItems(items=prepend_items, limit=limit, offset=offset)
+                return PagedItems(
+                    items=prepend_items, limit=limit, offset=offset, total=len(prepend_items)
+                )
         elif offset == 0:
             back_path = f"{provider_instance}://" + "/".join(sub_path.split("/")[:-1])
             prepend_items.append(
@@ -356,6 +358,8 @@ class MusicController(CoreController):
         # limit -1 to account for the prepended items
         prov_items = await prov.browse(path, offset=offset, limit=limit)
         prov_items.items = prepend_items + prov_items.items
+        if prov_items.total is not None:
+            prov_items.total += len(prepend_items)
         return prov_items
 
     @api_command("music/recently_played_items")
index c93323c8f9dabaa14129950c26955395b65245f6..aa83238490be4fd1473b3372049735e4f1e92031 100644 (file)
@@ -209,7 +209,7 @@ class PlayerQueuesController(CoreController):
     def items(self, queue_id: str, limit: int = 500, offset: int = 0) -> PagedItems[QueueItem]:
         """Return all QueueItems for given PlayerQueue."""
         if queue_id not in self._queue_items:
-            return PagedItems(items=[], limit=limit, offset=offset)
+            return PagedItems(items=[], limit=limit, offset=offset, total=0)
 
         return PagedItems(
             items=self._queue_items[queue_id][offset : offset + limit],
index 09c3e395a3d377d4aa739c3099803d86b8445277..33d99bdd2ae9ae1c6e2c26a6e426027eab2722f9 100644 (file)
@@ -320,6 +320,8 @@ class MusicProvider(Provider):
                 items.append(item)
                 if len(items) >= limit:
                     break
+            # explicitly set total to None as we don't know the total count
+            total = None
         else:
             # no subpath: return main listing
             if ProviderFeature.LIBRARY_ARTISTS in self.supported_features:
@@ -372,7 +374,8 @@ class MusicProvider(Provider):
                         label="radios",
                     )
                 )
-        return PagedItems(items=items, limit=limit, offset=offset)
+            total = len(items)
+        return PagedItems(items=items, limit=limit, offset=offset, total=total)
 
     async def recommendations(self) -> list[MediaItemType]:
         """Get this provider's recommendations.
index 13f719dd5eb3e596a5db5813dcd66a3d79bcda67..5c0d6fa21f97d68d5c238c3c47710db82a9ca6f9 100644 (file)
@@ -352,7 +352,8 @@ class BuiltinProvider(MusicProvider):
     ) -> list[Track]:
         """Get playlist tracks."""
         if prov_playlist_id in BUILTIN_PLAYLISTS:
-            return await self._get_builtin_playlist_tracks(prov_playlist_id)
+            result = await self._get_builtin_playlist_tracks(prov_playlist_id)
+            return result[offset : offset + limit]
         # user created universal playlist
         result: list[Track] = []
         playlist_items = await self._read_playlist_file_items(prov_playlist_id, offset, limit)
index 153770952bc4a5309126533e3ef0a494f4c6751a..0e87992fdf1c643567f52fe195f3db4d804b1ce6 100644 (file)
@@ -327,7 +327,7 @@ class DeezerProvider(MusicProvider):  # pylint: disable=W0223
         result: list[Track] = []
         # TODO: implement pagination!
         playlist = await self.client.get_playlist(int(prov_playlist_id))
-        for index, deezer_track in enumerate(await playlist.get_tracks()):
+        for index, deezer_track in enumerate(await playlist.get_tracks(offset=offset, limit=limit)):
             result.append(
                 self.parse_track(
                     track=deezer_track,
index e0b498945fe96e094780b0882f02cdbdc30976ee..7e563e43a7e46a37003b1f3e553c388d05d3e4fd 100644 (file)
@@ -336,7 +336,8 @@ class FileSystemProviderBase(MusicProvider):
             index += 1
             if len(items) >= limit:
                 break
-        return PagedItems(items=items, limit=limit, offset=offset)
+        total = len(items) if len(items) < limit else None
+        return PagedItems(items=items, limit=limit, offset=offset, total=total)
 
     async def sync_library(self, media_types: tuple[MediaType, ...]) -> None:
         """Run library sync for this provider."""
index 268beecab525e398918b5e109e524373c14d3ca8..da94ee9eaffcebb5ebf58fd951d028977af4db49 100644 (file)
@@ -186,7 +186,7 @@ class SMBFileSystemProvider(LocalFileSystemProvider):
 
         if platform.system() == "Darwin":
             password_str = f":{password}" if password else ""
-            mount_cmd = f"mount -t smbfs //{username}{password_str}@{server}/{share}{subfolder} {self.base_path}"  # noqa: E501
+            mount_cmd = f'mount -t smbfs "//{username}{password_str}@{server}/{share}{subfolder}" "{self.base_path}"'  # noqa: E501
 
         elif platform.system() == "Linux":
             options = [
@@ -197,7 +197,12 @@ class SMBFileSystemProvider(LocalFileSystemProvider):
                 options.append(f'password="{password}"')
             if mount_options := self.config.get_value(CONF_MOUNT_OPTIONS):
                 options += mount_options.split(",")
-            mount_cmd = f"mount -t cifs -o {','.join(options)} //{server}/{share}{subfolder} {self.base_path}"  # noqa: E501
+
+            options_str = ",".join(options)
+            mount_cmd = (
+                f"mount -t cifs -o {options_str} "
+                f'"//{server}/{share}{subfolder}" "{self.base_path}"'
+            )
 
         else:
             msg = f"SMB provider is not supported on {platform.system()}"
index e67f58eca530e74ac04ce30c718893e16f643652..9004bce42c5f1ca24989ff71e4126941bb9a059b 100644 (file)
@@ -691,7 +691,7 @@ class JellyfinProvider(MusicProvider):
         )\r
         if not playlist_items:\r
             return result\r
-        for index, jellyfin_track in enumerate(playlist_items):\r
+        for index, jellyfin_track in enumerate(playlist_items[offset : offset + limit]):\r
             try:\r
                 if track := await self._parse_track(jellyfin_track):\r
                     if not track.position:\r
index d77394e0648b64f4efb3dd37ba3dc255ee77fed0..0c35318e1791842f6599f3311bdc4dd19096865d 100644 (file)
@@ -665,7 +665,7 @@ class OpenSonicProvider(MusicProvider):
         except (ParameterError, DataNotFoundError) as e:
             msg = f"Playlist {prov_playlist_id} not found"
             raise MediaNotFoundError(msg) from e
-        for index, sonic_song in enumerate(sonic_playlist.songs):
+        for index, sonic_song in enumerate(sonic_playlist.songs[offset : offset + limit]):
             track = self._parse_track(sonic_song)
             track.position = index
             result.append(track)
index 32f5fc8b3d3716189b818fddd19b19074008b60b..3ef3a30400e18c774846b2ccbd62b6f7cd40674c 100644 (file)
@@ -814,7 +814,7 @@ class PlexProvider(MusicProvider):
         plex_playlist: PlexPlaylist = await self._get_data(prov_playlist_id, PlexPlaylist)
         if not (playlist_items := await self._run_async(plex_playlist.items)):
             return result
-        for index, plex_track in enumerate(playlist_items):
+        for index, plex_track in enumerate(playlist_items[offset : offset + limit]):
             if track := await self._parse_track(plex_track):
                 track.position = index
                 result.append(track)
index a4f5d9dad9f95ff40bf907533712ae8771ce886b..eb8e6c07bd9de70c1e01609933225a596290221b 100644 (file)
@@ -138,12 +138,13 @@ class RadioBrowserProvider(MusicProvider):
             ]
 
         if subpath == "popular":
-            items = await self.get_by_popularity()
+            items = await self.get_by_popularity(limit=limit, offset=offset)
 
         if subpath == "tag":
             tags = await self.radios.tags(
                 hide_broken=True,
-                limit=100,
+                limit=limit,
+                offset=offset,
                 order=Order.STATION_COUNT,
                 reverse=True,
             )
@@ -159,7 +160,9 @@ class RadioBrowserProvider(MusicProvider):
             ]
 
         if subpath == "country":
-            for country in await self.radios.countries(order=Order.NAME):
+            for country in await self.radios.countries(
+                order=Order.NAME, hide_broken=True, limit=limit, offset=offset
+            ):
                 folder = BrowseFolder(
                     item_id=country.code.lower(),
                     provider=self.domain,
@@ -176,18 +179,20 @@ class RadioBrowserProvider(MusicProvider):
                 ]
                 items.append(folder)
 
-        if subsubpath in await self.get_tag_names():
+        if subsubpath in await self.get_tag_names(limit=limit, offset=offset):
             items = await self.get_by_tag(subsubpath)
 
-        if subsubpath in await self.get_country_codes():
+        if subsubpath in await self.get_country_codes(limit=limit, offset=offset):
             items = await self.get_by_country(subsubpath)
-        return PagedItems(items=items, limit=limit, offset=offset)
+        total = len(items) if len(items) < limit else None
+        return PagedItems(items=items, limit=limit, offset=offset, total=total)
 
-    async def get_tag_names(self):
+    async def get_tag_names(self, limit: int, offset: int):
         """Get a list of tag names."""
         tags = await self.radios.tags(
             hide_broken=True,
-            limit=100,
+            limit=limit,
+            offset=offset,
             order=Order.STATION_COUNT,
             reverse=True,
         )
@@ -197,19 +202,22 @@ class RadioBrowserProvider(MusicProvider):
             tag_names.append(tag.name.lower())
         return tag_names
 
-    async def get_country_codes(self):
+    async def get_country_codes(self, limit: int, offset: int):
         """Get a list of country names."""
-        countries = await self.radios.countries(order=Order.NAME)
+        countries = await self.radios.countries(
+            order=Order.NAME, hide_broken=True, limit=limit, offset=offset
+        )
         country_codes = []
         for country in countries:
             country_codes.append(country.code.lower())
         return country_codes
 
-    async def get_by_popularity(self):
+    async def get_by_popularity(self, limit: int, offset: int):
         """Get radio stations by popularity."""
         stations = await self.radios.stations(
             hide_broken=True,
-            limit=250,
+            limit=limit,
+            offset=offset,
             order=Order.CLICK_COUNT,
             reverse=True,
         )
index cb94eca32cf0416f01531292bdb5a1a4a3339a6d..7148fc47fabb0f3373fe1a399b603c59a9bf9cfc 100644 (file)
@@ -264,7 +264,7 @@ class SoundcloudMusicProvider(MusicProvider):
         playlist_obj = await self._soundcloud.get_playlist_details(playlist_id=prov_playlist_id)
         if "tracks" not in playlist_obj:
             return result
-        for index, item in enumerate(playlist_obj["tracks"]):
+        for index, item in enumerate(playlist_obj["tracks"][offset : offset + limit]):
             song = await self._soundcloud.get_track_details(item["id"])
             try:
                 # TODO: is it really needed to grab the entire track with an api call ?
index a489de9f2e0a5a33625c15825d15d87813a49108..0090073bc5a278c6590e36870feb2f358341e131 100644 (file)
@@ -20,7 +20,6 @@ from music_assistant.common.models.media_items import (
 )
 from music_assistant.common.models.streamdetails import StreamDetails
 from music_assistant.constants import CONF_USERNAME
-from music_assistant.server.helpers.tags import parse_tags
 from music_assistant.server.models.music_provider import MusicProvider
 
 SUPPORTED_FEATURES = (
@@ -109,13 +108,12 @@ class TuneInProvider(MusicProvider):
                     if "preset_id" not in item:
                         continue
                     # each radio station can have multiple streams add each one as different quality
-                    stream_info = await self.__get_data("Tune.ashx", id=item["preset_id"])
-                    for stream in stream_info["body"]:
-                        yield await self._parse_radio(item, stream, folder)
+                    stream_info = await self._get_stream_info(item["preset_id"])
+                    yield self._parse_radio(item, stream_info, folder)
                 elif item_type == "link" and item.get("item") == "url":
                     # custom url
                     try:
-                        yield await self._parse_radio(item)
+                        yield self._parse_radio(item)
                     except InvalidDataError as err:
                         # there may be invalid custom urls, ignore those
                         self.logger.warning(str(err))
@@ -137,16 +135,19 @@ class TuneInProvider(MusicProvider):
     async def get_radio(self, prov_radio_id: str) -> Radio:
         """Get radio station details."""
         if not prov_radio_id.startswith("http"):
-            prov_radio_id, media_type = prov_radio_id.split("--", 1)
+            if "--" in prov_radio_id:
+                prov_radio_id, media_type = prov_radio_id.split("--", 1)
+            else:
+                media_type = None
             params = {"c": "composite", "detail": "listing", "id": prov_radio_id}
             result = await self.__get_data("Describe.ashx", **params)
             if result and result.get("body") and result["body"][0].get("children"):
                 item = result["body"][0]["children"][0]
-                stream_info = await self.__get_data("Tune.ashx", id=prov_radio_id)
-                for stream in stream_info["body"]:
-                    if stream["media_type"] != media_type:
+                stream_info = await self._get_stream_info(prov_radio_id)
+                for stream in stream_info:
+                    if media_type and stream["media_type"] != media_type:
                         continue
-                    return await self._parse_radio(item, stream)
+                    return self._parse_radio(item, [stream])
         # fallback - e.g. for handle custom urls ...
         async for radio in self.get_library_radios():
             if radio.item_id == prov_radio_id:
@@ -154,8 +155,8 @@ class TuneInProvider(MusicProvider):
         msg = f"Item {prov_radio_id} not found"
         raise MediaNotFoundError(msg)
 
-    async def _parse_radio(
-        self, details: dict, stream: dict | None = None, folder: str | None = None
+    def _parse_radio(
+        self, details: dict, stream_info: list[dict] | None = None, folder: str | None = None
     ) -> Radio:
         """Parse Radio object from json obj returned from api."""
         if "name" in details:
@@ -167,37 +168,47 @@ class TuneInProvider(MusicProvider):
                 name = name.split(" | ")[1]
             name = name.split(" (")[0]
 
-        if stream is None:
-            # custom url (no stream object present)
-            url = details["URL"]
-            item_id = url
-            media_info = await parse_tags(url)
-            content_type = ContentType.try_parse(media_info.format)
-            bit_rate = media_info.bit_rate
+        if stream_info is not None:
+            # stream info is provided: parse stream objects into provider mappings
+            radio = Radio(
+                item_id=details["preset_id"],
+                provider=self.lookup_key,
+                name=name,
+                provider_mappings={
+                    ProviderMapping(
+                        item_id=f'{details["preset_id"]}--{stream["media_type"]}',
+                        provider_domain=self.domain,
+                        provider_instance=self.instance_id,
+                        audio_format=AudioFormat(
+                            content_type=ContentType.try_parse(stream["media_type"]),
+                            bit_rate=stream.get("bitrate", 128),
+                        ),
+                        details=stream["url"],
+                        available=details.get("is_available", True),
+                    )
+                    for stream in stream_info
+                },
+            )
         else:
-            url = stream["url"]
-            item_id = f'{details["preset_id"]}--{stream["media_type"]}'
-            content_type = ContentType.try_parse(stream["media_type"])
-            bit_rate = stream.get("bitrate", 128)  # TODO !
+            # custom url (no stream object present)
+            radio = Radio(
+                item_id=details["URL"],
+                provider=self.lookup_key,
+                name=name,
+                provider_mappings={
+                    ProviderMapping(
+                        item_id=details["URL"],
+                        provider_domain=self.domain,
+                        provider_instance=self.instance_id,
+                        audio_format=AudioFormat(
+                            content_type=ContentType.UNKNOWN,
+                        ),
+                        details=details["URL"],
+                        available=details.get("is_available", True),
+                    )
+                },
+            )
 
-        radio = Radio(
-            item_id=item_id,
-            provider=self.domain,
-            name=name,
-            provider_mappings={
-                ProviderMapping(
-                    item_id=item_id,
-                    provider_domain=self.domain,
-                    provider_instance=self.instance_id,
-                    audio_format=AudioFormat(
-                        content_type=content_type,
-                        bit_rate=bit_rate,
-                    ),
-                    details=url,
-                    available=details.get("is_available", True),
-                )
-            },
-        )
         # preset number is used for sorting (not present at stream time)
         preset_number = details.get("preset_number", 0)
         radio.position = preset_number
@@ -215,6 +226,15 @@ class TuneInProvider(MusicProvider):
             ]
         return radio
 
+    async def _get_stream_info(self, preset_id: str) -> list[dict]:
+        """Get stream info for a radio station."""
+        cache_key = f"tunein_stream_{preset_id}"
+        if cache := await self.mass.cache.get(cache_key):
+            return cache
+        result = (await self.__get_data("Tune.ashx", id=preset_id))["body"]
+        await self.mass.cache.set(cache_key, result)
+        return result
+
     async def get_stream_details(self, item_id: str) -> StreamDetails:
         """Get streamdetails for a radio station."""
         if item_id.startswith("http"):
@@ -230,10 +250,13 @@ class TuneInProvider(MusicProvider):
                 path=item_id,
                 can_seek=False,
             )
-        stream_item_id, media_type = item_id.split("--", 1)
-        stream_info = await self.__get_data("Tune.ashx", id=stream_item_id)
-        for stream in stream_info["body"]:
-            if stream["media_type"] != media_type:
+        if "--" in item_id:
+            stream_item_id, media_type = item_id.split("--", 1)
+        else:
+            media_type = None
+            stream_item_id = item_id
+        for stream in await self._get_stream_info(stream_item_id):
+            if media_type and stream["media_type"] != media_type:
                 continue
             return StreamDetails(
                 provider=self.domain,