Skip to content

Commit

Permalink
a collection of small tweaks
Browse files Browse the repository at this point in the history
  • Loading branch information
marcelveldt committed Oct 18, 2024
1 parent ada4dce commit fa865f8
Show file tree
Hide file tree
Showing 12 changed files with 271 additions and 171 deletions.
2 changes: 2 additions & 0 deletions music_assistant/common/models/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,7 @@ class PlayerFeature(StrEnum):
PAUSE = "pause"
SYNC = "sync"
SEEK = "seek"
NEXT_PREVIOUS = "next_previous"
PLAY_ANNOUNCEMENT = "play_announcement"
UNKNOWN = "unknown"

Expand Down Expand Up @@ -426,6 +427,7 @@ class StreamType(StrEnum):
HTTP = "http" # regular http stream
ENCRYPTED_HTTP = "encrypted_http" # encrypted http stream
HLS = "hls" # http HLS stream
ENCRYPTED_HLS = "encrypted_hls" # encrypted HLS stream
ICY = "icy" # http stream with icy metadata
LOCAL_FILE = "local_file"
CUSTOM = "custom"
Expand Down
44 changes: 28 additions & 16 deletions music_assistant/server/controllers/player_queues.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
RepeatMode,
)
from music_assistant.common.models.errors import (
InvalidCommand,
MediaNotFoundError,
MusicAssistantError,
PlayerUnavailableError,
Expand Down Expand Up @@ -633,12 +634,17 @@ async def stop(self, queue_id: str) -> None:
- queue_id: queue_id of the playerqueue to handle the command.
"""
if queue := self.get(queue_id):
queue_player: Player = self.mass.players.get(queue_id, True)
if queue_player.announcement_in_progress:
self.logger.warning("Ignore queue command: An announcement is in progress")
return
if (queue := self.get(queue_id)) and queue.active:
queue.resume_pos = queue.corrected_elapsed_time
queue.stream_finished = None
queue.end_of_track_reached = None
# forward the actual command to the player controller
await self.mass.players.cmd_stop(queue_id, skip_redirect=True)
# forward the actual command to the player provider
if player_provider := self.mass.players.get_player_provider(queue.queue_id):
await player_provider.cmd_stop(queue_id)

@api_command("player_queues/play")
async def play(self, queue_id: str) -> None:
Expand All @@ -654,13 +660,14 @@ async def play(self, queue_id: str) -> None:
if (
(queue := self._queues.get(queue_id))
and queue.active
and queue_player.powered
and queue.state == PlayerState.PAUSED
and queue_player.state == PlayerState.PAUSED
):
# forward the actual command to the player controller
await self.mass.players.cmd_play(queue_id, skip_redirect=True)
else:
await self.resume(queue_id)
# forward the actual play/unpause command to the player provider
if player_provider := self.mass.players.get_player_provider(queue.queue_id):
await player_provider.cmd_play(queue_id)
return
# player is not paused, perform resume instead
await self.resume(queue_id)

@api_command("player_queues/pause")
async def pause(self, queue_id: str) -> None:
Expand Down Expand Up @@ -754,14 +761,19 @@ async def seek(self, queue_id: str, position: int = 10) -> None:
if queue_player.announcement_in_progress:
self.logger.warning("Ignore queue command: An announcement is in progress")
return
if (queue := self.get(queue_id)) is None or not queue.active:
# TODO: forward to underlying player if not active
if not (queue := self.get(queue_id)):
return
assert queue.current_item, "No item loaded"
assert queue.current_item.media_item.media_type == MediaType.TRACK
assert queue.current_item.duration
assert position < queue.current_item.duration
await self.play_index(queue_id, queue.current_index, position)
if not queue.current_item:
raise InvalidCommand(f"Queue {queue_player.display_name} has no item(s) loaded.")
if (
queue.current_item.media_item.media_type != MediaType.TRACK
or not queue.current_item.duration
):
raise InvalidCommand("Can not seek on non track items.")
position = max(0, int(position))
if position > queue.current_item.duration:
raise InvalidCommand("Can not seek outside of duration range.")
await self.play_index(queue_id, queue.current_index, seek_position=position)

@api_command("player_queues/resume")
async def resume(self, queue_id: str, fade_in: bool | None = None) -> None:
Expand Down
132 changes: 81 additions & 51 deletions music_assistant/server/controllers/players.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,55 +170,55 @@ def get_by_name(self, name: str) -> Player | None:

@api_command("players/cmd/stop")
@handle_player_command
async def cmd_stop(self, player_id: str, skip_redirect: bool = False) -> None:
async def cmd_stop(self, player_id: str) -> None:
"""Send STOP command to given player.
- player_id: player_id of the player to handle the command.
"""
player = self._get_player_with_redirect(player_id, skip_redirect=skip_redirect)
# Redirect to queue controller if active (as it also handles some other logic)
# Note that skip_redirect will be set by the queue controller
# to prevent an endless loop.
if not skip_redirect and player.active_source == player_id:
await self.mass.player_queues.stop(player_id)
player = self._get_player_with_redirect(player_id)
# Redirect to queue controller if it is active
if active_queue := self.mass.player_queues.get(player.active_source):
await self.mass.player_queues.stop(active_queue.queue_id)
return
if player_provider := self.get_player_provider(player_id):
await player_provider.cmd_stop(player_id)
# send to player provider
async with self._player_throttlers[player_id]:
if player_provider := self.get_player_provider(player_id):
await player_provider.cmd_stop(player_id)

@api_command("players/cmd/play")
@handle_player_command
async def cmd_play(self, player_id: str, skip_redirect: bool = False) -> None:
async def cmd_play(self, player_id: str) -> None:
"""Send PLAY (unpause) command to given player.
- player_id: player_id of the player to handle the command.
"""
player = self._get_player_with_redirect(player_id, skip_redirect=skip_redirect)
if player.announcement_in_progress:
self.logger.warning("Ignore queue command: An announcement is in progress")
return
# Redirect to queue controller if active (as it also handles some other logic)
# Note that skip_redirect will be set by the queue controller
# to prevent an endless loop.
if not skip_redirect and player.active_source == player_id:
await self.mass.player_queues.play(player_id)
player = self._get_player_with_redirect(player_id)
# Redirect to queue controller if it is active
active_source = player.active_source or player.player_id
if (active_queue := self.mass.player_queues.get(active_source)) and active_queue.items:
await self.mass.player_queues.play(active_queue.queue_id)
return
# send to player provider
player_provider = self.get_player_provider(player_id)
async with self._player_throttlers[player_id]:
await player_provider.cmd_play(player_id)

@api_command("players/cmd/pause")
@handle_player_command
async def cmd_pause(self, player_id: str, skip_redirect: bool = False) -> None:
async def cmd_pause(self, player_id: str) -> None:
"""Send PAUSE command to given player.
- player_id: player_id of the player to handle the command.
"""
player = self._get_player_with_redirect(player_id, skip_redirect=skip_redirect)
player = self._get_player_with_redirect(player_id)
if player.announcement_in_progress:
self.logger.warning("Ignore command: An announcement is in progress")
return
if PlayerFeature.PAUSE not in player.supported_features:
# if player does not support pause, we need to send stop
self.logger.info(
"Player %s does not support pause, using STOP instead", player.display_name
)
await self.cmd_stop(player_id)
return
player_provider = self.get_player_provider(player_id)
Expand All @@ -243,25 +243,73 @@ async def _watch_pause(_player_id: str) -> None:
await self.cmd_stop(_player_id)

# we auto stop a player from paused when its paused for 30 seconds
self.mass.create_task(_watch_pause(player_id))
if not player.announcement_in_progress:
self.mass.create_task(_watch_pause(player_id))

@api_command("players/cmd/play_pause")
async def cmd_play_pause(self, player_id: str) -> None:
"""Toggle play/pause on given player.
- player_id: player_id of the player to handle the command.
"""
player = self._get_player_with_redirect(player_id, skip_redirect=False)
player = self._get_player_with_redirect(player_id)
if player.state == PlayerState.PLAYING:
await self.cmd_pause(player_id)
else:
await self.cmd_play(player_id)

@api_command("players/cmd/seek")
async def cmd_seek(self, player_id: str, position: int) -> None:
"""Handle SEEK command for given player.
- player_id: player_id of the player to handle the command.
- position: position in seconds to seek to in the current playing item.
"""
player = self._get_player_with_redirect(player_id)
# Redirect to queue controller if it is active
active_source = player.active_source or player.player_id
if active_queue := self.mass.player_queues.get(active_source):
await self.mass.player_queues.seek(active_queue.queue_id, position)
return
if PlayerFeature.SEEK not in player.supported_features:
msg = f"Player {player.display_name} does not support seeking"
raise UnsupportedFeaturedException(msg)
player_prov = self.mass.players.get_player_provider(player_id)
await player_prov.cmd_seek(player_id, position)

@api_command("players/cmd/next")
async def cmd_next_track(self, player_id: str) -> None:
"""Handle NEXT TRACK command for given player."""
player = self._get_player_with_redirect(player_id)
# Redirect to queue controller if it is active
active_source = player.active_source or player.player_id
if active_queue := self.mass.player_queues.get(active_source):
await self.mass.player_queues.next(active_queue.queue_id)
return
if PlayerFeature.NEXT_PREVIOUS not in player.supported_features:
msg = f"Player {player.display_name} does not support skipping to the next track."
raise UnsupportedFeaturedException(msg)
player_prov = self.mass.players.get_player_provider(player_id)
await player_prov.cmd_next(player_id)

@api_command("players/cmd/previous")
async def cmd_previous_track(self, player_id: str) -> None:
"""Handle PREVIOUS TRACK command for given player."""
player = self._get_player_with_redirect(player_id)
# Redirect to queue controller if it is active
active_source = player.active_source or player.player_id
if active_queue := self.mass.player_queues.get(active_source):
await self.mass.player_queues.previous(active_queue.queue_id)
return
if PlayerFeature.NEXT_PREVIOUS not in player.supported_features:
msg = f"Player {player.display_name} does not support skipping to the previous track."
raise UnsupportedFeaturedException(msg)
player_prov = self.mass.players.get_player_provider(player_id)
await player_prov.cmd_previous(player_id)

@api_command("players/cmd/power")
@handle_player_command
async def cmd_power(
self, player_id: str, powered: bool, skip_redirect: bool = False, skip_update: bool = False
) -> None:
async def cmd_power(self, player_id: str, powered: bool, skip_update: bool = False) -> None:
"""Send POWER command to given player.
- player_id: player_id of the player to handle the command.
Expand All @@ -272,7 +320,7 @@ async def cmd_power(
if player.powered == powered:
return # nothing to do

if player.active_group and not powered and not skip_redirect:
if player.active_group and not powered:
# this is simply not possible (well, not without major headaches)
# the player is part of a permanent (sync)group and the user tries to power off
# one child player... we can't allow this, as it would break the group so we
Expand Down Expand Up @@ -426,20 +474,6 @@ async def cmd_volume_mute(self, player_id: str, muted: bool) -> None:
async with self._player_throttlers[player_id]:
await player_provider.cmd_volume_mute(player_id, muted)

@api_command("players/cmd/seek")
async def cmd_seek(self, player_id: str, position: int) -> None:
"""Handle SEEK command for given player (directly).
- player_id: player_id of the player to handle the command.
- position: position in seconds to seek to in the current playing item.
"""
player = self._get_player_with_redirect(player_id)
if PlayerFeature.SEEK not in player.supported_features:
msg = f"Player {player.display_name} does not support seeking"
raise UnsupportedFeaturedException(msg)
player_prov = self.mass.players.get_player_provider(player_id)
await player_prov.cmd_seek(player_id, position)

@api_command("players/cmd/play_announcement")
async def play_announcement(
self,
Expand Down Expand Up @@ -529,15 +563,13 @@ async def play_announcement(
player.announcement_in_progress = False

@handle_player_command
async def play_media(
self, player_id: str, media: PlayerMedia, skip_redirect: bool = False
) -> None:
async def play_media(self, player_id: str, media: PlayerMedia) -> None:
"""Handle PLAY MEDIA on given player.
- player_id: player_id of the player to handle the command.
- media: The Media that needs to be played on the player.
"""
player = self._get_player_with_redirect(player_id, skip_redirect=skip_redirect)
player = self._get_player_with_redirect(player_id)
# power on the player if needed
if not player.powered:
await self.cmd_power(player_id, True)
Expand Down Expand Up @@ -768,7 +800,7 @@ def remove(self, player_id: str, cleanup_config: bool = True) -> None:
self.mass.signal_event(EventType.PLAYER_REMOVED, player_id)

def update(
self, player_id: str, skip_redirect: bool = False, force_update: bool = False
self, player_id: str, skip_forward: bool = False, force_update: bool = False
) -> None:
"""Update player state."""
if self.mass.closing:
Expand Down Expand Up @@ -833,13 +865,13 @@ def update(

self.mass.signal_event(EventType.PLAYER_UPDATED, object_id=player_id, data=player)

if skip_redirect:
if skip_forward:
return

# update/signal group player(s) child's when group updates
if player.type == PlayerType.GROUP:
for child_player in self.iter_group_members(player, exclude_self=True):
self.update(child_player.player_id, skip_redirect=True)
self.update(child_player.player_id, skip_forward=True)
# update/signal group player(s) when child updates
for group_player in self._get_player_groups(player, powered_only=False):
if player_prov := self.mass.get_provider(group_player.provider):
Expand Down Expand Up @@ -889,11 +921,9 @@ def get_announcement_volume(self, player_id: str, volume_override: int | None) -
# ensure the result is an integer
return None if volume_level is None else int(volume_level)

def _get_player_with_redirect(self, player_id: str, skip_redirect: bool = False) -> Player:
def _get_player_with_redirect(self, player_id: str) -> Player:
"""Get player with check if playback related command should be redirected."""
player = self.get(player_id, True)
if skip_redirect:
return player
if player.synced_to and (sync_leader := self.get(player.synced_to)):
self.logger.info(
"Player %s is synced to %s and can not accept "
Expand Down
Loading

0 comments on commit fa865f8

Please sign in to comment.