From b4a38cc12b89aa289fb9eae0f4bb53abab23d67c Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Sun, 21 Jul 2024 15:44:31 +0200 Subject: [PATCH] Fix issues with encoded radio streams --- music_assistant/server/controllers/music.py | 5 ++++- music_assistant/server/controllers/streams.py | 2 +- music_assistant/server/helpers/audio.py | 21 +++++++++---------- music_assistant/server/helpers/playlists.py | 2 +- .../server/providers/builtin/__init__.py | 4 ++-- 5 files changed, 18 insertions(+), 16 deletions(-) diff --git a/music_assistant/server/controllers/music.py b/music_assistant/server/controllers/music.py index 4580717f..6492e9dc 100644 --- a/music_assistant/server/controllers/music.py +++ b/music_assistant/server/controllers/music.py @@ -596,7 +596,10 @@ class MusicController(CoreController): """Mark item as played in playlog.""" timestamp = utc_timestamp() - if provider_instance_id_or_domain == "builtin" and media_type != MediaType.PLAYLIST: + if ( + provider_instance_id_or_domain.startswith("builtin") + and media_type != MediaType.PLAYLIST + ): # we deliberately skip builtin provider items as those are often # one-off items like TTS or some sound effect etc. return diff --git a/music_assistant/server/controllers/streams.py b/music_assistant/server/controllers/streams.py index 33451e26..d84900fa 100644 --- a/music_assistant/server/controllers/streams.py +++ b/music_assistant/server/controllers/streams.py @@ -407,7 +407,7 @@ class StreamsController(CoreController): command = request.match_info["command"] if command == "next": self.mass.create_task(self.mass.player_queues.next(queue_id)) - return web.FileResponse(SILENCE_FILE) + return web.FileResponse(SILENCE_FILE, headers={"icy-name": "Music Assistant"}) async def serve_announcement_stream(self, request: web.Request) -> web.Response: """Stream announcement audio to a player.""" diff --git a/music_assistant/server/helpers/audio.py b/music_assistant/server/helpers/audio.py index 5ab06d53..6b7b5c8e 100644 --- a/music_assistant/server/helpers/audio.py +++ b/music_assistant/server/helpers/audio.py @@ -477,9 +477,8 @@ async def resolve_radio_stream(mass: MusicAssistant, url: str) -> tuple[str, boo timeout = ClientTimeout(total=0, connect=10, sock_read=5) try: async with mass.http_session.get( - url, headers=HTTP_HEADERS_ICY, allow_redirects=True, timeout=timeout, encoded="%" in url + url, headers=HTTP_HEADERS_ICY, allow_redirects=True, timeout=timeout ) as resp: - resolved_url = str(resp.real_url) headers = resp.headers resp.raise_for_status() if not resp.headers: @@ -491,24 +490,24 @@ async def resolve_radio_stream(mass: MusicAssistant, url: str) -> tuple[str, boo or headers.get("content-type") == "audio/x-mpegurl" ): # url is playlist, we need to unfold it - substreams = await fetch_playlist(mass, resolved_url) + substreams = await fetch_playlist(mass, url) if not any(x for x in substreams if x.length): try: for line in substreams: if not line.is_url: continue # unfold first url of playlist - return await resolve_radio_stream(mass, line.path) + resolved_url, is_icy, is_hls = await resolve_radio_stream(mass, line.path) raise InvalidDataError("No content found in playlist") except IsHLSPlaylist: is_hls = True except Exception as err: LOGGER.warning("Error while parsing radio URL %s: %s", url, err) - return (resolved_url, is_icy, is_hls) + return (url, is_icy, is_hls) result = (resolved_url, is_icy, is_hls) - cache_expiration = 30 * 24 * 3600 if url == resolved_url else 600 + cache_expiration = 3600 * 3 await mass.cache.set(cache_key, result, expiration=cache_expiration) return result @@ -520,7 +519,7 @@ async def get_icy_stream( timeout = ClientTimeout(total=0, connect=30, sock_read=5 * 60) LOGGER.debug("Start streaming radio with ICY metadata from url %s", url) async with mass.http_session.get( - url, headers=HTTP_HEADERS_ICY, timeout=timeout, encoded="%" in url + url, allow_redirects=True, headers=HTTP_HEADERS_ICY, timeout=timeout ) as resp: headers = resp.headers meta_int = int(headers["icy-metaint"]) @@ -576,7 +575,7 @@ async def get_hls_stream( while True: logger.log(VERBOSE_LOG_LEVEL, "start streaming chunks from substream %s", substream_url) async with mass.http_session.get( - substream_url, headers=HTTP_HEADERS, timeout=timeout, encoded="%" in substream_url + substream_url, allow_redirects=True, headers=HTTP_HEADERS, timeout=timeout ) as resp: resp.raise_for_status() charset = resp.charset or "utf-8" @@ -658,7 +657,7 @@ async def get_hls_substream( # fetch master playlist and select (best) child playlist # https://datatracker.ietf.org/doc/html/draft-pantos-http-live-streaming-19#section-10 async with mass.http_session.get( - url, headers=HTTP_HEADERS, timeout=timeout, encoded="%" in url + url, allow_redirects=True, headers=HTTP_HEADERS, timeout=timeout ) as resp: resp.raise_for_status() charset = resp.charset or "utf-8" @@ -688,7 +687,7 @@ async def get_http_stream( # try to get filesize with a head request seek_supported = streamdetails.can_seek if seek_position or not streamdetails.size: - async with mass.http_session.head(url, headers=HTTP_HEADERS, encoded="%" in url) as resp: + async with mass.http_session.head(url, allow_redirects=True, headers=HTTP_HEADERS) as resp: resp.raise_for_status() if size := resp.headers.get("Content-Length"): streamdetails.size = int(size) @@ -722,7 +721,7 @@ async def get_http_stream( # start the streaming from http bytes_received = 0 async with mass.http_session.get( - url, headers=headers, timeout=timeout, encoded="%" in url + url, allow_redirects=True, headers=headers, timeout=timeout ) as resp: is_partial = resp.status == 206 if seek_position and not is_partial: diff --git a/music_assistant/server/helpers/playlists.py b/music_assistant/server/helpers/playlists.py index 05442da7..8df3606e 100644 --- a/music_assistant/server/helpers/playlists.py +++ b/music_assistant/server/helpers/playlists.py @@ -142,7 +142,7 @@ def parse_pls(pls_data: str) -> list[PlaylistItem]: async def fetch_playlist(mass: MusicAssistant, url: str) -> list[PlaylistItem]: """Parse an online m3u or pls playlist.""" try: - async with mass.http_session.get(url, timeout=5) as resp: + async with mass.http_session.get(url, allow_redirects=True, timeout=5) as resp: charset = resp.charset or "utf-8" try: playlist_data = (await resp.content.read(64 * 1024)).decode(charset) diff --git a/music_assistant/server/providers/builtin/__init__.py b/music_assistant/server/providers/builtin/__init__.py index 1a9767e9..8ab9368c 100644 --- a/music_assistant/server/providers/builtin/__init__.py +++ b/music_assistant/server/providers/builtin/__init__.py @@ -6,7 +6,7 @@ import asyncio import os import time from collections.abc import AsyncGenerator -from typing import TYPE_CHECKING, NotRequired, TypedDict +from typing import TYPE_CHECKING, NotRequired, TypedDict, cast import aiofiles import shortuuid @@ -162,7 +162,7 @@ class BuiltinProvider(MusicProvider): async def get_track(self, prov_track_id: str) -> Track: """Get full track details by id.""" - parsed_item: Track = await self.parse_item(prov_track_id) + parsed_item = cast(Track, await self.parse_item(prov_track_id)) stored_items: list[StoredItem] = self.mass.config.get(CONF_KEY_TRACKS, []) if stored_item := next((x for x in stored_items if x["item_id"] == prov_track_id), None): # always prefer the stored info, such as the name -- 2.34.1