Skip to content

Improve stream URL handling with failover support#2996

Draft
benklop wants to merge 5 commits intomusic-assistant:devfrom
benklop:fallback-when-multiple-urls-present
Draft

Improve stream URL handling with failover support#2996
benklop wants to merge 5 commits intomusic-assistant:devfrom
benklop:fallback-when-multiple-urls-present

Conversation

@benklop
Copy link
Contributor

@benklop benklop commented Jan 19, 2026

Enhance the Digitally Incorporated provider to handle 403 (and other) errors by attempting the next URL in the list. Refactor stream URL handling to support multiple URL types and improve error logging. Include tests for JSON serialization of stream details.

This change requires an update to music-assistant/server, music-assistant/models#155

Supercedes #2984

nicholas-gh and others added 3 commits January 16, 2026 18:01
…e giving up

I'm finding that prem2 (often the first URL in in the list) returns 403 'Too many clients'. Failing over to the next URL works and plays the stream.
Copilot AI review requested due to automatic review settings January 19, 2026 22:50
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds mirrored stream URL support for the Digitally Incorporated provider so playback can fail over to the next URL on errors, and updates serialization/helpers to handle the new stream path types.

Changes:

  • Digitally Incorporated provider now returns multiple stream URLs as mirrors and uses HTTP stream type.
  • Audio streaming helper now supports mirrored URL lists via a new get_mirror_stream path.
  • JSON serialization helper enhanced to better handle dataclasses/containers; adds a small serialization test.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 7 comments.

File Description
tests/test_json_serialization.py Adds a regression test ensuring lists of MultiPartPath can be JSON serialized.
music_assistant/providers/digitally_incorporated/__init__.py Refactors stream URL retrieval to return multiple StreamMirror entries and switches to HTTP stream type.
music_assistant/helpers/json.py Extends get_serializable_value to handle dataclasses and adds defensive to_json logging.
music_assistant/helpers/audio.py Adds mirror-stream handling and introduces get_mirror_stream for URL failover.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 46 to 47
isinstance(obj, list | set | filter | tuple | dict_values | dict_keys | dict_values)
or obj.__class__ == "dict_valueiterator"
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This check is ineffective: obj.__class__ is a type, so comparing it to the string "dict_valueiterator" will always be False. If you need to special-case dict value iterators, compare type(obj).__name__ (or use an Iterator/Iterable check) instead. Also, dict_values is listed twice in the isinstance union.

Suggested change
isinstance(obj, list | set | filter | tuple | dict_values | dict_keys | dict_values)
or obj.__class__ == "dict_valueiterator"
isinstance(obj, list | set | filter | tuple | dict_values | dict_keys)
or type(obj).__name__ == "dict_valueiterator"

Copilot uses AI. Check for mistakes.
return stream_url
# Log all available URLs
for i, url in enumerate(stream_list):
self.logger.debug("%s: Available stream URL %d: %s", self.domain, i + 1, url)
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

stream_list contains StreamMirror objects, but the debug log prints the object (%s, url) rather than the actual URL string. This makes logs harder to read and can omit the URL if StreamMirror.__str__ isn’t implemented. Log url.path (and optionally url.priority) instead.

Suggested change
self.logger.debug("%s: Available stream URL %d: %s", self.domain, i + 1, url)
self.logger.debug(
"%s: Available stream URL %d: %s (priority: %s)",
self.domain,
i + 1,
url.path,
url.priority,
)

Copilot uses AI. Check for mistakes.
@use_cache(CACHE_STREAM_URLS)
async def _get_stream_urls(self, network_key: str, channel_key: str) -> list[StreamMirror]:
"""Get the streaming URLs for a channel."""
self.logger.debug("%s: Getting stream URL for %s:%s", self.domain, network_key, channel_key)
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This debug line still says "Getting stream URL" even though the method now returns multiple URLs. Consider updating the message to "stream URLs" to match the new behavior and avoid confusion when troubleshooting.

Copilot uses AI. Check for mistakes.
Comment on lines 336 to 338
# Get the stream URL
stream_url = await self._get_stream_url(network_key, channel_key)
stream_url = await self._get_stream_urls(network_key, channel_key)

Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

stream_url now holds a list of StreamMirror entries (multiple URLs). Renaming to something like stream_urls/mirrors would make the type/intent clearer and avoid confusion for future readers.

Copilot uses AI. Check for mistakes.
@benklop
Copy link
Contributor Author

benklop commented Jan 22, 2026

@copilot open a new pull request to apply changes based on the comments in this thread

Copilot AI review requested due to automatic review settings January 22, 2026 00:42
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 345 to 348
media_type=MediaType.RADIO,
stream_type=StreamType.ICY,
# Use HTTP stream type with mirrors so we can try multiple URLs
stream_type=StreamType.HTTP,
path=stream_url,
Copy link

Copilot AI Jan 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changing the Digitally Incorporated radio stream from StreamType.ICY to StreamType.HTTP will bypass the ICY-specific streaming path (get_icy_radio_stream) and likely disables ICY metadata (stream title) updates. If these streams provide ICY metadata, consider keeping StreamType.ICY and adding mirror failover support for ICY streams (or enhancing the ICY streamer to accept a mirror list) instead of downgrading to plain HTTP.

Copilot uses AI. Check for mistakes.
@MarvinSchenkel
Copy link
Contributor

Could you have a look at the failing tests and open co pilot suggestions? Marking this PR as draft so we can keep track of which PRs needs our attention

@benklop
Copy link
Contributor Author

benklop commented Jan 30, 2026

I will try to update this PR this weekend. sorry for the delays, been a crazy week.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants