From 126c6f95ae93ca2626ee3a242f2f5623eaaa5496 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Wed, 12 Jul 2023 00:33:17 +0200 Subject: [PATCH] Some small bugfixes and improvements (#770) * 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 | 9 ++--- .../server/controllers/player_queues.py | 6 +-- music_assistant/server/controllers/players.py | 14 +++++-- music_assistant/server/controllers/streams.py | 6 +-- .../server/providers/airplay/__init__.py | 9 ++--- .../server/providers/slimproto/__init__.py | 5 +++ .../server/providers/ugp/__init__.py | 40 ++++++++++++++++--- 7 files changed, 64 insertions(+), 25 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5794c2d7..ec70f371 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -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 }}" - diff --git a/music_assistant/server/controllers/player_queues.py b/music_assistant/server/controllers/player_queues.py index 2530f010..9aa0cf85 100755 --- a/music_assistant/server/controllers/player_queues.py +++ b/music_assistant/server/controllers/player_queues.py @@ -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 = ( diff --git a/music_assistant/server/controllers/players.py b/music_assistant/server/controllers/players.py index e34376f2..c8db41a8 100755 --- a/music_assistant/server/controllers/players.py +++ b/music_assistant/server/controllers/players.py @@ -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 ( diff --git a/music_assistant/server/controllers/streams.py b/music_assistant/server/controllers/streams.py index 0f2ce5d2..cd461da4 100644 --- a/music_assistant/server/controllers/streams.py +++ b/music_assistant/server/controllers/streams.py @@ -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): diff --git a/music_assistant/server/providers/airplay/__init__.py b/music_assistant/server/providers/airplay/__init__.py index 1ee21e12..0fb82ed9 100644 --- a/music_assistant/server/providers/airplay/__init__.py +++ b/music_assistant/server/providers/airplay/__init__.py @@ -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(): diff --git a/music_assistant/server/providers/slimproto/__init__.py b/music_assistant/server/providers/slimproto/__init__.py index 3d8e3a5d..26941e8e 100644 --- a/music_assistant/server/providers/slimproto/__init__.py +++ b/music_assistant/server/providers/slimproto/__init__.py @@ -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 diff --git a/music_assistant/server/providers/ugp/__init__.py b/music_assistant/server/providers/ugp/__init__.py index 80cef769..5e6f93b4 100644 --- a/music_assistant/server/providers/ugp/__init__.py +++ b/music_assistant/server/providers/ugp/__init__.py @@ -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 -- 2.34.1