Bugfixes (#1044)
authorMarcel van der Veldt <m.vanderveldt@outlook.com>
Mon, 29 Jan 2024 13:43:42 +0000 (14:43 +0100)
committerGitHub <noreply@github.com>
Mon, 29 Jan 2024 13:43:42 +0000 (14:43 +0100)
* Fix for corrupted sonos config

* do not support late joining on multi client stream

* prevent SoCoUPnPException

music_assistant/server/controllers/config.py
music_assistant/server/controllers/streams.py
music_assistant/server/providers/slimproto/__init__.py
music_assistant/server/providers/sonos/__init__.py
music_assistant/server/providers/sonos/player.py

index c46b7ad5460c16d000b61737c234f1a4ab7336b9..dc7b26c02bf61b6a55fe46a9a239eae6441271ae 100644 (file)
@@ -71,6 +71,15 @@ class ConfigController:
         """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 @@ class ConfigController:
 
         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 @@ class ConfigController:
 
         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:
index b7e5c4e7b0b69643012edd119e436ed3d26b2414..33080999293026d11876f9b3928c892abf7c3090 100644 (file)
@@ -172,12 +172,11 @@ class MultiClientStreamJob:
             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
index a0407d71516a8b1c4e6afa6408ad14a5391717f7..482187944662cc4e72675afdd691d6d4410558e5 100644 (file)
@@ -890,14 +890,10 @@ class SlimprotoProvider(PlayerProvider):
 
     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
index 5066ae37a06432b2e87ed8a251f69120a3ffc910..ce2e09845180d48110daf31b71af9ef20829bdcc 100644 (file)
@@ -20,6 +20,7 @@ from requests.exceptions import RequestException
 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 @@ class SonosPlayerProvider(PlayerProvider):
     ) -> 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 @@ class SonosPlayerProvider(PlayerProvider):
                 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 @@ class SonosPlayerProvider(PlayerProvider):
                 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 @@ class SonosPlayerProvider(PlayerProvider):
         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)
 
index 70da75c5a7ffcaaa02c7d777f8e2fd37db0bfa15..b1f3b0579be76941db6c669c3e307bffd0d6f4ff 100644 (file)
@@ -133,6 +133,9 @@ class SonosPlayer:
         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 @@ class SonosPlayer:
             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 @@ class SonosPlayer:
 
         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 @@ class SonosPlayer:
         ):
             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()