From: Marcel van der Veldt Date: Tue, 8 Apr 2025 23:26:03 +0000 (+0200) Subject: Various minor bugfixes and enhancements (#2120) X-Git-Url: https://git.kitaultman.com/?a=commitdiff_plain;h=178ad5dfa75db3c0ccda5c20213035162d362403;p=music-assistant-server.git Various minor bugfixes and enhancements (#2120) * Fix invalid loudness measurements in volume normalization * Fix sort order of podcast feed * Prefer cache for podcast episodes * Fix fade-in effect only when resuming from idle * Chore: fix comments --- diff --git a/music_assistant/controllers/music.py b/music_assistant/controllers/music.py index 223c924e..7104643d 100644 --- a/music_assistant/controllers/music.py +++ b/music_assistant/controllers/music.py @@ -811,13 +811,16 @@ class MusicController(CoreController): """Store (EBU-R128) Integrated Loudness Measurement for a mediaitem in db.""" if not (provider := self.mass.get_provider(provider_instance_id_or_domain)): return + if loudness in (None, inf, -inf): + # skip invalid values + return values = { "item_id": item_id, "media_type": media_type.value, "provider": provider.lookup_key, "loudness": loudness, } - if album_loudness is not None: + if album_loudness not in (None, inf, -inf): values["loudness_album"] = album_loudness await self.database.insert_or_replace(DB_TABLE_LOUDNESS_MEASUREMENTS, values) @@ -826,7 +829,7 @@ class MusicController(CoreController): item_id: str, provider_instance_id_or_domain: str, media_type: MediaType = MediaType.TRACK, - ) -> tuple[float, float] | None: + ) -> tuple[float, float | None] | None: """Get (EBU-R128) Integrated Loudness Measurement for a mediaitem in db.""" if not (provider := self.mass.get_provider(provider_instance_id_or_domain)): return None @@ -839,7 +842,11 @@ class MusicController(CoreController): }, ) if db_row and db_row["loudness"] != inf and db_row["loudness"] != -inf: - return (db_row["loudness"], db_row["loudness_album"]) + loudness = db_row["loudness"] + loudness_album = db_row["loudness_album"] + if loudness_album in (inf, -inf): + loudness_album = None + return (loudness, loudness_album) return None diff --git a/music_assistant/controllers/player_queues.py b/music_assistant/controllers/player_queues.py index 897812cb..b6ffc4fa 100644 --- a/music_assistant/controllers/player_queues.py +++ b/music_assistant/controllers/player_queues.py @@ -26,6 +26,7 @@ from music_assistant_models.enums import ( ContentType, EventType, MediaType, + PlayerFeature, PlayerState, ProviderFeature, QueueOption, @@ -638,7 +639,36 @@ class PlayerQueuesController(CoreController): if queue.state == PlayerState.PLAYING: queue.resume_pos = queue.corrected_elapsed_time # forward the actual command to the player controller - await self.mass.players.cmd_pause(queue_id) + queue_player = self.mass.players.get(queue_id) + if not (player_provider := self.mass.players.get_player_provider(queue.queue_id)): + return # guard + + if PlayerFeature.PAUSE not in queue_player.supported_features: + # if player does not support pause, we need to send stop + await player_provider.cmd_stop(queue_player.player_id) + return + await player_provider.cmd_pause(queue_player.player_id) + + async def _watch_pause() -> None: + count = 0 + # wait for pause + while count < 5 and queue_player.state == PlayerState.PLAYING: + count += 1 + await asyncio.sleep(1) + # wait for unpause + if queue_player.state != PlayerState.PAUSED: + return + count = 0 + while count < 30 and queue_player.state == PlayerState.PAUSED: + count += 1 + await asyncio.sleep(1) + # if player is still paused when the limit is reached, send stop + if queue_player.state == PlayerState.PAUSED: + await player_provider.cmd_stop(queue_player.player_id) + + # we auto stop a player from paused when its paused for 30 seconds + if not queue_player.announcement_in_progress: + self.mass.create_task(_watch_pause()) @api_command("player_queues/play_pause") async def play_pause(self, queue_id: str) -> None: @@ -733,6 +763,7 @@ class PlayerQueuesController(CoreController): # resume requested while already playing, # use current position as resume position resume_pos = queue.corrected_elapsed_time + fade_in = False else: resume_pos = queue.resume_pos or queue.elapsed_time @@ -745,9 +776,13 @@ class PlayerQueuesController(CoreController): resume_pos = 0 if resume_item is not None: - resume_pos = resume_pos if resume_pos > 10 else 0 queue_player = self.mass.players.get(queue_id) - if fade_in is None and queue_player.state == PlayerState.IDLE: + if ( + fade_in is None + and queue_player.state == PlayerState.IDLE + and (time.time() - queue.elapsed_time_last_updated) > 60 + ): + # enable fade in effect if the player is idle for a while fade_in = resume_pos > 0 if resume_item.media_type == MediaType.RADIO: # we're not able to skip in online radio so this is pointless diff --git a/music_assistant/controllers/players.py b/music_assistant/controllers/players.py index 2d6885b2..48efe183 100644 --- a/music_assistant/controllers/players.py +++ b/music_assistant/controllers/players.py @@ -243,9 +243,13 @@ class PlayerController(CoreController): - player_id: player_id of the player to handle the command. """ 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.pause(active_queue.queue_id) + return if PlayerFeature.PAUSE not in player.supported_features: # if player does not support pause, we need to send stop - self.logger.info( + self.logger.debug( "Player %s does not support pause, using STOP instead", player.display_name, ) @@ -254,28 +258,6 @@ class PlayerController(CoreController): player_provider = self.get_player_provider(player.player_id) await player_provider.cmd_pause(player.player_id) - async def _watch_pause(_player_id: str) -> None: - player = self.get(_player_id, True) - count = 0 - # wait for pause - while count < 5 and player.state == PlayerState.PLAYING: - count += 1 - await asyncio.sleep(1) - # wait for unpause - if player.state != PlayerState.PAUSED: - return - count = 0 - while count < 30 and player.state == PlayerState.PAUSED: - count += 1 - await asyncio.sleep(1) - # if player is still paused when the limit is reached, send stop - if player.state == PlayerState.PAUSED: - await self.cmd_stop(_player_id) - - # we auto stop a player from paused when its paused for 30 seconds - 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. @@ -1355,7 +1337,7 @@ class PlayerController(CoreController): elif not player_disabled and resume_queue and resume_queue.state == PlayerState.PLAYING: # always stop first to ensure the player uses the new config await self.mass.player_queues.stop(resume_queue.queue_id) - self.mass.call_later(1, self.mass.player_queues.resume, resume_queue.queue_id) + self.mass.call_later(1, self.mass.player_queues.resume, resume_queue.queue_id, False) # check for group memberships that need to be updated if player_disabled and player.active_group and player_provider: # try to remove from the group @@ -1375,7 +1357,7 @@ class PlayerController(CoreController): if player.state == PlayerState.PLAYING: self.logger.info("Restarting playback of Player %s after DSP change", player_id) # this will restart ffmpeg with the new settings - self.mass.call_later(0, self.mass.player_queues.resume, player.active_source) + self.mass.call_later(0, self.mass.player_queues.resume, player.active_source, False) def _get_player_with_redirect(self, player_id: str) -> Player: """Get player with check if playback related command should be redirected.""" diff --git a/music_assistant/controllers/streams.py b/music_assistant/controllers/streams.py index 6733850e..dd944d15 100644 --- a/music_assistant/controllers/streams.py +++ b/music_assistant/controllers/streams.py @@ -14,7 +14,6 @@ import shutil import urllib.parse from collections.abc import AsyncGenerator from dataclasses import dataclass, field -from math import inf from typing import TYPE_CHECKING from aiofiles.os import wrap @@ -1022,10 +1021,7 @@ class StreamsController(CoreController): elif streamdetails.volume_normalization_mode == VolumeNormalizationMode.MEASUREMENT_ONLY: # volume normalization with known loudness measurement # apply volume/gain correction - if streamdetails.prefer_album_loudness and streamdetails.loudness_album not in ( - inf, - -inf, - ): + if streamdetails.prefer_album_loudness and streamdetails.loudness_album is not None: gain_correct = streamdetails.target_loudness - streamdetails.loudness_album else: gain_correct = streamdetails.target_loudness - streamdetails.loudness diff --git a/music_assistant/helpers/audio.py b/music_assistant/helpers/audio.py index 495e4f96..85620486 100644 --- a/music_assistant/helpers/audio.py +++ b/music_assistant/helpers/audio.py @@ -676,15 +676,20 @@ async def _is_cache_allowed(mass: MusicAssistant, streamdetails: StreamDetails) elif streamdetails.stream_type == StreamType.CUSTOM: # prefer cache for custom streams (to speedup seeking) max_filesize = 250 * 1024 * 1024 # 250MB - return get_chunksize(streamdetails.audio_format, streamdetails.duration) < max_filesize elif streamdetails.stream_type == StreamType.HLS: # prefer cache for HLS streams (to speedup seeking) max_filesize = 250 * 1024 * 1024 # 250MB + elif streamdetails.media_type in ( + MediaType.AUDIOBOOK, + MediaType.PODCAST_EPISODE, + ): + # prefer cache for audiobooks and episodes (to speedup seeking) + max_filesize = 2 * 1024 * 1024 * 1024 # 2GB elif streamdetails.provider in SLOW_PROVIDERS: # prefer cache for slow providers - max_filesize = 500 * 1024 * 1024 # 500MB + max_filesize = 2 * 1024 * 1024 * 1024 # 2GB else: - max_filesize = 25 * 1024 * 1024 + max_filesize = 50 * 1024 * 1024 return estimated_filesize < max_filesize diff --git a/music_assistant/providers/airplay/raop.py b/music_assistant/providers/airplay/raop.py index a8395859..93151f2b 100644 --- a/music_assistant/providers/airplay/raop.py +++ b/music_assistant/providers/airplay/raop.py @@ -449,7 +449,7 @@ class RaopStream: lost_packets += 1 if lost_packets == 100: logger.error("High packet loss detected, restarting playback...") - self.mass.create_task(self.mass.player_queues.resume(queue.queue_id)) + self.mass.create_task(self.mass.player_queues.resume(queue.queue_id, False)) else: logger.warning("Packet loss detected!") if "end of stream reached" in line: diff --git a/music_assistant/providers/player_group/__init__.py b/music_assistant/providers/player_group/__init__.py index 637a20f5..b354dc75 100644 --- a/music_assistant/providers/player_group/__init__.py +++ b/music_assistant/providers/player_group/__init__.py @@ -664,7 +664,7 @@ class PlayerGroupProvider(PlayerProvider): player_provider = self.mass.players.get_player_provider(child_player.player_id) if group_type == GROUP_TYPE_UNIVERSAL: if was_playing: - # stop playing the group player + # stop playing the child player that was unjoined from the UGP await player_provider.cmd_stop(child_player.player_id) self._update_attributes(group_player) return @@ -672,7 +672,7 @@ class PlayerGroupProvider(PlayerProvider): if child_player.group_childs: # this is the sync leader, unsync all its childs! # NOTE that some players/providers might support this in a less intrusive way - # but for now we just ungroup all childs to keep thinngs universal + # but for now we just ungroup all childs to keep things universal self.logger.info("Detected ungroup of sync leader, ungrouping all childs") async with TaskManager(self.mass) as tg: for sync_child_id in child_player.group_childs: @@ -915,7 +915,7 @@ class PlayerGroupProvider(PlayerProvider): changed = True if changed and player.state == PlayerState.PLAYING: # Restart playback to ensure all members play the same content - await self.mass.player_queues.resume(player.player_id) + await self.mass.player_queues.resume(player.player_id, False) async def _serve_ugp_stream(self, request: web.Request) -> web.Response: """Serve the UGP (multi-client) flow stream audio to a player.""" diff --git a/music_assistant/providers/podcastfeed/__init__.py b/music_assistant/providers/podcastfeed/__init__.py index 7d4a1ee6..c63487fe 100644 --- a/music_assistant/providers/podcastfeed/__init__.py +++ b/music_assistant/providers/podcastfeed/__init__.py @@ -23,18 +23,11 @@ from music_assistant_models.enums import ( StreamType, ) from music_assistant_models.errors import InvalidProviderURI, MediaNotFoundError -from music_assistant_models.media_items import ( - AudioFormat, - Podcast, - PodcastEpisode, -) +from music_assistant_models.media_items import AudioFormat, Podcast, PodcastEpisode from music_assistant_models.streamdetails import StreamDetails from music_assistant.helpers.compare import create_safe_string -from music_assistant.helpers.podcast_parsers import ( - parse_podcast, - parse_podcast_episode, -) +from music_assistant.helpers.podcast_parsers import parse_podcast, parse_podcast_episode from music_assistant.models.music_provider import MusicProvider if TYPE_CHECKING: @@ -158,7 +151,11 @@ class PodcastMusicprovider(MusicProvider): """List all episodes for the podcast.""" if prov_podcast_id != self.podcast_id: raise Exception(f"Podcast id not in provider: {prov_podcast_id}") - for idx, episode in enumerate(self.parsed_podcast["episodes"]): + # sort episodes by published date + episodes: list[dict[str, Any]] = self.parsed_podcast["episodes"] + if episodes and episodes[0].get("published", 0) != 0: + episodes.sort(key=lambda x: x.get("published", 0)) + for idx, episode in enumerate(episodes): yield await self._parse_episode(episode, idx) async def get_stream_details(self, item_id: str, media_type: MediaType) -> StreamDetails: