From 636b3831b53819aa40236ef3daad35a162fd0f96 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Wed, 5 Mar 2025 22:12:04 +0100 Subject: [PATCH] Fix announcement feature - Fix duration calculation for announcements - Fix typo in announcements handling --- music_assistant/controllers/players.py | 9 ++-- music_assistant/helpers/ffmpeg.py | 6 +-- music_assistant/helpers/tags.py | 44 +++++++++++++++++-- .../providers/hass_players/__init__.py | 2 +- music_assistant/providers/sonos/provider.py | 2 +- 5 files changed, 49 insertions(+), 14 deletions(-) diff --git a/music_assistant/controllers/players.py b/music_assistant/controllers/players.py index 16e1c9a3..65e113de 100644 --- a/music_assistant/controllers/players.py +++ b/music_assistant/controllers/players.py @@ -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, diff --git a/music_assistant/helpers/ffmpeg.py b/music_assistant/helpers/ffmpeg.py index 717000d0..72f26a14 100644 --- a/music_assistant/helpers/ffmpeg.py +++ b/music_assistant/helpers/ffmpeg.py @@ -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, diff --git a/music_assistant/helpers/tags.py b/music_assistant/helpers/tags.py index 06c8bcb5..3d86d080 100644 --- a/music_assistant/helpers/tags.py +++ b/music_assistant/helpers/tags.py @@ -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. diff --git a/music_assistant/providers/hass_players/__init__.py b/music_assistant/providers/hass_players/__init__.py index 195436b5..42bccb2d 100644 --- a/music_assistant/providers/hass_players/__init__.py +++ b/music_assistant/providers/hass_players/__init__.py @@ -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( diff --git a/music_assistant/providers/sonos/provider.py b/music_assistant/providers/sonos/provider.py index 6c66b766..61ce4ba5 100644 --- a/music_assistant/providers/sonos/provider.py +++ b/music_assistant/providers/sonos/provider.py @@ -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) -- 2.34.1