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)
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:
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")):
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
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)