diff --git a/music_assistant/server/helpers/audio.py b/music_assistant/server/helpers/audio.py index bcb672318..f37e4d1d4 100644 --- a/music_assistant/server/helpers/audio.py +++ b/music_assistant/server/helpers/audio.py @@ -679,15 +679,21 @@ async def get_hls_substream( raw_data = await resp.read() encoding = resp.charset or await detect_charset(raw_data) master_m3u_data = raw_data.decode(encoding) - if not allow_encrypted and "EXT-X-KEY:METHOD=AES-128" in master_m3u_data: - # for now we don't support encrypted HLS streams + if not allow_encrypted and "EXT-X-KEY:METHOD=" in master_m3u_data: + # for now we do not (yet) support encrypted HLS streams raise InvalidDataError("HLS stream is encrypted, not supported") substreams = parse_m3u(master_m3u_data) - if any(x for x in substreams if x.length or x.key): - # this is already a substream! - return PlaylistItem( - path=url, - ) + # There is a chance that we did not get a master playlist with subplaylists + # but just a single master/sub playlist with the actual audio stream(s) + # so we need to detect if the playlist child's contain audio streams or + # sub-playlists. + if any( + x + for x in substreams + if (x.length or x.path.endswith((".mp4", ".aac"))) + and not x.path.endswith((".m3u", ".m3u8")) + ): + return PlaylistItem(path=url, key=substreams[0].key) # sort substreams on best quality (highest bandwidth) when available if any(x for x in substreams if x.stream_info): substreams.sort(key=lambda x: int(x.stream_info.get("BANDWIDTH", "0")), reverse=True) diff --git a/music_assistant/server/helpers/playlists.py b/music_assistant/server/helpers/playlists.py index d7e780ccb..2d15b2aed 100644 --- a/music_assistant/server/helpers/playlists.py +++ b/music_assistant/server/helpers/playlists.py @@ -145,7 +145,9 @@ def parse_pls(pls_data: str) -> list[PlaylistItem]: return playlist -async def fetch_playlist(mass: MusicAssistant, url: str) -> list[PlaylistItem]: +async def fetch_playlist( + mass: MusicAssistant, url: str, raise_on_hls: bool = True +) -> list[PlaylistItem]: """Parse an online m3u or pls playlist.""" try: async with mass.http_session.get(url, allow_redirects=True, timeout=5) as resp: @@ -164,7 +166,7 @@ async def fetch_playlist(mass: MusicAssistant, url: str) -> list[PlaylistItem]: msg = f"Error while fetching playlist {url}" raise InvalidDataError(msg) from err - if "#EXT-X-VERSION:" in playlist_data or "#EXT-X-STREAM-INF:" in playlist_data: + if raise_on_hls and "#EXT-X-VERSION:" in playlist_data or "#EXT-X-STREAM-INF:" in playlist_data: raise IsHLSPlaylist(encrypted="#EXT-X-KEY:" in playlist_data) if url.endswith((".m3u", ".m3u8")): diff --git a/music_assistant/server/providers/apple_music/__init__.py b/music_assistant/server/providers/apple_music/__init__.py index 79863152c..8ae48b40f 100644 --- a/music_assistant/server/providers/apple_music/__init__.py +++ b/music_assistant/server/providers/apple_music/__init__.py @@ -39,7 +39,7 @@ from music_assistant.common.models.streamdetails import StreamDetails from music_assistant.constants import CONF_PASSWORD from music_assistant.server.helpers.app_vars import app_var -from music_assistant.server.helpers.audio import get_hls_substream +from music_assistant.server.helpers.playlists import fetch_playlist from music_assistant.server.helpers.throttle_retry import ThrottlerManager, throttle_with_retries from music_assistant.server.models.music_provider import MusicProvider @@ -721,8 +721,14 @@ async def _parse_stream_url_and_uri(self, stream_assets: list[dict]) -> str: ctrp256_urls = [asset["URL"] for asset in stream_assets if asset["flavor"] == "28:ctrp256"] if len(ctrp256_urls) == 0: raise MediaNotFoundError("No ctrp256 URL found for song.") - playlist_item = await get_hls_substream(self.mass, ctrp256_urls[0]) - track_url = playlist_item.path + playlist_url = ctrp256_urls[0] + playlist_items = await fetch_playlist(self.mass, ctrp256_urls[0], raise_on_hls=False) + # Apple returns a HLS (substream) playlist but instead of chunks, + # each item is just the whole file. So we simply grab the first playlist item. + playlist_item = playlist_items[0] + # path is relative, stitch it together + base_path = playlist_url.rsplit("/", 1)[0] + track_url = base_path + "/" + playlist_items[0].path key = playlist_item.key return (track_url, key)