Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix parsing of HLS (sub)streams #1727

Merged
merged 2 commits into from
Oct 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 13 additions & 7 deletions music_assistant/server/helpers/audio.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
6 changes: 4 additions & 2 deletions music_assistant/server/helpers/playlists.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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")):
Expand Down
12 changes: 9 additions & 3 deletions music_assistant/server/providers/apple_music/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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)

Expand Down
Loading