From a6525726ab117ffbc598dc2c0d0a9f7ba573ae5a Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Mon, 29 Jan 2024 14:43:42 +0100 Subject: [PATCH] Bugfixes (#1044) * Fix for corrupted sonos config * do not support late joining on multi client stream * prevent SoCoUPnPException --- music_assistant/server/controllers/config.py | 15 ++++++ music_assistant/server/controllers/streams.py | 9 ++-- .../server/providers/slimproto/__init__.py | 6 +-- .../server/providers/sonos/__init__.py | 20 ++++--- .../server/providers/sonos/player.py | 52 ++++++++----------- 5 files changed, 55 insertions(+), 47 deletions(-) diff --git a/music_assistant/server/controllers/config.py b/music_assistant/server/controllers/config.py index c46b7ad54..dc7b26c02 100644 --- a/music_assistant/server/controllers/config.py +++ b/music_assistant/server/controllers/config.py @@ -71,6 +71,15 @@ async def setup(self) -> None: """Async initialize of controller.""" await self._load() self.initialized = True + #### temp fix issue introduced in b89 ########## + # TODO: remove after b92 + final_player_configs = {} + for player_id, player_conf in self.get(CONF_PLAYERS, {}).items(): + if "provider" in player_conf: + final_player_configs[player_id] = player_conf + self.set(CONF_PLAYERS, final_player_configs) + #### end of temp fix ############################ + # create default server ID if needed (also used for encrypting passwords) self.set_default(CONF_SERVER_ID, uuid4().hex) server_id: str = self.get(CONF_SERVER_ID) @@ -588,6 +597,9 @@ def set_raw_provider_config_value( Note that this only stores the (raw) value without any validation or default. """ + if not self.get(f"{CONF_PROVIDERS}/{provider_instance}"): + # only allow setting raw values if main entry exists + raise KeyError(f"Invalid provider_instance: {provider_instance}") self.set(f"{CONF_PROVIDERS}/{provider_instance}/{key}", value) def set_raw_player_config_value(self, player_id: str, key: str, value: ConfigValueType) -> None: @@ -596,6 +608,9 @@ def set_raw_player_config_value(self, player_id: str, key: str, value: ConfigVal Note that this only stores the (raw) value without any validation or default. """ + if not self.get(f"{CONF_PLAYERS}/{player_id}"): + # only allow setting raw values if main entry exists + raise KeyError(f"Invalid player_id: {player_id}") self.set(f"{CONF_PLAYERS}/{player_id}/values/{key}", value) def save(self, immediate: bool = False) -> None: diff --git a/music_assistant/server/controllers/streams.py b/music_assistant/server/controllers/streams.py index b7e5c4e7b..330809992 100644 --- a/music_assistant/server/controllers/streams.py +++ b/music_assistant/server/controllers/streams.py @@ -172,12 +172,11 @@ async def subscribe(self, player_id: str) -> AsyncGenerator[bytes, None]: self.subscribed_players[player_id] = sub_queue = asyncio.Queue(2) if self._all_clients_connected.is_set(): - # client subscribes while we're already started - self.logger.warning( - "Client %s is joining while the stream is already started", player_id + # client subscribes while we're already started - we dont support that (for now?) + raise RuntimeError( + f"Client {player_id} is joining while the stream is already started" ) - else: - self.logger.debug("Subscribed client %s", player_id) + self.logger.debug("Subscribed client %s", player_id) if len(self.subscribed_players) == len(self.expected_players): # we reached the number of expected subscribers, set event diff --git a/music_assistant/server/providers/slimproto/__init__.py b/music_assistant/server/providers/slimproto/__init__.py index a0407d715..482187944 100644 --- a/music_assistant/server/providers/slimproto/__init__.py +++ b/music_assistant/server/providers/slimproto/__init__.py @@ -890,14 +890,10 @@ def _get_sync_clients(self, player_id: str) -> list[SlimClient]: def _get_corrected_elapsed_milliseconds(self, client: SlimClient) -> int: """Return corrected elapsed milliseconds.""" - skipped_millis = 0 - active_queue = self.mass.player_queues.get_active_queue(client.player_id) - if stream_job := self.mass.streams.multi_client_jobs.get(active_queue.queue_id): - skipped_millis = stream_job.client_seconds_skipped.get(client.player_id, 0) * 1000 sync_delay = self.mass.config.get_raw_player_config_value( client.player_id, CONF_SYNC_ADJUST, 0 ) - current_millis = int(skipped_millis + client.elapsed_milliseconds) + current_millis = client.elapsed_milliseconds if sync_delay != 0: return current_millis - sync_delay return current_millis diff --git a/music_assistant/server/providers/sonos/__init__.py b/music_assistant/server/providers/sonos/__init__.py index 5066ae37a..ce2e09845 100644 --- a/music_assistant/server/providers/sonos/__init__.py +++ b/music_assistant/server/providers/sonos/__init__.py @@ -20,6 +20,7 @@ from soco import events_asyncio, zonegroupstate from soco.core import SoCo from soco.discovery import discover +from soco.exceptions import SoCoUPnPException from music_assistant.common.models.config_entries import ( CONF_ENTRY_CROSSFADE, @@ -177,13 +178,16 @@ async def get_player_config_entries( ) -> tuple[ConfigEntry, ...]: """Return Config Entries for the given player.""" base_entries = await super().get_player_config_entries(player_id) + if not (sonos_player := self.sonosplayers.get(player_id)): + return base_entries return base_entries + ( CONF_ENTRY_CROSSFADE, ConfigEntry( key="sonos_bass", type=ConfigEntryType.INTEGER, label="Bass", - default_value=0, + default_value=sonos_player.bass, + value=sonos_player.bass, range=(-10, 10), description="Set the Bass level for the Sonos player", advanced=True, @@ -192,7 +196,8 @@ async def get_player_config_entries( key="sonos_treble", type=ConfigEntryType.INTEGER, label="Treble", - default_value=0, + default_value=sonos_player.treble, + value=sonos_player.treble, range=(-10, 10), description="Set the Treble level for the Sonos player", advanced=True, @@ -201,7 +206,8 @@ async def get_player_config_entries( key="sonos_loudness", type=ConfigEntryType.BOOLEAN, label="Loudness compensation", - default_value=True, + default_value=sonos_player.loudness, + value=sonos_player.loudness, description="Enable loudness compensation on the Sonos player", advanced=True, ), @@ -401,11 +407,11 @@ async def enqueue_next_queue_item(self, player_id: str, queue_item: QueueItem): if sonos_player.crossfade != crossfade: def set_crossfade(): - with suppress(Exception): - sonos_player.soco.cross_fade = crossfade - sonos_player.crossfade = crossfade + sonos_player.soco.cross_fade = crossfade + sonos_player.crossfade = crossfade - await asyncio.to_thread(set_crossfade) + with suppress(SoCoUPnPException): + await asyncio.to_thread(set_crossfade) await self._enqueue_item(sonos_player, url=url, queue_item=queue_item) diff --git a/music_assistant/server/providers/sonos/player.py b/music_assistant/server/providers/sonos/player.py index 70da75c5a..b1f3b0579 100644 --- a/music_assistant/server/providers/sonos/player.py +++ b/music_assistant/server/providers/sonos/player.py @@ -133,6 +133,9 @@ def __init__( self.uri: str | None = None self.position: int | None = None self.position_updated_at: datetime.datetime | None = None + self.loudness: bool = False + self.bass: int = 0 + self.treble: int = 0 # Subscriptions and events self._subscriptions: list[SubscriptionBase] = [] self._subscription_lock: asyncio.Lock | None = None @@ -181,24 +184,9 @@ def setup(self) -> None: self.crossfade = self.soco.cross_fade self.mass_player.volume_level = self.soco.volume self.mass_player.volume_muted = self.soco.mute - self.mass.loop.call_soon_threadsafe( - self.mass.config.set_raw_player_config_value, - self.player_id, - "sonos_loudness", - self.soco.loudness, - ) - self.mass.loop.call_soon_threadsafe( - self.mass.config.set_raw_player_config_value, - self.player_id, - "sonos_bass", - self.soco.bass, - ) - self.mass.loop.call_soon_threadsafe( - self.mass.config.set_raw_player_config_value, - self.player_id, - "sonos_treble", - self.soco.treble, - ) + self.loudness = self.soco.loudness + self.bass = self.soco.bass + self.treble = self.soco.treble self.update_groups() if not self.sync_coordinator: self.poll_media() @@ -514,12 +502,14 @@ def _handle_rendering_control_event(self, event: SonosEvent) -> None: if loudness := variables.get("loudness"): # TODO: handle this is a better way - self.mass.loop.call_soon_threadsafe( - self.mass.config.set_raw_player_config_value, - self.player_id, - "sonos_loudness", - loudness["Master"] == "1", - ) + self.loudness = loudness["Master"] == "1" + with contextlib.suppress(KeyError): + self.mass.loop.call_soon_threadsafe( + self.mass.config.set_raw_player_config_value, + self.player_id, + "sonos_loudness", + loudness["Master"] == "1", + ) for int_var in ( "bass", @@ -527,12 +517,14 @@ def _handle_rendering_control_event(self, event: SonosEvent) -> None: ): if int_var in variables: # TODO: handle this is a better way - self.mass.loop.call_soon_threadsafe( - self.mass.config.set_raw_player_config_value, - self.player_id, - f"sonos_{int_var}", - variables[int_var], - ) + setattr(self, int_var, variables[int_var]) + with contextlib.suppress(KeyError): + self.mass.loop.call_soon_threadsafe( + self.mass.config.set_raw_player_config_value, + self.player_id, + f"sonos_{int_var}", + variables[int_var], + ) self.update_player()