Fixes for announcements
authorMarcel van der Veldt <m.vanderveldt@outlook.com>
Thu, 13 Jun 2024 19:40:45 +0000 (21:40 +0200)
committerMarcel van der Veldt <m.vanderveldt@outlook.com>
Thu, 13 Jun 2024 19:40:45 +0000 (21:40 +0200)
music_assistant/server/controllers/player_queues.py
music_assistant/server/controllers/players.py
music_assistant/server/helpers/tags.py
music_assistant/server/providers/deezer/__init__.py

index 5e3629c57245200a28e5ea01afb6eda67af3a0e5..76e953f0e06ab7a5a724af7e109614a0242ffea2 100644 (file)
@@ -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,
index 5d739d092338d71383e37002a619cab223657125..ff02d1c5bdbf2648a77d5d0cfd520e319095763c 100644 (file)
@@ -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,
+            )
index dc38e4c02f26d6de1b54f2ba0069ec2198df7d46..47adf238995d71078039800579c8480911868ac1 100644 (file)
@@ -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}"
index fb224dad8956d72a472a5eeeed451c1fb1c5e67d..3ae16b4457539db740cd909bd2ba7acd11511b42 100644 (file)
@@ -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: