From eb8122b6494a8f9f28055ba9f5ac554b20f7c99c Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Thu, 13 Jun 2024 21:40:45 +0200 Subject: [PATCH] Fixes for announcements --- .../server/controllers/player_queues.py | 6 +- music_assistant/server/controllers/players.py | 68 +++++++++++++------ music_assistant/server/helpers/tags.py | 6 +- .../server/providers/deezer/__init__.py | 17 ++++- 4 files changed, 68 insertions(+), 29 deletions(-) diff --git a/music_assistant/server/controllers/player_queues.py b/music_assistant/server/controllers/player_queues.py index 5e3629c5..76e953f0 100644 --- a/music_assistant/server/controllers/player_queues.py +++ b/music_assistant/server/controllers/player_queues.py @@ -536,11 +536,6 @@ class PlayerQueuesController(CoreController): - queue_id: queue_id of the playerqueue to handle the command. """ - # always fetch the underlying player so we can raise early if its not available - player = self.mass.players.get(queue_id, True) - if player.announcement_in_progress: - self.logger.warning("Ignore queue command: An announcement is in progress") - return if queue := self.get(queue_id): queue.stream_finished = None # forward the actual stop command to the player provider @@ -767,6 +762,7 @@ class PlayerQueuesController(CoreController): self._queue_items[queue_id] = queue_items # always call update to calculate state etc self.on_player_update(player, {}) + self.mass.signal_event(EventType.QUEUE_ADDED, object_id=queue_id, data=queue) def on_player_update( self, diff --git a/music_assistant/server/controllers/players.py b/music_assistant/server/controllers/players.py index 5d739d09..ff02d1c5 100644 --- a/music_assistant/server/controllers/players.py +++ b/music_assistant/server/controllers/players.py @@ -4,7 +4,7 @@ from __future__ import annotations import asyncio import functools -from contextlib import suppress +import time from typing import TYPE_CHECKING, Any, Concatenate, ParamSpec, TypeVar, cast from music_assistant.common.helpers.util import get_changed_values @@ -1136,11 +1136,7 @@ class PlayerController(CoreController): ) await self.cmd_stop(player.player_id) # wait for the player to stop - with suppress(TimeoutError): - await self.wait_for_state(player, PlayerState.IDLE, 10) - # a small amount of pause before the volume command - # prevents that the last piece of music is very loud - await asyncio.sleep(0.2) + await self.wait_for_state(player, PlayerState.IDLE, 10, 0.4) # adjust volume if needed # in case of a (sync) group, we need to do this for all child players prev_volumes: dict[str, int] = {} @@ -1175,18 +1171,18 @@ class PlayerController(CoreController): ) await self.play_media(player_id=player.player_id, media=announcement) # wait for the player(s) to play - with suppress(TimeoutError): - await self.wait_for_state(player, PlayerState.PLAYING, 10) - self.logger.debug( - "Announcement to player %s - waiting on the player to stop playing...", - player.display_name, - ) + await self.wait_for_state(player, PlayerState.PLAYING, 10, minimal_time=0.1) # wait for the player to stop playing if not announcement.duration: media_info = await parse_tags(announcement.custom_data["url"]) - announcement.duration = media_info.duration - with suppress(TimeoutError): - await self.wait_for_state(player, PlayerState.IDLE, (announcement.duration or 60) + 3) + announcement.duration = media_info.duration or 60 + media_info.duration += 2 + await self.wait_for_state( + player, + PlayerState.IDLE, + max(announcement.duration * 2, 60), + announcement.duration + 2, + ) self.logger.debug( "Announcement to player %s - restore previous state...", player.display_name ) @@ -1209,9 +1205,43 @@ class PlayerController(CoreController): # TODO !! async def wait_for_state( - self, player: Player, wanted_state: PlayerState, timeout: float = 60.0 + self, + player: Player, + wanted_state: PlayerState, + timeout: float = 60.0, + minimal_time: float = 0, ) -> None: """Wait for the given player to reach the given state.""" - async with asyncio.timeout(timeout): - while player.state != wanted_state: - await asyncio.sleep(0.1) + start_timestamp = time.time() + self.logger.debug( + "Waiting for player %s to reach state %s", player.display_name, wanted_state + ) + try: + async with asyncio.timeout(timeout): + while player.state != wanted_state: + await asyncio.sleep(0.1) + + except TimeoutError: + self.logger.debug( + "Player %s did not reach state %s within the timeout of %s seconds", + player.display_name, + wanted_state, + timeout, + ) + elapsed_time = round(time.time() - start_timestamp, 2) + if elapsed_time < minimal_time: + self.logger.debug( + "Player %s reached state %s too soon (%s vs %s seconds) - add fallback sleep...", + player.display_name, + wanted_state, + elapsed_time, + minimal_time, + ) + await asyncio.sleep(minimal_time - elapsed_time) + else: + self.logger.debug( + "Player %s reached state %s within %s seconds", + player.display_name, + wanted_state, + elapsed_time, + ) diff --git a/music_assistant/server/helpers/tags.py b/music_assistant/server/helpers/tags.py index dc38e4c0..47adf238 100644 --- a/music_assistant/server/helpers/tags.py +++ b/music_assistant/server/helpers/tags.py @@ -73,7 +73,7 @@ class AudioTags: bits_per_sample: int format: str bit_rate: int - duration: int | None + duration: float | None tags: dict[str, str] has_cover_image: bool filename: str @@ -346,7 +346,7 @@ class AudioTags: ), format=raw["format"]["format_name"], bit_rate=int(raw["format"].get("bit_rate", 320)), - duration=int(float(raw["format"].get("duration", 0))) or None, + duration=float(raw["format"].get("duration", 0)) or None, tags=tags, has_cover_image=has_cover_image, filename=raw["format"]["filename"], @@ -422,6 +422,8 @@ async def parse_tags( if not tags.duration and file_size and tags.bit_rate: # estimate duration from filesize/bitrate tags.duration = int((file_size * 8) / tags.bit_rate) + if not tags.duration and tags.raw.get("format", {}).get("duration"): + tags.duration = float(tags.raw["format"]["duration"]) return tags except (KeyError, ValueError, JSONDecodeError, InvalidDataError) as err: msg = f"Unable to retrieve info for {file_path}: {err!s}" diff --git a/music_assistant/server/providers/deezer/__init__.py b/music_assistant/server/providers/deezer/__init__.py index fb224dad..3ae16b44 100644 --- a/music_assistant/server/providers/deezer/__init__.py +++ b/music_assistant/server/providers/deezer/__init__.py @@ -463,11 +463,20 @@ class DeezerProvider(MusicProvider): # pylint: disable=W0223 chunk_index = 0 timeout = ClientTimeout(total=0, connect=30, sock_read=600) headers = {} + # if seek_position and streamdetails.size: + # chunk_count = ceil(streamdetails.size / 2048) + # chunk_index = int(chunk_count / streamdetails.duration) * seek_position + # skip_bytes = chunk_index * 2048 + # headers["Range"] = f"bytes={skip_bytes}-" + + # NOTE: Seek with using the Range header is not working properly + # causing malformed audio so this is a temporary patch + # by just skipping chunks if seek_position and streamdetails.size: chunk_count = ceil(streamdetails.size / 2048) - chunk_index = int(chunk_count / streamdetails.duration) * seek_position - skip_bytes = chunk_index * 2048 - headers["Range"] = f"bytes={skip_bytes}-" + skip_chunks = int(chunk_count / streamdetails.duration) * seek_position + else: + skip_chunks = 0 buffer = bytearray() streamdetails.data["start_ts"] = utc_timestamp() @@ -479,6 +488,8 @@ class DeezerProvider(MusicProvider): # pylint: disable=W0223 async for chunk in resp.content.iter_chunked(2048): buffer += chunk if len(buffer) >= 2048: + if chunk_index >= skip_chunks: + continue if chunk_index % 3 > 0: yield bytes(buffer[:2048]) else: -- 2.34.1