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
_PlayerControllerT = TypeVar("_PlayerControllerT", bound="PlayerController")
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
+ if not (player := self.get(config.player_id)):
+ return
+ resume_queue: PlayerQueue | None = self.mass.player_queues.get(player.active_source)
# signal player provider that the config changed
if player_provider := self.mass.get_provider(config.provider):
with suppress(PlayerUnavailableError):
await player_provider.on_player_config_change(config, changed_keys)
- if not (player := self.get(config.player_id)):
- return
if player_disabled:
# edge case: ensure that the player is powered off if the player gets disabled
await self.cmd_power(config.player_id, False)
player.available = False
- # if the player was playing, restart playback
- elif not player_disabled and player.state == PlayerState.PLAYING:
- self.mass.call_later(1, self.mass.player_queues.resume, player.active_source)
+ # 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.state == PlayerState.PLAYING:
+ self.mass.call_later(1, self.mass.player_queues.resume, resume_queue.queue_id)
# check for group memberships that need to be updated
if player_disabled and player.active_group and player_provider:
# try to remove from the group
import shortuuid
from aiohttp.client_exceptions import ClientConnectorError
from aiosonos.api.models import ContainerType, MusicService, SonosCapability
-from aiosonos.api.models import PlayBackState as SonosPlayBackState
from aiosonos.client import SonosLocalApiClient
from aiosonos.const import EventType as SonosEventType
from aiosonos.const import SonosEvent
self.player_id, CONF_AIRPLAY_MODE, False
)
+ @property
+ def airplay_mode_active(self) -> bool:
+ """Return if airplay mode is active for the player."""
+ return (
+ self.airplay_mode_enabled
+ and self.client.player.is_coordinator
+ and (active_group := self.client.player.group)
+ and active_group.container_type == ContainerType.AIRPLAY
+ and (airplay_player := self.get_linked_airplay_player(False))
+ and airplay_player.state in (PlayerState.PLAYING, PlayerState.PAUSED)
+ )
+
def get_linked_airplay_player(self, enabled_only: bool = True) -> Player | None:
"""Return the linked airplay player if available/enabled."""
if enabled_only and not self.airplay_mode_enabled:
name=self.discovery_info["device"]["name"]
or self.discovery_info["device"]["modelDisplayName"],
available=True,
- # treat as powered at start if the player is playing/paused
- powered=self.client.player.group.playback_state
- in (
- SonosPlayBackState.PLAYBACK_STATE_PLAYING,
- SonosPlayBackState.PLAYBACK_STATE_BUFFERING,
- SonosPlayBackState.PLAYBACK_STATE_PAUSED,
- ),
+ # sonos has no power support so we always assume its powered
+ powered=True,
device_info=DeviceInfo(
model=self.discovery_info["device"]["modelDisplayName"],
manufacturer=self.prov.manifest.name,
)
)
# register callback for playerqueue state changes
+ # note we don't filter on the player_id here because we also need to catch
+ # events from group players
self._on_cleanup_callbacks.append(
self.mass.subscribe(
self._on_mass_queue_items_event,
EventType.QUEUE_ITEMS_UPDATED,
- self.player_id,
+ )
+ )
+ self._on_cleanup_callbacks.append(
+ self.mass.subscribe(
+ self._on_mass_queue_event,
+ EventType.QUEUE_UPDATED,
)
)
if self.client.player.is_passive:
self.logger.debug("Ignore STOP command: Player is synced to another player.")
return
- if (airplay := self.get_linked_airplay_player(True)) and airplay.state != PlayerState.IDLE:
+ if (airplay := self.get_linked_airplay_player(True)) and self.airplay_mode_active:
# linked airplay player is active, redirect the command
self.logger.debug("Redirecting STOP command to linked airplay player.")
if player_provider := self.mass.get_provider(airplay.provider):
if self.client.player.is_passive:
self.logger.debug("Ignore STOP command: Player is synced to another player.")
return
- if (airplay := self.get_linked_airplay_player(True)) and airplay.state != PlayerState.IDLE:
+ if (airplay := self.get_linked_airplay_player(True)) and self.airplay_mode_active:
# linked airplay player is active, redirect the command
self.logger.debug("Redirecting PAUSE command to linked airplay player.")
if player_provider := self.mass.get_provider(airplay.provider):
return
if not self.connected:
return
+ if not self.client.player.is_coordinator:
+ return
# sync crossfade and repeat modes
queue = self.mass.player_queues.get(event.object_id)
if not queue or queue.state not in (PlayerState.PLAYING, PlayerState.PAUSED):
import asyncio
import time
-from typing import TYPE_CHECKING
+from typing import TYPE_CHECKING, Any
import shortuuid
from aiohttp import web
from aiohttp.client_exceptions import ClientError
from aiosonos.api.models import SonosCapability
from aiosonos.utils import get_discovery_info
-from music_assistant_models.config_entries import ConfigEntry
+from music_assistant_models.config_entries import ConfigEntry, PlayerConfig
from music_assistant_models.enums import ConfigEntryType, ContentType, ProviderFeature
from music_assistant_models.errors import PlayerCommandFailed
from music_assistant_models.player import DeviceInfo, PlayerMedia
from .player import SonosPlayer
if TYPE_CHECKING:
+ from music_assistant_models.queue_item import QueueItem
from zeroconf.asyncio import AsyncServiceInfo
CONF_IPS = "ips"
),
)
+ 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."""
+ await super().on_player_config_change(config, changed_keys)
+ if "values/airplay_mode" in changed_keys and (
+ (sonos_player := self.sonos_players.get(config.player_id))
+ and (airplay_player := sonos_player.get_linked_airplay_player(False))
+ and airplay_player.active_source == sonos_player.mass_player.active_source
+ ):
+ # edge case: we switched from airplay mode to sonos mode (or vice versa)
+ # we need to make sure that playback gets stopped on the airplay player
+ if airplay_prov := self.mass.get_provider(airplay_player.provider):
+ await airplay_prov.cmd_stop(airplay_player.player_id)
+ airplay_player.active_source = None
+ if not sonos_player.airplay_mode_enabled:
+ await self.mass.players.cmd_power(config.player_id, False)
+
async def cmd_stop(self, player_id: str) -> None:
"""Send STOP command to given player."""
if sonos_player := self.sonos_players[player_id]:
if airplay_player := sonos_player.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.
+ if sonos_player.mass_player.active_source and not self.mass.player_queues.get(
+ sonos_player.mass_player.active_source
+ ):
+ # edge case player is not playing a MA queue - fail this request
+ raise PlayerCommandFailed("Player is not playing a Music Assistant queue.")
airplay_child_ids = [x for x in child_player_ids if x.startswith("ap")]
child_player_ids = [x for x in child_player_ids if x not in airplay_child_ids]
if airplay_child_ids:
sonos_player_id, CONF_ENTRY_ENFORCE_MP3.key, CONF_ENTRY_ENFORCE_MP3.default_value
)
sonos_queue_items = [
- {
- "id": item.queue_item_id,
- "deleted": not item.media_item.available,
- "policies": {},
- "track": {
- "type": "track",
- "mediaUrl": self.mass.streams.resolve_stream_url(
- item, output_codec=ContentType.MP3 if enforce_mp3 else ContentType.FLAC
- ),
- "contentType": "audio/flac",
- "service": {
- "name": "Music Assistant",
- "id": "8",
- "accountId": "",
- "objectId": item.queue_item_id,
- },
- "name": item.name,
- "imageUrl": self.mass.metadata.get_image_url(
- item.image, prefer_proxy=False, image_format="jpeg"
- )
- if item.image
- else None,
- "durationMillis": item.duration * 1000 if item.duration else None,
- "artist": {
- "name": artist_str,
- }
- if item.media_item
- and (artist_str := getattr(item.media_item, "artist_str", None))
- else None,
- "album": {
- "name": album.name,
- }
- if item.media_item and (album := getattr(item.media_item, "album", None))
- else None,
- "quality": {
- "bitDepth": item.streamdetails.audio_format.bit_depth,
- "sampleRate": item.streamdetails.audio_format.sample_rate,
- "codec": item.streamdetails.audio_format.content_type.value,
- "lossless": item.streamdetails.audio_format.content_type.is_lossless(),
- }
- if item.streamdetails
- else None,
- },
- }
- for item in queue_items
+ self._parse_sonos_queue_item(item, enforce_mp3) for item in queue_items
]
result = {
"includesBeginningOfQueue": offset == 0,
self.mass.players.update(sonos_player_id)
break
return web.Response(status=204)
+
+ def _parse_sonos_queue_item(self, queue_item: QueueItem, enforce_mp3: bool) -> dict[str, Any]:
+ """Parse a Sonos queue item to a PlayerMedia object."""
+ available = queue_item.media_item.available if queue_item.media_item else True
+ return {
+ "id": queue_item.queue_item_id,
+ "deleted": not available,
+ "policies": {},
+ "track": {
+ "type": "track",
+ "mediaUrl": self.mass.streams.resolve_stream_url(
+ queue_item, output_codec=ContentType.MP3 if enforce_mp3 else ContentType.FLAC
+ ),
+ "contentType": "audio/flac",
+ "service": {
+ "name": "Music Assistant",
+ "id": "8",
+ "accountId": "",
+ "objectId": queue_item.queue_item_id,
+ },
+ "name": queue_item.media_item.name if queue_item.media_item else queue_item.name,
+ "imageUrl": self.mass.metadata.get_image_url(
+ queue_item.image, prefer_proxy=False, image_format="jpeg"
+ )
+ if queue_item.image
+ else None,
+ "durationMillis": queue_item.duration * 1000 if queue_item.duration else None,
+ "artist": {
+ "name": artist_str,
+ }
+ if queue_item.media_item
+ and (artist_str := getattr(queue_item.media_item, "artist_str", None))
+ else None,
+ "album": {
+ "name": album.name,
+ }
+ if queue_item.media_item
+ and (album := getattr(queue_item.media_item, "album", None))
+ else None,
+ "quality": {
+ "bitDepth": queue_item.streamdetails.audio_format.bit_depth,
+ "sampleRate": queue_item.streamdetails.audio_format.sample_rate,
+ "codec": queue_item.streamdetails.audio_format.content_type.value,
+ "lossless": queue_item.streamdetails.audio_format.content_type.is_lossless(),
+ }
+ if queue_item.streamdetails
+ else None,
+ },
+ }