Some small bugfixes and improvements (#770)
authorMarcel van der Veldt <m.vanderveldt@outlook.com>
Tue, 11 Jul 2023 22:33:17 +0000 (00:33 +0200)
committerGitHub <noreply@github.com>
Tue, 11 Jul 2023 22:33:17 +0000 (00:33 +0200)
* hide unavailable settings for group player

* ignore unavailable child players

* cleanup

* return the correct active_source

* slimproto: prevent flipflop to idle while playing

* support group in a group

* revert to iter_chunked

* another attempt to solve the airplay flipflop weirdness at reload

* Update release.yml

.github/workflows/release.yml
music_assistant/server/controllers/player_queues.py
music_assistant/server/controllers/players.py
music_assistant/server/controllers/streams.py
music_assistant/server/providers/airplay/__init__.py
music_assistant/server/providers/slimproto/__init__.py
music_assistant/server/providers/ugp/__init__.py

index 5794c2d7c91d397008def28892ba9a7f3e256232..ec70f3718eba1b423a11452fad20b701ae21884c 100644 (file)
@@ -58,9 +58,9 @@ jobs:
       - name: Log in to the GitHub container registry
         uses: docker/login-action@v2.2.0
         with:
-            registry: ghcr.io
-            username: ${{ github.repository_owner }}
-            password: ${{ secrets.GITHUB_TOKEN }}
+          registry: ghcr.io
+          username: ${{ github.repository_owner }}
+          password: ${{ secrets.GITHUB_TOKEN }}
       - name: Set up Docker Buildx
         uses: docker/setup-buildx-action@v2.9.0
       - name: Version number for tags
@@ -82,7 +82,7 @@ jobs:
         uses: docker/metadata-action@v4
         with:
           images: |
-            ghcr.io/music-assistant/server
+            ghcr.io/${{ github.repository_owner }}/server
 
       - name: Build and Push images
         uses: docker/build-push-action@v4.1.1
@@ -100,4 +100,3 @@ jobs:
           labels: ${{ steps.meta.outputs.labels }}
           build-args: |
             "MASS_VERSION=${{ needs.build-and-publish-pypi.outputs.version }}"
-      
index 2530f01083506f6359649b5f3a02558f8cece7e7..9aa0cf859a37f9c511c8b5140bcdcba283b27b27 100755 (executable)
@@ -89,7 +89,9 @@ class PlayerQueuesController(CoreController):
             if player.synced_to:
                 return self.get_active_queue(player.synced_to)
             # active_source may be filled with other queue id
-            if queue := self.get(player.active_source):
+            if player.active_source != player_id and (
+                queue := self.get_active_queue(player.active_source)
+            ):
                 return queue
         return self.get(player_id)
 
@@ -495,8 +497,6 @@ class PlayerQueuesController(CoreController):
         queue.index_in_buffer = index
         # power on player if needed
         await self.mass.players.cmd_power(queue_id, True)
-        # always send stop command first
-        # await self.mass.players.cmd_stop(queue_id)
         # execute the play_media command on the player
         queue_player = self.mass.players.get(queue_id)
         need_multi_stream = (
index e34376f27634b5c6cc1a6fbe8734135ff437bc56..c8db41a82190b98aab5f5750114f2f17f2becef7 100755 (executable)
@@ -186,6 +186,8 @@ class PlayerController(CoreController):
         player.active_source = self._get_active_source(player)
         # calculate group volume
         player.group_volume = self._get_group_volume_level(player)
+        if player.type == PlayerType.GROUP:
+            player.volume_level = player.group_volume
         # prefer any overridden name from config
         player.display_name = (
             self.mass.config.get(f"{CONF_PLAYERS}/{player_id}/name")
@@ -388,6 +390,10 @@ class PlayerController(CoreController):
         """
         # TODO: Implement PlayerControl
         player = self.get(player_id, True)
+        if player.type == PlayerType.GROUP:
+            # redirect to group volume control
+            await self.cmd_group_volume(player_id, volume_level)
+            return
         if PlayerFeature.VOLUME_SET not in player.supported_features:
             LOGGER.warning(
                 "Volume set command called but player %s does not support volume",
@@ -556,9 +562,9 @@ class PlayerController(CoreController):
 
     def _get_active_source(self, player: Player) -> str:
         """Return the active_source id for given player."""
-        # if player is synced, return master/group leader
-        if player.synced_to and player.synced_to in self._players:
-            return player.synced_to
+        # if player is synced, return master/group leader's active source
+        if player.synced_to and (parent_player := self.get(player.synced_to)):
+            return self._get_active_source(parent_player)
         # iterate player groups to find out if one is playing
         if group_players := self._get_player_groups(player.player_id):
             # prefer the first playing (or paused) group parent
@@ -607,6 +613,8 @@ class PlayerController(CoreController):
         child_players: list[Player] = []
         for child_id in player.group_childs:
             if child_player := self.get(child_id, False):
+                if not child_player.available:
+                    continue
                 if not (not only_powered or child_player.powered):
                     continue
                 if not (
index 0f2ce5d2694176b19d60a5fb07da5551a37c82fe..cd461da421669513926533510e6960801f553f03 100644 (file)
@@ -533,7 +533,7 @@ class StreamsController(CoreController):
             ffmpeg_proc.attach_task(read_audio())
 
             # read final chunks from stdout
-            async for chunk in ffmpeg_proc.iter_chunked():
+            async for chunk in ffmpeg_proc.iter_any(768000):
                 try:
                     await resp.write(chunk)
                 except (BrokenPipeError, ConnectionResetError):
@@ -624,7 +624,7 @@ class StreamsController(CoreController):
             iterator = (
                 ffmpeg_proc.iter_chunked(icy_meta_interval)
                 if enable_icy
-                else ffmpeg_proc.iter_chunked(256000)
+                else ffmpeg_proc.iter_any(768000)
             )
             async for chunk in iterator:
                 try:
@@ -736,7 +736,7 @@ class StreamsController(CoreController):
             ffmpeg_proc.attach_task(read_audio())
 
             # read final chunks from stdout
-            async for chunk in ffmpeg_proc.iter_chunked():
+            async for chunk in ffmpeg_proc.iter_any(768000):
                 try:
                     await resp.write(chunk)
                 except (BrokenPipeError, ConnectionResetError):
index 1ee21e12c1ea0fb299f4538cf092f85974338ce6..0fb82ed90a0cd68cd84420a958147a337f76fbfc 100644 (file)
@@ -380,20 +380,19 @@ class AirplayProvider(PlayerProvider):
                 if not start_success:
                     raise err
                 self.logger.exception("Error in Airplay bridge", exc_info=err)
-            else:
-                self.logger.debug("Airplay Bridge process stopped")
             if self._closing:
                 break
-            await asyncio.sleep(1)
+            await asyncio.sleep(10)
 
     async def _stop_bridge(self) -> None:
         """Stop the bridge process."""
         if self._bridge_proc:
             try:
-                self.logger.debug("Stopping bridge process...")
+                self.logger.info("Stopping bridge process...")
                 self._bridge_proc.terminate()
                 await self._bridge_proc.wait()
-                self.logger.debug("Bridge process stopped.")
+                self.logger.info("Bridge process stopped.")
+                await asyncio.sleep(5)
             except ProcessLookupError:
                 pass
         if self._log_reader_task and not self._log_reader_task.done():
index 3d8e3a5df3d101232885a92486858af0827d50c0..26941e8efa244c7639ec2dc2a914cba19d3f0bbe 100644 (file)
@@ -854,3 +854,8 @@ class SlimClient(SlimClientOrg):
     def _process_stat_stmo(self, data: bytes) -> None:  # noqa: ARG002
         """Process incoming stat STMo message: Output Underrun."""
         self.callback("output_underrun", self)
+
+    def _process_stat_stmf(self, data: bytes) -> None:  # noqa: ARG002
+        """Process incoming stat STMf message (connection closed)."""
+        self.logger.debug("STMf received - connection closed.")
+        # we should ignore this event, its not relevant
index 80cef769c6ef63ce34c7ab159e9c7a692385ae25..5e6f93b4bf36d9a57a4927a9eba0e95e07b5c2a9 100644 (file)
@@ -10,9 +10,13 @@ import asyncio
 from typing import TYPE_CHECKING
 
 from music_assistant.common.models.config_entries import (
+    CONF_ENTRY_EQ_BASS,
+    CONF_ENTRY_EQ_MID,
+    CONF_ENTRY_EQ_TREBLE,
     CONF_ENTRY_FLOW_MODE,
     CONF_ENTRY_HIDE_GROUP_MEMBERS,
     CONF_ENTRY_OUTPUT_CHANNELS,
+    CONF_ENTRY_OUTPUT_CODEC,
     ConfigEntry,
     ConfigValueOption,
     ConfigValueType,
@@ -50,6 +54,14 @@ CONF_ENTRY_OUTPUT_CHANNELS_FORCED_STEREO = ConfigEntry.from_dict(
 CONF_ENTRY_FORCED_FLOW_MODE = ConfigEntry.from_dict(
     {**CONF_ENTRY_FLOW_MODE.to_dict(), "default_value": True, "value": True, "hidden": True}
 )
+CONF_ENTRY_EQ_BASS_HIDDEN = ConfigEntry.from_dict({**CONF_ENTRY_EQ_BASS.to_dict(), "hidden": True})
+CONF_ENTRY_EQ_MID_HIDDEN = ConfigEntry.from_dict({**CONF_ENTRY_EQ_MID.to_dict(), "hidden": True})
+CONF_ENTRY_EQ_TREBLE_HIDDEN = ConfigEntry.from_dict(
+    {**CONF_ENTRY_EQ_TREBLE.to_dict(), "hidden": True}
+)
+CONF_ENTRY_OUTPUT_CODEC_HIDDEN = ConfigEntry.from_dict(
+    {**CONF_ENTRY_OUTPUT_CODEC.to_dict(), "hidden": True}
+)
 CONF_ENTRY_GROUPED_POWER_ON = ConfigEntry(
     key=CONF_GROUPED_POWER_ON,
     type=ConfigEntryType.BOOLEAN,
@@ -188,6 +200,12 @@ class UniversalGroupProvider(PlayerProvider):
             ),
             CONF_ENTRY_OUTPUT_CHANNELS_FORCED_STEREO,
             CONF_ENTRY_FORCED_FLOW_MODE,
+            # group player outputs to individual members so
+            # these settings make no sense, hide them
+            CONF_ENTRY_EQ_BASS_HIDDEN,
+            CONF_ENTRY_EQ_MID_HIDDEN,
+            CONF_ENTRY_EQ_TREBLE_HIDDEN,
+            CONF_ENTRY_OUTPUT_CODEC_HIDDEN,
         )
 
     async def cmd_stop(self, player_id: str) -> None:
@@ -357,6 +375,10 @@ class UniversalGroupProvider(PlayerProvider):
     def update_attributes(self, player_id: str) -> None:
         """Update player attributes."""
         group_player = self.mass.players.get(player_id)
+        if not group_player.powered:
+            group_player.state = PlayerState.IDLE
+            return
+
         all_members = self._get_active_members(
             player_id, only_powered=False, skip_sync_childs=False
         )
@@ -365,15 +387,17 @@ class UniversalGroupProvider(PlayerProvider):
         for member in all_members:
             if member.synced_to:
                 continue
-            if not member.current_url or player_id not in member.current_url:
+            if member.mute_as_power:
+                player_powered = member.powered and not member.volume_muted
+            else:
+                player_powered = member.powered
+            if not player_powered:
                 continue
             group_player.current_url = member.current_url
             group_player.elapsed_time = member.elapsed_time
             group_player.elapsed_time_last_updated = member.elapsed_time_last_updated
             group_player.state = member.state
             break
-        else:
-            group_player.state = group_player.extra_data["optimistic_state"]
 
     async def on_child_power(self, player_id: str, child_player: Player, new_power: bool) -> None:
         """
@@ -420,7 +444,8 @@ class UniversalGroupProvider(PlayerProvider):
                     self.mass.players.cmd_sync, child_player.player_id, sync_leader
                 )
             else:
-                self.mass.create_task(self.mass.player_queues.resume, player_id)
+                # send atcive source because the group may be within another group
+                self.mass.create_task(self.mass.player_queues.resume, group_player.active_source)
         elif (
             not new_power
             and group_player.extra_data["optimistic_state"] == PlayerState.PLAYING
@@ -429,7 +454,8 @@ class UniversalGroupProvider(PlayerProvider):
         ):
             # a sync master player turned OFF while the group player
             # should still be playing - we need to resync/resume
-            self.mass.create_task(self.mass.player_queues.resume, player_id)
+            # send atcive source because the group may be within another group
+            self.mass.create_task(self.mass.player_queues.resume, group_player.active_source)
 
     def _get_active_members(
         self,
@@ -441,6 +467,8 @@ class UniversalGroupProvider(PlayerProvider):
         child_players: list[Player] = []
         conf_members: list[str] = self.config.get_value(player_id)
         ignore_ids = set()
+        group_player = self.mass.players.get(player_id)
+        parent_source = group_player.active_source
         for child_id in conf_members:
             if child_player := self.mass.players.get(child_id, False):
                 # work out power state
@@ -452,7 +480,7 @@ class UniversalGroupProvider(PlayerProvider):
                     continue
                 if child_player.synced_to and skip_sync_childs:
                     continue
-                allowed_sources = [child_player.player_id, player_id] + conf_members
+                allowed_sources = [child_player.player_id, player_id, parent_source] + conf_members
                 if child_player.active_source not in allowed_sources:
                     # edge case: the child player has another group already active!
                     continue