if self.database:
await self.database.close()
+ async def on_provider_loaded(self, provider: MusicProvider) -> None:
+ """Handle logic when a provider is loaded."""
+ await self.schedule_provider_sync(provider.instance_id)
+
+ async def on_provider_unload(self, provider: MusicProvider) -> None:
+ """Handle logic when a provider is (about to get) unloaded."""
+ # make sure to stop any running sync tasks first
+ for sync_task in self.in_progress_syncs:
+ if sync_task.provider_instance == provider.instance_id:
+ if sync_task.task:
+ sync_task.task.cancel()
+
@property
def providers(self) -> list[MusicProvider]:
"""Return all loaded/running MusicProviders (instances)."""
+++ /dev/null
-"""
-MusicAssistant PlayerController.
-
-Handles all logic to control supported players,
-which are provided by Player Providers.
-
-Note that the PlayerController has a concept of a 'player' and a 'playerstate'.
-The Player is the actual object that is provided by the provider,
-which incorporates the actual state of the player (e.g. volume, state, etc)
-and functions for controlling the player (e.g. play, pause, etc).
-
-The playerstate is the (final) state of the player, including any user customizations
-and transformations that are applied to the player.
-The playerstate is the object that is exposed to the outside world (via the API).
-"""
-
-from __future__ import annotations
-
-import asyncio
-import functools
-import time
-from contextlib import suppress
-from typing import TYPE_CHECKING, Any, Concatenate, ParamSpec, TypeVar, cast
-
-from music_assistant_models.constants import (
- PLAYER_CONTROL_FAKE,
- PLAYER_CONTROL_NATIVE,
- PLAYER_CONTROL_NONE,
-)
-from music_assistant_models.enums import (
- EventType,
- MediaType,
- PlaybackState,
- PlayerFeature,
- PlayerType,
- ProviderFeature,
- ProviderType,
-)
-from music_assistant_models.errors import (
- AlreadyRegisteredError,
- MusicAssistantError,
- PlayerCommandFailed,
- PlayerUnavailableError,
- ProviderUnavailableError,
- UnsupportedFeaturedException,
-)
-from music_assistant_models.player_control import PlayerControl # noqa: TC002
-
-from music_assistant.constants import (
- ANNOUNCE_ALERT_FILE,
- ATTR_ANNOUNCEMENT_IN_PROGRESS,
- ATTR_FAKE_MUTE,
- ATTR_FAKE_POWER,
- ATTR_FAKE_VOLUME,
- ATTR_GROUP_MEMBERS,
- ATTR_LAST_POLL,
- ATTR_PREVIOUS_VOLUME,
- CONF_AUTO_PLAY,
- CONF_ENTRY_ANNOUNCE_VOLUME,
- CONF_ENTRY_ANNOUNCE_VOLUME_MAX,
- CONF_ENTRY_ANNOUNCE_VOLUME_MIN,
- CONF_ENTRY_ANNOUNCE_VOLUME_STRATEGY,
- CONF_ENTRY_TTS_PRE_ANNOUNCE,
- CONF_PLAYER_DSP,
- CONF_PLAYERS,
- CONF_PRE_ANNOUNCE_CHIME_URL,
-)
-from music_assistant.controllers.streams import AnnounceData
-from music_assistant.helpers.api import api_command
-from music_assistant.helpers.tags import async_parse_tags
-from music_assistant.helpers.throttle_retry import Throttler
-from music_assistant.helpers.util import TaskManager, validate_announcement_chime_url
-from music_assistant.models.core_controller import CoreController
-from music_assistant.models.player import Player, PlayerMedia, PlayerState
-from music_assistant.models.player_provider import PlayerProvider
-from music_assistant.models.plugin import PluginProvider, PluginSource
-
-if TYPE_CHECKING:
- from collections.abc import Awaitable, Callable, Coroutine, Iterator
-
- from music_assistant_models.config_entries import CoreConfig, PlayerConfig
- from music_assistant_models.player_queue import PlayerQueue
-
-CACHE_CATEGORY_PLAYER_POWER = 1
-
-
-_PlayerControllerT = TypeVar("_PlayerControllerT", bound="PlayerController")
-_R = TypeVar("_R")
-_P = ParamSpec("_P")
-
-
-def handle_player_command[PlayerControllerT: "PlayerController", **P, R](
- func: Callable[Concatenate[_PlayerControllerT, _P], Awaitable[_R]],
-) -> Callable[Concatenate[_PlayerControllerT, _P], Coroutine[Any, Any, _R | None]]:
- """Check and log commands to players."""
-
- @functools.wraps(func)
- async def wrapper(self: _PlayerControllerT, *args: _P.args, **kwargs: _P.kwargs) -> _R | None:
- """Log and handle_player_command commands to players."""
- player_id = kwargs["player_id"] if "player_id" in kwargs else args[0]
- if (player := self._players.get(player_id)) is None or not player.available:
- # player not existent
- self.logger.warning(
- "Ignoring command %s for unavailable player %s",
- func.__name__,
- player_id,
- )
- return
-
- self.logger.debug(
- "Handling command %s for player %s",
- func.__name__,
- player.display_name,
- )
- try:
- await func(self, *args, **kwargs)
- except Exception as err:
- raise PlayerCommandFailed(str(err)) from err
-
- return wrapper
-
-
-class PlayerController(CoreController):
- """Controller holding all logic to control registered players."""
-
- domain: str = "players"
-
- def __init__(self, *args, **kwargs) -> None:
- """Initialize core controller."""
- super().__init__(*args, **kwargs)
- self._players: dict[str, Player] = {}
- self._controls: dict[str, PlayerControl] = {}
- self.manifest.name = "Player Controller"
- self.manifest.description = (
- "Music Assistant's core controller which manages all players from all providers."
- )
- self.manifest.icon = "speaker-multiple"
- self._poll_task: asyncio.Task | None = None
- self._player_throttlers: dict[str, Throttler] = {}
- self._announce_locks: dict[str, asyncio.Lock] = {}
-
- async def setup(self, config: CoreConfig) -> None:
- """Async initialize of module."""
- self._poll_task = self.mass.create_task(self._poll_players())
-
- async def close(self) -> None:
- """Cleanup on exit."""
- if self._poll_task and not self._poll_task.done():
- self._poll_task.cancel()
-
- @property
- def providers(self) -> list[PlayerProvider]:
- """Return all loaded/running MusicProviders."""
- return self.mass.get_providers(ProviderType.PLAYER) # type: ignore=return-value
-
- def all(
- self,
- return_unavailable: bool = True,
- return_disabled: bool = False,
- provider_filter: str | None = None,
- ) -> list[Player]:
- """
- Return all registered players.
-
- :param return_unavailable [bool]: Include unavailable players.
- :param return_disabled [bool]: Include disabled players.
- :param provider_filter [str]: Optional filter by provider lookup key.
-
- :return: List of Player objects.
- """
- return [
- player
- for player in self._players.values()
- if (player.available or return_unavailable)
- and (player.enabled or return_disabled)
- and (provider_filter is None or player.provider.lookup_key == provider_filter)
- ]
-
- @api_command("players/all")
- def all_states(
- self,
- return_unavailable: bool = True,
- return_disabled: bool = False,
- provider_filter: str | None = None,
- ) -> list[PlayerState]:
- """
- Return PlayerState for all registered players.
-
- :param return_unavailable [bool]: Include unavailable players.
- :param return_disabled [bool]: Include disabled players.
- :param provider_filter [str]: Optional filter by provider lookup key.
-
- :return: List of PlayerState objects.
- """
- return [
- player.state
- for player in self.all(
- return_unavailable=return_unavailable,
- return_disabled=return_disabled,
- provider_filter=provider_filter,
- )
- ]
-
- def get(
- self,
- player_id: str,
- raise_unavailable: bool = False,
- ) -> Player | None:
- """
- Return Player by player_id.
-
- :param player_id [str]: ID of the player.
- :param raise_unavailable [bool]: Raise if player is unavailable.
-
- :raises PlayerUnavailableError: If player is unavailable and raise_unavailable is True.
- :return: Player object or None.
- """
- if player := self._players.get(player_id):
- if (not player.available or not player.enabled) and raise_unavailable:
- msg = f"Player {player_id} is not available"
- raise PlayerUnavailableError(msg)
- return player
- if raise_unavailable:
- msg = f"Player {player_id} is not available"
- raise PlayerUnavailableError(msg)
- return None
-
- @api_command("players/get")
- def get_state(
- self,
- player_id: str,
- raise_unavailable: bool = False,
- ) -> PlayerState | None:
- """
- Return PlayerState by player_id.
-
- :param player_id [str]: ID of the player.
- :param raise_unavailable [bool]: Raise if player is unavailable.
-
- :raises PlayerUnavailableError: If player is unavailable and raise_unavailable is True.
- :return: Player object or None.
- """
- if player := self.get(player_id, raise_unavailable):
- return player.state
- return None
-
- def get_player_by_name(self, name: str) -> Player | None:
- """
- Return Player by name.
-
- :param name: Name of the player.
- :return: Player object or None.
- """
- return next((x for x in self._players.values() if x.name == name), None)
-
- @api_command("players/get_by_name")
- def get_player_state_by_name(self, name: str) -> PlayerState | None:
- """
- Return PlayerState by name.
-
- :param name: Name of the player.
- :return: PlayerState object or None.
- """
- if player := self.get_player_by_name(name):
- return player.state
- return None
-
- @api_command("players/player_controls")
- def player_controls(
- self,
- ) -> list[PlayerControl]:
- """Return all registered playercontrols."""
- return list(self._controls.values())
-
- @api_command("players/player_control")
- def get_player_control(
- self,
- control_id: str,
- ) -> PlayerControl | None:
- """
- Return PlayerControl by control_id.
-
- :param control_id: ID of the player control.
- :return: PlayerControl object or None.
- """
- if control := self._controls.get(control_id):
- return control
- return None
-
- @api_command("players/plugin_sources")
- def get_plugin_sources(self) -> list[PluginSource]:
- """Return all available plugin sources."""
- return [
- plugin_prov.get_source()
- for plugin_prov in self.mass.get_providers(ProviderType.PLUGIN)
- if isinstance(plugin_prov, PluginProvider)
- and ProviderFeature.AUDIO_SOURCE in plugin_prov.supported_features
- ]
-
- @api_command("players/plugin_source")
- def get_plugin_source(
- self,
- source_id: str,
- ) -> PluginSource | None:
- """
- Return PluginSource by source_id.
-
- :param source_id: ID of the plugin source.
- :return: PluginSource object or None.
- """
- for plugin_prov in self.mass.get_providers(ProviderType.PLUGIN):
- assert isinstance(plugin_prov, PluginProvider) # for type checking
- if ProviderFeature.AUDIO_SOURCE not in plugin_prov.supported_features:
- continue
- if (source := plugin_prov.get_source()) and source.id == source_id:
- return source
- return None
-
- # Player commands
-
- @api_command("players/cmd/stop")
- @handle_player_command
- async def cmd_stop(self, player_id: str) -> None:
- """Send STOP command to given player.
-
- - player_id: player_id of the player to handle the command.
- """
- player = self._get_player_with_redirect(player_id)
- # Redirect to queue controller if it is active
- if active_queue := self.get_active_queue(player):
- await self.mass.player_queues.stop(active_queue.queue_id)
- return
- # handle command on player directly
- async with self._player_throttlers[player.player_id]:
- await player.stop()
-
- @api_command("players/cmd/play")
- @handle_player_command
- async def cmd_play(self, player_id: str) -> None:
- """Send PLAY (unpause) command to given player.
-
- - player_id: player_id of the player to handle the command.
- """
- player = self._get_player_with_redirect(player_id)
- if player.playback_state == PlaybackState.PLAYING:
- self.logger.info(
- "Ignore PLAY request to player %s: player is already playing", player.display_name
- )
- return
- # Redirect to queue controller if it is active
- if active_queue := self.get_active_queue(player):
- await self.mass.player_queues.play(active_queue.queue_id)
- return
- # handle command on player directly
- async with self._player_throttlers[player.player_id]:
- await player.play()
-
- @api_command("players/cmd/pause")
- @handle_player_command
- async def cmd_pause(self, player_id: str) -> None:
- """Send PAUSE command to given player.
-
- - player_id: player_id of the player to handle the command.
- """
- player = self._get_player_with_redirect(player_id)
- # Redirect to queue controller if it is active
- if active_queue := self.get_active_queue(player):
- await self.mass.player_queues.pause(active_queue.queue_id)
- return
- if PlayerFeature.PAUSE not in player.supported_features:
- # if player does not support pause, we need to send stop
- self.logger.debug(
- "Player %s does not support pause, using STOP instead",
- player.display_name,
- )
- await self.cmd_stop(player.player_id)
- return
- # handle command on player directly
- await player.pause()
-
- @api_command("players/cmd/play_pause")
- async def cmd_play_pause(self, player_id: str) -> None:
- """Toggle play/pause on given player.
-
- - player_id: player_id of the player to handle the command.
- """
- player = self._get_player_with_redirect(player_id)
- if player.playback_state == PlaybackState.PLAYING:
- await self.cmd_pause(player.player_id)
- else:
- await self.cmd_play(player.player_id)
-
- @api_command("players/cmd/seek")
- async def cmd_seek(self, player_id: str, position: int) -> None:
- """Handle SEEK command for given player.
-
- - player_id: player_id of the player to handle the command.
- - position: position in seconds to seek to in the current playing item.
- """
- player = self._get_player_with_redirect(player_id)
- # Redirect to queue controller if it is active
- if active_queue := self.get_active_queue(player):
- await self.mass.player_queues.seek(active_queue.queue_id, position)
- return
- if PlayerFeature.SEEK not in player.supported_features:
- msg = f"Player {player.display_name} does not support seeking"
- raise UnsupportedFeaturedException(msg)
- # handle command on player directly
- await player.seek(position)
-
- @api_command("players/cmd/next")
- async def cmd_next_track(self, player_id: str) -> None:
- """Handle NEXT TRACK command for given player."""
- player = self._get_player_with_redirect(player_id)
- active_source_id = player.active_source or player.player_id
-
- # Redirect to queue controller if it is active
- if active_queue := self.get_active_queue(player):
- await self.mass.player_queues.next(active_queue.queue_id)
- return
-
- if PlayerFeature.NEXT_PREVIOUS in player.supported_features:
- # player has some other source active and native next/previous support
- active_source = next((x for x in player.source_list if x.id == active_source_id), None)
- if active_source and active_source.can_next_previous:
- await player.next_track()
- return
- msg = "This action is (currently) unavailable for this source."
- raise PlayerCommandFailed(msg)
-
- msg = f"Player {player.display_name} does not support skipping to the next track."
- raise UnsupportedFeaturedException(msg)
-
- @api_command("players/cmd/previous")
- async def cmd_previous_track(self, player_id: str) -> None:
- """Handle PREVIOUS TRACK command for given player."""
- player = self._get_player_with_redirect(player_id)
- active_source_id = player.active_source or player.player_id
- # Redirect to queue controller if it is active
- if active_queue := self.get_active_queue(player):
- await self.mass.player_queues.previous(active_queue.queue_id)
- return
-
- if PlayerFeature.NEXT_PREVIOUS in player.supported_features:
- # player has some other source active and native next/previous support
- active_source = next((x for x in player.source_list if x.id == active_source_id), None)
- if active_source and active_source.can_next_previous:
- await player.previous_track()
- return
- msg = "This action is (currently) unavailable for this source."
- raise PlayerCommandFailed(msg)
-
- msg = f"Player {player.display_name} does not support skipping to the previous track."
- raise UnsupportedFeaturedException(msg)
-
- @api_command("players/cmd/power")
- @handle_player_command
- async def cmd_power(self, player_id: str, powered: bool, skip_update: bool = False) -> None:
- """Send POWER command to given player.
-
- - player_id: player_id of the player to handle the command.
- - powered: bool if player should be powered on or off.
- """
- player = self.get(player_id, True)
- assert player is not None # for type checking
- player_state = player.state
-
- if player_state.powered == powered:
- self.logger.debug(
- "Ignoring power %s command for player %s: already in state %s",
- "ON" if powered else "OFF",
- player_state.name,
- "ON" if player_state.powered else "OFF",
- )
- return # nothing to do
-
- # ungroup player at power off
- player_was_synced = player.synced_to is not None
- if player.type == PlayerType.PLAYER and not powered:
- # ungroup player if it is synced (or is a sync leader itself)
- # NOTE: ungroup will be ignored if the player is not grouped or synced
- await self.cmd_ungroup(player_id)
-
- # always stop player at power off
- if (
- not powered
- and not player_was_synced
- and player.playback_state in (PlaybackState.PLAYING, PlaybackState.PAUSED)
- ):
- await self.cmd_stop(player_id)
- # short sleep: allow the stop command to process and prevent race conditions
- await asyncio.sleep(0.2)
-
- # power off all synced childs when player is a sync leader
- elif not powered and player.type == PlayerType.PLAYER and player.group_members:
- async with TaskManager(self.mass) as tg:
- for member in self.iter_group_members(player, True):
- if member.power_control == PLAYER_CONTROL_NONE:
- continue
- tg.create_task(self.cmd_power(member.player_id, False))
-
- # handle actual power command
- if player.power_control == PLAYER_CONTROL_NONE:
- raise UnsupportedFeaturedException(
- f"Player {player.display_name} does not support power control"
- )
- if player.power_control == PLAYER_CONTROL_NATIVE:
- # player supports power command natively: forward to player provider
- async with self._player_throttlers[player_id]:
- await player.power(powered)
- elif player.power_control == PLAYER_CONTROL_FAKE:
- # user wants to use fake power control - so we (optimistically) update the state
- # and store the state in the cache
- player.extra_data[ATTR_FAKE_POWER] = powered
- await self.mass.cache.set(
- key=player_id,
- data=powered,
- provider=self.domain,
- category=CACHE_CATEGORY_PLAYER_POWER,
- )
- else:
- # handle external player control
- player_control = self._controls.get(player.power_control)
- control_name = player_control.name if player_control else player.power_control
- self.logger.debug("Redirecting power command to PlayerControl %s", control_name)
- if not player_control or not player_control.supports_power:
- raise UnsupportedFeaturedException(
- f"Player control {control_name} is not available"
- )
- if powered:
- assert player_control.power_on is not None # for type checking
- await player_control.power_on()
- else:
- assert player_control.power_off is not None # for type checking
- await player_control.power_off()
-
- # always optimistically set the power state to update the UI
- # as fast as possible and prevent race conditions
- player_state.powered = powered
- # reset active source on power off
- if not powered:
- player_state.active_source = None
-
- if not skip_update:
- player.update_state()
-
- # handle 'auto play on power on' feature
- if (
- not player.active_group
- and powered
- and player.config.get_value(CONF_AUTO_PLAY)
- and player.active_source in (None, player_id)
- and not player.extra_data.get(ATTR_ANNOUNCEMENT_IN_PROGRESS)
- ):
- await self.mass.player_queues.resume(player_id)
-
- @api_command("players/cmd/volume_set")
- @handle_player_command
- async def cmd_volume_set(self, player_id: str, volume_level: int) -> None:
- """Send VOLUME_SET command to given player.
-
- - player_id: player_id of the player to handle the command.
- - volume_level: volume level (0..100) to set on the player.
- """
- player = self.get(player_id, True)
- assert player is not None # for type checker
- if player.type == PlayerType.GROUP:
- # redirect to special group volume control
- await self.cmd_group_volume(player_id, volume_level)
- return
-
- if player.volume_control == PLAYER_CONTROL_NONE:
- raise UnsupportedFeaturedException(
- f"Player {player.display_name} does not support volume control"
- )
-
- if player.mute_control != PLAYER_CONTROL_NONE and player.volume_muted:
- # if player is muted, we unmute it first
- self.logger.debug(
- "Unmuting player %s before setting volume",
- player.display_name,
- )
- await self.cmd_volume_mute(player_id, False)
-
- if player.volume_control == PLAYER_CONTROL_NATIVE:
- # player supports volume command natively: forward to player
- async with self._player_throttlers[player_id]:
- await player.volume_set(volume_level)
- return
- if player.volume_control == PLAYER_CONTROL_FAKE:
- # user wants to use fake volume control - so we (optimistically) update the state
- # and store the state in the cache
- player.extra_data[ATTR_FAKE_VOLUME] = volume_level
- # trigger update
- player.update_state()
- return
- # else: handle external player control
- player_control = self._controls.get(player.volume_control)
- control_name = player_control.name if player_control else player.volume_control
- self.logger.debug("Redirecting volume command to PlayerControl %s", control_name)
- if not player_control or not player_control.supports_volume:
- raise UnsupportedFeaturedException(f"Player control {control_name} is not available")
- async with self._player_throttlers[player_id]:
- assert player_control.volume_set is not None
- await player_control.volume_set(volume_level)
-
- @api_command("players/cmd/volume_up")
- @handle_player_command
- async def cmd_volume_up(self, player_id: str) -> None:
- """Send VOLUME_UP command to given player.
-
- - player_id: player_id of the player to handle the command.
- """
- if not (player := self.get(player_id)):
- return
- current_volume = player.volume_state or 0
- if current_volume < 5 or current_volume > 95:
- step_size = 1
- elif current_volume < 20 or current_volume > 80:
- step_size = 2
- else:
- step_size = 5
- new_volume = min(100, current_volume + step_size)
- await self.cmd_volume_set(player_id, new_volume)
-
- @api_command("players/cmd/volume_down")
- @handle_player_command
- async def cmd_volume_down(self, player_id: str) -> None:
- """Send VOLUME_DOWN command to given player.
-
- - player_id: player_id of the player to handle the command.
- """
- if not (player := self.get(player_id)):
- return
- current_volume = player.volume_state or 0
- if current_volume < 5 or current_volume > 95:
- step_size = 1
- elif current_volume < 20 or current_volume > 80:
- step_size = 2
- else:
- step_size = 5
- new_volume = max(0, current_volume - step_size)
- await self.cmd_volume_set(player_id, new_volume)
-
- @api_command("players/cmd/group_volume")
- @handle_player_command
- async def cmd_group_volume(
- self,
- player_id: str,
- volume_level: int,
- ) -> None:
- """
- Handle adjusting the overall/group volume to a playergroup (or synced players).
-
- Will set a new (overall) volume level to a group player or syncgroup.
-
- :param group_player: dedicated group player or syncleader to handle the command.
- :param volume_level: volume level (0..100) to set to the group.
- """
- player = self.get(player_id, True)
- assert player is not None # for type checker
- if player.type == PlayerType.GROUP or player.group_members:
- # dedicated group player or sync leader
- await self.set_group_volume(player, volume_level)
- return
- if player.synced_to and (sync_leader := self.get(player.synced_to)):
- # redirect to sync leader
- await self.set_group_volume(sync_leader, volume_level)
- return
- # treat as normal player volume change
- await self.cmd_volume_set(player_id, volume_level)
-
- @api_command("players/cmd/group_volume_up")
- @handle_player_command
- async def cmd_group_volume_up(self, player_id: str) -> None:
- """Send VOLUME_UP command to given playergroup.
-
- - player_id: player_id of the player to handle the command.
- """
- group_player = self.get(player_id, True)
- assert group_player
- cur_volume = group_player.group_volume
- if cur_volume < 5 or cur_volume > 95:
- step_size = 1
- elif cur_volume < 20 or cur_volume > 80:
- step_size = 2
- else:
- step_size = 5
- new_volume = min(100, cur_volume + step_size)
- await self.cmd_group_volume(player_id, new_volume)
-
- @api_command("players/cmd/group_volume_down")
- @handle_player_command
- async def cmd_group_volume_down(self, player_id: str) -> None:
- """Send VOLUME_DOWN command to given playergroup.
-
- - player_id: player_id of the player to handle the command.
- """
- group_player = self.get(player_id, True)
- assert group_player
- cur_volume = group_player.group_volume
- if cur_volume < 5 or cur_volume > 95:
- step_size = 1
- elif cur_volume < 20 or cur_volume > 80:
- step_size = 2
- else:
- step_size = 5
- new_volume = max(0, cur_volume - step_size)
- await self.cmd_group_volume(player_id, new_volume)
-
- @api_command("players/cmd/volume_mute")
- @handle_player_command
- async def cmd_volume_mute(self, player_id: str, muted: bool) -> None:
- """Send VOLUME_MUTE command to given player.
-
- - player_id: player_id of the player to handle the command.
- - muted: bool if player should be muted.
- """
- player = self.get(player_id, True)
- assert player
- if player.mute_control == PLAYER_CONTROL_NONE:
- raise UnsupportedFeaturedException(
- f"Player {player.display_name} does not support muting"
- )
- if player.mute_control == PLAYER_CONTROL_NATIVE:
- # player supports mute command natively: forward to player
- async with self._player_throttlers[player_id]:
- await player.volume_mute(muted)
- elif player.mute_control == PLAYER_CONTROL_FAKE:
- # user wants to use fake mute control - so we use volume instead
- self.logger.debug(
- "Using volume for muting for player %s",
- player.display_name,
- )
- if muted:
- player.extra_data[ATTR_PREVIOUS_VOLUME] = player.volume_state
- player.extra_data[ATTR_FAKE_MUTE] = True
- await self.cmd_volume_set(player_id, 0)
- else:
- player._attr_volume_muted = False
- prev_volume = player.extra_data.get(ATTR_PREVIOUS_VOLUME, 1)
- player.extra_data[ATTR_FAKE_MUTE] = False
- await self.cmd_volume_set(player_id, prev_volume)
- else:
- # handle external player control
- player_control = self._controls.get(player.mute_control)
- control_name = player_control.name if player_control else player.mute_control
- self.logger.debug("Redirecting mute command to PlayerControl %s", control_name)
- if not player_control or not player_control.supports_mute:
- raise UnsupportedFeaturedException(
- f"Player control {control_name} is not available"
- )
- async with self._player_throttlers[player_id]:
- assert player_control.mute_set is not None
- await player_control.mute_set(muted)
-
- @api_command("players/cmd/play_announcement")
- async def play_announcement(
- self,
- player_id: str,
- url: str,
- pre_announce: bool | str | None = None,
- volume_level: int | None = None,
- pre_announce_url: str | None = None,
- ) -> None:
- """
- Handle playback of an announcement (url) on given player.
-
- - player_id: player_id of the player to handle the command.
- - url: URL of the announcement to play.
- - pre_announce: optional bool if pre-announce should be used.
- - volume_level: optional volume level to set for the announcement.
- - pre_announce_url: optional custom URL to use for the pre-announce chime.
- """
- player = self.get(player_id, True)
- assert player is not None # for type checking
- if not url.startswith("http"):
- raise PlayerCommandFailed("Only URLs are supported for announcements")
- if (
- pre_announce
- and pre_announce_url
- and not validate_announcement_chime_url(pre_announce_url)
- ):
- raise PlayerCommandFailed("Invalid pre-announce chime URL specified.")
- # prevent multiple announcements at the same time to the same player with a lock
- if player_id not in self._announce_locks:
- self._announce_locks[player_id] = lock = asyncio.Lock()
- else:
- lock = self._announce_locks[player_id]
- async with lock:
- try:
- # mark announcement_in_progress on player
- player.extra_data[ATTR_ANNOUNCEMENT_IN_PROGRESS] = True
- # determine if the player has native announcements support
- native_announce_support = (
- PlayerFeature.PLAY_ANNOUNCEMENT in player.supported_features
- )
- # determine pre-announce from (group)player config
- if pre_announce is None and "tts" in url:
- conf_pre_announce = self.mass.config.get_raw_player_config_value(
- player_id,
- CONF_ENTRY_TTS_PRE_ANNOUNCE.key,
- CONF_ENTRY_TTS_PRE_ANNOUNCE.default_value,
- )
- pre_announce = cast("bool", conf_pre_announce)
- if pre_announce_url is None:
- if conf_pre_announce_url := self.mass.config.get_raw_player_config_value(
- player_id,
- CONF_PRE_ANNOUNCE_CHIME_URL,
- ):
- # player default custom chime url
- pre_announce_url = cast("str", conf_pre_announce_url)
- else:
- # use global default chime url
- pre_announce_url = ANNOUNCE_ALERT_FILE
- # if player type is group with all members supporting announcements,
- # we forward the request to each individual player
- if player.type == PlayerType.GROUP and (
- all(
- PlayerFeature.PLAY_ANNOUNCEMENT in x.supported_features
- for x in self.iter_group_members(player)
- )
- ):
- # forward the request to each individual player
- async with TaskManager(self.mass) as tg:
- for group_member in player.group_members:
- tg.create_task(
- self.play_announcement(
- group_member,
- url=url,
- pre_announce=pre_announce,
- volume_level=volume_level,
- pre_announce_url=pre_announce_url,
- )
- )
- return
- self.logger.info(
- "Playback announcement to player %s (with pre-announce: %s): %s",
- player.display_name,
- pre_announce,
- url,
- )
- # create a PlayerMedia object for the announcement so
- # we can send a regular play-media call downstream
- announce_data = AnnounceData(
- announcement_url=url,
- pre_announce=pre_announce,
- pre_announce_url=pre_announce_url,
- )
- announcement = PlayerMedia(
- uri=self.mass.streams.get_announcement_url(player_id, url, announce_data),
- media_type=MediaType.ANNOUNCEMENT,
- title="Announcement",
- custom_data=announce_data,
- )
- # handle native announce support
- if native_announce_support:
- announcement_volume = self.get_announcement_volume(player_id, volume_level)
- await player.play_announcement(announcement, announcement_volume)
- return
- # use fallback/default implementation
- await self._play_announcement(player, announcement, volume_level)
- finally:
- player.extra_data[ATTR_ANNOUNCEMENT_IN_PROGRESS] = False
-
- @handle_player_command
- async def play_media(self, player_id: str, media: PlayerMedia) -> None:
- """Handle PLAY MEDIA on given player.
-
- - player_id: player_id of the player to handle the command.
- - media: The Media that needs to be played on the player.
- """
- player = self._get_player_with_redirect(player_id)
- # power on the player if needed
- if player.powered is False and player.power_control != PLAYER_CONTROL_NONE:
- await self.cmd_power(player.player_id, True)
- await player.play_media(media)
-
- @api_command("players/cmd/select_source")
- async def select_source(self, player_id: str, source: str) -> None:
- """
- Handle SELECT SOURCE command on given player.
-
- - player_id: player_id of the player to handle the command.
- - source: The ID of the source that needs to be activated/selected.
- """
- player = self.get(player_id, True)
- assert player is not None # for type checking
- if player.synced_to or player.active_group:
- raise PlayerCommandFailed(f"Player {player.display_name} is currently grouped")
- # check if player is already playing and source is different
- # in that case we need to stop the player first
- prev_source = player.active_source
- if prev_source and source != prev_source:
- if player.playback_state != PlaybackState.IDLE:
- await self.cmd_stop(player_id)
- await asyncio.sleep(0.5) # small delay to allow stop to process
- player.active_source = None
- player.current_media = None
- # check if source is a pluginsource
- # in that case the source id is the instance_id of the plugin provider
- if plugin_prov := self.mass.get_provider(source):
- await self._handle_select_plugin_source(player, plugin_prov)
- return
- # check if source is a mass queue
- # this can be used to restore the queue after a source switch
- if mass_queue := self.mass.player_queues.get(source):
- await self.mass.player_queues.play(mass_queue.queue_id)
- return
- # basic check if player supports source selection
- if PlayerFeature.SELECT_SOURCE not in player.supported_features:
- raise UnsupportedFeaturedException(
- f"Player {player.display_name} does not support source selection"
- )
- # basic check if source is valid for player
- if not any(x for x in player.source_list if x.id == source):
- raise PlayerCommandFailed(
- f"{source} is an invalid source for player {player.display_name}"
- )
- # forward to player
- await player.select_source(source)
-
- async def enqueue_next_media(self, player_id: str, media: PlayerMedia) -> None:
- """
- Handle enqueuing of a next media item on the player.
-
- :param player_id: player_id of the player to handle the command.
- :param media: The Media that needs to be enqueued on the player.
- :raises UnsupportedFeaturedException: if the player does not support enqueueing.
- :raises PlayerUnavailableError: if the player is not available.
- """
- player = self.get(player_id, raise_unavailable=True)
- assert player is not None # for type checking
- if PlayerFeature.ENQUEUE not in player.supported_features:
- raise UnsupportedFeaturedException(
- f"Player {player.display_name} does not support enqueueing"
- )
- async with self._player_throttlers[player_id]:
- await player.enqueue_next_media(media)
-
- @api_command("players/cmd/set_members")
- async def cmd_set_members(
- self,
- target_player: str,
- player_ids_to_add: list[str] | None = None,
- player_ids_to_remove: list[str] | None = None,
- ) -> None:
- """
- Join/unjoin given player(s) to/from target player.
-
- Will add the given player(s) to the target player (sync leader or group player).
-
- :param target_player: player_id of the syncgroup leader or group player.
- :param player_ids_to_add: List of player_id's to add to the target player.
- :param player_ids_to_remove: List of player_id's to remove from the target player.
-
- :raises UnsupportedFeaturedException: if the target player does not support grouping.
- :raises PlayerUnavailableError: if the target player is not available.
- """
- parent_player: Player | None = self.get(target_player, True)
- assert parent_player is not None # for type checking
- if PlayerFeature.SET_MEMBERS not in parent_player.supported_features:
- msg = f"Player {parent_player.name} does not support group commands"
- raise UnsupportedFeaturedException(msg)
-
- if parent_player.synced_to:
- # guard edge case: player already synced to another player
- raise PlayerCommandFailed(
- f"Player {parent_player.name} is already synced to another player on its own, "
- "you need to ungroup it first before you can join other players to it.",
- )
-
- # filter all player ids on compatibility and availability
- final_player_ids_to_add: list[str] = []
- for child_player_id in player_ids_to_add or []:
- if child_player_id == target_player:
- continue
- if child_player_id in final_player_ids_to_add:
- continue
- if not (child_player := self.get(child_player_id)) or not child_player.available:
- self.logger.warning("Player %s is not available", child_player_id)
- continue
-
- # check if player can be synced/grouped with the target player
- if not (
- child_player_id in parent_player.can_group_with
- or child_player.provider.lookup_key in parent_player.can_group_with
- or "*" in parent_player.can_group_with
- ):
- raise UnsupportedFeaturedException(
- f"Player {child_player.name} can not be grouped with {parent_player.name}"
- )
-
- if (
- child_player.synced_to
- and child_player.synced_to == target_player
- and child_player_id in parent_player.group_members
- ):
- continue # already synced to this target
-
- # Check if player is already part of another group and try to automatically ungroup it
- # first. If that fails, power off the group
- if child_player.active_group and child_player.active_group != target_player:
- if (
- other_group := self.get(child_player.active_group)
- ) and PlayerFeature.SET_MEMBERS in other_group.supported_features:
- self.logger.warning(
- "Player %s is already part of another group (%s), "
- "removing from that group first",
- child_player.name,
- child_player.active_group,
- )
- if child_player.player_id in other_group.static_group_members:
- self.logger.warning(
- "Player %s is a static member of group %s: removing is not possible, "
- "powering the group off instead",
- child_player.name,
- child_player.active_group,
- )
- await self.cmd_power(child_player.active_group, False)
- else:
- await other_group.set_members(player_ids_to_remove=[child_player.player_id])
- else:
- self.logger.warning(
- "Player %s is already part of another group (%s), powering it off first",
- child_player.name,
- child_player.active_group,
- )
- await self.cmd_power(child_player.active_group, False)
- elif child_player.synced_to and child_player.synced_to != target_player:
- self.logger.warning(
- "Player %s is already synced to another player, ungrouping first",
- child_player.name,
- )
- await self.cmd_ungroup(child_player.player_id)
-
- # power on the player if needed
- if not child_player.powered and child_player.power_control != PLAYER_CONTROL_NONE:
- await self.cmd_power(child_player.player_id, True, skip_update=True)
- # if we reach here, all checks passed
- final_player_ids_to_add.append(child_player_id)
-
- final_player_ids_to_remove: list[str] = []
- if player_ids_to_remove:
- static_members = set(parent_player.static_group_members)
- for child_player_id in player_ids_to_remove:
- if child_player_id == target_player:
- raise UnsupportedFeaturedException(
- f"Cannot remove {parent_player.name} from itself as a member!"
- )
- if child_player_id not in parent_player.group_members:
- continue
- if child_player_id in static_members:
- raise UnsupportedFeaturedException(
- f"Cannot remove {child_player_id} from {parent_player.name} "
- "as it is a static member of this group"
- )
- final_player_ids_to_remove.append(child_player_id)
-
- # forward command to the player after all (base) sanity checks
- async with self._player_throttlers[target_player]:
- await parent_player.set_members(
- player_ids_to_add=final_player_ids_to_add or None,
- player_ids_to_remove=final_player_ids_to_remove or None,
- )
-
- @api_command("players/cmd/group")
- @handle_player_command
- async def cmd_group(self, player_id: str, target_player: str) -> None:
- """Handle GROUP command for given player.
-
- Join/add the given player(id) to the given (leader) player/sync group.
- If the target player itself is already synced to another player, this may fail.
- If the player can not be synced with the given target player, this may fail.
-
- :param player_id: player_id of the player to handle the command.
- :param target_player: player_id of the syncgroup leader or group player.
-
- :raises UnsupportedFeaturedException: if the target player does not support grouping.
- :raises PlayerCommandFailed: if the target player is already synced to another player.
- :raises PlayerUnavailableError: if the target player is not available.
- :raises PlayerCommandFailed: if the player is already grouped to another player.
- """
- await self.cmd_set_members(target_player, player_ids_to_add=[player_id])
-
- @api_command("players/cmd/group_many")
- async def cmd_group_many(self, target_player: str, child_player_ids: list[str]) -> None:
- """
- Join given player(s) to target player.
-
- Will add the given player(s) to the target player (sync leader or group player).
- NOTE: This is a (deprecated) alias for cmd_set_members.
- """
- await self.cmd_set_members(target_player, player_ids_to_add=child_player_ids)
-
- @api_command("players/cmd/ungroup")
- @handle_player_command
- async def cmd_ungroup(self, player_id: str) -> None:
- """Handle UNGROUP command for given player.
-
- Remove the given player from any (sync)groups it currently is synced to.
- If the player is not currently grouped to any other player,
- this will silently be ignored.
-
- NOTE: This is a (deprecated) alias for cmd_set_members.
- """
- if not (player := self.get(player_id)):
- self.logger.warning("Player %s is not available", player_id)
- return
-
- if (
- player.active_group
- and (group_player := self.get(player.active_group))
- and (PlayerFeature.SET_MEMBERS in group_player.supported_features)
- ):
- # the player is part of a (permanent) groupplayer and the user tries to ungroup
- if player_id in group_player.static_group_members:
- raise UnsupportedFeaturedException(
- f"Player {player.name} is a static member of group {group_player.name} "
- "and cannot be removed from that group!"
- )
- await group_player.set_members(player_ids_to_remove=[player_id])
- return
-
- if player.synced_to and (synced_player := self.get(player.synced_to)):
- # player is a sync member
- await synced_player.set_members(player_ids_to_remove=[player_id])
- return
-
- if not (player.synced_to or player.group_members):
- return # nothing to do
-
- if PlayerFeature.SET_MEMBERS not in player.supported_features:
- self.logger.warning("Player %s does not support (un)group commands", player.name)
- return
-
- # forward command to the player once all checks passed
- await player.ungroup()
-
- @api_command("players/cmd/ungroup_many")
- async def cmd_ungroup_many(self, player_ids: list[str]) -> None:
- """Handle UNGROUP command for all the given players."""
- for player_id in list(player_ids):
- await self.cmd_ungroup(player_id)
-
- @api_command("players/create_group_player")
- async def create_group_player(
- self, provider: str, name: str, members: list[str], dynamic: bool = True
- ):
- """
- Create a new (permanent) Group Player.
-
- :param provider: The provider to create the group player for
- :param name: Name of the new group player
- :param members: List of player ids to add to the group
- :param dynamic: Whether the group is dynamic (members can change)
- """
- if not (provider_instance := self.mass.get_provider(provider)):
- raise ProviderUnavailableError(f"Provider {provider} not found")
- provider_instance.check_feature(ProviderFeature.CREATE_GROUP_PLAYER)
- provider_instance = cast("PlayerProvider", provider_instance)
- # create the group player
- return await provider_instance.create_group_player(name, members, dynamic)
-
- @api_command("players/remove_group_player")
- async def remove_group_player(self, player_id: str) -> None:
- """
- Remove a group player.
-
- :param player_id: ID of the group player to remove.
- """
- if not (player := self.get(player_id)):
- # we simply permanently delete the player by wiping its config
- self.mass.config.remove(f"players/{player_id}")
- return
- if player.type != PlayerType.GROUP:
- raise UnsupportedFeaturedException(
- f"Player {player.display_name} is not a group player"
- )
- player.provider.check_feature(ProviderFeature.REMOVE_GROUP_PLAYER)
- await player.provider.remove_group_player(player_id)
-
- @api_command("players/add_currently_playing_to_favorites")
- async def add_currently_playing_to_favorites(self, player_id: str) -> None:
- """
- Add the currently playing item/track on given player to the favorites.
-
- This tries to resolve the currently playing media to an actual media item
- and add that to the favorites in the library.
-
- Will raise an error if the player is not currently playing anything
- or if the currently playing media can not be resolved to a media item.
- """
- player = self._get_player_with_redirect(player_id)
- # handle mass player queue active
- if mass_queue := self.get_active_queue(player):
- if not (current_item := mass_queue.current_item) or not current_item.media_item:
- raise PlayerCommandFailed("No current item to add to favorites")
- # if we're playing a radio station, try to resolve the currently playing track
- if current_item.media_item.media_type == MediaType.RADIO:
- if not (
- (streamdetails := mass_queue.current_item.streamdetails)
- and (stream_title := streamdetails.stream_title)
- and " - " in stream_title
- ):
- # no stream title available, so we can't resolve the track
- # this can happen if the radio station does not provide metadata
- # or there's a commercial break
- # Possible future improvement could be to actually detect the song with a
- # shazam-like approach.
- raise PlayerCommandFailed("No current item to add to favorites")
- # send the streamtitle into a global search query
- search_artist, search_title_title = stream_title.split(" - ", 1)
- # strip off any additional comments in the title (such as from Radio Paradise)
- search_title_title = search_title_title.split(" | ")[0].strip()
- if track := await self.mass.music.get_track_by_name(
- search_title_title, search_artist
- ):
- # we found a track, so add it to the favorites
- await self.mass.music.add_item_to_favorites(track)
- return
- # we could not resolve the track, so raise an error
- raise PlayerCommandFailed("No current item to add to favorites")
-
- # else: any other media item, just add it to the favorites directly
- await self.mass.music.add_item_to_favorites(current_item.media_item)
- return
-
- # guard for player with no active source
- if not player.active_source:
- raise PlayerCommandFailed("Player has no active source")
- # handle other source active using the current_media with uri
- if current_media := player.current_media:
- # prefer the uri of the current media item
- if current_media.uri:
- with suppress(MusicAssistantError):
- await self.mass.music.add_item_to_favorites(current_media.uri)
- return
- # fallback to search based on artist and title (and album if available)
- if current_media.artist and current_media.title:
- if track := await self.mass.music.get_track_by_name(
- current_media.title,
- current_media.artist,
- current_media.album,
- ):
- # we found a track, so add it to the favorites
- await self.mass.music.add_item_to_favorites(track)
- return
- # if we reach here, we could not resolve the currently playing item
- raise PlayerCommandFailed("No current item to add to favorites")
-
- async def register(self, player: Player) -> None:
- """Register a player on the Player Controller."""
- if self.mass.closing:
- return
- player_id = player.player_id
-
- if player_id in self._players:
- msg = f"Player {player_id} is already registered!"
- raise AlreadyRegisteredError(msg)
-
- # ignore disabled players
- if not player.enabled:
- return
-
- # register throttler for this player
- self._player_throttlers[player_id] = Throttler(1, 0.05)
-
- # restore 'fake' power state from cache if available
- cached_value = await self.mass.cache.get(
- key=player.player_id,
- provider=self.domain,
- category=CACHE_CATEGORY_PLAYER_POWER,
- default=False,
- )
- if cached_value is not None:
- player.extra_data[ATTR_FAKE_POWER] = cached_value
-
- # finally actually register it
- self._players[player_id] = player
-
- # ensure we fetch and set the latest/full config for the player
- player_config = await self.mass.config.get_player_config(player_id)
- player.set_config(player_config)
- # call hook after the player is registered and config is set
- await player.on_config_updated()
- # always call update to fix special attributes like display name, group volume etc.
- player.update_state()
-
- self.logger.info(
- "Player registered: %s/%s",
- player_id,
- player.display_name,
- )
- # signal event that a player was added
- self.mass.signal_event(EventType.PLAYER_ADDED, object_id=player.player_id, data=player)
-
- # register playerqueue for this player
- await self.mass.player_queues.on_player_register(player)
-
- async def register_or_update(self, player: Player) -> None:
- """Register a new player on the controller or update existing one."""
- if self.mass.closing:
- return
-
- if player.player_id in self._players:
- self._players[player.player_id] = player
- player.update_state()
- return
-
- await self.register(player)
-
- def trigger_player_update(self, player_id: str, force_update: bool = False) -> None:
- """Trigger an update for the given player."""
- if self.mass.closing:
- return
- player = self.get(player_id, True)
- assert player is not None # for type checker
- self.mass.loop.call_soon(player.update_state, force_update)
-
- async def unregister(self, player_id: str, permanent: bool = False) -> None:
- """
- Unregister a player from the player controller.
-
- Called (by a PlayerProvider) when a player is removed
- or no longer available (for a longer period of time).
-
- This will remove the player from the player controller and
- optionally remove the player's config from the mass config.
-
- - player_id: player_id of the player to unregister.
- - permanent: if True, remove the player permanently by deleting
- the player's config from the mass config. If False, the player config will not be removed,
- allowing for re-registration (with the same config) later.
-
- If the player is not registered, this will silently be ignored.
- """
- player = self._players.get(player_id)
- if player is None:
- return
- await self._cleanup_player_memberships(player_id)
- del self._players[player_id]
- self.logger.info("Player removed: %s", player.name)
- self.mass.player_queues.on_player_remove(player_id, permanent=permanent)
- await player.on_unload()
- if permanent:
- self.delete_player_config(player_id)
- self.mass.signal_event(EventType.PLAYER_REMOVED, player_id)
-
- @api_command("players/remove")
- async def remove(self, player_id: str) -> None:
- """
- Remove a player from a provider.
-
- Can only be called when a PlayerProvider supports ProviderFeature.REMOVE_PLAYER.
- """
- player = self.get(player_id)
- if player is None:
- # we simply permanently delete the player config since it is not registered
- self.delete_player_config(player_id)
- return
- if player.type == PlayerType.GROUP:
- # Handle group player removal
- await player.provider.remove_group_player(player_id)
- return
- player.provider.check_feature(ProviderFeature.REMOVE_PLAYER)
- await player.provider.remove_player(player_id)
- # check for group memberships that need to be updated
- if player.active_group and (group_player := self.mass.players.get(player.active_group)):
- # try to remove from the group
- with suppress(UnsupportedFeaturedException, PlayerCommandFailed):
- await group_player.set_members(
- player_ids_to_remove=[player_id],
- )
- # We removed the player and can now clean up its config
- self.delete_player_config(player_id)
-
- def delete_player_config(self, player_id: str) -> None:
- """
- Permanently delete a player's configuration.
-
- Should only be called for players that are not registered by the player controller.
- """
- # we simply permanently delete the player by wiping its config
- conf_key = f"{CONF_PLAYERS}/{player_id}"
- dsp_conf_key = f"{CONF_PLAYER_DSP}/{player_id}"
- for key in (conf_key, dsp_conf_key):
- self.mass.config.remove(key)
-
- def signal_player_state_update(
- self,
- player: Player,
- changed_values: dict[str, tuple[Any, Any]],
- force_update: bool = False,
- skip_forward: bool = False,
- ) -> None:
- """
- Signal a player state update.
-
- Called by a Player when its state has changed.
- This will update the player state in the controller and signal the event bus.
- """
- player_id = player.player_id
- if self.mass.closing:
- return
-
- # ignore updates for disabled players
- if not player.enabled and "enabled" not in changed_values:
- return
-
- if len(changed_values) == 0 and not force_update:
- # nothing changed
- return
-
- # always signal update to the playerqueue
- self.mass.player_queues.on_player_update(player, changed_values)
-
- if changed_values.keys() == {"elapsed_time"} and not force_update:
- # ignore elapsed_time only changes
- prev_value = changed_values["elapsed_time"][0] or 0
- new_value = changed_values["elapsed_time"][1] or 0
- if abs(prev_value - new_value) < 30:
- # ignore small changes in elapsed time
- return
-
- # handle DSP reload of the leader when grouping/ungrouping
- if ATTR_GROUP_MEMBERS in changed_values:
- prev_group_members, new_group_members = changed_values[ATTR_GROUP_MEMBERS]
- self._handle_group_dsp_change(player, prev_group_members or [], new_group_members)
-
- if ATTR_GROUP_MEMBERS in changed_values:
- # Removed group members also need to be updated since they are no longer part
- # of this group and are available for playback again
- prev_group_members = changed_values[ATTR_GROUP_MEMBERS][0] or []
- new_group_members = changed_values[ATTR_GROUP_MEMBERS][1] or []
- removed_members = set(prev_group_members) - set(new_group_members)
- for player_id in removed_members:
- if removed_player := self.get(player_id):
- removed_player.update_state()
-
- became_inactive = False
- if "available" in changed_values:
- became_inactive = changed_values["available"][1] is False
- if not became_inactive and "enabled" in changed_values:
- became_inactive = changed_values["enabled"][1] is False
- if became_inactive and (player.active_group or player.synced_to):
- self.mass.create_task(self._cleanup_player_memberships(player.player_id))
-
- # signal player update on the eventbus
- self.mass.signal_event(EventType.PLAYER_UPDATED, object_id=player_id, data=player)
-
- if skip_forward and not force_update:
- return
-
- # update/signal group player(s) child's when group updates
- for child_player in self.iter_group_members(player, exclude_self=True):
- child_player.update_state()
- # update/signal group player(s) when child updates
- for group_player in self._get_player_groups(player, powered_only=False):
- group_player.update_state()
- # update/signal manually synced to player when child updates
- if (synced_to := player.synced_to) and (synced_to_player := self.get(synced_to)):
- synced_to_player.update_state()
- # update/signal active groups when a group member updates
- if (active_group := player.active_group) and (
- active_group_player := self.get(active_group)
- ):
- active_group_player.update_state()
-
- async def register_player_control(self, player_control: PlayerControl) -> None:
- """Register a new PlayerControl on the controller."""
- if self.mass.closing:
- return
- control_id = player_control.id
-
- if control_id in self._controls:
- msg = f"PlayerControl {control_id} is already registered"
- raise AlreadyRegisteredError(msg)
-
- # make sure that the playercontrol's provider is set to the instance_id
- prov = self.mass.get_provider(player_control.provider)
- if not prov or prov.instance_id != player_control.provider:
- raise RuntimeError(f"Invalid provider ID given: {player_control.provider}")
-
- self._controls[control_id] = player_control
-
- self.logger.info(
- "PlayerControl registered: %s/%s",
- control_id,
- player_control.name,
- )
-
- # always call update to update any attached players etc.
- self.update_player_control(player_control.id)
-
- async def register_or_update_player_control(self, player_control: PlayerControl) -> None:
- """Register a new playercontrol on the controller or update existing one."""
- if self.mass.closing:
- return
- if player_control.id in self._controls:
- self._controls[player_control.id] = player_control
- self.update_player_control(player_control.id)
- return
- await self.register_player_control(player_control)
-
- def update_player_control(self, control_id: str) -> None:
- """Update playercontrol state."""
- if self.mass.closing:
- return
- # update all players that are using this control
- for player in self._players.values():
- if control_id in (player.power_control, player.volume_control, player.mute_control):
- self.mass.loop.call_soon(player.update_state)
-
- def remove_player_control(self, control_id: str) -> None:
- """Remove a player_control from the player manager."""
- control = self._controls.pop(control_id, None)
- if control is None:
- return
- self._controls.pop(control_id, None)
- self.logger.info("PlayerControl removed: %s", control.name)
-
- def get_player_provider(self, player_id: str) -> PlayerProvider:
- """Return PlayerProvider for given player."""
- player = self._players[player_id]
- assert player # for type checker
- return player.provider
-
- def get_active_queue(self, player: Player) -> PlayerQueue | None:
- """Return the current active queue for a player (if any)."""
- # account for player that is synced (sync child)
- if player.synced_to and player.synced_to != player.player_id:
- if sync_leader := self.get(player.synced_to):
- return self.get_active_queue(sync_leader)
- # handle active group player
- if player.active_group and player.active_group != player.player_id:
- if group_player := self.get(player.active_group):
- return self.get_active_queue(group_player)
- # active_source may be filled queue id (or None)
- active_source = player.active_source or player.player_id
- if active_queue := self.mass.player_queues.get(active_source):
- return active_queue
- return None
-
- async def set_group_volume(self, group_player: Player, volume_level: int) -> None:
- """Handle adjusting the overall/group volume to a playergroup (or synced players)."""
- cur_volume = group_player.state.group_volume
- volume_dif = volume_level - cur_volume
- coros = []
- # handle group volume by only applying the volume to powered members
- for child_player in self.iter_group_members(
- group_player, only_powered=True, exclude_self=False
- ):
- if child_player.volume_control == PLAYER_CONTROL_NONE:
- continue
- cur_child_volume = child_player.volume_level or 0
- new_child_volume = int(cur_child_volume + volume_dif)
- new_child_volume = max(0, new_child_volume)
- new_child_volume = min(100, new_child_volume)
- coros.append(self.cmd_volume_set(child_player.player_id, new_child_volume))
- await asyncio.gather(*coros)
-
- def get_announcement_volume(self, player_id: str, volume_override: int | None) -> int | None:
- """Get the (player specific) volume for a announcement."""
- volume_strategy = self.mass.config.get_raw_player_config_value(
- player_id,
- CONF_ENTRY_ANNOUNCE_VOLUME_STRATEGY.key,
- CONF_ENTRY_ANNOUNCE_VOLUME_STRATEGY.default_value,
- )
- volume_strategy_volume = self.mass.config.get_raw_player_config_value(
- player_id,
- CONF_ENTRY_ANNOUNCE_VOLUME.key,
- CONF_ENTRY_ANNOUNCE_VOLUME.default_value,
- )
- if volume_strategy == "none":
- return None
- volume_level = volume_override
- if volume_level is None and volume_strategy == "absolute":
- volume_level = volume_strategy_volume
- elif volume_level is None and volume_strategy == "relative":
- player = self.get(player_id)
- volume_level = player.volume_level + volume_strategy_volume
- elif volume_level is None and volume_strategy == "percentual":
- player = self.get(player_id)
- percentual = (player.volume_level / 100) * volume_strategy_volume
- volume_level = player.volume_level + percentual
- if volume_level is not None:
- announce_volume_min = self.mass.config.get_raw_player_config_value(
- player_id,
- CONF_ENTRY_ANNOUNCE_VOLUME_MIN.key,
- CONF_ENTRY_ANNOUNCE_VOLUME_MIN.default_value,
- )
- volume_level = max(announce_volume_min, volume_level)
- announce_volume_max = self.mass.config.get_raw_player_config_value(
- player_id,
- CONF_ENTRY_ANNOUNCE_VOLUME_MAX.key,
- CONF_ENTRY_ANNOUNCE_VOLUME_MAX.default_value,
- )
- volume_level = min(announce_volume_max, volume_level)
- # ensure the result is an integer
- return None if volume_level is None else int(volume_level)
-
- def iter_group_members(
- self,
- group_player: Player,
- only_powered: bool = False,
- only_playing: bool = False,
- active_only: bool = False,
- exclude_self: bool = True,
- ) -> Iterator[Player]:
- """Get (child) players attached to a group player or syncgroup."""
- for child_id in list(group_player.group_members):
- if child_player := self.get(child_id, False):
- if not child_player.available or not child_player.enabled:
- continue
- if only_powered and child_player.powered is False:
- continue
- if active_only and child_player.active_group != group_player.player_id:
- continue
- if exclude_self and child_player.player_id == group_player.player_id:
- continue
- if only_playing and child_player.playback_state not in (
- PlaybackState.PLAYING,
- PlaybackState.PAUSED,
- ):
- continue
- yield child_player
-
- async def wait_for_state(
- self,
- player: Player,
- wanted_state: PlaybackState,
- timeout: float = 60.0,
- minimal_time: float = 0,
- ) -> None:
- """Wait for the given player to reach the given state."""
- start_timestamp = time.time()
- self.logger.debug(
- "Waiting for player %s to reach state %s", player.display_name, wanted_state
- )
- try:
- async with asyncio.timeout(timeout):
- while player.playback_state != wanted_state:
- await asyncio.sleep(0.1)
-
- except TimeoutError:
- self.logger.debug(
- "Player %s did not reach state %s within the timeout of %s seconds",
- player.display_name,
- wanted_state,
- timeout,
- )
- elapsed_time = round(time.time() - start_timestamp, 2)
- if elapsed_time < minimal_time:
- self.logger.debug(
- "Player %s reached state %s too soon (%s vs %s seconds) - add fallback sleep...",
- player.display_name,
- wanted_state,
- elapsed_time,
- minimal_time,
- )
- await asyncio.sleep(minimal_time - elapsed_time)
- else:
- self.logger.debug(
- "Player %s reached state %s within %s seconds",
- player.display_name,
- wanted_state,
- elapsed_time,
- )
-
- async def on_player_config_change(self, config: PlayerConfig, changed_keys: set[str]) -> None:
- """Call (by config manager) when the configuration of a player changes."""
- player_disabled = "enabled" in changed_keys and not config.enabled
- # signal player provider that the player got enabled/disabled
- if player_provider := self.mass.get_provider(config.provider):
- assert isinstance(player_provider, PlayerProvider) # for type checking
- if "enabled" in changed_keys and not config.enabled:
- player_provider.on_player_disabled(config.player_id)
- elif "enabled" in changed_keys and config.enabled:
- player_provider.on_player_enabled(config.player_id)
- # ensure player state gets updated with any updated config
- if not (player := self.get(config.player_id)):
- return # guard against player not being registered (yet)
- player.set_config(config)
- await player.on_config_updated()
- player.update_state()
- resume_queue: PlayerQueue | None = (
- self.mass.player_queues.get(player.active_source) if player.active_source else None
- )
- if player_disabled:
- # edge case: ensure that the player is powered off if the player gets disabled
- if player.power_control != PLAYER_CONTROL_NONE:
- await self.cmd_power(config.player_id, False)
- elif player.playback_state != PlaybackState.IDLE:
- await self.cmd_stop(config.player_id)
- player.available = False
- # if the PlayerQueue was playing, restart playback
- # TODO: add property to ConfigEntry if it requires a restart of playback on change
- elif not player_disabled and resume_queue and resume_queue.state == PlaybackState.PLAYING:
- # always stop first to ensure the player uses the new config
- await self.mass.player_queues.stop(resume_queue.queue_id)
- self.mass.call_later(1, self.mass.player_queues.resume, resume_queue.queue_id, False)
-
- async def on_player_dsp_change(self, player_id: str) -> None:
- """Call (by config manager) when the DSP settings of a player change."""
- # signal player provider that the config changed
- if not (player := self.get(player_id)):
- return
- if player.playback_state == PlaybackState.PLAYING:
- self.logger.info("Restarting playback of Player %s after DSP change", player_id)
- # this will restart the queue stream/playback
- if player.mass_queue_active:
- self.mass.call_later(0, self.mass.player_queues.resume, player.active_source, False)
- return
- # if the player is not using a queue, we need to stop and start playback
- await self.cmd_stop(player_id)
- await self.cmd_play(player_id)
-
- async def _cleanup_player_memberships(self, player_id: str) -> None:
- """Ensure a player is detached from any groups or syncgroups."""
- if not (player := self.get(player_id)):
- return
-
- if (
- player.active_group
- and (group := self.get(player.active_group))
- and group.supports_feature(PlayerFeature.SET_MEMBERS)
- ):
- # Ungroup the player if its part of an active group, this will ignore
- # static_group_members since that is only checked when using cmd_set_members
- with suppress(UnsupportedFeaturedException, PlayerCommandFailed):
- await group.set_members(player_ids_to_remove=[player_id])
- elif player.synced_to and player.supports_feature(PlayerFeature.SET_MEMBERS):
- # Remove the player if it was synced, otherwise it will still show as
- # synced to the other player after it gets registered again
- with suppress(UnsupportedFeaturedException, PlayerCommandFailed):
- await player.ungroup()
-
- def _get_player_with_redirect(self, player_id: str) -> Player:
- """Get player with check if playback related command should be redirected."""
- player = self.get(player_id, True)
- assert player is not None # for type checking
- if player.synced_to and (sync_leader := self.get(player.synced_to)):
- self.logger.info(
- "Player %s is synced to %s and can not accept "
- "playback related commands itself, "
- "redirected the command to the sync leader.",
- player.name,
- sync_leader.name,
- )
- return sync_leader
- if player.active_group and (active_group := self.get(player.active_group)):
- self.logger.info(
- "Player %s is part of a playergroup and can not accept "
- "playback related commands itself, "
- "redirected the command to the group leader.",
- player.name,
- )
- return active_group
- return player
-
- def _get_player_groups(
- self, player: Player, available_only: bool = True, powered_only: bool = False
- ) -> Iterator[Player]:
- """Return all groupplayers the given player belongs to."""
- for _player in self.all(return_unavailable=not available_only):
- if _player.player_id == player.player_id:
- continue
- if _player.type != PlayerType.GROUP:
- continue
- if powered_only and _player.powered is False:
- continue
- if player.player_id in _player.group_members:
- yield _player
-
- async def _play_announcement( # noqa: PLR0915
- self,
- player: Player,
- announcement: PlayerMedia,
- volume_level: int | None = None,
- ) -> None:
- """Handle (default/fallback) implementation of the play announcement feature.
-
- This default implementation will;
- - stop playback of the current media (if needed)
- - power on the player (if needed)
- - raise the volume a bit
- - play the announcement (from given url)
- - wait for the player to finish playing
- - restore the previous power and volume
- - restore playback (if needed and if possible)
-
- This default implementation will only be used if the player
- (provider) has no native support for the PLAY_ANNOUNCEMENT feature.
- """
- prev_power = player.powered
- prev_state = player.playback_state
- prev_synced_to = player.synced_to
- prev_group = self.get(player.active_group) if player.active_group else None
- prev_source = player.active_source
- prev_queue = self.get_active_queue(player)
- prev_media = player.current_media
- prev_media_name = prev_media.title or prev_media.uri if prev_media else None
- if prev_synced_to:
- # ungroup player if its currently synced
- self.logger.debug(
- "Announcement to player %s - ungrouping player from %s...",
- player.display_name,
- prev_synced_to,
- )
- await self.cmd_ungroup(player.player_id)
- elif prev_group:
- # if the player is part of a group player, we need to ungroup it
- if PlayerFeature.SET_MEMBERS in prev_group.supported_features:
- self.logger.debug(
- "Announcement to player %s - ungrouping from group player %s...",
- player.display_name,
- prev_group.display_name,
- )
- await prev_group.set_members(player_ids_to_remove=[player.player_id])
- else:
- # if the player is part of a group player that does not support ungrouping,
- # we need to power off the groupplayer instead
- self.logger.debug(
- "Announcement to player %s - turning off group player %s...",
- player.display_name,
- prev_group.display_name,
- )
- await self.cmd_power(player.player_id, False)
- elif prev_state in (PlaybackState.PLAYING, PlaybackState.PAUSED):
- # normal/standalone player: stop player if its currently playing
- self.logger.debug(
- "Announcement to player %s - stop existing content (%s)...",
- player.display_name,
- prev_media_name,
- )
- await self.cmd_stop(player.player_id)
- # wait for the player to stop
- await self.wait_for_state(player, PlaybackState.IDLE, 10, 0.4)
- # adjust volume if needed
- # in case of a (sync) group, we need to do this for all child players
- prev_volumes: dict[str, int] = {}
- async with TaskManager(self.mass) as tg:
- for volume_player_id in player.group_members or (player.player_id,):
- if not (volume_player := self.get(volume_player_id)):
- continue
- # catch any players that have a different source active
- if (
- volume_player.active_source
- not in (
- player.active_source,
- volume_player.player_id,
- None,
- )
- and volume_player.playback_state == PlaybackState.PLAYING
- ):
- self.logger.warning(
- "Detected announcement to playergroup %s while group member %s is playing "
- "other content, this may lead to unexpected behavior.",
- player.display_name,
- volume_player.display_name,
- )
- tg.create_task(self.cmd_stop(volume_player.player_id))
- if volume_player.volume_control == PLAYER_CONTROL_NONE:
- continue
- if (prev_volume := volume_player.volume_level) is None:
- continue
- announcement_volume = self.get_announcement_volume(volume_player_id, volume_level)
- if announcement_volume is None:
- continue
- temp_volume = announcement_volume or player.volume_level
- if temp_volume != prev_volume:
- prev_volumes[volume_player_id] = prev_volume
- self.logger.debug(
- "Announcement to player %s - setting temporary volume (%s)...",
- volume_player.display_name,
- announcement_volume,
- )
- tg.create_task(
- self.cmd_volume_set(volume_player.player_id, announcement_volume)
- )
- # play the announcement
- self.logger.debug(
- "Announcement to player %s - playing the announcement on the player...",
- player.display_name,
- )
- await self.play_media(player_id=player.player_id, media=announcement)
- # wait for the player(s) to play
- await self.wait_for_state(player, PlaybackState.PLAYING, 10, minimal_time=0.1)
- # wait for the player to stop playing
- if not announcement.duration:
- media_info = await async_parse_tags(
- announcement.custom_data["url"], require_duration=True
- )
- announcement.duration = media_info.duration
- await self.wait_for_state(
- player,
- PlaybackState.IDLE,
- timeout=announcement.duration + 6,
- minimal_time=announcement.duration,
- )
- self.logger.debug(
- "Announcement to player %s - restore previous state...", player.display_name
- )
- # restore volume
- async with TaskManager(self.mass) as tg:
- for volume_player_id, prev_volume in prev_volumes.items():
- tg.create_task(self.cmd_volume_set(volume_player_id, prev_volume))
- await asyncio.sleep(0.2)
- player.current_media = prev_media
- player.active_source = prev_source
- # either power off the player or resume playing
- if not prev_power and player.power_control != PLAYER_CONTROL_NONE:
- await self.cmd_power(player.player_id, False)
- return
- elif prev_synced_to:
- await self.cmd_group(player.player_id, prev_synced_to)
- elif prev_group:
- if PlayerFeature.SET_MEMBERS in prev_group.supported_features:
- self.logger.debug(
- "Announcement to player %s - grouping back to group player %s...",
- player.display_name,
- prev_group.display_name,
- )
- await prev_group.set_members(player_ids_to_add=[player.player_id])
- elif prev_state == PlaybackState.PLAYING:
- # if the player is part of a group player that does not support set_members,
- # we need to restart the groupplayer
- self.logger.debug(
- "Announcement to player %s - restarting playback on group player %s...",
- player.display_name,
- prev_group.display_name,
- )
- await self.cmd_play(prev_group.player_id)
- elif prev_queue and prev_state == PlaybackState.PLAYING:
- await self.mass.player_queues.resume(prev_queue.queue_id, True)
- await self.wait_for_state(player, PlaybackState.PLAYING, 5)
- elif prev_state == PlaybackState.PLAYING:
- # player was playing something else - try to resume that here
- for source in player.source_list_state:
- if source.id == prev_source and not source.passive:
- await player.select_source(source.id)
- break
- else:
- # no source found, try to resume the previous media
- await self.cmd_play(player.player_id)
-
- async def _poll_players(self) -> None:
- """Background task that polls players for updates."""
- while True:
- for player in list(self._players.values()):
- # if the player is playing, update elapsed time every tick
- # to ensure the queue has accurate details
- player_playing = player.playback_state == PlaybackState.PLAYING
- if player_playing:
- self.mass.loop.call_soon(
- self.mass.player_queues.on_player_update,
- player,
- {"corrected_elapsed_time": player.corrected_elapsed_time},
- )
- # Poll player;
- if not player.needs_poll:
- continue
- try:
- last_poll: float = player.extra_data[ATTR_LAST_POLL]
- except KeyError:
- last_poll = 0.0
- if (self.mass.loop.time() - last_poll) < player.poll_interval:
- continue
- player.extra_data[ATTR_LAST_POLL] = self.mass.loop.time()
- try:
- await player.poll()
- except Exception as err:
- self.logger.warning(
- "Error while requesting latest state from player %s: %s",
- player.display_name,
- str(err),
- exc_info=err if self.logger.isEnabledFor(10) else None,
- )
- finally:
- # always update player state
- self.mass.loop.call_soon(player.update_state)
- await asyncio.sleep(1)
-
- async def _handle_select_plugin_source(
- self, player: Player, plugin_prov: PluginProvider
- ) -> None:
- """Handle playback/select of given plugin source on player."""
- plugin_source = plugin_prov.get_source()
- stream_url = await self.mass.streams.get_plugin_source_url(
- plugin_source.id, player.player_id
- )
- await self.play_media(
- player_id=player.player_id,
- media=PlayerMedia(
- uri=stream_url,
- media_type=MediaType.PLUGIN_SOURCE,
- title=plugin_source.name,
- custom_data={
- "provider": plugin_prov.instance_id,
- "source_id": plugin_source.id,
- "player_id": player.player_id,
- "audio_format": plugin_source.audio_format,
- },
- ),
- )
- # trigger player update to ensure the source is set
- self.trigger_player_update(player.player_id)
-
- def _handle_group_dsp_change(
- self, player: Player, prev_group_members: list[str], new_group_members: list[str]
- ) -> None:
- """Handle DSP reload when group membership changes."""
- prev_child_count = len(prev_group_members)
- new_child_count = len(new_group_members)
- is_player_group = player.type == PlayerType.GROUP
-
- # handle special case for PlayerGroups: since there are no leaders,
- # DSP still always work with a single player in the group.
- multi_device_dsp_threshold = 1 if is_player_group else 0
-
- prev_is_multiple_devices = prev_child_count > multi_device_dsp_threshold
- new_is_multiple_devices = new_child_count > multi_device_dsp_threshold
-
- if prev_is_multiple_devices == new_is_multiple_devices:
- return # no change in multi-device status
-
- supports_multi_device_dsp = PlayerFeature.MULTI_DEVICE_DSP in player.supported_features
-
- dsp_enabled: bool
- if player.type == PlayerType.GROUP:
- # Since player groups do not have leaders, we will use the only child
- # that was in the group before and after the change
- if prev_is_multiple_devices:
- if childs := new_group_members:
- # We shrank the group from multiple players to a single player
- # So the now only child will control the DSP
- dsp_enabled = self.mass.config.get_player_dsp_config(childs[0]).enabled
- else:
- dsp_enabled = False
- elif childs := prev_group_members:
- # We grew the group from a single player to multiple players,
- # let's see if the previous single player had DSP enabled
- dsp_enabled = self.mass.config.get_player_dsp_config(childs[0]).enabled
- else:
- dsp_enabled = False
- else:
- dsp_enabled = self.mass.config.get_player_dsp_config(player.player_id).enabled
-
- if dsp_enabled and not supports_multi_device_dsp:
- # We now know that the group configuration has changed so:
- # - multi-device DSP is not supported
- # - we switched from a group with multiple players to a single player
- # (or vice versa)
- # - the leader has DSP enabled
- self.mass.create_task(self.mass.players.on_player_dsp_change(player.player_id))
-
- def __iter__(self) -> Iterator[Player]:
- """Iterate over all players."""
- return iter(self._players.values())
--- /dev/null
+"""
+MusicAssistant PlayerController.
+
+Handles all logic to control supported players,
+which are provided by Player Providers.
+
+Note that the PlayerController has a concept of a 'player' and a 'playerstate'.
+The Player is the actual object that is provided by the provider,
+which incorporates the actual state of the player (e.g. volume, state, etc)
+and functions for controlling the player (e.g. play, pause, etc).
+
+The playerstate is the (final) state of the player, including any user customizations
+and transformations that are applied to the player.
+The playerstate is the object that is exposed to the outside world (via the API).
+"""
+
+from __future__ import annotations
+
+from .player_controller import PlayerController
+
+__all__ = ["PlayerController"]
--- /dev/null
+"""
+MusicAssistant PlayerController.
+
+Handles all logic to control supported players,
+which are provided by Player Providers.
+
+Note that the PlayerController has a concept of a 'player' and a 'playerstate'.
+The Player is the actual object that is provided by the provider,
+which incorporates the actual state of the player (e.g. volume, state, etc)
+and functions for controlling the player (e.g. play, pause, etc).
+
+The playerstate is the (final) state of the player, including any user customizations
+and transformations that are applied to the player.
+The playerstate is the object that is exposed to the outside world (via the API).
+"""
+
+from __future__ import annotations
+
+import asyncio
+import functools
+import time
+from contextlib import suppress
+from typing import TYPE_CHECKING, Any, Concatenate, TypedDict, cast
+
+from music_assistant_models.constants import (
+ PLAYER_CONTROL_FAKE,
+ PLAYER_CONTROL_NATIVE,
+ PLAYER_CONTROL_NONE,
+)
+from music_assistant_models.enums import (
+ EventType,
+ MediaType,
+ PlaybackState,
+ PlayerFeature,
+ PlayerType,
+ ProviderFeature,
+ ProviderType,
+)
+from music_assistant_models.errors import (
+ AlreadyRegisteredError,
+ MusicAssistantError,
+ PlayerCommandFailed,
+ PlayerUnavailableError,
+ ProviderUnavailableError,
+ UnsupportedFeaturedException,
+)
+from music_assistant_models.player_control import PlayerControl # noqa: TC002
+
+from music_assistant.constants import (
+ ANNOUNCE_ALERT_FILE,
+ ATTR_ANNOUNCEMENT_IN_PROGRESS,
+ ATTR_FAKE_MUTE,
+ ATTR_FAKE_POWER,
+ ATTR_FAKE_VOLUME,
+ ATTR_GROUP_MEMBERS,
+ ATTR_LAST_POLL,
+ ATTR_PREVIOUS_VOLUME,
+ CONF_AUTO_PLAY,
+ CONF_ENTRY_ANNOUNCE_VOLUME,
+ CONF_ENTRY_ANNOUNCE_VOLUME_MAX,
+ CONF_ENTRY_ANNOUNCE_VOLUME_MIN,
+ CONF_ENTRY_ANNOUNCE_VOLUME_STRATEGY,
+ CONF_ENTRY_TTS_PRE_ANNOUNCE,
+ CONF_PLAYER_DSP,
+ CONF_PLAYERS,
+ CONF_PRE_ANNOUNCE_CHIME_URL,
+)
+from music_assistant.helpers.api import api_command
+from music_assistant.helpers.tags import async_parse_tags
+from music_assistant.helpers.throttle_retry import Throttler
+from music_assistant.helpers.util import TaskManager, validate_announcement_chime_url
+from music_assistant.models.core_controller import CoreController
+from music_assistant.models.player import Player, PlayerMedia, PlayerState
+from music_assistant.models.player_provider import PlayerProvider
+from music_assistant.models.plugin import PluginProvider, PluginSource
+
+from .sync_groups import SyncGroupController, SyncGroupPlayer
+
+if TYPE_CHECKING:
+ from collections.abc import Awaitable, Callable, Coroutine, Iterator
+
+ from music_assistant_models.config_entries import CoreConfig, PlayerConfig
+ from music_assistant_models.player_queue import PlayerQueue
+
+CACHE_CATEGORY_PLAYER_POWER = 1
+
+
+class AnnounceData(TypedDict):
+ """Announcement data."""
+
+ announcement_url: str
+ pre_announce: bool
+ pre_announce_url: str
+
+
+def handle_player_command[PlayerControllerT: "PlayerController", **P, R](
+ func: Callable[Concatenate[PlayerControllerT, P], Awaitable[R]],
+) -> Callable[Concatenate[PlayerControllerT, P], Coroutine[Any, Any, R | None]]:
+ """Check and log commands to players."""
+
+ @functools.wraps(func)
+ async def wrapper(self: PlayerControllerT, *args: P.args, **kwargs: P.kwargs) -> R | None:
+ """Log and handle_player_command commands to players."""
+ player_id = kwargs["player_id"] if "player_id" in kwargs else args[0]
+ if (player := self._players.get(player_id)) is None or not player.available:
+ # player not existent
+ self.logger.warning(
+ "Ignoring command %s for unavailable player %s",
+ func.__name__,
+ player_id,
+ )
+ return
+
+ self.logger.debug(
+ "Handling command %s for player %s",
+ func.__name__,
+ player.display_name,
+ )
+ try:
+ await func(self, *args, **kwargs)
+ except Exception as err:
+ raise PlayerCommandFailed(str(err)) from err
+
+ return wrapper
+
+
+class PlayerController(CoreController):
+ """Controller holding all logic to control registered players."""
+
+ domain: str = "players"
+
+ def __init__(self, *args, **kwargs) -> None:
+ """Initialize core controller."""
+ super().__init__(*args, **kwargs)
+ self._players: dict[str, Player] = {}
+ self._controls: dict[str, PlayerControl] = {}
+ self.manifest.name = "Player Controller"
+ self.manifest.description = (
+ "Music Assistant's core controller which manages all players from all providers."
+ )
+ self.manifest.icon = "speaker-multiple"
+ self._poll_task: asyncio.Task | None = None
+ self._player_throttlers: dict[str, Throttler] = {}
+ self._announce_locks: dict[str, asyncio.Lock] = {}
+ self._sync_groups: SyncGroupController = SyncGroupController(self)
+
+ async def setup(self, config: CoreConfig) -> None:
+ """Async initialize of module."""
+ self._poll_task = self.mass.create_task(self._poll_players())
+
+ async def close(self) -> None:
+ """Cleanup on exit."""
+ if self._poll_task and not self._poll_task.done():
+ self._poll_task.cancel()
+
+ async def on_provider_loaded(self, provider: PlayerProvider) -> None:
+ """Handle logic when a provider is loaded."""
+ if ProviderFeature.SYNC_PLAYERS in provider.supported_features:
+ await self._sync_groups.on_provider_loaded(provider)
+
+ async def on_provider_unload(self, provider: PlayerProvider) -> None:
+ """Handle logic when a provider is (about to get) unloaded."""
+ if ProviderFeature.SYNC_PLAYERS in provider.supported_features:
+ await self._sync_groups.on_provider_unload(provider)
+
+ @property
+ def providers(self) -> list[PlayerProvider]:
+ """Return all loaded/running MusicProviders."""
+ return self.mass.get_providers(ProviderType.PLAYER) # type: ignore=return-value
+
+ def all(
+ self,
+ return_unavailable: bool = True,
+ return_disabled: bool = False,
+ provider_filter: str | None = None,
+ return_sync_groups: bool = True,
+ ) -> list[Player]:
+ """
+ Return all registered players.
+
+ :param return_unavailable [bool]: Include unavailable players.
+ :param return_disabled [bool]: Include disabled players.
+ :param provider_filter [str]: Optional filter by provider lookup key.
+
+ :return: List of Player objects.
+ """
+ return [
+ player
+ for player in self._players.values()
+ if (player.available or return_unavailable)
+ and (player.enabled or return_disabled)
+ and (provider_filter is None or player.provider.lookup_key == provider_filter)
+ and (return_sync_groups or not isinstance(player, SyncGroupPlayer))
+ ]
+
+ @api_command("players/all")
+ def all_states(
+ self,
+ return_unavailable: bool = True,
+ return_disabled: bool = False,
+ provider_filter: str | None = None,
+ ) -> list[PlayerState]:
+ """
+ Return PlayerState for all registered players.
+
+ :param return_unavailable [bool]: Include unavailable players.
+ :param return_disabled [bool]: Include disabled players.
+ :param provider_filter [str]: Optional filter by provider lookup key.
+
+ :return: List of PlayerState objects.
+ """
+ return [
+ player.state
+ for player in self.all(
+ return_unavailable=return_unavailable,
+ return_disabled=return_disabled,
+ provider_filter=provider_filter,
+ )
+ ]
+
+ def get(
+ self,
+ player_id: str,
+ raise_unavailable: bool = False,
+ ) -> Player | None:
+ """
+ Return Player by player_id.
+
+ :param player_id [str]: ID of the player.
+ :param raise_unavailable [bool]: Raise if player is unavailable.
+
+ :raises PlayerUnavailableError: If player is unavailable and raise_unavailable is True.
+ :return: Player object or None.
+ """
+ if player := self._players.get(player_id):
+ if (not player.available or not player.enabled) and raise_unavailable:
+ msg = f"Player {player_id} is not available"
+ raise PlayerUnavailableError(msg)
+ return player
+ if raise_unavailable:
+ msg = f"Player {player_id} is not available"
+ raise PlayerUnavailableError(msg)
+ return None
+
+ @api_command("players/get")
+ def get_state(
+ self,
+ player_id: str,
+ raise_unavailable: bool = False,
+ ) -> PlayerState | None:
+ """
+ Return PlayerState by player_id.
+
+ :param player_id [str]: ID of the player.
+ :param raise_unavailable [bool]: Raise if player is unavailable.
+
+ :raises PlayerUnavailableError: If player is unavailable and raise_unavailable is True.
+ :return: Player object or None.
+ """
+ if player := self.get(player_id, raise_unavailable):
+ return player.state
+ return None
+
+ def get_player_by_name(self, name: str) -> Player | None:
+ """
+ Return Player by name.
+
+ :param name: Name of the player.
+ :return: Player object or None.
+ """
+ return next((x for x in self._players.values() if x.name == name), None)
+
+ @api_command("players/get_by_name")
+ def get_player_state_by_name(self, name: str) -> PlayerState | None:
+ """
+ Return PlayerState by name.
+
+ :param name: Name of the player.
+ :return: PlayerState object or None.
+ """
+ if player := self.get_player_by_name(name):
+ return player.state
+ return None
+
+ @api_command("players/player_controls")
+ def player_controls(
+ self,
+ ) -> list[PlayerControl]:
+ """Return all registered playercontrols."""
+ return list(self._controls.values())
+
+ @api_command("players/player_control")
+ def get_player_control(
+ self,
+ control_id: str,
+ ) -> PlayerControl | None:
+ """
+ Return PlayerControl by control_id.
+
+ :param control_id: ID of the player control.
+ :return: PlayerControl object or None.
+ """
+ if control := self._controls.get(control_id):
+ return control
+ return None
+
+ @api_command("players/plugin_sources")
+ def get_plugin_sources(self) -> list[PluginSource]:
+ """Return all available plugin sources."""
+ return [
+ plugin_prov.get_source()
+ for plugin_prov in self.mass.get_providers(ProviderType.PLUGIN)
+ if isinstance(plugin_prov, PluginProvider)
+ and ProviderFeature.AUDIO_SOURCE in plugin_prov.supported_features
+ ]
+
+ @api_command("players/plugin_source")
+ def get_plugin_source(
+ self,
+ source_id: str,
+ ) -> PluginSource | None:
+ """
+ Return PluginSource by source_id.
+
+ :param source_id: ID of the plugin source.
+ :return: PluginSource object or None.
+ """
+ for plugin_prov in self.mass.get_providers(ProviderType.PLUGIN):
+ assert isinstance(plugin_prov, PluginProvider) # for type checking
+ if ProviderFeature.AUDIO_SOURCE not in plugin_prov.supported_features:
+ continue
+ if (source := plugin_prov.get_source()) and source.id == source_id:
+ return source
+ return None
+
+ # Player commands
+
+ @api_command("players/cmd/stop")
+ @handle_player_command
+ async def cmd_stop(self, player_id: str) -> None:
+ """Send STOP command to given player.
+
+ - player_id: player_id of the player to handle the command.
+ """
+ player = self._get_player_with_redirect(player_id)
+ # Redirect to queue controller if it is active
+ if active_queue := self.get_active_queue(player):
+ await self.mass.player_queues.stop(active_queue.queue_id)
+ return
+ # handle command on player directly
+ async with self._player_throttlers[player.player_id]:
+ await player.stop()
+
+ @api_command("players/cmd/play")
+ @handle_player_command
+ async def cmd_play(self, player_id: str) -> None:
+ """Send PLAY (unpause) command to given player.
+
+ - player_id: player_id of the player to handle the command.
+ """
+ player = self._get_player_with_redirect(player_id)
+ if player.playback_state == PlaybackState.PLAYING:
+ self.logger.info(
+ "Ignore PLAY request to player %s: player is already playing", player.display_name
+ )
+ return
+ # Redirect to queue controller if it is active
+ if active_queue := self.get_active_queue(player):
+ await self.mass.player_queues.play(active_queue.queue_id)
+ return
+ # handle command on player directly
+ async with self._player_throttlers[player.player_id]:
+ await player.play()
+
+ @api_command("players/cmd/pause")
+ @handle_player_command
+ async def cmd_pause(self, player_id: str) -> None:
+ """Send PAUSE command to given player.
+
+ - player_id: player_id of the player to handle the command.
+ """
+ player = self._get_player_with_redirect(player_id)
+ # Redirect to queue controller if it is active
+ if active_queue := self.get_active_queue(player):
+ await self.mass.player_queues.pause(active_queue.queue_id)
+ return
+ if PlayerFeature.PAUSE not in player.supported_features:
+ # if player does not support pause, we need to send stop
+ self.logger.debug(
+ "Player %s does not support pause, using STOP instead",
+ player.display_name,
+ )
+ await self.cmd_stop(player.player_id)
+ return
+ # handle command on player directly
+ await player.pause()
+
+ @api_command("players/cmd/play_pause")
+ async def cmd_play_pause(self, player_id: str) -> None:
+ """Toggle play/pause on given player.
+
+ - player_id: player_id of the player to handle the command.
+ """
+ player = self._get_player_with_redirect(player_id)
+ if player.playback_state == PlaybackState.PLAYING:
+ await self.cmd_pause(player.player_id)
+ else:
+ await self.cmd_play(player.player_id)
+
+ @api_command("players/cmd/seek")
+ async def cmd_seek(self, player_id: str, position: int) -> None:
+ """Handle SEEK command for given player.
+
+ - player_id: player_id of the player to handle the command.
+ - position: position in seconds to seek to in the current playing item.
+ """
+ player = self._get_player_with_redirect(player_id)
+ # Redirect to queue controller if it is active
+ if active_queue := self.get_active_queue(player):
+ await self.mass.player_queues.seek(active_queue.queue_id, position)
+ return
+ if PlayerFeature.SEEK not in player.supported_features:
+ msg = f"Player {player.display_name} does not support seeking"
+ raise UnsupportedFeaturedException(msg)
+ # handle command on player directly
+ await player.seek(position)
+
+ @api_command("players/cmd/next")
+ async def cmd_next_track(self, player_id: str) -> None:
+ """Handle NEXT TRACK command for given player."""
+ player = self._get_player_with_redirect(player_id)
+ active_source_id = player.active_source or player.player_id
+
+ # Redirect to queue controller if it is active
+ if active_queue := self.get_active_queue(player):
+ await self.mass.player_queues.next(active_queue.queue_id)
+ return
+
+ if PlayerFeature.NEXT_PREVIOUS in player.supported_features:
+ # player has some other source active and native next/previous support
+ active_source = next((x for x in player.source_list if x.id == active_source_id), None)
+ if active_source and active_source.can_next_previous:
+ await player.next_track()
+ return
+ msg = "This action is (currently) unavailable for this source."
+ raise PlayerCommandFailed(msg)
+
+ msg = f"Player {player.display_name} does not support skipping to the next track."
+ raise UnsupportedFeaturedException(msg)
+
+ @api_command("players/cmd/previous")
+ async def cmd_previous_track(self, player_id: str) -> None:
+ """Handle PREVIOUS TRACK command for given player."""
+ player = self._get_player_with_redirect(player_id)
+ active_source_id = player.active_source or player.player_id
+ # Redirect to queue controller if it is active
+ if active_queue := self.get_active_queue(player):
+ await self.mass.player_queues.previous(active_queue.queue_id)
+ return
+
+ if PlayerFeature.NEXT_PREVIOUS in player.supported_features:
+ # player has some other source active and native next/previous support
+ active_source = next((x for x in player.source_list if x.id == active_source_id), None)
+ if active_source and active_source.can_next_previous:
+ await player.previous_track()
+ return
+ msg = "This action is (currently) unavailable for this source."
+ raise PlayerCommandFailed(msg)
+
+ msg = f"Player {player.display_name} does not support skipping to the previous track."
+ raise UnsupportedFeaturedException(msg)
+
+ @api_command("players/cmd/power")
+ @handle_player_command
+ async def cmd_power(self, player_id: str, powered: bool, skip_update: bool = False) -> None:
+ """Send POWER command to given player.
+
+ - player_id: player_id of the player to handle the command.
+ - powered: bool if player should be powered on or off.
+ """
+ player = self.get(player_id, True)
+ assert player is not None # for type checking
+ player_state = player.state
+
+ if player_state.powered == powered:
+ self.logger.debug(
+ "Ignoring power %s command for player %s: already in state %s",
+ "ON" if powered else "OFF",
+ player_state.name,
+ "ON" if player_state.powered else "OFF",
+ )
+ return # nothing to do
+
+ # ungroup player at power off
+ player_was_synced = player.synced_to is not None
+ if player.type == PlayerType.PLAYER and not powered:
+ # ungroup player if it is synced (or is a sync leader itself)
+ # NOTE: ungroup will be ignored if the player is not grouped or synced
+ await self.cmd_ungroup(player_id)
+
+ # always stop player at power off
+ if (
+ not powered
+ and not player_was_synced
+ and player.playback_state in (PlaybackState.PLAYING, PlaybackState.PAUSED)
+ ):
+ await self.cmd_stop(player_id)
+ # short sleep: allow the stop command to process and prevent race conditions
+ await asyncio.sleep(0.2)
+
+ # power off all synced childs when player is a sync leader
+ elif not powered and player.type == PlayerType.PLAYER and player.group_members:
+ async with TaskManager(self.mass) as tg:
+ for member in self.iter_group_members(player, True):
+ if member.power_control == PLAYER_CONTROL_NONE:
+ continue
+ tg.create_task(self.cmd_power(member.player_id, False))
+
+ # handle actual power command
+ if player.power_control == PLAYER_CONTROL_NONE:
+ raise UnsupportedFeaturedException(
+ f"Player {player.display_name} does not support power control"
+ )
+ if player.power_control == PLAYER_CONTROL_NATIVE:
+ # player supports power command natively: forward to player provider
+ async with self._player_throttlers[player_id]:
+ await player.power(powered)
+ elif player.power_control == PLAYER_CONTROL_FAKE:
+ # user wants to use fake power control - so we (optimistically) update the state
+ # and store the state in the cache
+ player.extra_data[ATTR_FAKE_POWER] = powered
+ await self.mass.cache.set(
+ key=player_id,
+ data=powered,
+ provider=self.domain,
+ category=CACHE_CATEGORY_PLAYER_POWER,
+ )
+ else:
+ # handle external player control
+ player_control = self._controls.get(player.power_control)
+ control_name = player_control.name if player_control else player.power_control
+ self.logger.debug("Redirecting power command to PlayerControl %s", control_name)
+ if not player_control or not player_control.supports_power:
+ raise UnsupportedFeaturedException(
+ f"Player control {control_name} is not available"
+ )
+ if powered:
+ assert player_control.power_on is not None # for type checking
+ await player_control.power_on()
+ else:
+ assert player_control.power_off is not None # for type checking
+ await player_control.power_off()
+
+ # always optimistically set the power state to update the UI
+ # as fast as possible and prevent race conditions
+ player_state.powered = powered
+ # reset active source on power off
+ if not powered:
+ player_state.active_source = None
+
+ if not skip_update:
+ player.update_state()
+
+ # handle 'auto play on power on' feature
+ if (
+ not player.active_group
+ and powered
+ and player.config.get_value(CONF_AUTO_PLAY)
+ and player.active_source in (None, player_id)
+ and not player.extra_data.get(ATTR_ANNOUNCEMENT_IN_PROGRESS)
+ ):
+ await self.mass.player_queues.resume(player_id)
+
+ @api_command("players/cmd/volume_set")
+ @handle_player_command
+ async def cmd_volume_set(self, player_id: str, volume_level: int) -> None:
+ """Send VOLUME_SET command to given player.
+
+ - player_id: player_id of the player to handle the command.
+ - volume_level: volume level (0..100) to set on the player.
+ """
+ player = self.get(player_id, True)
+ assert player is not None # for type checker
+ if player.type == PlayerType.GROUP:
+ # redirect to special group volume control
+ await self.cmd_group_volume(player_id, volume_level)
+ return
+
+ if player.volume_control == PLAYER_CONTROL_NONE:
+ raise UnsupportedFeaturedException(
+ f"Player {player.display_name} does not support volume control"
+ )
+
+ if player.mute_control != PLAYER_CONTROL_NONE and player.volume_muted:
+ # if player is muted, we unmute it first
+ self.logger.debug(
+ "Unmuting player %s before setting volume",
+ player.display_name,
+ )
+ await self.cmd_volume_mute(player_id, False)
+
+ if player.volume_control == PLAYER_CONTROL_NATIVE:
+ # player supports volume command natively: forward to player
+ async with self._player_throttlers[player_id]:
+ await player.volume_set(volume_level)
+ return
+ if player.volume_control == PLAYER_CONTROL_FAKE:
+ # user wants to use fake volume control - so we (optimistically) update the state
+ # and store the state in the cache
+ player.extra_data[ATTR_FAKE_VOLUME] = volume_level
+ # trigger update
+ player.update_state()
+ return
+ # else: handle external player control
+ player_control = self._controls.get(player.volume_control)
+ control_name = player_control.name if player_control else player.volume_control
+ self.logger.debug("Redirecting volume command to PlayerControl %s", control_name)
+ if not player_control or not player_control.supports_volume:
+ raise UnsupportedFeaturedException(f"Player control {control_name} is not available")
+ async with self._player_throttlers[player_id]:
+ assert player_control.volume_set is not None
+ await player_control.volume_set(volume_level)
+
+ @api_command("players/cmd/volume_up")
+ @handle_player_command
+ async def cmd_volume_up(self, player_id: str) -> None:
+ """Send VOLUME_UP command to given player.
+
+ - player_id: player_id of the player to handle the command.
+ """
+ if not (player := self.get(player_id)):
+ return
+ current_volume = player.volume_state or 0
+ if current_volume < 5 or current_volume > 95:
+ step_size = 1
+ elif current_volume < 20 or current_volume > 80:
+ step_size = 2
+ else:
+ step_size = 5
+ new_volume = min(100, current_volume + step_size)
+ await self.cmd_volume_set(player_id, new_volume)
+
+ @api_command("players/cmd/volume_down")
+ @handle_player_command
+ async def cmd_volume_down(self, player_id: str) -> None:
+ """Send VOLUME_DOWN command to given player.
+
+ - player_id: player_id of the player to handle the command.
+ """
+ if not (player := self.get(player_id)):
+ return
+ current_volume = player.volume_state or 0
+ if current_volume < 5 or current_volume > 95:
+ step_size = 1
+ elif current_volume < 20 or current_volume > 80:
+ step_size = 2
+ else:
+ step_size = 5
+ new_volume = max(0, current_volume - step_size)
+ await self.cmd_volume_set(player_id, new_volume)
+
+ @api_command("players/cmd/group_volume")
+ @handle_player_command
+ async def cmd_group_volume(
+ self,
+ player_id: str,
+ volume_level: int,
+ ) -> None:
+ """
+ Handle adjusting the overall/group volume to a playergroup (or synced players).
+
+ Will set a new (overall) volume level to a group player or syncgroup.
+
+ :param group_player: dedicated group player or syncleader to handle the command.
+ :param volume_level: volume level (0..100) to set to the group.
+ """
+ player = self.get(player_id, True)
+ assert player is not None # for type checker
+ if player.type == PlayerType.GROUP or player.group_members:
+ # dedicated group player or sync leader
+ await self.set_group_volume(player, volume_level)
+ return
+ if player.synced_to and (sync_leader := self.get(player.synced_to)):
+ # redirect to sync leader
+ await self.set_group_volume(sync_leader, volume_level)
+ return
+ # treat as normal player volume change
+ await self.cmd_volume_set(player_id, volume_level)
+
+ @api_command("players/cmd/group_volume_up")
+ @handle_player_command
+ async def cmd_group_volume_up(self, player_id: str) -> None:
+ """Send VOLUME_UP command to given playergroup.
+
+ - player_id: player_id of the player to handle the command.
+ """
+ group_player = self.get(player_id, True)
+ assert group_player
+ cur_volume = group_player.group_volume
+ if cur_volume < 5 or cur_volume > 95:
+ step_size = 1
+ elif cur_volume < 20 or cur_volume > 80:
+ step_size = 2
+ else:
+ step_size = 5
+ new_volume = min(100, cur_volume + step_size)
+ await self.cmd_group_volume(player_id, new_volume)
+
+ @api_command("players/cmd/group_volume_down")
+ @handle_player_command
+ async def cmd_group_volume_down(self, player_id: str) -> None:
+ """Send VOLUME_DOWN command to given playergroup.
+
+ - player_id: player_id of the player to handle the command.
+ """
+ group_player = self.get(player_id, True)
+ assert group_player
+ cur_volume = group_player.group_volume
+ if cur_volume < 5 or cur_volume > 95:
+ step_size = 1
+ elif cur_volume < 20 or cur_volume > 80:
+ step_size = 2
+ else:
+ step_size = 5
+ new_volume = max(0, cur_volume - step_size)
+ await self.cmd_group_volume(player_id, new_volume)
+
+ @api_command("players/cmd/volume_mute")
+ @handle_player_command
+ async def cmd_volume_mute(self, player_id: str, muted: bool) -> None:
+ """Send VOLUME_MUTE command to given player.
+
+ - player_id: player_id of the player to handle the command.
+ - muted: bool if player should be muted.
+ """
+ player = self.get(player_id, True)
+ assert player
+ if player.mute_control == PLAYER_CONTROL_NONE:
+ raise UnsupportedFeaturedException(
+ f"Player {player.display_name} does not support muting"
+ )
+ if player.mute_control == PLAYER_CONTROL_NATIVE:
+ # player supports mute command natively: forward to player
+ async with self._player_throttlers[player_id]:
+ await player.volume_mute(muted)
+ elif player.mute_control == PLAYER_CONTROL_FAKE:
+ # user wants to use fake mute control - so we use volume instead
+ self.logger.debug(
+ "Using volume for muting for player %s",
+ player.display_name,
+ )
+ if muted:
+ player.extra_data[ATTR_PREVIOUS_VOLUME] = player.volume_state
+ player.extra_data[ATTR_FAKE_MUTE] = True
+ await self.cmd_volume_set(player_id, 0)
+ else:
+ player._attr_volume_muted = False
+ prev_volume = player.extra_data.get(ATTR_PREVIOUS_VOLUME, 1)
+ player.extra_data[ATTR_FAKE_MUTE] = False
+ await self.cmd_volume_set(player_id, prev_volume)
+ else:
+ # handle external player control
+ player_control = self._controls.get(player.mute_control)
+ control_name = player_control.name if player_control else player.mute_control
+ self.logger.debug("Redirecting mute command to PlayerControl %s", control_name)
+ if not player_control or not player_control.supports_mute:
+ raise UnsupportedFeaturedException(
+ f"Player control {control_name} is not available"
+ )
+ async with self._player_throttlers[player_id]:
+ assert player_control.mute_set is not None
+ await player_control.mute_set(muted)
+
+ @api_command("players/cmd/play_announcement")
+ async def play_announcement(
+ self,
+ player_id: str,
+ url: str,
+ pre_announce: bool | str | None = None,
+ volume_level: int | None = None,
+ pre_announce_url: str | None = None,
+ ) -> None:
+ """
+ Handle playback of an announcement (url) on given player.
+
+ - player_id: player_id of the player to handle the command.
+ - url: URL of the announcement to play.
+ - pre_announce: optional bool if pre-announce should be used.
+ - volume_level: optional volume level to set for the announcement.
+ - pre_announce_url: optional custom URL to use for the pre-announce chime.
+ """
+ player = self.get(player_id, True)
+ assert player is not None # for type checking
+ if not url.startswith("http"):
+ raise PlayerCommandFailed("Only URLs are supported for announcements")
+ if (
+ pre_announce
+ and pre_announce_url
+ and not validate_announcement_chime_url(pre_announce_url)
+ ):
+ raise PlayerCommandFailed("Invalid pre-announce chime URL specified.")
+ # prevent multiple announcements at the same time to the same player with a lock
+ if player_id not in self._announce_locks:
+ self._announce_locks[player_id] = lock = asyncio.Lock()
+ else:
+ lock = self._announce_locks[player_id]
+ async with lock:
+ try:
+ # mark announcement_in_progress on player
+ player.extra_data[ATTR_ANNOUNCEMENT_IN_PROGRESS] = True
+ # determine if the player has native announcements support
+ native_announce_support = (
+ PlayerFeature.PLAY_ANNOUNCEMENT in player.supported_features
+ )
+ # determine pre-announce from (group)player config
+ if pre_announce is None and "tts" in url:
+ conf_pre_announce = self.mass.config.get_raw_player_config_value(
+ player_id,
+ CONF_ENTRY_TTS_PRE_ANNOUNCE.key,
+ CONF_ENTRY_TTS_PRE_ANNOUNCE.default_value,
+ )
+ pre_announce = cast("bool", conf_pre_announce)
+ if pre_announce_url is None:
+ if conf_pre_announce_url := self.mass.config.get_raw_player_config_value(
+ player_id,
+ CONF_PRE_ANNOUNCE_CHIME_URL,
+ ):
+ # player default custom chime url
+ pre_announce_url = cast("str", conf_pre_announce_url)
+ else:
+ # use global default chime url
+ pre_announce_url = ANNOUNCE_ALERT_FILE
+ # if player type is group with all members supporting announcements,
+ # we forward the request to each individual player
+ if player.type == PlayerType.GROUP and (
+ all(
+ PlayerFeature.PLAY_ANNOUNCEMENT in x.supported_features
+ for x in self.iter_group_members(player)
+ )
+ ):
+ # forward the request to each individual player
+ async with TaskManager(self.mass) as tg:
+ for group_member in player.group_members:
+ tg.create_task(
+ self.play_announcement(
+ group_member,
+ url=url,
+ pre_announce=pre_announce,
+ volume_level=volume_level,
+ pre_announce_url=pre_announce_url,
+ )
+ )
+ return
+ self.logger.info(
+ "Playback announcement to player %s (with pre-announce: %s): %s",
+ player.display_name,
+ pre_announce,
+ url,
+ )
+ # create a PlayerMedia object for the announcement so
+ # we can send a regular play-media call downstream
+ announce_data = AnnounceData(
+ announcement_url=url,
+ pre_announce=pre_announce,
+ pre_announce_url=pre_announce_url,
+ )
+ announcement = PlayerMedia(
+ uri=self.mass.streams.get_announcement_url(player_id, url, announce_data),
+ media_type=MediaType.ANNOUNCEMENT,
+ title="Announcement",
+ custom_data=announce_data,
+ )
+ # handle native announce support
+ if native_announce_support:
+ announcement_volume = self.get_announcement_volume(player_id, volume_level)
+ await player.play_announcement(announcement, announcement_volume)
+ return
+ # use fallback/default implementation
+ await self._play_announcement(player, announcement, volume_level)
+ finally:
+ player.extra_data[ATTR_ANNOUNCEMENT_IN_PROGRESS] = False
+
+ @handle_player_command
+ async def play_media(self, player_id: str, media: PlayerMedia) -> None:
+ """Handle PLAY MEDIA on given player.
+
+ - player_id: player_id of the player to handle the command.
+ - media: The Media that needs to be played on the player.
+ """
+ player = self._get_player_with_redirect(player_id)
+ # power on the player if needed
+ if player.powered is False and player.power_control != PLAYER_CONTROL_NONE:
+ await self.cmd_power(player.player_id, True)
+ await player.play_media(media)
+
+ @api_command("players/cmd/select_source")
+ async def select_source(self, player_id: str, source: str) -> None:
+ """
+ Handle SELECT SOURCE command on given player.
+
+ - player_id: player_id of the player to handle the command.
+ - source: The ID of the source that needs to be activated/selected.
+ """
+ player = self.get(player_id, True)
+ assert player is not None # for type checking
+ if player.synced_to or player.active_group:
+ raise PlayerCommandFailed(f"Player {player.display_name} is currently grouped")
+ # check if player is already playing and source is different
+ # in that case we need to stop the player first
+ prev_source = player.active_source
+ if prev_source and source != prev_source:
+ if player.playback_state != PlaybackState.IDLE:
+ await self.cmd_stop(player_id)
+ await asyncio.sleep(0.5) # small delay to allow stop to process
+ player.state.active_source = None
+ player.state.current_media = None
+ # check if source is a pluginsource
+ # in that case the source id is the instance_id of the plugin provider
+ if plugin_prov := self.mass.get_provider(source):
+ await self._handle_select_plugin_source(player, plugin_prov)
+ return
+ # check if source is a mass queue
+ # this can be used to restore the queue after a source switch
+ if mass_queue := self.mass.player_queues.get(source):
+ await self.mass.player_queues.play(mass_queue.queue_id)
+ return
+ # basic check if player supports source selection
+ if PlayerFeature.SELECT_SOURCE not in player.supported_features:
+ raise UnsupportedFeaturedException(
+ f"Player {player.display_name} does not support source selection"
+ )
+ # basic check if source is valid for player
+ if not any(x for x in player.source_list if x.id == source):
+ raise PlayerCommandFailed(
+ f"{source} is an invalid source for player {player.display_name}"
+ )
+ # forward to player
+ await player.select_source(source)
+
+ async def enqueue_next_media(self, player_id: str, media: PlayerMedia) -> None:
+ """
+ Handle enqueuing of a next media item on the player.
+
+ :param player_id: player_id of the player to handle the command.
+ :param media: The Media that needs to be enqueued on the player.
+ :raises UnsupportedFeaturedException: if the player does not support enqueueing.
+ :raises PlayerUnavailableError: if the player is not available.
+ """
+ player = self.get(player_id, raise_unavailable=True)
+ assert player is not None # for type checking
+ if PlayerFeature.ENQUEUE not in player.supported_features:
+ raise UnsupportedFeaturedException(
+ f"Player {player.display_name} does not support enqueueing"
+ )
+ async with self._player_throttlers[player_id]:
+ await player.enqueue_next_media(media)
+
+ @api_command("players/cmd/set_members")
+ async def cmd_set_members(
+ self,
+ target_player: str,
+ player_ids_to_add: list[str] | None = None,
+ player_ids_to_remove: list[str] | None = None,
+ ) -> None:
+ """
+ Join/unjoin given player(s) to/from target player.
+
+ Will add the given player(s) to the target player (sync leader or group player).
+
+ :param target_player: player_id of the syncgroup leader or group player.
+ :param player_ids_to_add: List of player_id's to add to the target player.
+ :param player_ids_to_remove: List of player_id's to remove from the target player.
+
+ :raises UnsupportedFeaturedException: if the target player does not support grouping.
+ :raises PlayerUnavailableError: if the target player is not available.
+ """
+ parent_player: Player | None = self.get(target_player, True)
+ assert parent_player is not None # for type checking
+ if PlayerFeature.SET_MEMBERS not in parent_player.supported_features:
+ msg = f"Player {parent_player.name} does not support group commands"
+ raise UnsupportedFeaturedException(msg)
+
+ if parent_player.synced_to:
+ # guard edge case: player already synced to another player
+ raise PlayerCommandFailed(
+ f"Player {parent_player.name} is already synced to another player on its own, "
+ "you need to ungroup it first before you can join other players to it.",
+ )
+
+ # filter all player ids on compatibility and availability
+ final_player_ids_to_add: list[str] = []
+ for child_player_id in player_ids_to_add or []:
+ if child_player_id == target_player:
+ continue
+ if child_player_id in final_player_ids_to_add:
+ continue
+ if not (child_player := self.get(child_player_id)) or not child_player.available:
+ self.logger.warning("Player %s is not available", child_player_id)
+ continue
+
+ # check if player can be synced/grouped with the target player
+ if not (
+ child_player_id in parent_player.can_group_with
+ or child_player.provider.lookup_key in parent_player.can_group_with
+ or "*" in parent_player.can_group_with
+ ):
+ raise UnsupportedFeaturedException(
+ f"Player {child_player.name} can not be grouped with {parent_player.name}"
+ )
+
+ if (
+ child_player.synced_to
+ and child_player.synced_to == target_player
+ and child_player_id in parent_player.group_members
+ ):
+ continue # already synced to this target
+
+ # Check if player is already part of another group and try to automatically ungroup it
+ # first. If that fails, power off the group
+ if child_player.active_group and child_player.active_group != target_player:
+ if (
+ other_group := self.get(child_player.active_group)
+ ) and PlayerFeature.SET_MEMBERS in other_group.supported_features:
+ self.logger.warning(
+ "Player %s is already part of another group (%s), "
+ "removing from that group first",
+ child_player.name,
+ child_player.active_group,
+ )
+ if child_player.player_id in other_group.static_group_members:
+ self.logger.warning(
+ "Player %s is a static member of group %s: removing is not possible, "
+ "powering the group off instead",
+ child_player.name,
+ child_player.active_group,
+ )
+ await self.cmd_power(child_player.active_group, False)
+ else:
+ await other_group.set_members(player_ids_to_remove=[child_player.player_id])
+ else:
+ self.logger.warning(
+ "Player %s is already part of another group (%s), powering it off first",
+ child_player.name,
+ child_player.active_group,
+ )
+ await self.cmd_power(child_player.active_group, False)
+ elif child_player.synced_to and child_player.synced_to != target_player:
+ self.logger.warning(
+ "Player %s is already synced to another player, ungrouping first",
+ child_player.name,
+ )
+ await self.cmd_ungroup(child_player.player_id)
+
+ # power on the player if needed
+ if not child_player.powered and child_player.power_control != PLAYER_CONTROL_NONE:
+ await self.cmd_power(child_player.player_id, True, skip_update=True)
+ # if we reach here, all checks passed
+ final_player_ids_to_add.append(child_player_id)
+
+ final_player_ids_to_remove: list[str] = []
+ if player_ids_to_remove:
+ static_members = set(parent_player.static_group_members)
+ for child_player_id in player_ids_to_remove:
+ if child_player_id == target_player:
+ raise UnsupportedFeaturedException(
+ f"Cannot remove {parent_player.name} from itself as a member!"
+ )
+ if child_player_id not in parent_player.group_members:
+ continue
+ if child_player_id in static_members:
+ raise UnsupportedFeaturedException(
+ f"Cannot remove {child_player_id} from {parent_player.name} "
+ "as it is a static member of this group"
+ )
+ final_player_ids_to_remove.append(child_player_id)
+
+ # forward command to the player after all (base) sanity checks
+ async with self._player_throttlers[target_player]:
+ await parent_player.set_members(
+ player_ids_to_add=final_player_ids_to_add or None,
+ player_ids_to_remove=final_player_ids_to_remove or None,
+ )
+
+ @api_command("players/cmd/group")
+ @handle_player_command
+ async def cmd_group(self, player_id: str, target_player: str) -> None:
+ """Handle GROUP command for given player.
+
+ Join/add the given player(id) to the given (leader) player/sync group.
+ If the target player itself is already synced to another player, this may fail.
+ If the player can not be synced with the given target player, this may fail.
+
+ :param player_id: player_id of the player to handle the command.
+ :param target_player: player_id of the syncgroup leader or group player.
+
+ :raises UnsupportedFeaturedException: if the target player does not support grouping.
+ :raises PlayerCommandFailed: if the target player is already synced to another player.
+ :raises PlayerUnavailableError: if the target player is not available.
+ :raises PlayerCommandFailed: if the player is already grouped to another player.
+ """
+ await self.cmd_set_members(target_player, player_ids_to_add=[player_id])
+
+ @api_command("players/cmd/group_many")
+ async def cmd_group_many(self, target_player: str, child_player_ids: list[str]) -> None:
+ """
+ Join given player(s) to target player.
+
+ Will add the given player(s) to the target player (sync leader or group player).
+ NOTE: This is a (deprecated) alias for cmd_set_members.
+ """
+ await self.cmd_set_members(target_player, player_ids_to_add=child_player_ids)
+
+ @api_command("players/cmd/ungroup")
+ @handle_player_command
+ async def cmd_ungroup(self, player_id: str) -> None:
+ """Handle UNGROUP command for given player.
+
+ Remove the given player from any (sync)groups it currently is synced to.
+ If the player is not currently grouped to any other player,
+ this will silently be ignored.
+
+ NOTE: This is a (deprecated) alias for cmd_set_members.
+ """
+ if not (player := self.get(player_id)):
+ self.logger.warning("Player %s is not available", player_id)
+ return
+
+ if (
+ player.active_group
+ and (group_player := self.get(player.active_group))
+ and (PlayerFeature.SET_MEMBERS in group_player.supported_features)
+ ):
+ # the player is part of a (permanent) groupplayer and the user tries to ungroup
+ if player_id in group_player.static_group_members:
+ raise UnsupportedFeaturedException(
+ f"Player {player.name} is a static member of group {group_player.name} "
+ "and cannot be removed from that group!"
+ )
+ await group_player.set_members(player_ids_to_remove=[player_id])
+ return
+
+ if player.synced_to and (synced_player := self.get(player.synced_to)):
+ # player is a sync member
+ await synced_player.set_members(player_ids_to_remove=[player_id])
+ return
+
+ if not (player.synced_to or player.group_members):
+ return # nothing to do
+
+ if PlayerFeature.SET_MEMBERS not in player.supported_features:
+ self.logger.warning("Player %s does not support (un)group commands", player.name)
+ return
+
+ # forward command to the player once all checks passed
+ await player.ungroup()
+
+ @api_command("players/cmd/ungroup_many")
+ async def cmd_ungroup_many(self, player_ids: list[str]) -> None:
+ """Handle UNGROUP command for all the given players."""
+ for player_id in list(player_ids):
+ await self.cmd_ungroup(player_id)
+
+ @api_command("players/create_group_player")
+ async def create_group_player(
+ self, provider: str, name: str, members: list[str], dynamic: bool = True
+ ):
+ """
+ Create a new (permanent) Group Player.
+
+ :param provider: The provider(id) to create the group player for
+ :param name: Name of the new group player
+ :param members: List of player ids to add to the group
+ :param dynamic: Whether the group is dynamic (members can change)
+ """
+ if not (provider_instance := self.mass.get_provider(provider)):
+ raise ProviderUnavailableError(f"Provider {provider} not found")
+ provider_instance = cast("PlayerProvider", provider_instance)
+ if ProviderFeature.CREATE_GROUP_PLAYER in provider_instance.supported_features:
+ return await provider_instance.create_group_player(name, members, dynamic)
+ if ProviderFeature.SYNC_PLAYERS in provider_instance.supported_features:
+ # provider supports syncing but not dedicated group players
+ # create a sync group instead
+ return await self._sync_groups.create_group_player(
+ provider_instance, name, members, dynamic=dynamic
+ )
+ raise UnsupportedFeaturedException(
+ f"Provider {provider} does not support creating group players"
+ )
+
+ @api_command("players/remove_group_player")
+ async def remove_group_player(self, player_id: str) -> None:
+ """
+ Remove a group player.
+
+ :param player_id: ID of the group player to remove.
+ """
+ if not (player := self.get(player_id)):
+ # we simply permanently delete the player by wiping its config
+ self.mass.config.remove(f"players/{player_id}")
+ return
+ if player.type != PlayerType.GROUP:
+ raise UnsupportedFeaturedException(
+ f"Player {player.display_name} is not a group player"
+ )
+ player.provider.check_feature(ProviderFeature.REMOVE_GROUP_PLAYER)
+ await player.provider.remove_group_player(player_id)
+
+ @api_command("players/add_currently_playing_to_favorites")
+ async def add_currently_playing_to_favorites(self, player_id: str) -> None:
+ """
+ Add the currently playing item/track on given player to the favorites.
+
+ This tries to resolve the currently playing media to an actual media item
+ and add that to the favorites in the library.
+
+ Will raise an error if the player is not currently playing anything
+ or if the currently playing media can not be resolved to a media item.
+ """
+ player = self._get_player_with_redirect(player_id)
+ # handle mass player queue active
+ if mass_queue := self.get_active_queue(player):
+ if not (current_item := mass_queue.current_item) or not current_item.media_item:
+ raise PlayerCommandFailed("No current item to add to favorites")
+ # if we're playing a radio station, try to resolve the currently playing track
+ if current_item.media_item.media_type == MediaType.RADIO:
+ if not (
+ (streamdetails := mass_queue.current_item.streamdetails)
+ and (stream_title := streamdetails.stream_title)
+ and " - " in stream_title
+ ):
+ # no stream title available, so we can't resolve the track
+ # this can happen if the radio station does not provide metadata
+ # or there's a commercial break
+ # Possible future improvement could be to actually detect the song with a
+ # shazam-like approach.
+ raise PlayerCommandFailed("No current item to add to favorites")
+ # send the streamtitle into a global search query
+ search_artist, search_title_title = stream_title.split(" - ", 1)
+ # strip off any additional comments in the title (such as from Radio Paradise)
+ search_title_title = search_title_title.split(" | ")[0].strip()
+ if track := await self.mass.music.get_track_by_name(
+ search_title_title, search_artist
+ ):
+ # we found a track, so add it to the favorites
+ await self.mass.music.add_item_to_favorites(track)
+ return
+ # we could not resolve the track, so raise an error
+ raise PlayerCommandFailed("No current item to add to favorites")
+
+ # else: any other media item, just add it to the favorites directly
+ await self.mass.music.add_item_to_favorites(current_item.media_item)
+ return
+
+ # guard for player with no active source
+ if not player.active_source:
+ raise PlayerCommandFailed("Player has no active source")
+ # handle other source active using the current_media with uri
+ if current_media := player.current_media:
+ # prefer the uri of the current media item
+ if current_media.uri:
+ with suppress(MusicAssistantError):
+ await self.mass.music.add_item_to_favorites(current_media.uri)
+ return
+ # fallback to search based on artist and title (and album if available)
+ if current_media.artist and current_media.title:
+ if track := await self.mass.music.get_track_by_name(
+ current_media.title,
+ current_media.artist,
+ current_media.album,
+ ):
+ # we found a track, so add it to the favorites
+ await self.mass.music.add_item_to_favorites(track)
+ return
+ # if we reach here, we could not resolve the currently playing item
+ raise PlayerCommandFailed("No current item to add to favorites")
+
+ async def register(self, player: Player) -> None:
+ """Register a player on the Player Controller."""
+ if self.mass.closing:
+ return
+ player_id = player.player_id
+
+ if player_id in self._players:
+ msg = f"Player {player_id} is already registered!"
+ raise AlreadyRegisteredError(msg)
+
+ # ignore disabled players
+ if not player.enabled:
+ return
+
+ # register throttler for this player
+ self._player_throttlers[player_id] = Throttler(1, 0.05)
+
+ # restore 'fake' power state from cache if available
+ cached_value = await self.mass.cache.get(
+ key=player.player_id,
+ provider=self.domain,
+ category=CACHE_CATEGORY_PLAYER_POWER,
+ default=False,
+ )
+ if cached_value is not None:
+ player.extra_data[ATTR_FAKE_POWER] = cached_value
+
+ # finally actually register it
+ self._players[player_id] = player
+
+ # ensure we fetch and set the latest/full config for the player
+ player_config = await self.mass.config.get_player_config(player_id)
+ player.set_config(player_config)
+ # call hook after the player is registered and config is set
+ await player.on_config_updated()
+ # always call update to fix special attributes like display name, group volume etc.
+ player.update_state()
+
+ self.logger.info(
+ "Player registered: %s/%s",
+ player_id,
+ player.display_name,
+ )
+ # signal event that a player was added
+ self.mass.signal_event(EventType.PLAYER_ADDED, object_id=player.player_id, data=player)
+
+ # register playerqueue for this player
+ await self.mass.player_queues.on_player_register(player)
+
+ async def register_or_update(self, player: Player) -> None:
+ """Register a new player on the controller or update existing one."""
+ if self.mass.closing:
+ return
+
+ if player.player_id in self._players:
+ self._players[player.player_id] = player
+ player.update_state()
+ return
+
+ await self.register(player)
+
+ def trigger_player_update(self, player_id: str, force_update: bool = False) -> None:
+ """Trigger an update for the given player."""
+ if self.mass.closing:
+ return
+ player = self.get(player_id, True)
+ assert player is not None # for type checker
+ self.mass.loop.call_soon(player.update_state, force_update)
+
+ async def unregister(self, player_id: str, permanent: bool = False) -> None:
+ """
+ Unregister a player from the player controller.
+
+ Called (by a PlayerProvider) when a player is removed
+ or no longer available (for a longer period of time).
+
+ This will remove the player from the player controller and
+ optionally remove the player's config from the mass config.
+
+ - player_id: player_id of the player to unregister.
+ - permanent: if True, remove the player permanently by deleting
+ the player's config from the mass config. If False, the player config will not be removed,
+ allowing for re-registration (with the same config) later.
+
+ If the player is not registered, this will silently be ignored.
+ """
+ player = self._players.get(player_id)
+ if player is None:
+ return
+ await self._cleanup_player_memberships(player_id)
+ del self._players[player_id]
+ self.logger.info("Player removed: %s", player.name)
+ self.mass.player_queues.on_player_remove(player_id, permanent=permanent)
+ await player.on_unload()
+ if permanent:
+ self.delete_player_config(player_id)
+ self.mass.signal_event(EventType.PLAYER_REMOVED, player_id)
+
+ @api_command("players/remove")
+ async def remove(self, player_id: str) -> None:
+ """
+ Remove a player from a provider.
+
+ Can only be called when a PlayerProvider supports ProviderFeature.REMOVE_PLAYER.
+ """
+ player = self.get(player_id)
+ if player is None:
+ # we simply permanently delete the player config since it is not registered
+ self.delete_player_config(player_id)
+ return
+ if player.type == PlayerType.GROUP:
+ # Handle group player removal
+ await player.provider.remove_group_player(player_id)
+ return
+ player.provider.check_feature(ProviderFeature.REMOVE_PLAYER)
+ await player.provider.remove_player(player_id)
+ # check for group memberships that need to be updated
+ if player.active_group and (group_player := self.mass.players.get(player.active_group)):
+ # try to remove from the group
+ with suppress(UnsupportedFeaturedException, PlayerCommandFailed):
+ await group_player.set_members(
+ player_ids_to_remove=[player_id],
+ )
+ # We removed the player and can now clean up its config
+ self.delete_player_config(player_id)
+
+ def delete_player_config(self, player_id: str) -> None:
+ """
+ Permanently delete a player's configuration.
+
+ Should only be called for players that are not registered by the player controller.
+ """
+ # we simply permanently delete the player by wiping its config
+ conf_key = f"{CONF_PLAYERS}/{player_id}"
+ dsp_conf_key = f"{CONF_PLAYER_DSP}/{player_id}"
+ for key in (conf_key, dsp_conf_key):
+ self.mass.config.remove(key)
+
+ def signal_player_state_update(
+ self,
+ player: Player,
+ changed_values: dict[str, tuple[Any, Any]],
+ force_update: bool = False,
+ skip_forward: bool = False,
+ ) -> None:
+ """
+ Signal a player state update.
+
+ Called by a Player when its state has changed.
+ This will update the player state in the controller and signal the event bus.
+ """
+ player_id = player.player_id
+ if self.mass.closing:
+ return
+
+ # ignore updates for disabled players
+ if not player.enabled and "enabled" not in changed_values:
+ return
+
+ if len(changed_values) == 0 and not force_update:
+ # nothing changed
+ return
+
+ # always signal update to the playerqueue
+ self.mass.player_queues.on_player_update(player, changed_values)
+
+ if changed_values.keys() == {"elapsed_time"} and not force_update:
+ # ignore elapsed_time only changes
+ prev_value = changed_values["elapsed_time"][0] or 0
+ new_value = changed_values["elapsed_time"][1] or 0
+ if abs(prev_value - new_value) < 30:
+ # ignore small changes in elapsed time
+ return
+
+ # handle DSP reload of the leader when grouping/ungrouping
+ if ATTR_GROUP_MEMBERS in changed_values:
+ prev_group_members, new_group_members = changed_values[ATTR_GROUP_MEMBERS]
+ self._handle_group_dsp_change(player, prev_group_members or [], new_group_members)
+
+ if ATTR_GROUP_MEMBERS in changed_values:
+ # Removed group members also need to be updated since they are no longer part
+ # of this group and are available for playback again
+ prev_group_members = changed_values[ATTR_GROUP_MEMBERS][0] or []
+ new_group_members = changed_values[ATTR_GROUP_MEMBERS][1] or []
+ removed_members = set(prev_group_members) - set(new_group_members)
+ for player_id in removed_members:
+ if removed_player := self.get(player_id):
+ removed_player.update_state()
+
+ became_inactive = False
+ if "available" in changed_values:
+ became_inactive = changed_values["available"][1] is False
+ if not became_inactive and "enabled" in changed_values:
+ became_inactive = changed_values["enabled"][1] is False
+ if became_inactive and (player.active_group or player.synced_to):
+ self.mass.create_task(self._cleanup_player_memberships(player.player_id))
+
+ # signal player update on the eventbus
+ self.mass.signal_event(EventType.PLAYER_UPDATED, object_id=player_id, data=player)
+
+ if skip_forward and not force_update:
+ return
+
+ # update/signal group player(s) child's when group updates
+ for child_player in self.iter_group_members(player, exclude_self=True):
+ child_player.update_state()
+ # update/signal group player(s) when child updates
+ for group_player in self._get_player_groups(player, powered_only=False):
+ group_player.update_state()
+ # update/signal manually synced to player when child updates
+ if (synced_to := player.synced_to) and (synced_to_player := self.get(synced_to)):
+ synced_to_player.update_state()
+ # update/signal active groups when a group member updates
+ if (active_group := player.active_group) and (
+ active_group_player := self.get(active_group)
+ ):
+ active_group_player.update_state()
+
+ async def register_player_control(self, player_control: PlayerControl) -> None:
+ """Register a new PlayerControl on the controller."""
+ if self.mass.closing:
+ return
+ control_id = player_control.id
+
+ if control_id in self._controls:
+ msg = f"PlayerControl {control_id} is already registered"
+ raise AlreadyRegisteredError(msg)
+
+ # make sure that the playercontrol's provider is set to the instance_id
+ prov = self.mass.get_provider(player_control.provider)
+ if not prov or prov.instance_id != player_control.provider:
+ raise RuntimeError(f"Invalid provider ID given: {player_control.provider}")
+
+ self._controls[control_id] = player_control
+
+ self.logger.info(
+ "PlayerControl registered: %s/%s",
+ control_id,
+ player_control.name,
+ )
+
+ # always call update to update any attached players etc.
+ self.update_player_control(player_control.id)
+
+ async def register_or_update_player_control(self, player_control: PlayerControl) -> None:
+ """Register a new playercontrol on the controller or update existing one."""
+ if self.mass.closing:
+ return
+ if player_control.id in self._controls:
+ self._controls[player_control.id] = player_control
+ self.update_player_control(player_control.id)
+ return
+ await self.register_player_control(player_control)
+
+ def update_player_control(self, control_id: str) -> None:
+ """Update playercontrol state."""
+ if self.mass.closing:
+ return
+ # update all players that are using this control
+ for player in self._players.values():
+ if control_id in (player.power_control, player.volume_control, player.mute_control):
+ self.mass.loop.call_soon(player.update_state)
+
+ def remove_player_control(self, control_id: str) -> None:
+ """Remove a player_control from the player manager."""
+ control = self._controls.pop(control_id, None)
+ if control is None:
+ return
+ self._controls.pop(control_id, None)
+ self.logger.info("PlayerControl removed: %s", control.name)
+
+ def get_player_provider(self, player_id: str) -> PlayerProvider:
+ """Return PlayerProvider for given player."""
+ player = self._players[player_id]
+ assert player # for type checker
+ return player.provider
+
+ def get_active_queue(self, player: Player) -> PlayerQueue | None:
+ """Return the current active queue for a player (if any)."""
+ # account for player that is synced (sync child)
+ if player.synced_to and player.synced_to != player.player_id:
+ if sync_leader := self.get(player.synced_to):
+ return self.get_active_queue(sync_leader)
+ # handle active group player
+ if player.active_group and player.active_group != player.player_id:
+ if group_player := self.get(player.active_group):
+ return self.get_active_queue(group_player)
+ # active_source may be filled queue id (or None)
+ active_source = player.active_source or player.player_id
+ if active_queue := self.mass.player_queues.get(active_source):
+ return active_queue
+ return None
+
+ async def set_group_volume(self, group_player: Player, volume_level: int) -> None:
+ """Handle adjusting the overall/group volume to a playergroup (or synced players)."""
+ cur_volume = group_player.state.group_volume
+ volume_dif = volume_level - cur_volume
+ coros = []
+ # handle group volume by only applying the volume to powered members
+ for child_player in self.iter_group_members(
+ group_player, only_powered=True, exclude_self=False
+ ):
+ if child_player.volume_control == PLAYER_CONTROL_NONE:
+ continue
+ cur_child_volume = child_player.volume_level or 0
+ new_child_volume = int(cur_child_volume + volume_dif)
+ new_child_volume = max(0, new_child_volume)
+ new_child_volume = min(100, new_child_volume)
+ coros.append(self.cmd_volume_set(child_player.player_id, new_child_volume))
+ await asyncio.gather(*coros)
+
+ def get_announcement_volume(self, player_id: str, volume_override: int | None) -> int | None:
+ """Get the (player specific) volume for a announcement."""
+ volume_strategy = self.mass.config.get_raw_player_config_value(
+ player_id,
+ CONF_ENTRY_ANNOUNCE_VOLUME_STRATEGY.key,
+ CONF_ENTRY_ANNOUNCE_VOLUME_STRATEGY.default_value,
+ )
+ volume_strategy_volume = self.mass.config.get_raw_player_config_value(
+ player_id,
+ CONF_ENTRY_ANNOUNCE_VOLUME.key,
+ CONF_ENTRY_ANNOUNCE_VOLUME.default_value,
+ )
+ if volume_strategy == "none":
+ return None
+ volume_level = volume_override
+ if volume_level is None and volume_strategy == "absolute":
+ volume_level = volume_strategy_volume
+ elif volume_level is None and volume_strategy == "relative":
+ player = self.get(player_id)
+ volume_level = player.volume_level + volume_strategy_volume
+ elif volume_level is None and volume_strategy == "percentual":
+ player = self.get(player_id)
+ percentual = (player.volume_level / 100) * volume_strategy_volume
+ volume_level = player.volume_level + percentual
+ if volume_level is not None:
+ announce_volume_min = self.mass.config.get_raw_player_config_value(
+ player_id,
+ CONF_ENTRY_ANNOUNCE_VOLUME_MIN.key,
+ CONF_ENTRY_ANNOUNCE_VOLUME_MIN.default_value,
+ )
+ volume_level = max(announce_volume_min, volume_level)
+ announce_volume_max = self.mass.config.get_raw_player_config_value(
+ player_id,
+ CONF_ENTRY_ANNOUNCE_VOLUME_MAX.key,
+ CONF_ENTRY_ANNOUNCE_VOLUME_MAX.default_value,
+ )
+ volume_level = min(announce_volume_max, volume_level)
+ # ensure the result is an integer
+ return None if volume_level is None else int(volume_level)
+
+ def iter_group_members(
+ self,
+ group_player: Player,
+ only_powered: bool = False,
+ only_playing: bool = False,
+ active_only: bool = False,
+ exclude_self: bool = True,
+ ) -> Iterator[Player]:
+ """Get (child) players attached to a group player or syncgroup."""
+ for child_id in list(group_player.group_members):
+ if child_player := self.get(child_id, False):
+ if not child_player.available or not child_player.enabled:
+ continue
+ if only_powered and child_player.powered is False:
+ continue
+ if active_only and child_player.active_group != group_player.player_id:
+ continue
+ if exclude_self and child_player.player_id == group_player.player_id:
+ continue
+ if only_playing and child_player.playback_state not in (
+ PlaybackState.PLAYING,
+ PlaybackState.PAUSED,
+ ):
+ continue
+ yield child_player
+
+ async def wait_for_state(
+ self,
+ player: Player,
+ wanted_state: PlaybackState,
+ timeout: float = 60.0,
+ minimal_time: float = 0,
+ ) -> None:
+ """Wait for the given player to reach the given state."""
+ start_timestamp = time.time()
+ self.logger.debug(
+ "Waiting for player %s to reach state %s", player.display_name, wanted_state
+ )
+ try:
+ async with asyncio.timeout(timeout):
+ while player.playback_state != wanted_state:
+ await asyncio.sleep(0.1)
+
+ except TimeoutError:
+ self.logger.debug(
+ "Player %s did not reach state %s within the timeout of %s seconds",
+ player.display_name,
+ wanted_state,
+ timeout,
+ )
+ elapsed_time = round(time.time() - start_timestamp, 2)
+ if elapsed_time < minimal_time:
+ self.logger.debug(
+ "Player %s reached state %s too soon (%s vs %s seconds) - add fallback sleep...",
+ player.display_name,
+ wanted_state,
+ elapsed_time,
+ minimal_time,
+ )
+ await asyncio.sleep(minimal_time - elapsed_time)
+ else:
+ self.logger.debug(
+ "Player %s reached state %s within %s seconds",
+ player.display_name,
+ wanted_state,
+ elapsed_time,
+ )
+
+ async def on_player_config_change(self, config: PlayerConfig, changed_keys: set[str]) -> None:
+ """Call (by config manager) when the configuration of a player changes."""
+ player_disabled = "enabled" in changed_keys and not config.enabled
+ # signal player provider that the player got enabled/disabled
+ if player_provider := self.mass.get_provider(config.provider):
+ assert isinstance(player_provider, PlayerProvider) # for type checking
+ if "enabled" in changed_keys and not config.enabled:
+ player_provider.on_player_disabled(config.player_id)
+ elif "enabled" in changed_keys and config.enabled:
+ player_provider.on_player_enabled(config.player_id)
+ # ensure player state gets updated with any updated config
+ if not (player := self.get(config.player_id)):
+ return # guard against player not being registered (yet)
+ player.set_config(config)
+ await player.on_config_updated()
+ player.update_state()
+ resume_queue: PlayerQueue | None = (
+ self.mass.player_queues.get(player.active_source) if player.active_source else None
+ )
+ if player_disabled:
+ # edge case: ensure that the player is powered off if the player gets disabled
+ if player.power_control != PLAYER_CONTROL_NONE:
+ await self.cmd_power(config.player_id, False)
+ elif player.playback_state != PlaybackState.IDLE:
+ await self.cmd_stop(config.player_id)
+ # if the PlayerQueue was playing, restart playback
+ # TODO: add property to ConfigEntry if it requires a restart of playback on change
+ elif not player_disabled and resume_queue and resume_queue.state == PlaybackState.PLAYING:
+ # always stop first to ensure the player uses the new config
+ await self.mass.player_queues.stop(resume_queue.queue_id)
+ self.mass.call_later(1, self.mass.player_queues.resume, resume_queue.queue_id, False)
+
+ async def on_player_dsp_change(self, player_id: str) -> None:
+ """Call (by config manager) when the DSP settings of a player change."""
+ # signal player provider that the config changed
+ if not (player := self.get(player_id)):
+ return
+ if player.playback_state == PlaybackState.PLAYING:
+ self.logger.info("Restarting playback of Player %s after DSP change", player_id)
+ # this will restart the queue stream/playback
+ if player.mass_queue_active:
+ self.mass.call_later(0, self.mass.player_queues.resume, player.active_source, False)
+ return
+ # if the player is not using a queue, we need to stop and start playback
+ await self.cmd_stop(player_id)
+ await self.cmd_play(player_id)
+
+ async def _cleanup_player_memberships(self, player_id: str) -> None:
+ """Ensure a player is detached from any groups or syncgroups."""
+ if not (player := self.get(player_id)):
+ return
+
+ if (
+ player.active_group
+ and (group := self.get(player.active_group))
+ and group.supports_feature(PlayerFeature.SET_MEMBERS)
+ ):
+ # Ungroup the player if its part of an active group, this will ignore
+ # static_group_members since that is only checked when using cmd_set_members
+ with suppress(UnsupportedFeaturedException, PlayerCommandFailed):
+ await group.set_members(player_ids_to_remove=[player_id])
+ elif player.synced_to and player.supports_feature(PlayerFeature.SET_MEMBERS):
+ # Remove the player if it was synced, otherwise it will still show as
+ # synced to the other player after it gets registered again
+ with suppress(UnsupportedFeaturedException, PlayerCommandFailed):
+ await player.ungroup()
+
+ def _get_player_with_redirect(self, player_id: str) -> Player:
+ """Get player with check if playback related command should be redirected."""
+ player = self.get(player_id, True)
+ assert player is not None # for type checking
+ if player.synced_to and (sync_leader := self.get(player.synced_to)):
+ self.logger.info(
+ "Player %s is synced to %s and can not accept "
+ "playback related commands itself, "
+ "redirected the command to the sync leader.",
+ player.name,
+ sync_leader.name,
+ )
+ return sync_leader
+ if player.active_group and (active_group := self.get(player.active_group)):
+ self.logger.info(
+ "Player %s is part of a playergroup and can not accept "
+ "playback related commands itself, "
+ "redirected the command to the group leader.",
+ player.name,
+ )
+ return active_group
+ return player
+
+ def _get_player_groups(
+ self, player: Player, available_only: bool = True, powered_only: bool = False
+ ) -> Iterator[Player]:
+ """Return all groupplayers the given player belongs to."""
+ for _player in self.all(return_unavailable=not available_only):
+ if _player.player_id == player.player_id:
+ continue
+ if _player.type != PlayerType.GROUP:
+ continue
+ if powered_only and _player.powered is False:
+ continue
+ if player.player_id in _player.group_members:
+ yield _player
+
+ async def _play_announcement( # noqa: PLR0915
+ self,
+ player: Player,
+ announcement: PlayerMedia,
+ volume_level: int | None = None,
+ ) -> None:
+ """Handle (default/fallback) implementation of the play announcement feature.
+
+ This default implementation will;
+ - stop playback of the current media (if needed)
+ - power on the player (if needed)
+ - raise the volume a bit
+ - play the announcement (from given url)
+ - wait for the player to finish playing
+ - restore the previous power and volume
+ - restore playback (if needed and if possible)
+
+ This default implementation will only be used if the player
+ (provider) has no native support for the PLAY_ANNOUNCEMENT feature.
+ """
+ prev_power = player.powered
+ prev_state = player.playback_state
+ prev_synced_to = player.synced_to
+ prev_group = self.get(player.active_group) if player.active_group else None
+ prev_source = player.active_source
+ prev_queue = self.get_active_queue(player)
+ prev_media = player.current_media
+ prev_media_name = prev_media.title or prev_media.uri if prev_media else None
+ if prev_synced_to:
+ # ungroup player if its currently synced
+ self.logger.debug(
+ "Announcement to player %s - ungrouping player from %s...",
+ player.display_name,
+ prev_synced_to,
+ )
+ await self.cmd_ungroup(player.player_id)
+ elif prev_group:
+ # if the player is part of a group player, we need to ungroup it
+ if PlayerFeature.SET_MEMBERS in prev_group.supported_features:
+ self.logger.debug(
+ "Announcement to player %s - ungrouping from group player %s...",
+ player.display_name,
+ prev_group.display_name,
+ )
+ await prev_group.set_members(player_ids_to_remove=[player.player_id])
+ else:
+ # if the player is part of a group player that does not support ungrouping,
+ # we need to power off the groupplayer instead
+ self.logger.debug(
+ "Announcement to player %s - turning off group player %s...",
+ player.display_name,
+ prev_group.display_name,
+ )
+ await self.cmd_power(player.player_id, False)
+ elif prev_state in (PlaybackState.PLAYING, PlaybackState.PAUSED):
+ # normal/standalone player: stop player if its currently playing
+ self.logger.debug(
+ "Announcement to player %s - stop existing content (%s)...",
+ player.display_name,
+ prev_media_name,
+ )
+ await self.cmd_stop(player.player_id)
+ # wait for the player to stop
+ await self.wait_for_state(player, PlaybackState.IDLE, 10, 0.4)
+ # adjust volume if needed
+ # in case of a (sync) group, we need to do this for all child players
+ prev_volumes: dict[str, int] = {}
+ async with TaskManager(self.mass) as tg:
+ for volume_player_id in player.group_members or (player.player_id,):
+ if not (volume_player := self.get(volume_player_id)):
+ continue
+ # catch any players that have a different source active
+ if (
+ volume_player.active_source
+ not in (
+ player.active_source,
+ volume_player.player_id,
+ None,
+ )
+ and volume_player.playback_state == PlaybackState.PLAYING
+ ):
+ self.logger.warning(
+ "Detected announcement to playergroup %s while group member %s is playing "
+ "other content, this may lead to unexpected behavior.",
+ player.display_name,
+ volume_player.display_name,
+ )
+ tg.create_task(self.cmd_stop(volume_player.player_id))
+ if volume_player.volume_control == PLAYER_CONTROL_NONE:
+ continue
+ if (prev_volume := volume_player.volume_level) is None:
+ continue
+ announcement_volume = self.get_announcement_volume(volume_player_id, volume_level)
+ if announcement_volume is None:
+ continue
+ temp_volume = announcement_volume or player.volume_level
+ if temp_volume != prev_volume:
+ prev_volumes[volume_player_id] = prev_volume
+ self.logger.debug(
+ "Announcement to player %s - setting temporary volume (%s)...",
+ volume_player.display_name,
+ announcement_volume,
+ )
+ tg.create_task(
+ self.cmd_volume_set(volume_player.player_id, announcement_volume)
+ )
+ # play the announcement
+ self.logger.debug(
+ "Announcement to player %s - playing the announcement on the player...",
+ player.display_name,
+ )
+ await self.play_media(player_id=player.player_id, media=announcement)
+ # wait for the player(s) to play
+ await self.wait_for_state(player, PlaybackState.PLAYING, 10, minimal_time=0.1)
+ # wait for the player to stop playing
+ if not announcement.duration:
+ media_info = await async_parse_tags(
+ announcement.custom_data["url"], require_duration=True
+ )
+ announcement.duration = media_info.duration
+ await self.wait_for_state(
+ player,
+ PlaybackState.IDLE,
+ timeout=announcement.duration + 6,
+ minimal_time=announcement.duration,
+ )
+ self.logger.debug(
+ "Announcement to player %s - restore previous state...", player.display_name
+ )
+ # restore volume
+ async with TaskManager(self.mass) as tg:
+ for volume_player_id, prev_volume in prev_volumes.items():
+ tg.create_task(self.cmd_volume_set(volume_player_id, prev_volume))
+ await asyncio.sleep(0.2)
+ player.current_media = prev_media
+ player.active_source = prev_source
+ # either power off the player or resume playing
+ if not prev_power and player.power_control != PLAYER_CONTROL_NONE:
+ await self.cmd_power(player.player_id, False)
+ return
+ elif prev_synced_to:
+ await self.cmd_group(player.player_id, prev_synced_to)
+ elif prev_group:
+ if PlayerFeature.SET_MEMBERS in prev_group.supported_features:
+ self.logger.debug(
+ "Announcement to player %s - grouping back to group player %s...",
+ player.display_name,
+ prev_group.display_name,
+ )
+ await prev_group.set_members(player_ids_to_add=[player.player_id])
+ elif prev_state == PlaybackState.PLAYING:
+ # if the player is part of a group player that does not support set_members,
+ # we need to restart the groupplayer
+ self.logger.debug(
+ "Announcement to player %s - restarting playback on group player %s...",
+ player.display_name,
+ prev_group.display_name,
+ )
+ await self.cmd_play(prev_group.player_id)
+ elif prev_queue and prev_state == PlaybackState.PLAYING:
+ await self.mass.player_queues.resume(prev_queue.queue_id, True)
+ await self.wait_for_state(player, PlaybackState.PLAYING, 5)
+ elif prev_state == PlaybackState.PLAYING:
+ # player was playing something else - try to resume that here
+ for source in player.source_list_state:
+ if source.id == prev_source and not source.passive:
+ await player.select_source(source.id)
+ break
+ else:
+ # no source found, try to resume the previous media
+ await self.cmd_play(player.player_id)
+
+ async def _poll_players(self) -> None:
+ """Background task that polls players for updates."""
+ while True:
+ for player in list(self._players.values()):
+ # if the player is playing, update elapsed time every tick
+ # to ensure the queue has accurate details
+ player_playing = player.playback_state == PlaybackState.PLAYING
+ if player_playing:
+ self.mass.loop.call_soon(
+ self.mass.player_queues.on_player_update,
+ player,
+ {"corrected_elapsed_time": player.corrected_elapsed_time},
+ )
+ # Poll player;
+ if not player.needs_poll:
+ continue
+ try:
+ last_poll: float = player.extra_data[ATTR_LAST_POLL]
+ except KeyError:
+ last_poll = 0.0
+ if (self.mass.loop.time() - last_poll) < player.poll_interval:
+ continue
+ player.extra_data[ATTR_LAST_POLL] = self.mass.loop.time()
+ try:
+ await player.poll()
+ except Exception as err:
+ self.logger.warning(
+ "Error while requesting latest state from player %s: %s",
+ player.display_name,
+ str(err),
+ exc_info=err if self.logger.isEnabledFor(10) else None,
+ )
+ await asyncio.sleep(1)
+
+ async def _handle_select_plugin_source(
+ self, player: Player, plugin_prov: PluginProvider
+ ) -> None:
+ """Handle playback/select of given plugin source on player."""
+ plugin_source = plugin_prov.get_source()
+ stream_url = await self.mass.streams.get_plugin_source_url(
+ plugin_source.id, player.player_id
+ )
+ await self.play_media(
+ player_id=player.player_id,
+ media=PlayerMedia(
+ uri=stream_url,
+ media_type=MediaType.PLUGIN_SOURCE,
+ title=plugin_source.name,
+ custom_data={
+ "provider": plugin_prov.instance_id,
+ "source_id": plugin_source.id,
+ "player_id": player.player_id,
+ "audio_format": plugin_source.audio_format,
+ },
+ ),
+ )
+ # trigger player update to ensure the source is set
+ self.trigger_player_update(player.player_id)
+
+ def _handle_group_dsp_change(
+ self, player: Player, prev_group_members: list[str], new_group_members: list[str]
+ ) -> None:
+ """Handle DSP reload when group membership changes."""
+ prev_child_count = len(prev_group_members)
+ new_child_count = len(new_group_members)
+ is_player_group = player.type == PlayerType.GROUP
+
+ # handle special case for PlayerGroups: since there are no leaders,
+ # DSP still always work with a single player in the group.
+ multi_device_dsp_threshold = 1 if is_player_group else 0
+
+ prev_is_multiple_devices = prev_child_count > multi_device_dsp_threshold
+ new_is_multiple_devices = new_child_count > multi_device_dsp_threshold
+
+ if prev_is_multiple_devices == new_is_multiple_devices:
+ return # no change in multi-device status
+
+ supports_multi_device_dsp = PlayerFeature.MULTI_DEVICE_DSP in player.supported_features
+
+ dsp_enabled: bool
+ if player.type == PlayerType.GROUP:
+ # Since player groups do not have leaders, we will use the only child
+ # that was in the group before and after the change
+ if prev_is_multiple_devices:
+ if childs := new_group_members:
+ # We shrank the group from multiple players to a single player
+ # So the now only child will control the DSP
+ dsp_enabled = self.mass.config.get_player_dsp_config(childs[0]).enabled
+ else:
+ dsp_enabled = False
+ elif childs := prev_group_members:
+ # We grew the group from a single player to multiple players,
+ # let's see if the previous single player had DSP enabled
+ dsp_enabled = self.mass.config.get_player_dsp_config(childs[0]).enabled
+ else:
+ dsp_enabled = False
+ else:
+ dsp_enabled = self.mass.config.get_player_dsp_config(player.player_id).enabled
+
+ if dsp_enabled and not supports_multi_device_dsp:
+ # We now know that the group configuration has changed so:
+ # - multi-device DSP is not supported
+ # - we switched from a group with multiple players to a single player
+ # (or vice versa)
+ # - the leader has DSP enabled
+ self.mass.create_task(self.mass.players.on_player_dsp_change(player.player_id))
+
+ def __iter__(self) -> Iterator[Player]:
+ """Iterate over all players."""
+ return iter(self._players.values())
--- /dev/null
+"""
+Controller for (provider specific) SyncGroup players.
+
+A SyncGroup player is a virtual player that automatically groups multiple players
+together in a sync group, where one player is the sync leader
+and the other players are synced to that leader.
+"""
+
+from __future__ import annotations
+
+import asyncio
+from copy import deepcopy
+from typing import TYPE_CHECKING, cast
+
+import shortuuid
+from music_assistant_models.config_entries import ConfigEntry, ConfigValueOption
+from music_assistant_models.constants import PLAYER_CONTROL_NONE
+from music_assistant_models.enums import (
+ ConfigEntryType,
+ PlaybackState,
+ PlayerFeature,
+ PlayerType,
+ ProviderFeature,
+)
+from music_assistant_models.errors import UnsupportedFeaturedException
+from music_assistant_models.player import DeviceInfo, PlayerMedia, PlayerSource
+from propcache import under_cached_property as cached_property
+
+from music_assistant.constants import (
+ CONF_CROSSFADE_DURATION,
+ CONF_DYNAMIC_GROUP_MEMBERS,
+ CONF_ENABLE_ICY_METADATA,
+ CONF_FLOW_MODE,
+ CONF_GROUP_MEMBERS,
+ CONF_HTTP_PROFILE,
+ CONF_OUTPUT_CODEC,
+ CONF_SAMPLE_RATES,
+ CONF_SMART_FADES_MODE,
+ SYNCGROUP_PREFIX,
+)
+from music_assistant.models.player import GroupPlayer, Player
+
+if TYPE_CHECKING:
+ from music_assistant.models.player_provider import PlayerProvider
+
+ from .player_controller import PlayerController
+
+
+SUPPORT_DYNAMIC_LEADER = {
+ # providers that support dynamic leader selection in a syncgroup
+ # meaning that if you would remove the current leader from the group,
+ # the provider will automatically select a new leader from the remaining members
+ # and the music keeps playing uninterrupted.
+ "airplay",
+ "squeezelite",
+ "resonate",
+ # TODO: Get this working with Sonos as well (need to handle range requests)
+}
+
+OPTIONAL_FEATURES = {
+ PlayerFeature.ENQUEUE,
+ PlayerFeature.GAPLESS_PLAYBACK,
+ PlayerFeature.GAPLESS_DIFFERENT_SAMPLERATE,
+ PlayerFeature.NEXT_PREVIOUS,
+ PlayerFeature.PAUSE,
+ PlayerFeature.PLAY_ANNOUNCEMENT,
+ PlayerFeature.SEEK,
+ PlayerFeature.SELECT_SOURCE,
+ PlayerFeature.VOLUME_MUTE,
+}
+
+
+class SyncGroupPlayer(GroupPlayer):
+ """Helper class for a (provider specific) SyncGroup player."""
+
+ _attr_type: PlayerType = PlayerType.GROUP
+ sync_leader: Player | None = None
+ """The active sync leader player for this syncgroup."""
+
+ @cached_property
+ def is_dynamic(self) -> bool:
+ """Return if the player is a dynamic group player."""
+ return bool(self.config.get_value(CONF_DYNAMIC_GROUP_MEMBERS, False))
+
+ def __init__(
+ self,
+ provider: PlayerProvider,
+ player_id: str,
+ ) -> None:
+ """Initialize GroupPlayer instance."""
+ super().__init__(provider, player_id)
+ self._attr_name = self.config.name or f"SyncGroup {player_id}"
+ self._attr_available = True
+ self._attr_powered = False # group players are always powered off by default
+ self._attr_active_source = None
+ self._attr_device_info = DeviceInfo(model="Sync Group", manufacturer=provider.name)
+ self._attr_supported_features = {
+ PlayerFeature.POWER,
+ PlayerFeature.VOLUME_SET,
+ }
+
+ async def on_config_updated(self) -> None:
+ """Handle logic when the player is loaded or updated."""
+ # Config is only available after the player was registered
+ static_members = cast("list[str]", self.config.get_value(CONF_GROUP_MEMBERS, []))
+ self._attr_static_group_members = static_members.copy()
+ if not self.powered:
+ self._attr_group_members = static_members.copy()
+ if self.is_dynamic:
+ self._attr_supported_features.add(PlayerFeature.SET_MEMBERS)
+ else:
+ self._attr_supported_features.discard(PlayerFeature.SET_MEMBERS)
+
+ @property
+ def supported_features(self) -> set[PlayerFeature]:
+ """Return the supported features of the player."""
+ if self.sync_leader:
+ base_features = self._attr_supported_features.copy()
+ # add features supported by the sync leader
+ for feature in OPTIONAL_FEATURES:
+ if feature in self.sync_leader.supported_features:
+ base_features.add(feature)
+ return base_features
+ return self._attr_supported_features
+
+ @property
+ def playback_state(self) -> PlaybackState:
+ """Return the current playback state of the player."""
+ if self.power_state:
+ return self.sync_leader.playback_state if self.sync_leader else PlaybackState.IDLE
+ else:
+ return PlaybackState.IDLE
+
+ @cached_property
+ def flow_mode(self) -> bool:
+ """
+ Return if the player needs flow mode.
+
+ Will by default be set to True if the player does not support PlayerFeature.ENQUEUE
+ or has a flow mode config entry set to True.
+ """
+ if leader := self.sync_leader:
+ return leader.flow_mode
+ return False
+
+ @property
+ def elapsed_time(self) -> float | None:
+ """Return the elapsed time in (fractional) seconds of the current track (if any)."""
+ return self.sync_leader.elapsed_time if self.sync_leader else None
+
+ @property
+ def elapsed_time_last_updated(self) -> float | None:
+ """Return when the elapsed time was last updated."""
+ return self.sync_leader.elapsed_time_last_updated if self.sync_leader else None
+
+ @property
+ def current_media(self) -> PlayerMedia | None:
+ """Return the current media item (if any) loaded in the player."""
+ return self.sync_leader.current_media if self.sync_leader else self._attr_current_media
+
+ @property
+ def active_source(self) -> str | None:
+ """Return the active source id (if any) of the player."""
+ return self._attr_active_source
+
+ @property
+ def source_list(self) -> list[PlayerSource]:
+ """Return list of available (native) sources for this player."""
+ if self.sync_leader:
+ return self.sync_leader.source_list
+ return []
+
+ @property
+ def can_group_with(self) -> set[str]:
+ """
+ Return the id's of players this player can group with.
+
+ This should return set of player_id's this player can group/sync with
+ or just the provider's instance_id if all players can group with each other.
+ """
+ if self.is_dynamic and (leader := self.sync_leader):
+ return leader.can_group_with
+ elif self.is_dynamic:
+ return {self.provider.lookup_key}
+ else:
+ return set()
+
+ async def get_config_entries(self) -> list[ConfigEntry]:
+ """Return all (provider/player specific) Config Entries for the given player (if any)."""
+ entries: list[ConfigEntry] = [
+ # default entries for player groups
+ *await super().get_config_entries(),
+ # add syncgroup specific entries
+ ConfigEntry(
+ key=CONF_GROUP_MEMBERS,
+ type=ConfigEntryType.STRING,
+ multi_value=True,
+ label="Group members",
+ default_value=[],
+ description="Select all players you want to be part of this group",
+ required=False, # needed for dynamic members (which allows empty members list)
+ options=[
+ ConfigValueOption(x.display_name, x.player_id)
+ for x in self.provider.players
+ if x.type != PlayerType.GROUP
+ ],
+ ),
+ ConfigEntry(
+ key="dynamic_members",
+ type=ConfigEntryType.BOOLEAN,
+ label="Enable dynamic members",
+ description="Allow (un)joining members dynamically, so the group more or less "
+ "behaves the same like manually syncing players together, "
+ "with the main difference being that the group player will hold the queue.",
+ default_value=False,
+ required=False,
+ ),
+ ]
+ # combine base group entries with (base) player entries for this player type
+ child_player = next((x for x in self.provider.players if x.type == PlayerType.PLAYER), None)
+ if child_player:
+ allowed_conf_entries = (
+ CONF_HTTP_PROFILE,
+ CONF_ENABLE_ICY_METADATA,
+ CONF_CROSSFADE_DURATION,
+ CONF_OUTPUT_CODEC,
+ CONF_FLOW_MODE,
+ CONF_SAMPLE_RATES,
+ CONF_SMART_FADES_MODE,
+ )
+ child_config_entries = await child_player.get_config_entries()
+ entries.extend(
+ [entry for entry in child_config_entries if entry.key in allowed_conf_entries]
+ )
+ return entries
+
+ async def stop(self) -> None:
+ """Send STOP command to given player."""
+ if sync_leader := self.sync_leader:
+ await sync_leader.stop()
+
+ async def play(self) -> None:
+ """Send PLAY command to given player."""
+ if sync_leader := self.sync_leader:
+ await sync_leader.play()
+
+ async def pause(self) -> None:
+ """Send PAUSE command to given player."""
+ if sync_leader := self.sync_leader:
+ await sync_leader.pause()
+
+ async def power(self, powered: bool) -> None:
+ """Handle POWER command to group player."""
+ prev_power = self._attr_powered
+ if powered == prev_power:
+ # no change
+ return
+
+ # always stop at power off
+ if not powered and self.playback_state in (PlaybackState.PLAYING, PlaybackState.PAUSED):
+ await self.stop()
+
+ # optimistically set the group state
+
+ self._attr_powered = powered
+ self.update_state()
+
+ if not prev_power and powered:
+ # ensure static members are present when powering on
+ for static_group_member in self._attr_static_group_members:
+ member_player = self.mass.players.get(static_group_member)
+ if not member_player or not member_player.available or not member_player.enabled:
+ if static_group_member in self._attr_group_members:
+ self._attr_group_members.remove(static_group_member)
+ continue
+ if static_group_member not in self._attr_group_members:
+ self._attr_group_members.append(static_group_member)
+ # Select sync leader and handle turn on
+ new_leader = self._select_sync_leader()
+ # handle TURN_ON of the group player by turning on all members
+ for member in self.mass.players.iter_group_members(
+ self, only_powered=False, active_only=False
+ ):
+ await self._handle_member_collisions(member)
+ if not member.powered and member.power_control != PLAYER_CONTROL_NONE:
+ await member.power(True)
+ # Set up the sync group with the new leader
+ await self._handle_leader_transition(new_leader)
+ elif prev_power and not powered:
+ # handle TURN_OFF of the group player by dissolving group and turning off all members
+ await self._dissolve_syncgroup()
+ # turn off all group members
+ for member in self.mass.players.iter_group_members(
+ self, only_powered=True, active_only=True
+ ):
+ if member.powered and member.power_control != PLAYER_CONTROL_NONE:
+ await member.power(False)
+
+ if not powered:
+ # Reset to unfiltered static members list when powered off
+ # (the frontend will hide unavailable members)
+ self._attr_group_members = self._attr_static_group_members.copy()
+ self._attr_active_source = None
+ # and clear the sync leader
+ self.sync_leader = None
+ self.update_state()
+
+ async def volume_set(self, volume_level: int) -> None:
+ """Send VOLUME_SET command to given player."""
+ # group volume is already handled in the player manager
+
+ async def play_media(self, media: PlayerMedia) -> None:
+ """Handle PLAY MEDIA on given player."""
+ # power on (which will also resync if needed)
+ await self.power(True)
+ # simply forward the command to the sync leader
+ if sync_leader := self.sync_leader:
+ await sync_leader.play_media(media)
+ self._attr_current_media = deepcopy(media)
+ self._attr_active_source = media.source_id
+ self.update_state()
+ else:
+ raise RuntimeError("an empty group cannot play media, consider adding members first")
+
+ async def enqueue_next_media(self, media: PlayerMedia) -> None:
+ """Handle enqueuing of a next media item on the player."""
+ if sync_leader := self.sync_leader:
+ await sync_leader.enqueue_next_media(media)
+
+ async def set_members(
+ self,
+ player_ids_to_add: list[str] | None = None,
+ player_ids_to_remove: list[str] | None = None,
+ ) -> None:
+ """Handle SET_MEMBERS command on the player."""
+ if not self.is_dynamic:
+ raise UnsupportedFeaturedException(
+ f"Group {self.display_name} does not allow dynamically adding/removing members!"
+ )
+ # handle additions
+ final_players_to_add: list[str] = []
+ for player_id in player_ids_to_add or []:
+ if player_id in self._attr_group_members:
+ continue
+ if player_id == self.player_id:
+ raise UnsupportedFeaturedException(
+ f"Cannot add {self.display_name} to itself as a member!"
+ )
+ self._attr_group_members.append(player_id)
+ final_players_to_add.append(player_id)
+ # handle removals
+ final_players_to_remove: list[str] = []
+ for player_id in player_ids_to_remove or []:
+ if player_id not in self._attr_group_members:
+ continue
+ if player_id == self.player_id:
+ raise UnsupportedFeaturedException(
+ f"Cannot remove {self.display_name} from itself as a member!"
+ )
+ self._attr_group_members.remove(player_id)
+ final_players_to_remove.append(player_id)
+ self.update_state()
+ if not self.powered:
+ # Don't need to do anything else if the group is powered off
+ # The syncing will be done once powered on
+ return
+ next_leader = self._select_sync_leader()
+ prev_leader = self.sync_leader
+
+ if prev_leader and next_leader is None:
+ # Edge case: we no longer have any members in the group (and thus no leader)
+ await self._handle_leader_transition(None)
+ elif prev_leader != next_leader:
+ # Edge case: we had changed the leader (or just got one)
+ await self._handle_leader_transition(next_leader)
+ elif self.sync_leader and (player_ids_to_add or player_ids_to_remove):
+ # if the group still has the same leader, we need to (re)sync the members
+ # Handle collisions for newly added players
+ for player_id in final_players_to_add:
+ if player := self.mass.players.get(player_id):
+ await self._handle_member_collisions(player)
+
+ await self.sync_leader.set_members(
+ player_ids_to_add=final_players_to_add,
+ player_ids_to_remove=final_players_to_remove,
+ )
+
+ async def _form_syncgroup(self) -> None:
+ """Form syncgroup by syncing all (possible) members."""
+ if self.sync_leader is None:
+ # This is an empty group, leader will be selected once a member is added
+ self._attr_group_members = []
+ self.update_state()
+ return
+ # ensure the sync leader is first in the list
+ self._attr_group_members = [
+ self.sync_leader.player_id,
+ *[x for x in self._attr_group_members if x != self.sync_leader.player_id],
+ ]
+ self.update_state()
+ members_to_sync: list[str] = []
+ for member in self.mass.players.iter_group_members(self, active_only=False):
+ # Handle collisions before attempting to sync
+ await self._handle_member_collisions(member)
+
+ if member.synced_to and member.synced_to != self.sync_leader.player_id:
+ # ungroup first
+ await member.ungroup()
+ if member.player_id == self.sync_leader.player_id:
+ # skip sync leader
+ continue
+ if (
+ member.synced_to == self.sync_leader.player_id
+ and member.player_id in self.sync_leader.group_members
+ ):
+ # already synced
+ continue
+ members_to_sync.append(member.player_id)
+ if members_to_sync:
+ await self.sync_leader.set_members(members_to_sync)
+
+ async def _dissolve_syncgroup(self) -> None:
+ """Dissolve the current syncgroup by ungrouping all members and restoring leader queue."""
+ if sync_leader := self.sync_leader:
+ # dissolve the temporary syncgroup from the sync leader
+ sync_children = [x for x in sync_leader.group_members if x != sync_leader.player_id]
+ if sync_children:
+ await sync_leader.set_members(player_ids_to_remove=sync_children)
+ # Reset the leaders queue since it is no longer part of this group
+ sync_leader.update_state()
+
+ async def _handle_leader_transition(self, new_leader: Player | None) -> None:
+ """Handle transition from current leader to new leader."""
+ prev_leader = self.sync_leader
+ was_playing = False
+
+ if (
+ prev_leader
+ and new_leader
+ and prev_leader != new_leader
+ and self.provider.domain in SUPPORT_DYNAMIC_LEADER
+ ):
+ # provider supports dynamic leader selection, so just remove/add members
+ await prev_leader.ungroup()
+ self.sync_leader = new_leader
+ # allow some time to propagate the changes before resyncing
+ await asyncio.sleep(2)
+ await self._form_syncgroup()
+ return
+
+ if prev_leader:
+ # Save current media and playback state for potential restart
+ was_playing = self.playback_state == PlaybackState.PLAYING
+ # Stop current playback and dissolve existing group
+ await self.stop()
+ await self._dissolve_syncgroup()
+ # allow some time to propagate the changes before resyncing
+ await asyncio.sleep(2)
+
+ # Set new leader
+ self.sync_leader = new_leader
+
+ if new_leader:
+ # form a syncgroup with the new leader
+ await self._form_syncgroup()
+
+ # Restart playback if requested and we have media to play
+ if was_playing and self.current_media is not None:
+ await new_leader.play_media(self.current_media)
+
+ def _select_sync_leader(self) -> Player | None:
+ """Select the active sync leader player for a syncgroup."""
+ if self.sync_leader and self.sync_leader.player_id in self.group_members:
+ # Don't change the sync leader if we already have one
+ return self.sync_leader
+ for prefer_sync_leader in (True, False):
+ for child_player in self.mass.players.iter_group_members(self):
+ if prefer_sync_leader and child_player.synced_to:
+ # prefer the first player that already has sync children
+ continue
+ if child_player.active_group not in (
+ None,
+ self.player_id,
+ child_player.player_id,
+ ):
+ # this should not happen (because its already handled in the power on logic),
+ # but guard it just in case bad things happen
+ continue
+ return child_player
+ return None
+
+ async def _handle_member_collisions(self, member: Player) -> None:
+ """Handle collisions when adding a member to the sync group."""
+ active_groups = member.active_groups
+ for group in active_groups:
+ if group == self.player_id:
+ continue
+ # collision: child player is part another group that is already active !
+ # solve this by trying to leave the group first
+ if other_group := self.mass.players.get(group):
+ if (
+ other_group.supports_feature(PlayerFeature.SET_MEMBERS)
+ and member.player_id not in other_group.static_group_members
+ ):
+ await other_group.set_members(player_ids_to_remove=[member.player_id])
+ else:
+ # if the other group does not support SET_MEMBERS or it is a static
+ # member, we need to power it off to leave the group
+ await other_group.power(False)
+ if (
+ member.synced_to is not None
+ and member.synced_to != self.sync_leader
+ and (synced_to_player := self.mass.players.get(member.synced_to))
+ and member.player_id in synced_to_player.group_members
+ ):
+ # collision: child player is synced to another player and still in that group
+ # ungroup it first
+ await synced_to_player.set_members(player_ids_to_remove=[member.player_id])
+
+
+class SyncGroupController:
+ """Controller managing SyncGroup players."""
+
+ def __init__(self, player_controller: PlayerController) -> None:
+ """Initialize SyncGroupController."""
+ self.player_controller = player_controller
+ self.mass = player_controller.mass
+
+ async def create_group_player(
+ self, provider: PlayerProvider, name: str, members: list[str], dynamic: bool = True
+ ) -> Player:
+ """
+ Create new SyncGroup Player.
+
+ :param provider: The provider to create the group player for
+ :param name: Name of the group player
+ :param members: List of player ids to add to the group
+ :param dynamic: Whether the group is dynamic (members can change)
+ """
+ # default implementation for providers that support syncing players
+ if ProviderFeature.SYNC_PLAYERS not in provider.supported_features:
+ # the frontend should already prevent this, but just in case
+ raise UnsupportedFeaturedException(
+ f"Provider {provider.name} does not support player syncing!"
+ )
+ # Create a new syncgroup player with the given members
+ members = [x for x in members if x in [y.player_id for y in provider.players]]
+ player_id = f"{SYNCGROUP_PREFIX}{shortuuid.random(8).lower()}"
+ self.mass.config.create_default_player_config(
+ player_id=player_id,
+ provider=provider.lookup_key,
+ name=name,
+ enabled=True,
+ values={
+ CONF_GROUP_MEMBERS: members,
+ CONF_DYNAMIC_GROUP_MEMBERS: dynamic,
+ },
+ )
+ return await self._register_syncgroup_player(player_id, provider)
+
+ async def remove_group_player(self, player_id: str) -> None:
+ """
+ Remove a group player.
+
+ :param player_id: ID of the group player to remove.
+ """
+ # we simply permanently unregister the syncgroup player and wipe its config
+ await self.mass.players.unregister(player_id, True)
+
+ async def _register_syncgroup_player(self, player_id: str, provider: PlayerProvider) -> Player:
+ """Register a syncgroup player."""
+ syncgroup = SyncGroupPlayer(provider, player_id)
+ await self.mass.players.register_or_update(syncgroup)
+ return syncgroup
+
+ async def on_provider_loaded(self, provider: PlayerProvider) -> None:
+ """Handle logic when a provider is loaded."""
+ # register existing syncgroup players for this provider
+ for player_conf in await self.mass.config.get_player_configs(provider.lookup_key):
+ if player_conf.player_id.startswith(SYNCGROUP_PREFIX):
+ await self._register_syncgroup_player(player_conf.player_id, provider)
+
+ async def on_provider_unload(self, provider: PlayerProvider) -> None:
+ """Handle logic when a provider is (about to get) unloaded."""
+ # unregister existing syncgroup players for this provider
+ for player in self.mass.players.all(
+ provider_filter=provider.lookup_key, return_sync_groups=True
+ ):
+ if player.player_id.startswith(SYNCGROUP_PREFIX):
+ await self.mass.players.unregister(player.player_id, False)
import urllib.parse
from collections.abc import AsyncGenerator
from dataclasses import dataclass
-from typing import TYPE_CHECKING, TypedDict
+from typing import TYPE_CHECKING
from aiofiles.os import wrap
from aiohttp import web
SILENCE_FILE,
VERBOSE_LOG_LEVEL,
)
+from music_assistant.controllers.players.player_controller import AnnounceData
from music_assistant.helpers.audio import (
CACHE_FILES_IN_USE,
get_chunksize,
session_id: str
-class AnnounceData(TypedDict):
- """Announcement data."""
-
- announcement_url: str
- pre_announce: bool
- pre_announce_url: str
-
-
class StreamsController(CoreController):
"""Webserver Controller to stream audio to players."""
if plugin_source.stream_type == StreamType.CUSTOM
else plugin_source.path
)
- player.active_source = plugin_source_id
+ player.state.active_source = plugin_source_id
plugin_source.in_use_by = player_id
try:
async for chunk in get_ffmpeg_stream(
"Finished streaming PluginSource %s to %s", plugin_source_id, player_id
)
await asyncio.sleep(0.5)
- player.active_source = player.player_id
+ player.state.active_source = player.player_id
plugin_source.in_use_by = None
async def get_queue_item_stream(
MASS_LOGGER_NAME,
VERBOSE_LOG_LEVEL,
)
+from music_assistant.controllers.players.sync_groups import SyncGroupPlayer
from music_assistant.helpers.json import JSON_DECODE_EXCEPTIONS, json_loads
from music_assistant.helpers.throttle_retry import BYPASS_THROTTLER
from music_assistant.helpers.util import clean_stream_title, remove_file
-from music_assistant.models.player import SyncGroupPlayer
from .datetime import utc
from .dsp import filter_to_ffmpeg_params
from music_assistant.controllers.metadata import MetaDataController
from music_assistant.controllers.music import MusicController
from music_assistant.controllers.player_queues import PlayerQueuesController
-from music_assistant.controllers.players import PlayerController
+from music_assistant.controllers.players.player_controller import PlayerController
from music_assistant.controllers.streams import StreamsController
from music_assistant.controllers.webserver import WebserverController
from music_assistant.helpers.aiohttp_client import create_clientsession
if provider.manifest.mdns_discovery:
for mdns_type in provider.manifest.mdns_discovery:
self._aiobrowser.types.discard(mdns_type)
- # make sure to stop any running sync tasks first
- for sync_task in self.music.in_progress_syncs:
- if sync_task.provider_instance == instance_id:
- if sync_task.task:
- sync_task.task.cancel()
+ if isinstance(provider, PlayerProvider):
+ await self.players.on_provider_unload(provider)
+ if isinstance(provider, MusicProvider):
+ await self.music.on_provider_unload(provider)
# check if there are no other providers dependent of this provider
for dep_prov in self.providers:
if dep_prov.manifest.depends_on == provider.domain:
self.signal_event(EventType.PROVIDERS_UPDATED, data=self.get_providers())
await self._update_available_providers_cache()
if isinstance(provider, MusicProvider):
- await self.music.schedule_provider_sync(provider.instance_id)
+ await self.music.on_provider_loaded(provider)
+ if isinstance(provider, PlayerProvider):
+ await self.players.on_provider_loaded(provider)
async def __load_provider_manifests(self) -> None:
"""Preload all available provider manifest files."""
ATTR_FAKE_MUTE,
ATTR_FAKE_POWER,
ATTR_FAKE_VOLUME,
- CONF_CROSSFADE_DURATION,
- CONF_DYNAMIC_GROUP_MEMBERS,
- CONF_ENABLE_ICY_METADATA,
CONF_ENTRY_ANNOUNCE_VOLUME,
CONF_ENTRY_ANNOUNCE_VOLUME_MAX,
CONF_ENTRY_ANNOUNCE_VOLUME_MIN,
CONF_ENTRY_VOLUME_NORMALIZATION_TARGET,
CONF_EXPOSE_PLAYER_TO_HA,
CONF_FLOW_MODE,
- CONF_GROUP_MEMBERS,
CONF_HIDE_PLAYER_IN_UI,
- CONF_HTTP_PROFILE,
CONF_MUTE_CONTROL,
- CONF_OUTPUT_CODEC,
CONF_POWER_CONTROL,
CONF_PRE_ANNOUNCE_CHIME_URL,
- CONF_SAMPLE_RATES,
- CONF_SMART_FADES_MODE,
CONF_VOLUME_CONTROL,
)
from music_assistant.helpers.util import (
"""Return if the player is available."""
return self._attr_available
- @available.setter
- def available(self, value: bool) -> None:
- """
- Set the availability of the player.
-
- :param value: bool if the player is available or not.
- """
- if self._attr_available != value:
- self._attr_available = value
- # also update the state
- self._state.available = value
-
@property
def name(self) -> str | None:
"""Return the name of the player."""
"""Return the elapsed time in (fractional) seconds of the current track (if any)."""
return self._attr_elapsed_time
- @elapsed_time.setter
- def elapsed_time(self, value: float | None) -> None:
- """Set the elapsed time on the player."""
- if self._attr_elapsed_time != value:
- self._attr_elapsed_time = value
- # also update the state
- self._state.elapsed_time = value
- # update the last updated time
- self._attr_elapsed_time_last_updated = time.time()
-
@property
def elapsed_time_last_updated(self) -> float | None:
"""
"""
return self._attr_active_source
- @active_source.setter
- def active_source(self, value: str | None) -> None:
- """Set the active source of the player."""
- self._attr_active_source = value
-
@property
def source_list(self) -> list[PlayerSource]:
"""Return list of available (native) sources for this player."""
"""Return the current media being played by the player."""
return self._attr_current_media
- @current_media.setter
- def current_media(self, value: PlayerMedia | None) -> None:
- """Set the current media being played by the player."""
- self._attr_current_media = value
-
@property
def needs_poll(self) -> bool:
"""Return if the player needs to be polled for state updates."""
"""
# if the player is grouped/synced, use the active source of the group/parent player
if parent_player_id := (self.synced_to or self.active_group):
- if parent_player := self.mass.players.get(parent_player_id):
- return parent_player.active_source_state
+ return parent_player_id
# in case player's source is None, return the player_id (to indicate MA is active source)
return self.active_source or self.player_id
"""
return bool(self._config.get_value(CONF_EXPOSE_PLAYER_TO_HA))
- @cached_property
+ @property
@final
def mass_queue_active(self) -> bool:
"""
Returns a dict with the state attributes that have changed.
"""
prev_state = deepcopy(self._state)
- self._state.name = self.display_name
- self._state.available = self.available
- self._state.device_info = self.device_info
- self._state.supported_features = self.supported_features
- self._state.playback_state = self.playback_state
- self._state.elapsed_time = self.elapsed_time
- self._state.elapsed_time_last_updated = self.elapsed_time_last_updated
- self._state.powered = self.power_state
- self._state.volume_level = self.volume_state
- self._state.volume_muted = self.volume_muted_state
- self._state.group_members = UniqueList(self.group_members)
- self._state.static_group_members = UniqueList(self.static_group_members)
- self._state.can_group_with = self.can_group_with
- self._state.synced_to = self.synced_to
- self._state.active_source = self.active_source_state
- self._state.source_list = self.source_list_state
- self._state.active_group = self.active_group
- self._state.current_media = self.current_media
- self._state.enabled = self.enabled
- self._state.hide_player_in_ui = self.hide_player_in_ui
- self._state.expose_to_ha = self.expose_to_ha
- self._state.icon = self.icon
- self._state.group_volume = self.group_volume
- self._state.extra_attributes = self.extra_attributes
- self._state.power_control = self.power_control
- self._state.volume_control = self.volume_control
- self._state.mute_control = self.mute_control
-
- # correct available state if needed
- if not self._state.enabled:
- self._state.available = False
+ self._state = PlayerState(
+ player_id=self.player_id,
+ provider=self.provider_id,
+ type=self.type,
+ available=self.enabled and self.available,
+ device_info=self.device_info,
+ supported_features=self.supported_features,
+ playback_state=self.playback_state,
+ elapsed_time=self.elapsed_time,
+ elapsed_time_last_updated=self.elapsed_time_last_updated,
+ powered=self.powered,
+ volume_level=self.volume_level,
+ volume_muted=self.volume_muted,
+ group_members=UniqueList(self.group_members),
+ static_group_members=UniqueList(self.static_group_members),
+ can_group_with=self.can_group_with,
+ synced_to=self.synced_to,
+ active_source=self.active_source_state,
+ source_list=self.source_list_state,
+ active_group=self.active_group,
+ current_media=self.current_media,
+ name=self.display_name,
+ enabled=self.enabled,
+ hide_player_in_ui=self.hide_player_in_ui,
+ expose_to_ha=self.expose_to_ha,
+ icon=self.icon,
+ group_volume=self.group_volume,
+ extra_attributes=self.extra_attributes,
+ power_control=self.power_control,
+ volume_control=self.volume_control,
+ mute_control=self.mute_control,
+ )
# correct group_members if needed
if self._state.group_members == [self.player_id]:
return not self.__eq__(other)
+__all__ = [
+ # explicitly re-export the models we imported from the models package,
+ # for convenience reasons
+ "EXTRA_ATTRIBUTES_TYPES",
+ "DeviceInfo",
+ "Player",
+ "PlayerMedia",
+ "PlayerSource",
+ "PlayerState",
+]
+
+
class GroupPlayer(Player):
"""Helper class for a (generic) group player."""
# This will set the (relative) volume level on all child players.
# free to override if you want to handle this differently.
await self.mass.players.set_group_volume(self, volume_level)
-
-
-class SyncGroupPlayer(GroupPlayer):
- """Helper class for a (provider specific) SyncGroup player."""
-
- _attr_type: PlayerType = PlayerType.GROUP
- sync_leader: Player | None = None
- """The active sync leader player for this syncgroup."""
-
- @cached_property
- def is_dynamic(self) -> bool:
- """Return if the player is a dynamic group player."""
- return bool(self.config.get_value(CONF_DYNAMIC_GROUP_MEMBERS, False))
-
- def __init__(
- self,
- provider: PlayerProvider,
- player_id: str,
- ) -> None:
- """Initialize GroupPlayer instance."""
- super().__init__(provider, player_id)
- self._attr_name = self.config.name or f"SyncGroup {player_id}"
- self._attr_available = True
- self._attr_powered = False # group players are always powered off by default
- self._attr_active_source = player_id
- self._attr_device_info = DeviceInfo(model="Sync Group", manufacturer=provider.name)
- self._attr_supported_features = {
- PlayerFeature.POWER,
- PlayerFeature.VOLUME_SET,
- }
-
- async def on_config_updated(self) -> None:
- """Handle logic when the player is loaded or updated."""
- # Config is only available after the player was registered
- static_members = cast("list[str]", self.config.get_value(CONF_GROUP_MEMBERS, []))
- self._attr_static_group_members = static_members.copy()
- if not self.powered:
- self._attr_group_members = static_members.copy()
- if self.is_dynamic:
- self._attr_supported_features.add(PlayerFeature.SET_MEMBERS)
- else:
- self._attr_supported_features.discard(PlayerFeature.SET_MEMBERS)
-
- @property
- def supported_features(self) -> set[PlayerFeature]:
- """Return the supported features of the player."""
- return self._attr_supported_features
-
- @property
- def playback_state(self) -> PlaybackState:
- """Return the current playback state of the player."""
- if self.power_state:
- return self.sync_leader.playback_state if self.sync_leader else PlaybackState.IDLE
- else:
- return PlaybackState.IDLE
-
- @cached_property
- def flow_mode(self) -> bool:
- """
- Return if the player needs flow mode.
-
- Will by default be set to True if the player does not support PlayerFeature.ENQUEUE
- or has a flow mode config entry set to True.
- """
- if leader := self.sync_leader:
- return leader.flow_mode
- return False
-
- @property
- def elapsed_time(self) -> float | None:
- """Return the elapsed time in (fractional) seconds of the current track (if any)."""
- return self.sync_leader.elapsed_time if self.sync_leader else None
-
- @elapsed_time.setter
- def elapsed_time(self, value: float | None) -> None:
- """Set the elapsed time on the player."""
- raise NotImplementedError("elapsed_time is read-only on a SyncGroup player")
-
- @property
- def elapsed_time_last_updated(self) -> float | None:
- """Return when the elapsed time was last updated."""
- return self.sync_leader.elapsed_time_last_updated if self.sync_leader else None
-
- @property
- def can_group_with(self) -> set[str]:
- """
- Return the id's of players this player can group with.
-
- This should return set of player_id's this player can group/sync with
- or just the provider's instance_id if all players can group with each other.
- """
- if self.is_dynamic and (leader := self.sync_leader):
- return leader.can_group_with
- elif self.is_dynamic:
- return {self.provider.lookup_key}
- else:
- return set()
-
- async def get_config_entries(self) -> list[ConfigEntry]:
- """Return all (provider/player specific) Config Entries for the given player (if any)."""
- entries: list[ConfigEntry] = [
- # default entries for player groups
- *await super().get_config_entries(),
- # add syncgroup specific entries
- ConfigEntry(
- key=CONF_GROUP_MEMBERS,
- type=ConfigEntryType.STRING,
- multi_value=True,
- label="Group members",
- default_value=[],
- description="Select all players you want to be part of this group",
- required=False, # needed for dynamic members (which allows empty members list)
- options=[
- ConfigValueOption(x.display_name, x.player_id)
- for x in self.provider.players
- if x.type != PlayerType.GROUP
- ],
- ),
- ConfigEntry(
- key="dynamic_members",
- type=ConfigEntryType.BOOLEAN,
- label="Enable dynamic members",
- description="Allow (un)joining members dynamically, so the group more or less "
- "behaves the same like manually syncing players together, "
- "with the main difference being that the group player will hold the queue.",
- default_value=False,
- required=False,
- ),
- ]
- # combine base group entries with (base) player entries for this player type
- child_player = next((x for x in self.provider.players if x.type != PlayerType.GROUP), None)
- if child_player:
- allowed_conf_entries = (
- CONF_HTTP_PROFILE,
- CONF_ENABLE_ICY_METADATA,
- CONF_CROSSFADE_DURATION,
- CONF_OUTPUT_CODEC,
- CONF_FLOW_MODE,
- CONF_SAMPLE_RATES,
- CONF_SMART_FADES_MODE,
- )
- child_config_entries = await child_player.get_config_entries()
- entries.extend(
- [entry for entry in child_config_entries if entry.key in allowed_conf_entries]
- )
- return entries
-
- async def stop(self) -> None:
- """Send STOP command to given player."""
- if sync_leader := self.sync_leader:
- await sync_leader.stop()
-
- async def play(self) -> None:
- """Send PLAY command to given player."""
- if sync_leader := self.sync_leader:
- await sync_leader.play()
-
- async def pause(self) -> None:
- """Send PAUSE command to given player."""
- if sync_leader := self.sync_leader:
- await sync_leader.pause()
-
- async def _handle_member_collisions(self, member: Player) -> None:
- """Handle collisions when adding a member to the sync group."""
- active_groups = member.active_groups
- for group in active_groups:
- if group == self.player_id:
- continue
- # collision: child player is part another group that is already active !
- # solve this by trying to leave the group first
- if other_group := self.mass.players.get(group):
- if (
- other_group.supports_feature(PlayerFeature.SET_MEMBERS)
- and member.player_id not in other_group.static_group_members
- ):
- await other_group.set_members(player_ids_to_remove=[member.player_id])
- else:
- # if the other group does not support SET_MEMBERS or it is a static
- # member, we need to power it off to leave the group
- await other_group.power(False)
- if (
- member.synced_to is not None
- and member.synced_to != self.sync_leader
- and (synced_to_player := self.mass.players.get(member.synced_to))
- and member.player_id in synced_to_player.group_members
- ):
- # collision: child player is synced to another player and still in that group
- # ungroup it first
- await synced_to_player.set_members(player_ids_to_remove=[member.player_id])
-
- async def power(self, powered: bool) -> None:
- """Handle POWER command to group player."""
- # always stop at power off
- if not powered and self.playback_state in (PlaybackState.PLAYING, PlaybackState.PAUSED):
- await self.stop()
-
- # optimistically set the group state
- prev_power = self._attr_powered
- self._attr_powered = powered
- self.update_state()
-
- if powered:
- # reset the group members to the available static members when powering on
- self._attr_group_members = []
- for static_group_member in self._attr_static_group_members:
- if (
- (member_player := self.mass.players.get(static_group_member))
- and member_player.available
- and member_player.enabled
- ):
- self._attr_group_members.append(static_group_member)
- # Select sync leader and handle turn on
- new_leader = self._select_sync_leader()
- # handle TURN_ON of the group player by turning on all members
- for member in self.mass.players.iter_group_members(
- self, only_powered=False, active_only=False
- ):
- await self._handle_member_collisions(member)
- if not member.powered and member.power_control != PLAYER_CONTROL_NONE:
- await member.power(True)
- # Set up the sync group with the new leader
- await self._handle_leader_transition(new_leader)
- elif prev_power:
- # handle TURN_OFF of the group player by dissolving group and turning off all members
- await self._dissolve_syncgroup()
- # turn off all group members
- for member in self.mass.players.iter_group_members(
- self, only_powered=True, active_only=True
- ):
- if member.powered and member.power_control != PLAYER_CONTROL_NONE:
- await member.power(False)
-
- if not powered:
- # Reset to unfiltered static members list when powered off
- # (the frontend will hide unavailable members)
- self._attr_group_members = self._attr_static_group_members.copy()
- # and clear the sync leader
- self.sync_leader = None
-
- async def _dissolve_syncgroup(self) -> None:
- """Dissolve the current syncgroup by ungrouping all members and restoring leader queue."""
- if sync_leader := self.sync_leader:
- # dissolve the temporary syncgroup from the sync leader
- sync_children = [x for x in sync_leader.group_members if x != sync_leader.player_id]
- if sync_children:
- await sync_leader.set_members(player_ids_to_remove=sync_children)
- # Reset the leaders queue since it is no longer part of this group
- sync_leader.active_source = None
- sync_leader.current_media = None
- sync_leader.update_state()
-
- async def _handle_leader_transition(self, new_leader: Player | None) -> None:
- """Handle transition from current leader to new leader."""
- prev_leader = self.sync_leader
- was_playing = False
-
- if prev_leader:
- # Save current media and playback state for potential restart
- was_playing = self.playback_state == PlaybackState.PLAYING
- # Stop current playback and dissolve existing group
- await self.stop()
- await self._dissolve_syncgroup()
-
- # Set new leader
- self.sync_leader = new_leader
-
- if new_leader:
- # form a syncgroup with the new leader
- await self._form_syncgroup()
-
- # Restart playback if requested and we have media to play
- if was_playing and self.current_media is not None:
- await new_leader.play_media(self.current_media)
-
- async def volume_set(self, volume_level: int) -> None:
- """Send VOLUME_SET command to given player."""
- # group volume is already handled in the player manager
-
- async def play_media(self, media: PlayerMedia) -> None:
- """Handle PLAY MEDIA on given player."""
- # power on (which will also resync if needed)
- await self.power(True)
- # simply forward the command to the sync leader
- if sync_leader := self.sync_leader:
- await sync_leader.play_media(media)
- self._attr_current_media = media
- self._attr_active_source = media.source_id
- self.update_state()
- else:
- raise RuntimeError("an empty group cannot play media, consider adding members first")
-
- async def enqueue_next_media(self, media: PlayerMedia) -> None:
- """Handle enqueuing of a next media item on the player."""
- if sync_leader := self.sync_leader:
- await sync_leader.enqueue_next_media(media)
-
- async def set_members(
- self,
- player_ids_to_add: list[str] | None = None,
- player_ids_to_remove: list[str] | None = None,
- ) -> None:
- """Handle SET_MEMBERS command on the player."""
- if not self.is_dynamic:
- raise UnsupportedFeaturedException(
- f"Group {self.display_name} does not allow dynamically adding/removing members!"
- )
- # handle additions
- final_players_to_add: list[str] = []
- for player_id in player_ids_to_add or []:
- if player_id in self._attr_group_members:
- continue
- if player_id == self.player_id:
- raise UnsupportedFeaturedException(
- f"Cannot add {self.display_name} to itself as a member!"
- )
- self._attr_group_members.append(player_id)
- final_players_to_add.append(player_id)
- # handle removals
- final_players_to_remove: list[str] = []
- for player_id in player_ids_to_remove or []:
- if player_id not in self._attr_group_members:
- continue
- if player_id == self.player_id:
- raise UnsupportedFeaturedException(
- f"Cannot remove {self.display_name} from itself as a member!"
- )
- self._attr_group_members.remove(player_id)
- final_players_to_remove.append(player_id)
- self.update_state()
- if not self.powered:
- # Don't need to do anything else if the group is powered off
- # The syncing will be done once powered on
- return
- next_leader = self._select_sync_leader()
- prev_leader = self.sync_leader
-
- if prev_leader and next_leader is None:
- # Edge case: we no longer have any members in the group (and thus no leader)
- await self._handle_leader_transition(None)
- elif prev_leader != next_leader:
- # Edge case: we had changed the leader (or just got one)
- await self._handle_leader_transition(next_leader)
- elif self.sync_leader and (player_ids_to_add or player_ids_to_remove):
- # if the group still has the same leader, we need to (re)sync the members
- # Handle collisions for newly added players
- for player_id in final_players_to_add:
- if player := self.mass.players.get(player_id):
- await self._handle_member_collisions(player)
-
- await self.sync_leader.set_members(
- player_ids_to_add=final_players_to_add,
- player_ids_to_remove=final_players_to_remove,
- )
-
- async def _form_syncgroup(self) -> None:
- """Form syncgroup by syncing all (possible) members."""
- if self.sync_leader is None:
- # This is an empty group, leader will be selected once a member is added
- self._attr_group_members = []
- self.update_state()
- return
- # ensure the sync leader is first in the list
- self._attr_group_members = [
- self.sync_leader.player_id,
- *[x for x in self._attr_group_members if x != self.sync_leader.player_id],
- ]
- self.update_state()
- members_to_sync: list[str] = []
- for member in self.mass.players.iter_group_members(self, active_only=False):
- # Handle collisions before attempting to sync
- await self._handle_member_collisions(member)
-
- if member.synced_to and member.synced_to != self.sync_leader.player_id:
- # ungroup first
- await member.ungroup()
- if member.player_id == self.sync_leader.player_id:
- # skip sync leader
- continue
- if (
- member.synced_to == self.sync_leader.player_id
- and member.player_id in self.sync_leader.group_members
- ):
- # already synced
- continue
- members_to_sync.append(member.player_id)
- if members_to_sync:
- await self.sync_leader.set_members(members_to_sync)
-
- def _select_sync_leader(self) -> Player | None:
- """Select the active sync leader player for a syncgroup."""
- if self.sync_leader and self.sync_leader.player_id in self.group_members:
- # Don't change the sync leader if we already have one
- return self.sync_leader
- for prefer_sync_leader in (True, False):
- for child_player in self.mass.players.iter_group_members(self):
- if prefer_sync_leader and child_player.synced_to:
- # prefer the first player that already has sync children
- continue
- if child_player.active_group not in (
- None,
- self.player_id,
- child_player.player_id,
- ):
- # this should not happen (because its already handled in the power on logic),
- # but guard it just in case bad things happen
- continue
- return child_player
- return None
-
-
-__all__ = [
- # explicitly re-export the models we imported from the models package,
- # for convenience reasons
- "EXTRA_ATTRIBUTES_TYPES",
- "DeviceInfo",
- "GroupPlayer",
- "Player",
- "PlayerMedia",
- "PlayerSource",
- "PlayerState",
- "SyncGroupPlayer",
-]
from typing import TYPE_CHECKING
-import shortuuid
-from music_assistant_models.enums import ProviderFeature
from zeroconf import ServiceStateChange
from zeroconf.asyncio import AsyncServiceInfo
-from music_assistant.constants import (
- CONF_DYNAMIC_GROUP_MEMBERS,
- CONF_GROUP_MEMBERS,
- SYNCGROUP_PREFIX,
-)
-from music_assistant.models.player import SyncGroupPlayer
-
from .provider import Provider
if TYPE_CHECKING:
:param members: List of player ids to add to the group
:param dynamic: Whether the group is dynamic (members can change)
"""
- # default implementation for providers that support syncing players
- if ProviderFeature.SYNC_PLAYERS in self.supported_features:
- # we simply create a new syncgroup player with the given members
- # feel free to override or extend this method in your provider
- members = [x for x in members if x in [y.player_id for y in self.players]]
- player_id = f"{SYNCGROUP_PREFIX}{shortuuid.random(8).lower()}"
- self.mass.config.create_default_player_config(
- player_id=player_id,
- provider=self.lookup_key,
- name=name,
- enabled=True,
- values={
- CONF_GROUP_MEMBERS: members,
- CONF_DYNAMIC_GROUP_MEMBERS: dynamic,
- },
- )
- return await self._register_syncgroup_player(player_id)
- # all other providers should implement this method
raise NotImplementedError
async def remove_group_player(self, player_id: str) -> None:
:param player_id: ID of the group player to remove.
"""
- # default implementation for providers that support syncing players
- if ProviderFeature.SYNC_PLAYERS in self.supported_features and player_id.startswith(
- SYNCGROUP_PREFIX
- ):
- # we simply permanently unregister the syncgroup player and wipe its config
- await self.mass.players.unregister(player_id, True)
- return
- # all other providers should implement this method
raise NotImplementedError
async def discover_players(self) -> None:
await self.on_mdns_service_state_change(
mdns_name, ServiceStateChange.Added, info
)
- # discover syncgroup players
- if (
- ProviderFeature.SYNC_PLAYERS in self.supported_features
- and ProviderFeature.CREATE_GROUP_PLAYER in self.supported_features
- ):
- for player_conf in await self.mass.config.get_player_configs(self.lookup_key):
- if player_conf.player_id.startswith(SYNCGROUP_PREFIX):
- await self._register_syncgroup_player(player_conf.player_id)
-
- async def _register_syncgroup_player(self, player_id: str) -> Player:
- """Register a syncgroup player."""
- syncgroup = SyncGroupPlayer(self, player_id)
- await self.mass.players.register_or_update(syncgroup)
- return syncgroup
@property
def players(self) -> list[Player]:
"""Return all players belonging to this provider."""
- return self.mass.players.all(provider_filter=self.lookup_key)
+ return self.mass.players.all(provider_filter=self.lookup_key, return_sync_groups=False)
# that your provider supports or an empty set if none.
# see the ProviderFeature enum for all available features
ProviderFeature.SYNC_PLAYERS,
- ProviderFeature.CREATE_GROUP_PLAYER,
- ProviderFeature.REMOVE_GROUP_PLAYER,
}
SUPPORTED_FEATURES = {
ProviderFeature.SYNC_PLAYERS,
- # support sync groups by reporting create/remove player group support
- ProviderFeature.CREATE_GROUP_PLAYER,
- ProviderFeature.REMOVE_GROUP_PLAYER,
}
SUPPORTED_FEATURES = {
ProviderFeature.SYNC_PLAYERS,
- ProviderFeature.CREATE_GROUP_PLAYER,
- ProviderFeature.REMOVE_GROUP_PLAYER,
}
Blowfish.MODE_CBC,
b"\x00\x01\x02\x03\x04\x05\x06\x07",
)
- return cipher.decrypt(chunk) # type: ignore[no-any-return]
+
+ return cipher.decrypt(chunk) # type: ignore[no-any-return,unused-ignore]
SUPPORTED_FEATURES = {
ProviderFeature.SYNC_PLAYERS,
- # support sync groups by reporting create/remove player group support
- ProviderFeature.CREATE_GROUP_PLAYER,
- ProviderFeature.REMOVE_GROUP_PLAYER,
}
+++ /dev/null
-"""Podcast Index provider for Music Assistant."""
-
-from __future__ import annotations
-
-from typing import TYPE_CHECKING
-
-from music_assistant_models.config_entries import ConfigEntry, ConfigValueType
-from music_assistant_models.enums import ConfigEntryType, ProviderFeature
-
-from .constants import CONF_API_KEY, CONF_API_SECRET, CONF_STORED_PODCASTS
-from .provider import PodcastIndexProvider
-
-if TYPE_CHECKING:
- from music_assistant_models.config_entries import ProviderConfig
- from music_assistant_models.provider import ProviderManifest
-
- from music_assistant.mass import MusicAssistant
- from music_assistant.models import ProviderInstanceType
-
-SUPPORTED_FEATURES = {
- ProviderFeature.SEARCH,
- ProviderFeature.BROWSE,
-}
-
-
-async def setup(
- mass: MusicAssistant, manifest: ProviderManifest, config: ProviderConfig
-) -> ProviderInstanceType:
- """Initialize provider(instance) with given configuration."""
- return PodcastIndexProvider(mass, manifest, config, SUPPORTED_FEATURES)
-
-
-async def get_config_entries(
- mass: MusicAssistant,
- instance_id: str | None = None,
- action: str | None = None,
- values: dict[str, ConfigValueType] | None = None,
-) -> tuple[ConfigEntry, ...]:
- """
- Return Config entries to setup this provider.
-
- instance_id: id of an existing provider instance (None if new instance setup).
- action: [optional] action key called from config entries UI.
- values: the (intermediate) raw values for config entries sent with the action.
- """
- # ruff: noqa: ARG001
- return (
- ConfigEntry(
- key=CONF_API_KEY,
- type=ConfigEntryType.STRING,
- label="API Key",
- required=True,
- description="Your Podcast Index API key. Get your free API credentials at https://api.podcastindex.org/",
- ),
- ConfigEntry(
- key=CONF_API_SECRET,
- type=ConfigEntryType.SECURE_STRING,
- label="API Secret",
- required=True,
- description="Your Podcast Index API secret",
- ),
- ConfigEntry(
- key=CONF_STORED_PODCASTS,
- type=ConfigEntryType.STRING,
- multi_value=True,
- label="Subscribed Podcasts",
- default_value=[],
- required=False,
- hidden=True,
- ),
- )
+++ /dev/null
-"""Constants for Podcast Index provider."""
-
-# Configuration keys
-CONF_API_KEY = "api_key"
-CONF_API_SECRET = "api_secret"
-CONF_STORED_PODCASTS = "stored_podcasts"
-
-# API settings
-API_BASE_URL = "https://api.podcastindex.org/api/1.0"
-
-# Browse categories
-BROWSE_TRENDING = "trending"
-BROWSE_RECENT = "recent"
-BROWSE_CATEGORIES = "categories"
+++ /dev/null
-"""Helper functions for Podcast Index provider."""
-
-from __future__ import annotations
-
-import hashlib
-import time
-from datetime import UTC, datetime
-from typing import TYPE_CHECKING, Any
-
-import aiohttp
-from music_assistant_models.enums import ContentType, ImageType, MediaType
-from music_assistant_models.errors import (
- InvalidDataError,
- LoginFailed,
- ProviderUnavailableError,
-)
-from music_assistant_models.media_items import (
- AudioFormat,
- ItemMapping,
- MediaItemImage,
- Podcast,
- PodcastEpisode,
- ProviderMapping,
- UniqueList,
-)
-
-from .constants import API_BASE_URL
-
-if TYPE_CHECKING:
- from music_assistant.mass import MusicAssistant
-
-
-async def make_api_request(
- mass: MusicAssistant,
- api_key: str,
- api_secret: str,
- endpoint: str,
- params: dict[str, Any] | None = None,
-) -> dict[str, Any]:
- """
- Make authenticated request to Podcast Index API.
-
- Handles authentication using SHA1 hash of API key, secret, and timestamp.
- Maps HTTP errors appropriately: 401 -> LoginFailed, others -> ProviderUnavailableError.
- """
- # Prepare authentication headers
- auth_date = str(int(time.time()))
- auth_string = api_key + api_secret + auth_date
- auth_hash = hashlib.sha1(auth_string.encode()).hexdigest()
-
- headers = {
- "X-Auth-Key": api_key,
- "X-Auth-Date": auth_date,
- "Authorization": auth_hash,
- }
-
- url = f"{API_BASE_URL}/{endpoint}"
-
- try:
- async with mass.http_session.get(url, headers=headers, params=params or {}) as response:
- response.raise_for_status()
-
- try:
- data: dict[str, Any] = await response.json()
- except aiohttp.ContentTypeError as err:
- raise InvalidDataError("Invalid JSON response from API") from err
-
- if str(data.get("status")).lower() != "true":
- raise InvalidDataError(data.get("description") or "API error")
-
- return data
-
- except aiohttp.ClientConnectorError as err:
- raise ProviderUnavailableError(f"Failed to connect to Podcast Index API: {err}") from err
- except aiohttp.ServerTimeoutError as err:
- raise ProviderUnavailableError(f"Podcast Index API timeout: {err}") from err
- except aiohttp.ClientResponseError as err:
- if err.status == 401:
- raise LoginFailed(f"Authentication failed: {err.status}") from err
- raise ProviderUnavailableError(f"API request failed: {err.status}") from err
-
-
-def parse_podcast_from_feed(
- feed_data: dict[str, Any], lookup_key: str, domain: str, instance_id: str
-) -> Podcast | None:
- """Parse podcast from API feed data."""
- feed_url = feed_data.get("url")
- podcast_id = feed_data.get("id")
-
- if not feed_url or not podcast_id:
- return None
-
- podcast = Podcast(
- item_id=str(podcast_id),
- name=feed_data.get("title", "Unknown Podcast"),
- publisher=feed_data.get("author") or feed_data.get("ownerName", "Unknown"),
- provider=lookup_key,
- provider_mappings={
- ProviderMapping(
- item_id=str(podcast_id),
- provider_domain=domain,
- provider_instance=instance_id,
- url=feed_url,
- )
- },
- )
-
- # Add metadata
- podcast.metadata.description = feed_data.get("description", "")
- podcast.metadata.explicit = bool(feed_data.get("explicit", False))
-
- # Set episode count only if provided
- episode_count = feed_data.get("episodeCount")
- if episode_count is not None:
- podcast.total_episodes = int(episode_count) or 0
-
- # Add image - prefer 'image' field, fallback to 'artwork'
- image_url = feed_data.get("image") or feed_data.get("artwork")
- if image_url:
- podcast.metadata.add_image(
- MediaItemImage(
- type=ImageType.THUMB,
- path=image_url,
- provider=lookup_key,
- remotely_accessible=True,
- )
- )
-
- # Add categories as genres - categories is a dict {id: name}
- categories = feed_data.get("categories", {})
- if categories and isinstance(categories, dict):
- podcast.metadata.genres = set(categories.values())
-
- # Add language
- language = feed_data.get("language", "")
- if language:
- podcast.metadata.languages = UniqueList([language])
-
- return podcast
-
-
-def parse_episode_from_data(
- episode_data: dict[str, Any],
- podcast_id: str,
- episode_idx: int,
- lookup_key: str,
- domain: str,
- instance_id: str,
- podcast_name: str | None = None,
-) -> PodcastEpisode | None:
- """Parse episode from API episode data."""
- episode_api_id = episode_data.get("id")
- if not episode_api_id:
- return None
-
- episode_id = f"{podcast_id}|{episode_api_id}"
-
- position = episode_data.get("episode")
- if position is None:
- position = episode_idx + 1
-
- if podcast_name is None:
- podcast_name = episode_data.get("feedTitle") or "Unknown Podcast"
-
- raw_duration = episode_data.get("duration")
- try:
- duration = int(raw_duration) if raw_duration is not None else 0
- except (ValueError, TypeError):
- duration = 0
-
- episode = PodcastEpisode(
- item_id=episode_id,
- provider=lookup_key,
- name=episode_data.get("title", "Unknown Episode"),
- duration=duration,
- position=position,
- podcast=ItemMapping(
- item_id=podcast_id,
- provider=lookup_key,
- name=podcast_name,
- media_type=MediaType.PODCAST,
- ),
- provider_mappings={
- ProviderMapping(
- item_id=episode_id,
- provider_domain=domain,
- provider_instance=instance_id,
- available=True,
- audio_format=AudioFormat(
- content_type=ContentType.try_parse(
- episode_data.get("enclosureType") or "audio/mpeg"
- ),
- ),
- url=episode_data.get("enclosureUrl"),
- )
- },
- )
-
- # Add metadata
- episode.metadata.description = episode_data.get("description", "")
- episode.metadata.explicit = bool(episode_data.get("explicit", 0))
-
- date_published = episode_data.get("datePublished")
- if date_published:
- episode.metadata.release_date = datetime.fromtimestamp(date_published, tz=UTC)
-
- image_url = episode_data.get("image") or episode_data.get("feedImage")
- if image_url:
- episode.metadata.add_image(
- MediaItemImage(
- type=ImageType.THUMB,
- path=image_url,
- provider=lookup_key,
- remotely_accessible=True,
- )
- )
-
- return episode
+++ /dev/null
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<!-- Created with Inkscape (http://www.inkscape.org/) -->
-
-<svg
- width="512"
- height="512"
- viewBox="0 0 135.46665 135.46665"
- version="1.1"
- id="svg1"
- xml:space="preserve"
- inkscape:version="1.3.2 (091e20e, 2023-11-25, custom)"
- sodipodi:docname="icon.svg"
- xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
- xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
- xmlns:xlink="http://www.w3.org/1999/xlink"
- xmlns="http://www.w3.org/2000/svg"
- xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
- id="namedview1"
- pagecolor="#ffffff"
- bordercolor="#000000"
- borderopacity="0.25"
- inkscape:showpageshadow="2"
- inkscape:pageopacity="0.0"
- inkscape:pagecheckerboard="0"
- inkscape:deskcolor="#d1d1d1"
- inkscape:document-units="mm"
- inkscape:zoom="1.4142136"
- inkscape:cx="256.3262"
- inkscape:cy="247.13381"
- inkscape:window-width="1920"
- inkscape:window-height="1129"
- inkscape:window-x="-8"
- inkscape:window-y="-8"
- inkscape:window-maximized="1"
- inkscape:current-layer="layer1" /><defs
- id="defs1" /><g
- inkscape:label="Layer 1"
- inkscape:groupmode="layer"
- id="layer1"><image
- width="135.46666"
- height="135.46666"
- preserveAspectRatio="none"
- xlink:href="data:image/png;base64,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 rkJggg== "
- id="image1"
- x="0.086311005"
- y="0.29999173" /></g></svg>
+++ /dev/null
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<!-- Created with Inkscape (http://www.inkscape.org/) -->
-
-<svg
- width="512"
- height="512"
- viewBox="0 0 135.46665 135.46665"
- version="1.1"
- id="svg1"
- xml:space="preserve"
- inkscape:version="1.3.2 (091e20e, 2023-11-25, custom)"
- sodipodi:docname="icon_monochrome.svg"
- xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
- xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
- xmlns:xlink="http://www.w3.org/1999/xlink"
- xmlns="http://www.w3.org/2000/svg"
- xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
- id="namedview1"
- pagecolor="#73ffff"
- bordercolor="#000000"
- borderopacity="0.25"
- inkscape:showpageshadow="2"
- inkscape:pageopacity="0.0"
- inkscape:pagecheckerboard="0"
- inkscape:deskcolor="#d1d1d1"
- inkscape:document-units="mm"
- inkscape:zoom="1.4142136"
- inkscape:cx="256.3262"
- inkscape:cy="247.13381"
- inkscape:window-width="1920"
- inkscape:window-height="1129"
- inkscape:window-x="-8"
- inkscape:window-y="-8"
- inkscape:window-maximized="1"
- inkscape:current-layer="layer1" /><defs
- id="defs1" /><g
- inkscape:label="Layer 1"
- inkscape:groupmode="layer"
- id="layer1"><image
- width="135.46666"
- height="135.46666"
- preserveAspectRatio="none"
- xlink:href="data:image/png;base64,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 OvT20f8Bc/DKiBseNBIAAAAASUVORK5CYII= "
- id="image1"
- x="0.086311005"
- y="0.29999173" /></g></svg>
+++ /dev/null
-{
- "domain": "podcast-index",
- "name": "Podcast Index",
- "description": "Discover and play podcasts using the open Podcast Index.",
- "documentation": "https://music-assistant.io/music-providers/podcast-index/",
- "type": "music",
- "requirements": [],
- "codeowners": "@ozgav",
- "multi_instance": false,
- "stage": "beta"
-}
+++ /dev/null
-"""Podcast Index provider implementation."""
-
-from __future__ import annotations
-
-from collections.abc import AsyncGenerator, Sequence
-from typing import Any, cast
-
-import aiohttp
-from music_assistant_models.enums import ContentType, MediaType, StreamType
-from music_assistant_models.errors import (
- InvalidDataError,
- LoginFailed,
- MediaNotFoundError,
- ProviderUnavailableError,
-)
-from music_assistant_models.media_items import (
- AudioFormat,
- BrowseFolder,
- MediaItemType,
- Podcast,
- PodcastEpisode,
- SearchResults,
-)
-from music_assistant_models.streamdetails import StreamDetails
-
-from music_assistant.constants import VERBOSE_LOG_LEVEL
-from music_assistant.controllers.cache import use_cache
-from music_assistant.models.music_provider import MusicProvider
-
-from .constants import (
- BROWSE_CATEGORIES,
- BROWSE_RECENT,
- BROWSE_TRENDING,
- CONF_API_KEY,
- CONF_API_SECRET,
- CONF_STORED_PODCASTS,
-)
-from .helpers import make_api_request, parse_episode_from_data, parse_podcast_from_feed
-
-
-class PodcastIndexProvider(MusicProvider):
- """Podcast Index provider for Music Assistant."""
-
- api_key: str = ""
- api_secret: str = ""
-
- async def handle_async_init(self) -> None:
- """Handle async initialization of the provider."""
- self.api_key = str(self.config.get_value(CONF_API_KEY))
- self.api_secret = str(self.config.get_value(CONF_API_SECRET))
-
- if not self.api_key or not self.api_secret:
- raise LoginFailed("API key and secret are required")
-
- # Test API connection
- try:
- await self._api_request("stats/current")
- except (LoginFailed, ProviderUnavailableError):
- # Re-raise these specific errors as they have proper context
- raise
- except aiohttp.ClientConnectorError as err:
- raise ProviderUnavailableError(
- f"Failed to connect to Podcast Index API: {err}"
- ) from err
- except aiohttp.ServerTimeoutError as err:
- raise ProviderUnavailableError(f"Podcast Index API timeout: {err}") from err
- except Exception as err:
- raise LoginFailed(f"Failed to connect to API: {err}") from err
-
- async def search(
- self, search_query: str, media_types: list[MediaType], limit: int = 10
- ) -> SearchResults:
- """
- Perform search on Podcast Index.
-
- Searches for podcasts by term. Future enhancement could include
- category search if needed.
- """
- result = SearchResults()
- if MediaType.PODCAST not in media_types:
- return result
-
- response = await self._api_request(
- "search/byterm", params={"q": search_query, "max": limit}
- )
-
- podcasts = []
- for feed_data in response.get("feeds", []):
- podcast = parse_podcast_from_feed(
- feed_data, self.lookup_key, self.domain, self.instance_id
- )
- if podcast:
- podcasts.append(podcast)
-
- result.podcasts = podcasts
- return result
-
- async def browse(self, path: str) -> Sequence[BrowseFolder | Podcast | PodcastEpisode]:
- """Browse this provider's items."""
- base = f"{self.instance_id}://"
-
- if path == base:
- # Return main browse categories
- return [
- BrowseFolder(
- item_id=BROWSE_TRENDING,
- provider=self.domain,
- path=f"{base}{BROWSE_TRENDING}",
- name="Trending Podcasts",
- ),
- BrowseFolder(
- item_id=BROWSE_RECENT,
- provider=self.domain,
- path=f"{base}{BROWSE_RECENT}",
- name="Recent Episodes",
- ),
- BrowseFolder(
- item_id=BROWSE_CATEGORIES,
- provider=self.domain,
- path=f"{base}{BROWSE_CATEGORIES}",
- name="Categories",
- ),
- ]
-
- # Parse path after base
- if path.startswith(base):
- subpath_parts = path[len(base) :].split("/")
- subpath = subpath_parts[0] if subpath_parts else ""
-
- if subpath == BROWSE_TRENDING:
- return await self._browse_trending()
- elif subpath == BROWSE_RECENT:
- return await self._browse_recent_episodes()
- elif subpath == BROWSE_CATEGORIES:
- if len(subpath_parts) > 1:
- # Browse specific category - category name is directly in path
- category_name = subpath_parts[1]
- return await self._browse_category_podcasts(category_name)
- else:
- # Browse categories
- return await self._browse_categories()
-
- return []
-
- async def library_add(self, item: MediaItemType) -> bool:
- """
- Add podcast to library.
-
- Retrieves the RSS feed URL for the podcast and adds it to the stored
- podcasts configuration. Returns True if successfully added, False if
- the podcast was already in the library or if the feed URL couldn't be found.
- """
- # Only handle podcasts - delegate others to base class
- if not isinstance(item, Podcast):
- return await super().library_add(item)
-
- stored_podcasts = cast("list[str]", self.config.get_value(CONF_STORED_PODCASTS))
-
- # Get the RSS URL from the podcast via API
- try:
- feed_url = await self._get_feed_url_for_podcast(item.item_id)
- except Exception as err:
- self.logger.warning(
- "Failed to retrieve feed URL for podcast %s: %s", item.name, err, exc_info=True
- )
- return False
-
- if not feed_url:
- self.logger.warning(
- "No feed URL found for podcast %s (ID: %s)", item.name, item.item_id
- )
- return False
-
- if feed_url in stored_podcasts:
- return False
-
- self.logger.debug("Adding podcast %s to library", item.name)
- stored_podcasts.append(feed_url)
- self.update_config_value(CONF_STORED_PODCASTS, stored_podcasts)
- return True
-
- async def library_remove(self, prov_item_id: str, media_type: MediaType) -> bool:
- """
- Remove podcast from library.
-
- Removes the podcast's RSS feed URL from the stored podcasts configuration.
- Always returns True for idempotent operation. If feed URL retrieval fails,
- logs a warning but still returns True to maintain the idempotent contract
- as required by MA convention.
- """
- stored_podcasts = cast("list[str]", self.config.get_value(CONF_STORED_PODCASTS))
-
- # Get the RSS URL for this podcast
- try:
- feed_url = await self._get_feed_url_for_podcast(prov_item_id)
- except Exception as err:
- self.logger.warning(
- "Failed to retrieve feed URL for podcast removal %s: %s",
- prov_item_id,
- err,
- exc_info=True,
- )
- # Still return True for idempotent operation
- return True
-
- if not feed_url or feed_url not in stored_podcasts:
- return True
-
- self.logger.debug("Removing podcast %s from library", prov_item_id)
- stored_podcasts = [x for x in stored_podcasts if x != feed_url]
- self.update_config_value(CONF_STORED_PODCASTS, stored_podcasts)
- return True
-
- @use_cache(3600 * 24 * 14) # Cache for 14 days
- async def get_podcast(self, prov_podcast_id: str) -> Podcast:
- """Get podcast details."""
- try:
- # Try by ID first
- response = await self._api_request("podcasts/byfeedid", params={"id": prov_podcast_id})
- if response.get("feed"):
- podcast = parse_podcast_from_feed(
- response["feed"], self.lookup_key, self.domain, self.instance_id
- )
- if podcast:
- return podcast
- except (ProviderUnavailableError, InvalidDataError):
- # Re-raise these specific errors
- raise
- except Exception as err:
- self.logger.debug("Unexpected error getting podcast %s: %s", prov_podcast_id, err)
-
- raise MediaNotFoundError(f"Podcast {prov_podcast_id} not found")
-
- async def get_podcast_episodes(
- self, prov_podcast_id: str
- ) -> AsyncGenerator[PodcastEpisode, None]:
- """Get episodes for a podcast."""
- self.logger.debug("Getting episodes for podcast ID: %s", prov_podcast_id)
-
- # Try to get the podcast name from the current context first
- podcast_name = None
- try:
- podcast = await self.mass.music.podcasts.get_provider_item(
- prov_podcast_id, self.instance_id
- )
- if podcast:
- podcast_name = podcast.name
- self.logger.debug("Got podcast name from MA context: %s", podcast_name)
- except Exception as err:
- self.logger.debug("Could not get podcast from MA context: %s", err)
-
- # If we don't have the name, get it from the API
- if not podcast_name:
- try:
- podcast_response = await self._api_request(
- "podcasts/byfeedid", params={"id": prov_podcast_id}
- )
- if podcast_response.get("feed"):
- podcast_name = podcast_response["feed"].get("title")
- self.logger.debug("Got podcast name from API fallback: %s", podcast_name)
- except Exception as err:
- self.logger.warning("Could not get podcast name from API: %s", err)
-
- try:
- response = await self._api_request(
- "episodes/byfeedid", params={"id": prov_podcast_id, "max": 1000}
- )
-
- episodes = response.get("items", [])
- for idx, episode_data in enumerate(episodes):
- episode = parse_episode_from_data(
- episode_data,
- prov_podcast_id,
- idx,
- self.lookup_key,
- self.domain,
- self.instance_id,
- podcast_name,
- )
- if episode:
- yield episode
-
- except (ProviderUnavailableError, InvalidDataError):
- # Re-raise these specific errors
- raise
- except Exception as err:
- self.logger.warning(
- "Unexpected error getting episodes for %s: %s", prov_podcast_id, err
- )
-
- @use_cache(43200) # Cache for 12 hours
- async def get_podcast_episode(self, prov_episode_id: str) -> PodcastEpisode:
- """
- Get podcast episode details using direct API lookup.
-
- Uses the efficient episodes/byid endpoint for direct episode retrieval.
- """
- try:
- podcast_id, episode_id = prov_episode_id.split("|", 1)
-
- response = await self._api_request("episodes/byid", params={"id": episode_id})
- episode_data = response.get("episode")
-
- if episode_data:
- episode = parse_episode_from_data(
- episode_data, podcast_id, 0, self.lookup_key, self.domain, self.instance_id
- )
- if episode:
- return episode
-
- except (ProviderUnavailableError, InvalidDataError):
- # Re-raise these specific errors
- raise
- except ValueError as err:
- # Handle malformed episode ID
- raise InvalidDataError(f"Invalid episode ID format: {prov_episode_id}") from err
- except Exception as err:
- self.logger.warning("Unexpected error getting episode %s: %s", prov_episode_id, err)
-
- raise MediaNotFoundError(f"Episode {prov_episode_id} not found")
-
- @use_cache(86400)
- async def get_stream_details(self, item_id: str, media_type: MediaType) -> StreamDetails:
- """
- Get stream details for a podcast episode.
-
- Uses the Podcast Index episodes/byid endpoint for efficient direct lookup
- rather than fetching all episodes for a podcast.
- """
- if media_type != MediaType.PODCAST_EPISODE:
- raise MediaNotFoundError("Stream details only available for episodes")
-
- try:
- podcast_id, episode_id = item_id.split("|", 1)
-
- # Use direct episode lookup for efficiency
- response = await self._api_request("episodes/byid", params={"id": episode_id})
- episode_data = response.get("episode")
-
- if episode_data:
- stream_url = episode_data.get("enclosureUrl")
- if stream_url:
- return StreamDetails(
- provider=self.lookup_key,
- item_id=item_id,
- audio_format=AudioFormat(
- content_type=ContentType.try_parse(
- episode_data.get("enclosureType") or "audio/mpeg"
- ),
- ),
- media_type=MediaType.PODCAST_EPISODE,
- stream_type=StreamType.HTTP,
- path=stream_url,
- allow_seek=True,
- )
-
- except (ProviderUnavailableError, InvalidDataError):
- # Re-raise these specific errors
- raise
- except ValueError as err:
- # Handle malformed episode ID
- raise InvalidDataError(f"Invalid episode ID format: {item_id}") from err
- except Exception as err:
- self.logger.warning("Unexpected error getting stream for %s: %s", item_id, err)
-
- raise MediaNotFoundError(f"Stream not found for {item_id}")
-
- async def get_item(self, media_type: MediaType, prov_item_id: str) -> Podcast | PodcastEpisode:
- """Get single MediaItem from provider."""
- if media_type == MediaType.PODCAST:
- return await self.get_podcast(prov_item_id)
- elif media_type == MediaType.PODCAST_EPISODE:
- return await self.get_podcast_episode(prov_item_id)
- else:
- raise MediaNotFoundError(f"Media type {media_type} not supported by this provider")
-
- async def _fetch_podcasts(
- self, endpoint: str, params: dict[str, Any] | None = None
- ) -> list[Podcast]:
- """Fetch and parse podcasts from API endpoint."""
- response = await self._api_request(endpoint, params)
- podcasts = []
- for feed_data in response.get("feeds", []):
- podcast = parse_podcast_from_feed(
- feed_data, self.lookup_key, self.domain, self.instance_id
- )
- if podcast:
- podcasts.append(podcast)
- return podcasts
-
- async def _api_request(
- self, endpoint: str, params: dict[str, Any] | None = None
- ) -> dict[str, Any]:
- """Make authenticated request to Podcast Index API."""
- self.logger.log(
- VERBOSE_LOG_LEVEL, "Making API request to %s with params: %s", endpoint, params
- )
- return await make_api_request(self.mass, self.api_key, self.api_secret, endpoint, params)
-
- async def _get_feed_url_for_podcast(self, podcast_id: str) -> str | None:
- """Get RSS feed URL for a podcast ID."""
- try:
- response = await self._api_request("podcasts/byfeedid", params={"id": podcast_id})
- feed_data: dict[str, Any] = response.get("feed", {})
- return feed_data.get("url")
- except (ProviderUnavailableError, InvalidDataError):
- # Re-raise these specific errors
- raise
- except Exception as err:
- self.logger.warning(
- "Unexpected error getting feed URL for podcast %s: %s",
- podcast_id,
- err,
- exc_info=True,
- )
- return None
-
- @use_cache(7200) # Cache for 2 hours
- async def _browse_trending(self) -> list[Podcast]:
- """Browse trending podcasts."""
- try:
- return await self._fetch_podcasts("podcasts/trending", {"max": 50})
- except (ProviderUnavailableError, InvalidDataError):
- raise
- except Exception as err:
- self.logger.warning(
- "Unexpected error getting trending podcasts: %s", err, exc_info=True
- )
- return []
-
- @use_cache(14400) # Cache for 4 hours
- async def _browse_recent_episodes(self) -> list[PodcastEpisode]:
- """Browse recent episodes."""
- try:
- response = await self._api_request("recent/episodes", params={"max": 50})
-
- episodes = []
- for idx, episode_data in enumerate(response.get("items", [])):
- # Extract podcast ID from episode data
- podcast_id = str(episode_data.get("feedId", ""))
- # Pass feedTitle to avoid unnecessary API calls
- podcast_name = episode_data.get("feedTitle")
- episode = parse_episode_from_data(
- episode_data,
- podcast_id,
- idx,
- self.lookup_key,
- self.domain,
- self.instance_id,
- podcast_name,
- )
- if episode:
- episodes.append(episode)
-
- return episodes
-
- except (ProviderUnavailableError, InvalidDataError):
- # Re-raise these specific errors
- raise
- except Exception as err:
- self.logger.warning("Unexpected error getting recent episodes: %s", err, exc_info=True)
- return []
-
- @use_cache(86400) # Cache for 24 hours
- async def _browse_categories(self) -> list[BrowseFolder]:
- """Browse podcast categories."""
- try:
- response = await self._api_request("categories/list")
-
- categories = []
- # Categories API returns feeds array with {id, name} objects
- categories_data = response.get("feeds", [])
-
- for category in categories_data:
- cat_name = category.get("name", "Unknown Category")
-
- categories.append(
- BrowseFolder(
- item_id=cat_name, # Use name as ID
- provider=self.domain,
- path=f"{self.instance_id}://{BROWSE_CATEGORIES}/{cat_name}",
- name=cat_name,
- )
- )
-
- # Sort by name
- return sorted(categories, key=lambda x: x.name)
-
- except (ProviderUnavailableError, InvalidDataError):
- # Re-raise these specific errors
- raise
- except Exception as err:
- self.logger.warning("Unexpected error getting categories: %s", err, exc_info=True)
- return []
-
- @use_cache(43200) # Cache for 12 hours
- async def _browse_category_podcasts(self, category_name: str) -> list[Podcast]:
- """Browse podcasts in a specific category using search."""
- try:
- # Search for podcasts using the category name directly
- search_response = await self._api_request(
- "search/byterm", params={"q": category_name, "max": 50}
- )
-
- podcasts = []
- for feed_data in search_response.get("feeds", []):
- podcast = parse_podcast_from_feed(
- feed_data, self.lookup_key, self.domain, self.instance_id
- )
- if podcast:
- podcasts.append(podcast)
-
- return podcasts
-
- except (ProviderUnavailableError, InvalidDataError):
- raise
- except Exception as err:
- self.logger.warning(
- "Unexpected error getting category podcasts: %s", err, exc_info=True
- )
- return []
--- /dev/null
+"""Podcast Index provider for Music Assistant."""
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+
+from music_assistant_models.config_entries import ConfigEntry, ConfigValueType
+from music_assistant_models.enums import ConfigEntryType, ProviderFeature
+
+from .constants import CONF_API_KEY, CONF_API_SECRET, CONF_STORED_PODCASTS
+from .provider import PodcastIndexProvider
+
+if TYPE_CHECKING:
+ from music_assistant_models.config_entries import ProviderConfig
+ from music_assistant_models.provider import ProviderManifest
+
+ from music_assistant.mass import MusicAssistant
+ from music_assistant.models import ProviderInstanceType
+
+SUPPORTED_FEATURES = {
+ ProviderFeature.SEARCH,
+ ProviderFeature.BROWSE,
+}
+
+
+async def setup(
+ mass: MusicAssistant, manifest: ProviderManifest, config: ProviderConfig
+) -> ProviderInstanceType:
+ """Initialize provider(instance) with given configuration."""
+ return PodcastIndexProvider(mass, manifest, config, SUPPORTED_FEATURES)
+
+
+async def get_config_entries(
+ mass: MusicAssistant,
+ instance_id: str | None = None,
+ action: str | None = None,
+ values: dict[str, ConfigValueType] | None = None,
+) -> tuple[ConfigEntry, ...]:
+ """
+ Return Config entries to setup this provider.
+
+ instance_id: id of an existing provider instance (None if new instance setup).
+ action: [optional] action key called from config entries UI.
+ values: the (intermediate) raw values for config entries sent with the action.
+ """
+ # ruff: noqa: ARG001
+ return (
+ ConfigEntry(
+ key=CONF_API_KEY,
+ type=ConfigEntryType.STRING,
+ label="API Key",
+ required=True,
+ description="Your Podcast Index API key. Get your free API credentials at https://api.podcastindex.org/",
+ ),
+ ConfigEntry(
+ key=CONF_API_SECRET,
+ type=ConfigEntryType.SECURE_STRING,
+ label="API Secret",
+ required=True,
+ description="Your Podcast Index API secret",
+ ),
+ ConfigEntry(
+ key=CONF_STORED_PODCASTS,
+ type=ConfigEntryType.STRING,
+ multi_value=True,
+ label="Subscribed Podcasts",
+ default_value=[],
+ required=False,
+ hidden=True,
+ ),
+ )
--- /dev/null
+"""Constants for Podcast Index provider."""
+
+# Configuration keys
+CONF_API_KEY = "api_key"
+CONF_API_SECRET = "api_secret"
+CONF_STORED_PODCASTS = "stored_podcasts"
+
+# API settings
+API_BASE_URL = "https://api.podcastindex.org/api/1.0"
+
+# Browse categories
+BROWSE_TRENDING = "trending"
+BROWSE_RECENT = "recent"
+BROWSE_CATEGORIES = "categories"
--- /dev/null
+"""Helper functions for Podcast Index provider."""
+
+from __future__ import annotations
+
+import hashlib
+import time
+from datetime import UTC, datetime
+from typing import TYPE_CHECKING, Any
+
+import aiohttp
+from music_assistant_models.enums import ContentType, ImageType, MediaType
+from music_assistant_models.errors import (
+ InvalidDataError,
+ LoginFailed,
+ ProviderUnavailableError,
+)
+from music_assistant_models.media_items import (
+ AudioFormat,
+ ItemMapping,
+ MediaItemImage,
+ Podcast,
+ PodcastEpisode,
+ ProviderMapping,
+ UniqueList,
+)
+
+from .constants import API_BASE_URL
+
+if TYPE_CHECKING:
+ from music_assistant.mass import MusicAssistant
+
+
+async def make_api_request(
+ mass: MusicAssistant,
+ api_key: str,
+ api_secret: str,
+ endpoint: str,
+ params: dict[str, Any] | None = None,
+) -> dict[str, Any]:
+ """
+ Make authenticated request to Podcast Index API.
+
+ Handles authentication using SHA1 hash of API key, secret, and timestamp.
+ Maps HTTP errors appropriately: 401 -> LoginFailed, others -> ProviderUnavailableError.
+ """
+ # Prepare authentication headers
+ auth_date = str(int(time.time()))
+ auth_string = api_key + api_secret + auth_date
+ auth_hash = hashlib.sha1(auth_string.encode()).hexdigest()
+
+ headers = {
+ "X-Auth-Key": api_key,
+ "X-Auth-Date": auth_date,
+ "Authorization": auth_hash,
+ }
+
+ url = f"{API_BASE_URL}/{endpoint}"
+
+ try:
+ async with mass.http_session.get(url, headers=headers, params=params or {}) as response:
+ response.raise_for_status()
+
+ try:
+ data: dict[str, Any] = await response.json()
+ except aiohttp.ContentTypeError as err:
+ raise InvalidDataError("Invalid JSON response from API") from err
+
+ if str(data.get("status")).lower() != "true":
+ raise InvalidDataError(data.get("description") or "API error")
+
+ return data
+
+ except aiohttp.ClientConnectorError as err:
+ raise ProviderUnavailableError(f"Failed to connect to Podcast Index API: {err}") from err
+ except aiohttp.ServerTimeoutError as err:
+ raise ProviderUnavailableError(f"Podcast Index API timeout: {err}") from err
+ except aiohttp.ClientResponseError as err:
+ if err.status == 401:
+ raise LoginFailed(f"Authentication failed: {err.status}") from err
+ raise ProviderUnavailableError(f"API request failed: {err.status}") from err
+
+
+def parse_podcast_from_feed(
+ feed_data: dict[str, Any], lookup_key: str, domain: str, instance_id: str
+) -> Podcast | None:
+ """Parse podcast from API feed data."""
+ feed_url = feed_data.get("url")
+ podcast_id = feed_data.get("id")
+
+ if not feed_url or not podcast_id:
+ return None
+
+ podcast = Podcast(
+ item_id=str(podcast_id),
+ name=feed_data.get("title", "Unknown Podcast"),
+ publisher=feed_data.get("author") or feed_data.get("ownerName", "Unknown"),
+ provider=lookup_key,
+ provider_mappings={
+ ProviderMapping(
+ item_id=str(podcast_id),
+ provider_domain=domain,
+ provider_instance=instance_id,
+ url=feed_url,
+ )
+ },
+ )
+
+ # Add metadata
+ podcast.metadata.description = feed_data.get("description", "")
+ podcast.metadata.explicit = bool(feed_data.get("explicit", False))
+
+ # Set episode count only if provided
+ episode_count = feed_data.get("episodeCount")
+ if episode_count is not None:
+ podcast.total_episodes = int(episode_count) or 0
+
+ # Add image - prefer 'image' field, fallback to 'artwork'
+ image_url = feed_data.get("image") or feed_data.get("artwork")
+ if image_url:
+ podcast.metadata.add_image(
+ MediaItemImage(
+ type=ImageType.THUMB,
+ path=image_url,
+ provider=lookup_key,
+ remotely_accessible=True,
+ )
+ )
+
+ # Add categories as genres - categories is a dict {id: name}
+ categories = feed_data.get("categories", {})
+ if categories and isinstance(categories, dict):
+ podcast.metadata.genres = set(categories.values())
+
+ # Add language
+ language = feed_data.get("language", "")
+ if language:
+ podcast.metadata.languages = UniqueList([language])
+
+ return podcast
+
+
+def parse_episode_from_data(
+ episode_data: dict[str, Any],
+ podcast_id: str,
+ episode_idx: int,
+ lookup_key: str,
+ domain: str,
+ instance_id: str,
+ podcast_name: str | None = None,
+) -> PodcastEpisode | None:
+ """Parse episode from API episode data."""
+ episode_api_id = episode_data.get("id")
+ if not episode_api_id:
+ return None
+
+ episode_id = f"{podcast_id}|{episode_api_id}"
+
+ position = episode_data.get("episode")
+ if position is None:
+ position = episode_idx + 1
+
+ if podcast_name is None:
+ podcast_name = episode_data.get("feedTitle") or "Unknown Podcast"
+
+ raw_duration = episode_data.get("duration")
+ try:
+ duration = int(raw_duration) if raw_duration is not None else 0
+ except (ValueError, TypeError):
+ duration = 0
+
+ episode = PodcastEpisode(
+ item_id=episode_id,
+ provider=lookup_key,
+ name=episode_data.get("title", "Unknown Episode"),
+ duration=duration,
+ position=position,
+ podcast=ItemMapping(
+ item_id=podcast_id,
+ provider=lookup_key,
+ name=podcast_name,
+ media_type=MediaType.PODCAST,
+ ),
+ provider_mappings={
+ ProviderMapping(
+ item_id=episode_id,
+ provider_domain=domain,
+ provider_instance=instance_id,
+ available=True,
+ audio_format=AudioFormat(
+ content_type=ContentType.try_parse(
+ episode_data.get("enclosureType") or "audio/mpeg"
+ ),
+ ),
+ url=episode_data.get("enclosureUrl"),
+ )
+ },
+ )
+
+ # Add metadata
+ episode.metadata.description = episode_data.get("description", "")
+ episode.metadata.explicit = bool(episode_data.get("explicit", 0))
+
+ date_published = episode_data.get("datePublished")
+ if date_published:
+ episode.metadata.release_date = datetime.fromtimestamp(date_published, tz=UTC)
+
+ image_url = episode_data.get("image") or episode_data.get("feedImage")
+ if image_url:
+ episode.metadata.add_image(
+ MediaItemImage(
+ type=ImageType.THUMB,
+ path=image_url,
+ provider=lookup_key,
+ remotely_accessible=True,
+ )
+ )
+
+ return episode
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ width="512"
+ height="512"
+ viewBox="0 0 135.46665 135.46665"
+ version="1.1"
+ id="svg1"
+ xml:space="preserve"
+ inkscape:version="1.3.2 (091e20e, 2023-11-25, custom)"
+ sodipodi:docname="icon.svg"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:xlink="http://www.w3.org/1999/xlink"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
+ id="namedview1"
+ pagecolor="#ffffff"
+ bordercolor="#000000"
+ borderopacity="0.25"
+ inkscape:showpageshadow="2"
+ inkscape:pageopacity="0.0"
+ inkscape:pagecheckerboard="0"
+ inkscape:deskcolor="#d1d1d1"
+ inkscape:document-units="mm"
+ inkscape:zoom="1.4142136"
+ inkscape:cx="256.3262"
+ inkscape:cy="247.13381"
+ inkscape:window-width="1920"
+ inkscape:window-height="1129"
+ inkscape:window-x="-8"
+ inkscape:window-y="-8"
+ inkscape:window-maximized="1"
+ inkscape:current-layer="layer1" /><defs
+ id="defs1" /><g
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer"
+ id="layer1"><image
+ width="135.46666"
+ height="135.46666"
+ preserveAspectRatio="none"
+ xlink:href="data:image/png;base64,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 rkJggg== "
+ id="image1"
+ x="0.086311005"
+ y="0.29999173" /></g></svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ width="512"
+ height="512"
+ viewBox="0 0 135.46665 135.46665"
+ version="1.1"
+ id="svg1"
+ xml:space="preserve"
+ inkscape:version="1.3.2 (091e20e, 2023-11-25, custom)"
+ sodipodi:docname="icon_monochrome.svg"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:xlink="http://www.w3.org/1999/xlink"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
+ id="namedview1"
+ pagecolor="#73ffff"
+ bordercolor="#000000"
+ borderopacity="0.25"
+ inkscape:showpageshadow="2"
+ inkscape:pageopacity="0.0"
+ inkscape:pagecheckerboard="0"
+ inkscape:deskcolor="#d1d1d1"
+ inkscape:document-units="mm"
+ inkscape:zoom="1.4142136"
+ inkscape:cx="256.3262"
+ inkscape:cy="247.13381"
+ inkscape:window-width="1920"
+ inkscape:window-height="1129"
+ inkscape:window-x="-8"
+ inkscape:window-y="-8"
+ inkscape:window-maximized="1"
+ inkscape:current-layer="layer1" /><defs
+ id="defs1" /><g
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer"
+ id="layer1"><image
+ width="135.46666"
+ height="135.46666"
+ preserveAspectRatio="none"
+ xlink:href="data:image/png;base64,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 OvT20f8Bc/DKiBseNBIAAAAASUVORK5CYII= "
+ id="image1"
+ x="0.086311005"
+ y="0.29999173" /></g></svg>
--- /dev/null
+{
+ "domain": "podcast_index",
+ "name": "Podcast Index",
+ "description": "Discover and play podcasts using the open Podcast Index.",
+ "documentation": "https://music-assistant.io/music-providers/podcast_index/",
+ "type": "music",
+ "requirements": [],
+ "codeowners": "@ozgav",
+ "multi_instance": false,
+ "stage": "beta"
+}
--- /dev/null
+"""Podcast Index provider implementation."""
+
+from __future__ import annotations
+
+from collections.abc import AsyncGenerator, Sequence
+from typing import Any, cast
+
+import aiohttp
+from music_assistant_models.enums import ContentType, MediaType, StreamType
+from music_assistant_models.errors import (
+ InvalidDataError,
+ LoginFailed,
+ MediaNotFoundError,
+ ProviderUnavailableError,
+)
+from music_assistant_models.media_items import (
+ AudioFormat,
+ BrowseFolder,
+ MediaItemType,
+ Podcast,
+ PodcastEpisode,
+ SearchResults,
+)
+from music_assistant_models.streamdetails import StreamDetails
+
+from music_assistant.constants import VERBOSE_LOG_LEVEL
+from music_assistant.controllers.cache import use_cache
+from music_assistant.models.music_provider import MusicProvider
+
+from .constants import (
+ BROWSE_CATEGORIES,
+ BROWSE_RECENT,
+ BROWSE_TRENDING,
+ CONF_API_KEY,
+ CONF_API_SECRET,
+ CONF_STORED_PODCASTS,
+)
+from .helpers import make_api_request, parse_episode_from_data, parse_podcast_from_feed
+
+
+class PodcastIndexProvider(MusicProvider):
+ """Podcast Index provider for Music Assistant."""
+
+ api_key: str = ""
+ api_secret: str = ""
+
+ async def handle_async_init(self) -> None:
+ """Handle async initialization of the provider."""
+ self.api_key = str(self.config.get_value(CONF_API_KEY))
+ self.api_secret = str(self.config.get_value(CONF_API_SECRET))
+
+ if not self.api_key or not self.api_secret:
+ raise LoginFailed("API key and secret are required")
+
+ # Test API connection
+ try:
+ await self._api_request("stats/current")
+ except (LoginFailed, ProviderUnavailableError):
+ # Re-raise these specific errors as they have proper context
+ raise
+ except aiohttp.ClientConnectorError as err:
+ raise ProviderUnavailableError(
+ f"Failed to connect to Podcast Index API: {err}"
+ ) from err
+ except aiohttp.ServerTimeoutError as err:
+ raise ProviderUnavailableError(f"Podcast Index API timeout: {err}") from err
+ except Exception as err:
+ raise LoginFailed(f"Failed to connect to API: {err}") from err
+
+ async def search(
+ self, search_query: str, media_types: list[MediaType], limit: int = 10
+ ) -> SearchResults:
+ """
+ Perform search on Podcast Index.
+
+ Searches for podcasts by term. Future enhancement could include
+ category search if needed.
+ """
+ result = SearchResults()
+ if MediaType.PODCAST not in media_types:
+ return result
+
+ response = await self._api_request(
+ "search/byterm", params={"q": search_query, "max": limit}
+ )
+
+ podcasts = []
+ for feed_data in response.get("feeds", []):
+ podcast = parse_podcast_from_feed(
+ feed_data, self.lookup_key, self.domain, self.instance_id
+ )
+ if podcast:
+ podcasts.append(podcast)
+
+ result.podcasts = podcasts
+ return result
+
+ async def browse(self, path: str) -> Sequence[BrowseFolder | Podcast | PodcastEpisode]:
+ """Browse this provider's items."""
+ base = f"{self.instance_id}://"
+
+ if path == base:
+ # Return main browse categories
+ return [
+ BrowseFolder(
+ item_id=BROWSE_TRENDING,
+ provider=self.domain,
+ path=f"{base}{BROWSE_TRENDING}",
+ name="Trending Podcasts",
+ ),
+ BrowseFolder(
+ item_id=BROWSE_RECENT,
+ provider=self.domain,
+ path=f"{base}{BROWSE_RECENT}",
+ name="Recent Episodes",
+ ),
+ BrowseFolder(
+ item_id=BROWSE_CATEGORIES,
+ provider=self.domain,
+ path=f"{base}{BROWSE_CATEGORIES}",
+ name="Categories",
+ ),
+ ]
+
+ # Parse path after base
+ if path.startswith(base):
+ subpath_parts = path[len(base) :].split("/")
+ subpath = subpath_parts[0] if subpath_parts else ""
+
+ if subpath == BROWSE_TRENDING:
+ return await self._browse_trending()
+ elif subpath == BROWSE_RECENT:
+ return await self._browse_recent_episodes()
+ elif subpath == BROWSE_CATEGORIES:
+ if len(subpath_parts) > 1:
+ # Browse specific category - category name is directly in path
+ category_name = subpath_parts[1]
+ return await self._browse_category_podcasts(category_name)
+ else:
+ # Browse categories
+ return await self._browse_categories()
+
+ return []
+
+ async def library_add(self, item: MediaItemType) -> bool:
+ """
+ Add podcast to library.
+
+ Retrieves the RSS feed URL for the podcast and adds it to the stored
+ podcasts configuration. Returns True if successfully added, False if
+ the podcast was already in the library or if the feed URL couldn't be found.
+ """
+ # Only handle podcasts - delegate others to base class
+ if not isinstance(item, Podcast):
+ return await super().library_add(item)
+
+ stored_podcasts = cast("list[str]", self.config.get_value(CONF_STORED_PODCASTS))
+
+ # Get the RSS URL from the podcast via API
+ try:
+ feed_url = await self._get_feed_url_for_podcast(item.item_id)
+ except Exception as err:
+ self.logger.warning(
+ "Failed to retrieve feed URL for podcast %s: %s", item.name, err, exc_info=True
+ )
+ return False
+
+ if not feed_url:
+ self.logger.warning(
+ "No feed URL found for podcast %s (ID: %s)", item.name, item.item_id
+ )
+ return False
+
+ if feed_url in stored_podcasts:
+ return False
+
+ self.logger.debug("Adding podcast %s to library", item.name)
+ stored_podcasts.append(feed_url)
+ self.update_config_value(CONF_STORED_PODCASTS, stored_podcasts)
+ return True
+
+ async def library_remove(self, prov_item_id: str, media_type: MediaType) -> bool:
+ """
+ Remove podcast from library.
+
+ Removes the podcast's RSS feed URL from the stored podcasts configuration.
+ Always returns True for idempotent operation. If feed URL retrieval fails,
+ logs a warning but still returns True to maintain the idempotent contract
+ as required by MA convention.
+ """
+ stored_podcasts = cast("list[str]", self.config.get_value(CONF_STORED_PODCASTS))
+
+ # Get the RSS URL for this podcast
+ try:
+ feed_url = await self._get_feed_url_for_podcast(prov_item_id)
+ except Exception as err:
+ self.logger.warning(
+ "Failed to retrieve feed URL for podcast removal %s: %s",
+ prov_item_id,
+ err,
+ exc_info=True,
+ )
+ # Still return True for idempotent operation
+ return True
+
+ if not feed_url or feed_url not in stored_podcasts:
+ return True
+
+ self.logger.debug("Removing podcast %s from library", prov_item_id)
+ stored_podcasts = [x for x in stored_podcasts if x != feed_url]
+ self.update_config_value(CONF_STORED_PODCASTS, stored_podcasts)
+ return True
+
+ @use_cache(3600 * 24 * 14) # Cache for 14 days
+ async def get_podcast(self, prov_podcast_id: str) -> Podcast:
+ """Get podcast details."""
+ try:
+ # Try by ID first
+ response = await self._api_request("podcasts/byfeedid", params={"id": prov_podcast_id})
+ if response.get("feed"):
+ podcast = parse_podcast_from_feed(
+ response["feed"], self.lookup_key, self.domain, self.instance_id
+ )
+ if podcast:
+ return podcast
+ except (ProviderUnavailableError, InvalidDataError):
+ # Re-raise these specific errors
+ raise
+ except Exception as err:
+ self.logger.debug("Unexpected error getting podcast %s: %s", prov_podcast_id, err)
+
+ raise MediaNotFoundError(f"Podcast {prov_podcast_id} not found")
+
+ async def get_podcast_episodes(
+ self, prov_podcast_id: str
+ ) -> AsyncGenerator[PodcastEpisode, None]:
+ """Get episodes for a podcast."""
+ self.logger.debug("Getting episodes for podcast ID: %s", prov_podcast_id)
+
+ # Try to get the podcast name from the current context first
+ podcast_name = None
+ try:
+ podcast = await self.mass.music.podcasts.get_provider_item(
+ prov_podcast_id, self.instance_id
+ )
+ if podcast:
+ podcast_name = podcast.name
+ self.logger.debug("Got podcast name from MA context: %s", podcast_name)
+ except Exception as err:
+ self.logger.debug("Could not get podcast from MA context: %s", err)
+
+ # If we don't have the name, get it from the API
+ if not podcast_name:
+ try:
+ podcast_response = await self._api_request(
+ "podcasts/byfeedid", params={"id": prov_podcast_id}
+ )
+ if podcast_response.get("feed"):
+ podcast_name = podcast_response["feed"].get("title")
+ self.logger.debug("Got podcast name from API fallback: %s", podcast_name)
+ except Exception as err:
+ self.logger.warning("Could not get podcast name from API: %s", err)
+
+ try:
+ response = await self._api_request(
+ "episodes/byfeedid", params={"id": prov_podcast_id, "max": 1000}
+ )
+
+ episodes = response.get("items", [])
+ for idx, episode_data in enumerate(episodes):
+ episode = parse_episode_from_data(
+ episode_data,
+ prov_podcast_id,
+ idx,
+ self.lookup_key,
+ self.domain,
+ self.instance_id,
+ podcast_name,
+ )
+ if episode:
+ yield episode
+
+ except (ProviderUnavailableError, InvalidDataError):
+ # Re-raise these specific errors
+ raise
+ except Exception as err:
+ self.logger.warning(
+ "Unexpected error getting episodes for %s: %s", prov_podcast_id, err
+ )
+
+ @use_cache(43200) # Cache for 12 hours
+ async def get_podcast_episode(self, prov_episode_id: str) -> PodcastEpisode:
+ """
+ Get podcast episode details using direct API lookup.
+
+ Uses the efficient episodes/byid endpoint for direct episode retrieval.
+ """
+ try:
+ podcast_id, episode_id = prov_episode_id.split("|", 1)
+
+ response = await self._api_request("episodes/byid", params={"id": episode_id})
+ episode_data = response.get("episode")
+
+ if episode_data:
+ episode = parse_episode_from_data(
+ episode_data, podcast_id, 0, self.lookup_key, self.domain, self.instance_id
+ )
+ if episode:
+ return episode
+
+ except (ProviderUnavailableError, InvalidDataError):
+ # Re-raise these specific errors
+ raise
+ except ValueError as err:
+ # Handle malformed episode ID
+ raise InvalidDataError(f"Invalid episode ID format: {prov_episode_id}") from err
+ except Exception as err:
+ self.logger.warning("Unexpected error getting episode %s: %s", prov_episode_id, err)
+
+ raise MediaNotFoundError(f"Episode {prov_episode_id} not found")
+
+ @use_cache(86400)
+ async def get_stream_details(self, item_id: str, media_type: MediaType) -> StreamDetails:
+ """
+ Get stream details for a podcast episode.
+
+ Uses the Podcast Index episodes/byid endpoint for efficient direct lookup
+ rather than fetching all episodes for a podcast.
+ """
+ if media_type != MediaType.PODCAST_EPISODE:
+ raise MediaNotFoundError("Stream details only available for episodes")
+
+ try:
+ podcast_id, episode_id = item_id.split("|", 1)
+
+ # Use direct episode lookup for efficiency
+ response = await self._api_request("episodes/byid", params={"id": episode_id})
+ episode_data = response.get("episode")
+
+ if episode_data:
+ stream_url = episode_data.get("enclosureUrl")
+ if stream_url:
+ return StreamDetails(
+ provider=self.lookup_key,
+ item_id=item_id,
+ audio_format=AudioFormat(
+ content_type=ContentType.try_parse(
+ episode_data.get("enclosureType") or "audio/mpeg"
+ ),
+ ),
+ media_type=MediaType.PODCAST_EPISODE,
+ stream_type=StreamType.HTTP,
+ path=stream_url,
+ allow_seek=True,
+ )
+
+ except (ProviderUnavailableError, InvalidDataError):
+ # Re-raise these specific errors
+ raise
+ except ValueError as err:
+ # Handle malformed episode ID
+ raise InvalidDataError(f"Invalid episode ID format: {item_id}") from err
+ except Exception as err:
+ self.logger.warning("Unexpected error getting stream for %s: %s", item_id, err)
+
+ raise MediaNotFoundError(f"Stream not found for {item_id}")
+
+ async def get_item(self, media_type: MediaType, prov_item_id: str) -> Podcast | PodcastEpisode:
+ """Get single MediaItem from provider."""
+ if media_type == MediaType.PODCAST:
+ return await self.get_podcast(prov_item_id)
+ elif media_type == MediaType.PODCAST_EPISODE:
+ return await self.get_podcast_episode(prov_item_id)
+ else:
+ raise MediaNotFoundError(f"Media type {media_type} not supported by this provider")
+
+ async def _fetch_podcasts(
+ self, endpoint: str, params: dict[str, Any] | None = None
+ ) -> list[Podcast]:
+ """Fetch and parse podcasts from API endpoint."""
+ response = await self._api_request(endpoint, params)
+ podcasts = []
+ for feed_data in response.get("feeds", []):
+ podcast = parse_podcast_from_feed(
+ feed_data, self.lookup_key, self.domain, self.instance_id
+ )
+ if podcast:
+ podcasts.append(podcast)
+ return podcasts
+
+ async def _api_request(
+ self, endpoint: str, params: dict[str, Any] | None = None
+ ) -> dict[str, Any]:
+ """Make authenticated request to Podcast Index API."""
+ self.logger.log(
+ VERBOSE_LOG_LEVEL, "Making API request to %s with params: %s", endpoint, params
+ )
+ return await make_api_request(self.mass, self.api_key, self.api_secret, endpoint, params)
+
+ async def _get_feed_url_for_podcast(self, podcast_id: str) -> str | None:
+ """Get RSS feed URL for a podcast ID."""
+ try:
+ response = await self._api_request("podcasts/byfeedid", params={"id": podcast_id})
+ feed_data: dict[str, Any] = response.get("feed", {})
+ return feed_data.get("url")
+ except (ProviderUnavailableError, InvalidDataError):
+ # Re-raise these specific errors
+ raise
+ except Exception as err:
+ self.logger.warning(
+ "Unexpected error getting feed URL for podcast %s: %s",
+ podcast_id,
+ err,
+ exc_info=True,
+ )
+ return None
+
+ @use_cache(7200) # Cache for 2 hours
+ async def _browse_trending(self) -> list[Podcast]:
+ """Browse trending podcasts."""
+ try:
+ return await self._fetch_podcasts("podcasts/trending", {"max": 50})
+ except (ProviderUnavailableError, InvalidDataError):
+ raise
+ except Exception as err:
+ self.logger.warning(
+ "Unexpected error getting trending podcasts: %s", err, exc_info=True
+ )
+ return []
+
+ @use_cache(14400) # Cache for 4 hours
+ async def _browse_recent_episodes(self) -> list[PodcastEpisode]:
+ """Browse recent episodes."""
+ try:
+ response = await self._api_request("recent/episodes", params={"max": 50})
+
+ episodes = []
+ for idx, episode_data in enumerate(response.get("items", [])):
+ # Extract podcast ID from episode data
+ podcast_id = str(episode_data.get("feedId", ""))
+ # Pass feedTitle to avoid unnecessary API calls
+ podcast_name = episode_data.get("feedTitle")
+ episode = parse_episode_from_data(
+ episode_data,
+ podcast_id,
+ idx,
+ self.lookup_key,
+ self.domain,
+ self.instance_id,
+ podcast_name,
+ )
+ if episode:
+ episodes.append(episode)
+
+ return episodes
+
+ except (ProviderUnavailableError, InvalidDataError):
+ # Re-raise these specific errors
+ raise
+ except Exception as err:
+ self.logger.warning("Unexpected error getting recent episodes: %s", err, exc_info=True)
+ return []
+
+ @use_cache(86400) # Cache for 24 hours
+ async def _browse_categories(self) -> list[BrowseFolder]:
+ """Browse podcast categories."""
+ try:
+ response = await self._api_request("categories/list")
+
+ categories = []
+ # Categories API returns feeds array with {id, name} objects
+ categories_data = response.get("feeds", [])
+
+ for category in categories_data:
+ cat_name = category.get("name", "Unknown Category")
+
+ categories.append(
+ BrowseFolder(
+ item_id=cat_name, # Use name as ID
+ provider=self.domain,
+ path=f"{self.instance_id}://{BROWSE_CATEGORIES}/{cat_name}",
+ name=cat_name,
+ )
+ )
+
+ # Sort by name
+ return sorted(categories, key=lambda x: x.name)
+
+ except (ProviderUnavailableError, InvalidDataError):
+ # Re-raise these specific errors
+ raise
+ except Exception as err:
+ self.logger.warning("Unexpected error getting categories: %s", err, exc_info=True)
+ return []
+
+ @use_cache(43200) # Cache for 12 hours
+ async def _browse_category_podcasts(self, category_name: str) -> list[Podcast]:
+ """Browse podcasts in a specific category using search."""
+ try:
+ # Search for podcasts using the category name directly
+ search_response = await self._api_request(
+ "search/byterm", params={"q": category_name, "max": 50}
+ )
+
+ podcasts = []
+ for feed_data in search_response.get("feeds", []):
+ podcast = parse_podcast_from_feed(
+ feed_data, self.lookup_key, self.domain, self.instance_id
+ )
+ if podcast:
+ podcasts.append(podcast)
+
+ return podcasts
+
+ except (ProviderUnavailableError, InvalidDataError):
+ raise
+ except Exception as err:
+ self.logger.warning(
+ "Unexpected error getting category podcasts: %s", err, exc_info=True
+ )
+ return []
"""Return the features supported by this Provider."""
return {
ProviderFeature.SYNC_PLAYERS,
- ProviderFeature.CREATE_GROUP_PLAYER,
- ProviderFeature.REMOVE_GROUP_PLAYER,
}
async def loaded_in_mass(self) -> None:
SUPPORTED_FEATURES = {
ProviderFeature.SYNC_PLAYERS,
ProviderFeature.REMOVE_PLAYER,
- # support sync groups by reporting create/remove player group support
- ProviderFeature.CREATE_GROUP_PLAYER,
- ProviderFeature.REMOVE_GROUP_PLAYER,
}
SUPPORTED_FEATURES = {
ProviderFeature.SYNC_PLAYERS,
- # support sync groups by reporting create/remove player group support
- ProviderFeature.CREATE_GROUP_PLAYER,
- ProviderFeature.REMOVE_GROUP_PLAYER,
}
import asyncio
import time
+from copy import deepcopy
from typing import TYPE_CHECKING
from aiohttp import ClientConnectorError
await airplay_player.stop()
else:
await self.client.player.group.stop()
- self._attr_playback_state = PlaybackState.IDLE
self.update_state()
async def pause(self) -> None:
Will only be called if the player reports PlayerFeature.PAUSE is supported.
"""
-
- def _update_state() -> None:
- self._attr_playback_state = PlaybackState.PAUSED
- self.update_state()
-
if self.client.player.is_passive:
self.logger.debug("Ignore STOP command: Player is synced to another player.")
return
# linked airplay player is active, redirect the command
self.logger.debug("Redirecting PAUSE command to linked airplay player.")
await airplay_player.pause()
- _update_state()
return
active_source = self._attr_active_source
if self.mass.player_queues.get(active_source):
# TODO: revisit this later once we implemented support for range requests
# as I have the feeling the pause issue is related to seek support (=range requests)
await self.stop()
- _update_state()
return
if not self.client.player.group.playback_actions.can_pause:
await self.stop()
- _update_state()
return
await self.client.player.group.pause()
- _update_state()
async def next_track(self) -> None:
"""
:param media: Details of the item that needs to be played on the player.
"""
-
- def _update_state() -> None:
- self._attr_current_media = media
- self._attr_playback_state = PlaybackState.PLAYING
- self.update_state()
+ self._attr_current_media = deepcopy(media)
if self.client.player.is_passive:
# this should be already handled by the player manager, but just in case...
# airplay mode is enabled, redirect the command
self.logger.debug("Redirecting PLAY_MEDIA command to linked airplay player.")
await self._play_media_airplay(airplay_player, media)
- _update_state()
return
if media.media_type in (
MediaType.PLUGIN_SOURCE,
MediaType.FLOW_STREAM,
- ) or media.source_id.startswith(UGP_PREFIX):
+ ) or (media.source_id and media.source_id.startswith(UGP_PREFIX)):
# flow stream or plugin source playback
# always use the legacy (UPNP) playback method for this
await self._play_media_legacy(media)
- _update_state()
return
if media.source_id and media.queue_item_id:
queue_version=str(int(mass_queue.items_last_updated)),
)
self.mass.call_later(5, self.sync_play_modes, media.source_id)
- _update_state()
return
# All other playback types
await self.client.player.group.play_stream_url(
media.uri, {"name": media.title, "type": "track"}
)
- _update_state()
async def select_source(self, source: str) -> None:
"""
:param player_ids_to_add: List of player_id's to add to the group.
:param player_ids_to_remove: List of player_id's to remove from the group.
"""
+ player_ids_to_add = player_ids_to_add or []
+ player_ids_to_remove = player_ids_to_remove or []
if airplay_player := self.get_linked_airplay_player(False):
# if airplay mode is enabled, we could possibly receive child player id's that are
# not Sonos players, but AirPlay players. We redirect those.
- airplay_child_ids = [x for x in player_ids_to_add or [] if x.startswith("ap")]
- player_ids_to_add = [x for x in player_ids_to_add or [] if x not in airplay_child_ids]
- if airplay_child_ids:
- if (
- airplay_player.active_source != self._attr_active_source
- and airplay_player.playback_state == PlaybackState.PLAYING
- ):
- # edge case player is not playing a MA queue - fail this request
- raise PlayerCommandFailed("Player is not playing a Music Assistant queue.")
- await self.mass.players.cmd_group_many(airplay_player.player_id, airplay_child_ids)
- if player_ids_to_add:
+ airplay_player_ids_to_add = [x for x in player_ids_to_add if x.startswith("ap")]
+ player_ids_to_add = [x for x in player_ids_to_add if x not in airplay_player_ids_to_add]
+ airplay_player_ids_to_remove = [x for x in player_ids_to_remove if x.startswith("ap")]
+ player_ids_to_remove = [
+ x for x in player_ids_to_remove if x not in airplay_player_ids_to_remove
+ ]
+ if airplay_player_ids_to_add or airplay_player_ids_to_remove:
+ await self.mass.players.cmd_set_members(
+ airplay_player.player_id,
+ player_ids_to_add=airplay_player_ids_to_add,
+ player_ids_to_remove=airplay_player_ids_to_remove,
+ )
+ if player_ids_to_add or player_ids_to_remove:
await self.client.player.group.modify_group_members(
- player_ids_to_add=player_ids_to_add, player_ids_to_remove=[]
+ player_ids_to_add=player_ids_to_add, player_ids_to_remove=player_ids_to_remove
)
async def ungroup(self) -> None:
def on_player_event(self, event: SonosEvent | None) -> None:
"""Handle incoming event from player."""
- self.update_attributes()
- self.update_state()
+ try:
+ self.update_attributes()
+ except Exception as err:
+ self.logger.exception("Failed to update player attributes: %s", err)
+ return
+ try:
+ self.update_state()
+ except Exception as err:
+ self.logger.exception("Failed to update player state: %s", err)
def update_attributes(self) -> None: # noqa: PLR0915
"""Update the player attributes."""
self._attr_group_members.clear()
# map playback state
- self._playback_state = PLAYBACK_STATE_MAP[active_group.playback_state]
+ self._attr_playback_state = PLAYBACK_STATE_MAP[active_group.playback_state]
self._attr_elapsed_time = active_group.position
# figure out the active source based on the container
if SOURCE_SPOTIFY not in [x.id for x in self._attr_source_list]:
self._attr_source_list.append(PLAYER_SOURCE_MAP[SOURCE_SPOTIFY])
elif active_service == MusicService.MUSIC_ASSISTANT:
- if self.client.player.is_coordinator:
- self._attr_active_source = self._player_id
- elif object_id := container.get("id", {}).get("objectId"):
+ if object_id := container.get("id", {}).get("objectId"):
self._attr_active_source = object_id.split(":")[-1]
else:
self._attr_active_source = None
SUPPORTED_FEATURES = {
ProviderFeature.SYNC_PLAYERS,
- # support sync groups by reporting create/remove player group support
- ProviderFeature.CREATE_GROUP_PLAYER,
- ProviderFeature.REMOVE_GROUP_PLAYER,
}
SUPPORTED_FEATURES = {
ProviderFeature.SYNC_PLAYERS,
- # support sync groups by reporting create/remove player group support
- ProviderFeature.CREATE_GROUP_PLAYER,
- ProviderFeature.REMOVE_GROUP_PLAYER,
}
from __future__ import annotations
import asyncio
+from copy import deepcopy
from time import time
from typing import TYPE_CHECKING, cast
base_url = f"{self.mass.streams.base_url}/ugp/{self.player_id}.flac"
# set the state optimistically
- self._attr_current_media = media
+ self._attr_current_media = deepcopy(media)
self._attr_elapsed_time = 0
self._attr_elapsed_time_last_updated = time() - 1
self._attr_playback_state = PlaybackState.PLAYING
"ruff==0.12.12",
]
-
[project.scripts]
mass = "music_assistant.__main__:main"
"truthy-iterable",
]
exclude = [
- '^music_assistant/controllers/.*$',
+ '^music_assistant/controllers/__init__.py$',
+ '^music_assistant/controllers/cache.py$',
+ '^music_assistant/controllers/config.py$',
+ '^music_assistant/controllers/media/.*$',
+ '^music_assistant/controllers/metadata.py$',
+ '^music_assistant/controllers/music.py$',
+ '^music_assistant/controllers/player_queues.py$',
+ '^music_assistant/controllers/players/player_controller.py',
+ '^music_assistant/controllers/streams.py$',
+ '^music_assistant/controllers/webserver.py',
'^music_assistant/helpers/app_vars.py',
'^music_assistant/models/player_provider.py',
'^music_assistant/providers/apple_music/.*$',