Fix issues with encoded radio streams
authorMarcel van der Veldt <m.vanderveldt@outlook.com>
Sun, 21 Jul 2024 13:44:31 +0000 (15:44 +0200)
committerMarcel van der Veldt <m.vanderveldt@outlook.com>
Sun, 21 Jul 2024 13:44:31 +0000 (15:44 +0200)
music_assistant/server/controllers/music.py
music_assistant/server/controllers/streams.py
music_assistant/server/helpers/audio.py
music_assistant/server/helpers/playlists.py
music_assistant/server/providers/builtin/__init__.py

index 4580717f2e2b0ef0fbdd3d883ee92e8638bec4c5..6492e9dcb573732591e01972c7992d0535895411 100644 (file)
@@ -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
index 33451e268a735988e6d34ae6c6959d9d8540ab83..d84900fa3a523732133ea6aed539e07af6b9da3c 100644 (file)
@@ -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."""
index 5ab06d53e32b40776fd8f62a716926a963d21e7d..6b7b5c8e6b6ca28fbd3710f7e82d96f1e470bf49 100644 (file)
@@ -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:
index 05442da740dfa13594cdd20379d829f934edca30..8df3606e88a419874bbbf4095e9c254f07f8c0a4 100644 (file)
@@ -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)
index 1a9767e9d5fae0a8be0076f92c986b348b3be6b8..8ab9368c37734fb63766b727ec2d56fb091fe85d 100644 (file)
@@ -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