Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Some small bugfixes and improvements #770

Merged
merged 9 commits into from
Jul 11, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 4 additions & 5 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,9 @@ jobs:
- name: Log in to the GitHub container registry
uses: docker/[email protected]
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/[email protected]
- name: Version number for tags
Expand All @@ -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/[email protected]
Expand All @@ -100,4 +100,3 @@ jobs:
labels: ${{ steps.meta.outputs.labels }}
build-args: |
"MASS_VERSION=${{ needs.build-and-publish-pypi.outputs.version }}"

6 changes: 3 additions & 3 deletions music_assistant/server/controllers/player_queues.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,9 @@ def get_active_queue(self, player_id: str) -> PlayerQueue:
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)

Expand Down Expand Up @@ -495,8 +497,6 @@ async def play_index(
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 = (
Expand Down
14 changes: 11 additions & 3 deletions music_assistant/server/controllers/players.py
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,8 @@ def update(
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")
Expand Down Expand Up @@ -388,6 +390,10 @@ async def cmd_volume_set(self, player_id: str, volume_level: int) -> None:
"""
# 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",
Expand Down Expand Up @@ -556,9 +562,9 @@ def _get_player_groups(self, player_id: str) -> tuple[Player, ...]:

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
Expand Down Expand Up @@ -607,6 +613,8 @@ def _get_child_players(
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 (
Expand Down
6 changes: 3 additions & 3 deletions music_assistant/server/controllers/streams.py
Original file line number Diff line number Diff line change
Expand Up @@ -533,7 +533,7 @@ async def read_audio():
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):
Expand Down Expand Up @@ -624,7 +624,7 @@ async def read_audio():
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:
Expand Down Expand Up @@ -736,7 +736,7 @@ async def read_audio():
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):
Expand Down
9 changes: 4 additions & 5 deletions music_assistant/server/providers/airplay/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -380,20 +380,19 @@ async def _bridge_process_runner(self, slimproto_prov: SlimprotoProvider) -> Non
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():
Expand Down
5 changes: 5 additions & 0 deletions music_assistant/server/providers/slimproto/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
40 changes: 34 additions & 6 deletions music_assistant/server/providers/ugp/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,13 @@
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,
Expand Down Expand Up @@ -50,6 +54,14 @@
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,
Expand Down Expand Up @@ -188,6 +200,12 @@ async def get_player_config_entries(self, player_id: str) -> tuple[ConfigEntry]:
),
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:
Expand Down Expand Up @@ -357,6 +375,10 @@ async def poll_player(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
)
Expand All @@ -365,15 +387,17 @@ def update_attributes(self, player_id: str) -> None:
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:
"""
Expand Down Expand Up @@ -420,7 +444,8 @@ async def on_child_power(self, player_id: str, child_player: Player, new_power:
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
Expand All @@ -429,7 +454,8 @@ async def on_child_power(self, player_id: str, child_player: Player, new_power:
):
# 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,
Expand All @@ -441,6 +467,8 @@ def _get_active_members(
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
Expand All @@ -452,7 +480,7 @@ def _get_active_members(
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
Expand Down