From 77bee8dd3f64164ba78e37d4294219ce5b8461c1 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Sat, 19 Oct 2024 20:51:07 +0200 Subject: [PATCH] Fix parsing of HLS (sub)streams (#1727) --- music_assistant/server/helpers/audio.py | 20 ++++++++++++------- music_assistant/server/helpers/playlists.py | 6 ++++-- .../server/providers/apple_music/__init__.py | 12 ++++++++--- 3 files changed, 26 insertions(+), 12 deletions(-) diff --git a/music_assistant/server/helpers/audio.py b/music_assistant/server/helpers/audio.py index bcb67231..f37e4d1d 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 d7e780cc..2d15b2ae 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 79863152..8ae48b40 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.media_items import ( 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 @@ class AppleMusicProvider(MusicProvider): 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) -- 2.34.1