Skip to content

Update Alexa player provider#3167

Draft
alams154 wants to merge 8 commits intomusic-assistant:devfrom
alams154:prototype
Draft

Update Alexa player provider#3167
alams154 wants to merge 8 commits intomusic-assistant:devfrom
alams154:prototype

Conversation

@alams154
Copy link
Contributor

@alams154 alams154 commented Feb 15, 2026

This PR should bring some improvements to the Alexa Player Provider and help bring it more in line with the current state of the Alexa skill.

  • Change default port to 5000 to match the default Alexa skill port
  • Add api_request function for generic endpoints
  • Pull metadata that allows for display of metadata on Show devices
  • Migrate to custom commands in AlexaPy calls since the API calls do not work for APL devices

Copilot AI review requested due to automatic review settings February 15, 2026 00:26
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

This PR updates the Alexa player provider with several significant changes:

Changes:

  • Changed default API URL port from 3000 to 5000
  • Added new API request infrastructure with session management and basic auth support
  • Refactored playback control methods (stop, play, pause) to use Alexa intents instead of direct API calls
  • Implemented intent caching mechanism to improve performance
  • Enhanced metadata handling in play_media method

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



async def api_request(
provider: Any,
Copy link

Copilot AI Feb 15, 2026

Choose a reason for hiding this comment

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

The provider parameter is typed as Any, which provides no type safety. Consider using a more specific type such as AlexaProvider or a Protocol that defines the required interface (config.get_value and mass.http_session). This would improve type safety and IDE support.

Copilot uses AI. Check for mistakes.
Copy link
Contributor

Choose a reason for hiding this comment

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

Please use appropriate type here

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Should be the appropriate type now

Copilot AI review requested due to automatic review settings February 16, 2026 14:13
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 1 out of 1 changed files in this pull request and generated 9 comments.


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

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings February 16, 2026 15:25
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 1 out of 1 changed files in this pull request and generated no new comments.


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

url = f"{api_url.rstrip('/')}/{endpoint.lstrip('/')}"

mass_session = None
try:
Copy link
Contributor

Choose a reason for hiding this comment

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

I think you can just rely on mass.http_session to be there. No need for all this fallback logic.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Should be removed now

auth = BasicAuth(str(username), str(password))

stream_url = await self.provider.mass.streams.resolve_stream_url(self.player_id, media)
# Prefer the player's current_media (may contain enriched metadata),
Copy link
Contributor

Choose a reason for hiding this comment

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

Why prefer the current metadata? If MA is playing track A and you request play_media with Track B, the metadata of track B will be in media. I think you should just use media.xxx or "" here ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I thought that would be the case but when I was testing, media didn't contain relevant metadata. Album and artist were null and title was set to "Music Assistant" and image_url was the Music Assistant logo.

If there is some other field that contains the relevant metadata, I can update the assignments, or if there is a bug in the provider somewhere I could address that.

Copy link
Contributor

Choose a reason for hiding this comment

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

This is due to flow mode playing as 1 big continuous stream, so play_media is only called once in those cases. If you want rich metadata in flow mode, you need an endpoint on the player side that allows you to just update the metadata. You can implement _on_player_media_updated for this. Have a look at the Airplay or Chromecast provider as an example.

Copy link
Contributor

@MarvinSchenkel MarvinSchenkel left a comment

Choose a reason for hiding this comment

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

Could you please update the PR description with the goal? I miss the context to properly review the functional changes of the player commands. What is this PR improving?

Also gave some inline feedback. Please have a look, address the comments and let us know when you want us to have another look by selecting 'Ready for review' again

@MarvinSchenkel MarvinSchenkel marked this pull request as draft February 17, 2026 09:42
@alams154 alams154 marked this pull request as ready for review February 17, 2026 13:59
Copilot AI review requested due to automatic review settings February 17, 2026 13:59
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 1 out of 1 changed files in this pull request and generated 5 comments.


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

Copilot AI review requested due to automatic review settings February 17, 2026 14:30
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 1 out of 1 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 +427 to +428
state = getattr(self, "state", None)
current = getattr(state, "current_media", None) or media
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

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

The metadata extraction logic is overly defensive. Since state is always a property on Player instances (inherited from the Player base class), using getattr(self, "state", None) is unnecessary. The code should directly access self.state.current_media as done in other providers (e.g., airplay/player.py:621, roku_media_assistant/player.py:312, sendspin/player.py:550, squeezelite/player.py:354).

Suggested change
state = getattr(self, "state", None)
current = getattr(state, "current_media", None) or media
current = self.state.current_media or media

Copilot uses AI. Check for mistakes.
Comment on lines +320 to +329
except Exception as exc: # pylint: disable=broad-except
_LOGGER.error("Failed API request to %s: %s", url, exc)
raise ActionUnavailable("Failed to connect to the configured API endpoint.")

async with aiohttp.ClientSession() as session:
try:
return await _request_with_session(session, method, url, json_data, timeout, auth)
except Exception as exc: # pylint: disable=broad-except
_LOGGER.error("Failed API request to %s: %s", url, exc)
raise ActionUnavailable("Failed to connect to the configured API endpoint.")
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

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

The broad exception handler catches ActionUnavailable exceptions raised by _request_with_session and converts them to a less informative generic message. This loses the detailed error information (status code and response text) from the original exception. Consider catching ActionUnavailable separately and re-raising it to preserve the detailed error message, similar to the pattern used in _load_intents at line 547.

Suggested change
except Exception as exc: # pylint: disable=broad-except
_LOGGER.error("Failed API request to %s: %s", url, exc)
raise ActionUnavailable("Failed to connect to the configured API endpoint.")
async with aiohttp.ClientSession() as session:
try:
return await _request_with_session(session, method, url, json_data, timeout, auth)
except Exception as exc: # pylint: disable=broad-except
_LOGGER.error("Failed API request to %s: %s", url, exc)
raise ActionUnavailable("Failed to connect to the configured API endpoint.")
except ActionUnavailable:
# Preserve detailed ActionUnavailable from _request_with_session
raise
except Exception as exc: # pylint: disable=broad-except
_LOGGER.error("Failed API request to %s: %s", url, exc)
raise ActionUnavailable("Failed to connect to the configured API endpoint.") from exc
async with aiohttp.ClientSession() as session:
try:
return await _request_with_session(session, method, url, json_data, timeout, auth)
except ActionUnavailable:
# Preserve detailed ActionUnavailable from _request_with_session
raise
except Exception as exc: # pylint: disable=broad-except
_LOGGER.error("Failed API request to %s: %s", url, exc)
raise ActionUnavailable("Failed to connect to the configured API endpoint.") from exc

Copilot uses AI. Check for mistakes.
Comment on lines +324 to +329
async with aiohttp.ClientSession() as session:
try:
return await _request_with_session(session, method, url, json_data, timeout, auth)
except Exception as exc: # pylint: disable=broad-except
_LOGGER.error("Failed API request to %s: %s", url, exc)
raise ActionUnavailable("Failed to connect to the configured API endpoint.")
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

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

The broad exception handler catches ActionUnavailable exceptions raised by _request_with_session and converts them to a less informative generic message. This loses the detailed error information (status code and response text) from the original exception. Consider catching ActionUnavailable separately and re-raising it to preserve the detailed error message, similar to the pattern used in _load_intents at line 547.

Copilot uses AI. Check for mistakes.
_LOGGER.error("Failed API request to %s: %s", url, exc)
raise ActionUnavailable("Failed to connect to the configured API endpoint.")

async with aiohttp.ClientSession() as session:
Copy link
Contributor

Choose a reason for hiding this comment

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

I think this is practically dead code. I cannot think of a reason that mass.http_session is None? Have you encountered this in your development.

if mass_session:
try:
return await _request_with_session(mass_session, method, url, json_data, timeout, auth)
except Exception as exc: # pylint: disable=broad-except
Copy link
Contributor

Choose a reason for hiding this comment

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

Would be nice to narrow this down to specific Exceptions you are trying to catch here.

Copy link
Contributor

Choose a reason for hiding this comment

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

I see more of those "catch all" patterns. I think that, in combination with my comments above is all that's left. Then we should be good to merge this :)

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.

3 participants