From d89784b23c9a09c1dfec899191573f8ad5885c67 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 25 May 2026 04:35:41 +0200 Subject: [PATCH 1/4] Preserve underlying error when InternetSource fails to decode When streaming over HTTP via RAOP, the PatchedIceCastClient download thread captured exceptions as strings and logged them at DEBUG level, and InternetSource.open only inspected that string without waiting for the download thread to finish. If miniaudio raised DecodeError before the download thread had recorded its error, the real cause (e.g. HTTP 401, connection error, timeout) was discarded and the caller only saw "failed to init decoder". Store the actual exception, wait for the download thread to settle on DecodeError, and chain the original exception via "raise ... from" so the underlying cause is preserved in tracebacks. Co-Authored-By: Claude Opus 4.7 (1M context) --- pyatv/protocols/raop/audio_source.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/pyatv/protocols/raop/audio_source.py b/pyatv/protocols/raop/audio_source.py index ea97859e5..4f664422d 100644 --- a/pyatv/protocols/raop/audio_source.py +++ b/pyatv/protocols/raop/audio_source.py @@ -463,7 +463,7 @@ class PatchedIceCastClient(miniaudio.StreamableSource): def __init__(self, buffer: SemiSeekableBuffer, url: str) -> None: """Initialize a new PatchedIceCastClient instance.""" self.url = url - self.error_message: Optional[str] = None + self.error: Optional[BaseException] = None self._stop_stream: bool = False self._buffer: SemiSeekableBuffer = buffer self._buffer_lock = threading.Lock() @@ -509,8 +509,8 @@ def _stream_wrapper(self) -> None: try: self._download_stream() except Exception as ex: - self.error_message = str(ex) - _LOGGER.debug("Error during streaming: %s", self.error_message) + self.error = ex + _LOGGER.warning("Error during streaming from %s: %s", self.url, ex) self._stop_stream = True def _download_stream(self) -> None: # pylint: disable=too-many-branches @@ -607,8 +607,14 @@ async def open( ), ) except miniaudio.DecodeError as ex: - if source.error_message is not None: - raise ProtocolError(source.error_message) from ex + # Make sure the download thread has finished so any HTTP/network error + # it raised is captured in source.error before we inspect it. Otherwise + # we'd race the thread and re-raise the (less informative) DecodeError. + await loop.run_in_executor(None, source.close) + if source.error is not None: + raise ProtocolError( + f"Failed to stream from {url}: {source.error}" + ) from source.error raise return cls( From 131c7de2e7514aa70d769a56fe3b884d1fb02a89 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 2 Jun 2026 17:20:46 +0200 Subject: [PATCH 2/4] Update pyatv/protocols/raop/audio_source.py Co-authored-by: Quentame --- pyatv/protocols/raop/audio_source.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyatv/protocols/raop/audio_source.py b/pyatv/protocols/raop/audio_source.py index 4f664422d..9e98fe7da 100644 --- a/pyatv/protocols/raop/audio_source.py +++ b/pyatv/protocols/raop/audio_source.py @@ -606,7 +606,7 @@ async def open( sample_rate=sample_rate, ), ) - except miniaudio.DecodeError as ex: + except miniaudio.DecodeError: # Make sure the download thread has finished so any HTTP/network error # it raised is captured in source.error before we inspect it. Otherwise # we'd race the thread and re-raise the (less informative) DecodeError. From 2cce627aee5a043ea6b2b9599059fd24cee062a5 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 3 Jun 2026 05:30:07 +0200 Subject: [PATCH 3/4] Update pyatv/protocols/raop/audio_source.py Co-authored-by: Quentame --- pyatv/protocols/raop/audio_source.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyatv/protocols/raop/audio_source.py b/pyatv/protocols/raop/audio_source.py index 9e98fe7da..c40519275 100644 --- a/pyatv/protocols/raop/audio_source.py +++ b/pyatv/protocols/raop/audio_source.py @@ -606,7 +606,7 @@ async def open( sample_rate=sample_rate, ), ) - except miniaudio.DecodeError: + except miniaudio.DecodeError as ex: # Make sure the download thread has finished so any HTTP/network error # it raised is captured in source.error before we inspect it. Otherwise # we'd race the thread and re-raise the (less informative) DecodeError. @@ -614,7 +614,7 @@ async def open( if source.error is not None: raise ProtocolError( f"Failed to stream from {url}: {source.error}" - ) from source.error + ) from ex raise return cls( From cd4670f99ceec24fd71787cca63f87452961a06f Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 6 Jun 2026 11:41:13 -0400 Subject: [PATCH 4/4] Fix exception handling for DecodeError Refactor error handling in audio source decoding. --- pyatv/protocols/raop/audio_source.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyatv/protocols/raop/audio_source.py b/pyatv/protocols/raop/audio_source.py index c40519275..9e98fe7da 100644 --- a/pyatv/protocols/raop/audio_source.py +++ b/pyatv/protocols/raop/audio_source.py @@ -606,7 +606,7 @@ async def open( sample_rate=sample_rate, ), ) - except miniaudio.DecodeError as ex: + except miniaudio.DecodeError: # Make sure the download thread has finished so any HTTP/network error # it raised is captured in source.error before we inspect it. Otherwise # we'd race the thread and re-raise the (less informative) DecodeError. @@ -614,7 +614,7 @@ async def open( if source.error is not None: raise ProtocolError( f"Failed to stream from {url}: {source.error}" - ) from ex + ) from source.error raise return cls(