Fix parsing of HLS (sub)streams (#1727)
authorMarcel van der Veldt <m.vanderveldt@outlook.com>
Sat, 19 Oct 2024 18:51:07 +0000 (20:51 +0200)
committerGitHub <noreply@github.com>
Sat, 19 Oct 2024 18:51:07 +0000 (20:51 +0200)
music_assistant/server/helpers/audio.py
music_assistant/server/helpers/playlists.py
music_assistant/server/providers/apple_music/__init__.py

index bcb67231853a058d4a52345f61b8d68b7c665c6c..f37e4d1d4e02e44d30edd2416460af8b609c502b 100644 (file)
@@ -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)
index d7e780ccbe1409ffae9472b68fff0906a74d108d..2d15b2aed9c800142dafd9e3f4ce5d67642c57f1 100644 (file)
@@ -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")):
index 79863152c024aa896c5cd093c60a3021e33e5920..8ae48b40f818f44557bb2e6aa0b1f9a8ee1488d9 100644 (file)
@@ -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)