Skip to content
Closed
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
49 changes: 41 additions & 8 deletions music_assistant/providers/digitally_incorporated/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -634,16 +634,49 @@ async def _get_stream_url(self, network_key: str, channel_key: str) -> str:
for i, url in enumerate(playlist):
self.logger.debug("%s: Available stream URL %d: %s", self.domain, i + 1, url)

# Use the first URL - Digitally Incorporated typically returns them in priority order
stream_url: str = str(playlist[0])
self.logger.debug("%s: Selected stream URL: %s", self.domain, stream_url)

# Validate the stream URL
if not stream_url or not isinstance(stream_url, str):
msg = f"{self.domain}: Invalid stream URL received: {stream_url}"
# Try each URL until one responds without 403 (forbidden)
candidate_urls = [str(url) for url in playlist if isinstance(url, str) and str(url).strip()]
if not candidate_urls:
msg = f"{self.domain}: No valid stream URLs received from Digitally Incorporated API"
raise MediaNotFoundError(msg)
Comment on lines +637 to 641
Copy link
Contributor

Choose a reason for hiding this comment

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

this section says it's trying each URL to see if it returns a 403, but that's happening later. this just checks if it's a valid string, converts the string to a string, checks if it's not an empty string after being stripped, then returns the unstripped url converted to a tring again, for each candidate. this seems like a rather confusing way to just check the type of each candidate and make sure it has content.


return stream_url
total_candidates = len(candidate_urls)
for idx, stream_url in enumerate(candidate_urls, start=1):
try:
timeout = aiohttp.ClientTimeout(total=10)
async with self.mass.http_session.head(
stream_url, allow_redirects=True, timeout=timeout
) as resp:
if resp.status == 403:
self.logger.warning(
"%s: Stream URL %s returned 403 (candidate %d/%d), trying next",
self.domain,
stream_url,
idx,
total_candidates,
)
continue
resp.raise_for_status()
self.logger.debug(
"%s: Selected stream URL %d/%d: %s",
self.domain,
idx,
total_candidates,
stream_url,
)
return stream_url

except aiohttp.ClientError as err:
self.logger.debug(
"%s: Stream URL %s check failed (%s), trying next",
self.domain,
stream_url,
err,
)
continue

msg = f"{self.domain}: Unable to get working stream URL after {total_candidates} attempts"
raise MediaNotFoundError(msg)
Copy link
Contributor

@benklop benklop Jan 17, 2026

Choose a reason for hiding this comment

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

this whole thing seems a little over the top to me, there's a lot of extra stuff happening, conversions to string after determining we already have a string, and looping over the playlist multiple times.

I have nicholas-gh#1 as an alternative that should work and be simpler.

Copy link
Contributor

Choose a reason for hiding this comment

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

@OzGav if you prefer I can submit that as a PR directly here, just thought it would be courteous to @nicholas-gh to do it this way.


except (ProviderUnavailableError, MediaNotFoundError):
# Re-raise provider/media errors as-is (they already have domain prefix)
Expand Down
Loading