- 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
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
labels: ${{ steps.meta.outputs.labels }}
build-args: |
"MASS_VERSION=${{ needs.build-and-publish-pypi.outputs.version }}"
-
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)
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 = (
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")
"""
# 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",
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
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 (
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):
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:
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):
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():
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
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,
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,
),
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:
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
)
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:
"""
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
):
# 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,
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
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