Fix announcement feature
authorMarcel van der Veldt <m.vanderveldt@outlook.com>
Wed, 5 Mar 2025 21:12:04 +0000 (22:12 +0100)
committerMarcel van der Veldt <m.vanderveldt@outlook.com>
Wed, 5 Mar 2025 22:40:16 +0000 (23:40 +0100)
- Fix duration calculation for announcements

- Fix typo in announcements handling

music_assistant/controllers/players.py
music_assistant/helpers/ffmpeg.py
music_assistant/helpers/tags.py
music_assistant/providers/hass_players/__init__.py
music_assistant/providers/sonos/provider.py

index 16e1c9a38d889afb8b9c6286adde2ed6b6f3750e..65e113de24e3253896bbfdc78836a35d17975754 100644 (file)
@@ -1485,7 +1485,7 @@ class PlayerController(CoreController):
         player.volume_control = config.get_value(CONF_VOLUME_CONTROL)
         player.mute_control = config.get_value(CONF_MUTE_CONTROL)
 
-    async def _play_announcement(  # noqa: PLR0915
+    async def _play_announcement(
         self,
         player: Player,
         announcement: PlayerMedia,
@@ -1577,9 +1577,10 @@ class PlayerController(CoreController):
         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 async_parse_tags(announcement.custom_data["url"])
-            announcement.duration = media_info.duration or 60
-        media_info.duration += 2
+            media_info = await async_parse_tags(
+                announcement.custom_data["url"], require_duration=True
+            )
+            announcement.duration = media_info.duration
         await self.wait_for_state(
             player,
             PlayerState.IDLE,
index 717000d0280b94c13cba7b3071316bfc2e8f8e40..72f26a14ae0f7e35567e968cd49fb6133f8dd876 100644 (file)
@@ -175,8 +175,6 @@ async def get_ffmpeg_stream(
     extra_args: list[str] | None = None,
     chunk_size: int | None = None,
     extra_input_args: list[str] | None = None,
-    collect_log_history: bool = False,
-    loglevel: str = "info",
 ) -> AsyncGenerator[bytes, None]:
     """
     Get the ffmpeg audio stream as async generator.
@@ -191,8 +189,6 @@ async def get_ffmpeg_stream(
         filter_params=filter_params,
         extra_args=extra_args,
         extra_input_args=extra_input_args,
-        collect_log_history=collect_log_history,
-        loglevel=loglevel,
     ) as ffmpeg_proc:
         # read final chunks from stdout
         iterator = ffmpeg_proc.iter_chunked(chunk_size) if chunk_size else ffmpeg_proc.iter_any()
@@ -291,7 +287,7 @@ def get_ffmpeg_args(  # noqa: PLR0915
             "-f",
             output_format.content_type.value,
         ]
-    elif input_format == output_format:
+    elif input_format == output_format and not extra_args:
         # passthrough
         if output_format.content_type in (
             ContentType.MP4,
index 06c8bcb5108680e1c182b90502213f8a86f4b63d..3d86d0804ac9bb1f5e1c425110648dc24360ae20 100644 (file)
@@ -426,12 +426,16 @@ class AudioTags:
         return self.tags.get(key, default)
 
 
-async def async_parse_tags(input_file: str, file_size: int | None = None) -> AudioTags:
+async def async_parse_tags(
+    input_file: str, file_size: int | None = None, require_duration: bool = False
+) -> AudioTags:
     """Parse tags from a media file (or URL). Async friendly."""
-    return await asyncio.to_thread(parse_tags, input_file, file_size)
+    return await asyncio.to_thread(parse_tags, input_file, file_size, require_duration)
 
 
-def parse_tags(input_file: str, file_size: int | None = None) -> AudioTags:
+def parse_tags(
+    input_file: str, file_size: int | None = None, require_duration: bool = False
+) -> AudioTags:
     """
     Parse tags from a media file (or URL). NOT Async friendly.
 
@@ -470,6 +474,9 @@ def parse_tags(input_file: str, file_size: int | None = None) -> AudioTags:
         if not tags.duration and tags.raw.get("format", {}).get("duration"):
             tags.duration = float(tags.raw["format"]["duration"])
 
+        if not tags.duration and require_duration:
+            tags.duration = get_file_duration(input_file)
+
         # we parse all (basic) tags for all file formats using ffmpeg
         # but we also try to extract some extra tags for local files using mutagen
         if not input_file.startswith("http") and os.path.isfile(input_file):
@@ -489,6 +496,37 @@ def parse_tags(input_file: str, file_size: int | None = None) -> AudioTags:
         raise InvalidDataError(msg) from err
 
 
+def get_file_duration(input_file: str) -> float:
+    """
+    Parse file/stream duration from an audio file using ffmpeg.
+
+    NOT Async friendly.
+    """
+    args = (
+        "ffmpeg",
+        "-hide_banner",
+        "-loglevel",
+        "info",
+        "-i",
+        input_file,
+        "-f",
+        "null",
+        "-",
+    )
+    try:
+        res = subprocess.check_output(args, stderr=subprocess.STDOUT).decode()  # noqa: S603
+        # extract duration from ffmpeg output
+        duration_str = res.split("time=")[-1].split(" ")[0].strip()
+        duration_parts = duration_str.split(":")
+        duration = 0
+        for part in duration_parts:
+            duration = duration * 60 + float(part)
+        return duration
+    except Exception as err:
+        error_msg = f"Unable to retrieve duration for {input_file}"
+        raise InvalidDataError(error_msg) from err
+
+
 def parse_tags_mutagen(input_file: str) -> dict[str, Any]:
     """
     Parse tags from an audio file using Mutagen.
index 195436b58d73daf34735bf445cdaf03c70052547..42bccb2d1a4d08bcf960253c743a988690d87706 100644 (file)
@@ -357,7 +357,7 @@ class HomeAssistantPlayers(PlayerProvider):
         )
         # Wait until the announcement is finished playing
         # This is helpful for people who want to play announcements in a sequence
-        media_info = await async_parse_tags(announcement.uri)
+        media_info = await async_parse_tags(announcement.uri, require_duration=True)
         duration = media_info.duration or 5
         await asyncio.sleep(duration)
         self.logger.debug(
index 6c66b766308b07d35f66089c218d31ac8efe89ad..61ce4ba5cf1aa3803e51de9c660d7b1ff50907d9 100644 (file)
@@ -383,7 +383,7 @@ class SonosPlayerProvider(PlayerProvider):
         # Wait until the announcement is finished playing
         # This is helpful for people who want to play announcements in a sequence
         # yeah we can also setup a subscription on the sonos player for this, but this is easier
-        media_info = await async_parse_tags(announcement.uri)
+        media_info = await async_parse_tags(announcement.uri, require_duration=True)
         duration = media_info.duration or 10
         await asyncio.sleep(duration)