From 7383db5f2c586851d416e47e4a554838c5f6bf6d Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Fri, 28 Feb 2025 18:43:32 +0100 Subject: [PATCH] Various (very) small bugfixes and enhancements (#1992) * Fix: update player attributes before sending a player config updated event * Support WAV codec for ESPHome players Fixes https://github.com/music-assistant/support/issues/3634 * Fix for ESPHome players without a media pipeline * Fix pluginsource use correct codec * Fix playlist create feature missing on filesystem --- music_assistant/constants.py | 15 ++++++++++ music_assistant/controllers/config.py | 4 ++- music_assistant/controllers/players.py | 4 ++- music_assistant/controllers/streams.py | 28 ++++++++++--------- .../providers/filesystem_local/__init__.py | 1 + .../providers/hass_players/__init__.py | 25 +++++++++-------- 6 files changed, 50 insertions(+), 27 deletions(-) diff --git a/music_assistant/constants.py b/music_assistant/constants.py index fce17dac..ebc24a60 100644 --- a/music_assistant/constants.py +++ b/music_assistant/constants.py @@ -329,6 +329,16 @@ CONF_ENTRY_OUTPUT_CODEC_ENFORCE_FLAC = ConfigEntry.from_dict( ) +def create_output_codec_config_entry( + hidden: bool = False, default_value: str = "flac" +) -> ConfigEntry: + """Create output codec config entry based on player specific helpers.""" + conf_entry = ConfigEntry.from_dict(CONF_ENTRY_OUTPUT_CODEC.to_dict()) + conf_entry.hidden = hidden + conf_entry.default_value = default_value + return conf_entry + + CONF_ENTRY_SYNC_ADJUST = ConfigEntry( key=CONF_SYNC_ADJUST, type=ConfigEntryType.INTEGER, @@ -532,6 +542,11 @@ def create_sample_rates_config_entry( options: list[ConfigValueOption] = [] default_value: list[str] = [] + if not supported_sample_rates and max_sample_rate is None: + supported_sample_rates = [44100] + if not supported_bit_depths and max_bit_depth is None: + supported_bit_depths = [16] + for option in CONF_ENTRY_SAMPLE_RATES.options: option_value = cast(str, option.value) sample_rate_str, bit_depth_str = option_value.split(MULTI_VALUE_SPLITTER, 1) diff --git a/music_assistant/controllers/config.py b/music_assistant/controllers/config.py index 92514eca..63e37d00 100644 --- a/music_assistant/controllers/config.py +++ b/music_assistant/controllers/config.py @@ -405,13 +405,15 @@ class ConfigController: # actually store changes (if the above did not raise) conf_key = f"{CONF_PLAYERS}/{player_id}" self.set(conf_key, config.to_raw()) + # always update player attributes to calculate e.g. player controls etc. + self.mass.players.update(config.player_id, force_update=True) # send config updated event self.mass.signal_event( EventType.PLAYER_CONFIG_UPDATED, object_id=config.player_id, data=config, ) - self.mass.players.update(config.player_id, force_update=True) + # return full player config (just in case) return await self.get_player_config(player_id) diff --git a/music_assistant/controllers/players.py b/music_assistant/controllers/players.py index 61307ea0..16e1c9a3 100644 --- a/music_assistant/controllers/players.py +++ b/music_assistant/controllers/players.py @@ -1649,7 +1649,9 @@ class PlayerController(CoreController): """Handle playback/select of given plugin source on player.""" plugin_source = plugin_prov.get_source() player.active_source = plugin_source.id - stream_url = self.mass.streams.get_plugin_source_url(plugin_source.id, player.player_id) + stream_url = await self.mass.streams.get_plugin_source_url( + plugin_source.id, player.player_id + ) await self.play_media( player_id=player.player_id, media=PlayerMedia( diff --git a/music_assistant/controllers/streams.py b/music_assistant/controllers/streams.py index d45dc904..acc0384f 100644 --- a/music_assistant/controllers/streams.py +++ b/music_assistant/controllers/streams.py @@ -265,6 +265,21 @@ class StreamsController(CoreController): base_path = "flow" if flow_mode else "single" return f"{self._server.base_url}/{base_path}/{queue_item.queue_id}/{queue_item.queue_item_id}.{fmt}" # noqa: E501 + async def get_plugin_source_url( + self, + plugin_source: str, + player_id: str, + ) -> str: + """Get the url for the Plugin Source stream/proxy.""" + output_codec = ContentType.try_parse( + await self.mass.config.get_player_config_value(player_id, CONF_OUTPUT_CODEC) + ) + fmt = output_codec.value + # handle raw pcm without exact format specifiers + if output_codec.is_pcm() and ";" not in fmt: + fmt += f";codec=pcm;rate={44100};bitrate={16};channels={2}" + return f"{self._server.base_url}/pluginsource/{plugin_source}/{player_id}.{fmt}" + async def serve_queue_item_stream(self, request: web.Request) -> web.Response: """Stream single queueitem audio to a player.""" self._log_request(request) @@ -648,19 +663,6 @@ class StreamsController(CoreController): # like https hosts and it also offers the pre-announce 'bell' return f"{self.base_url}/announcement/{player_id}.{content_type.value}?pre_announce={use_pre_announce}" # noqa: E501 - def get_plugin_source_url( - self, - plugin_source: str, - player_id: str, - output_codec: ContentType = ContentType.FLAC, - ) -> str: - """Get the url for the Plugin Source stream/proxy.""" - fmt = output_codec.value - # handle raw pcm without exact format specifiers - if output_codec.is_pcm() and ";" not in fmt: - fmt += f";codec=pcm;rate={44100};bitrate={16};channels={2}" - return f"{self._server.base_url}/pluginsource/{plugin_source}/{player_id}.{fmt}" - async def get_queue_flow_stream( self, queue: PlayerQueue, diff --git a/music_assistant/providers/filesystem_local/__init__.py b/music_assistant/providers/filesystem_local/__init__.py index 1b8b4533..be2dc8c7 100644 --- a/music_assistant/providers/filesystem_local/__init__.py +++ b/music_assistant/providers/filesystem_local/__init__.py @@ -184,6 +184,7 @@ class LocalFileSystemProvider(MusicProvider): } if self.write_access: music_features.add(ProviderFeature.PLAYLIST_TRACKS_EDIT) + music_features.add(ProviderFeature.PLAYLIST_CREATE) return music_features @property diff --git a/music_assistant/providers/hass_players/__init__.py b/music_assistant/providers/hass_players/__init__.py index 08dca19e..195436b5 100644 --- a/music_assistant/providers/hass_players/__init__.py +++ b/music_assistant/providers/hass_players/__init__.py @@ -29,9 +29,8 @@ from music_assistant.constants import ( CONF_ENTRY_HTTP_PROFILE, CONF_ENTRY_HTTP_PROFILE_FORCED_2, CONF_ENTRY_OUTPUT_CODEC_DEFAULT_MP3, - CONF_ENTRY_OUTPUT_CODEC_ENFORCE_FLAC, - CONF_ENTRY_OUTPUT_CODEC_ENFORCE_MP3, HIDDEN_ANNOUNCE_VOLUME_CONFIG_ENTRIES, + create_output_codec_config_entry, create_sample_rates_config_entry, ) from music_assistant.helpers.datetime import from_iso_string @@ -110,7 +109,7 @@ async def _get_hass_media_players( class ESPHomeSupportedAudioFormat(TypedDict): """ESPHome Supported Audio Format.""" - format: str # flac or mp3 + format: str # flac, wav or mp3 sample_rate: int # e.g. 48000 num_channels: int # 1 for announcements, 2 for media purpose: int # 0 for media, 1 for announcements @@ -203,25 +202,27 @@ class HomeAssistantPlayers(PlayerProvider): # optimized config for new ESPHome mediaplayer supported_sample_rates: list[int] = [] supported_bit_depths: list[int] = [] - supports_flac: bool = False - for supported_format in player.extra_data["esphome_supported_audio_formats"]: - if supported_format["purpose"] != 0: - continue - if supported_format["format"] == "flac": - supports_flac = True + codec: str | None = None + supported_formats: list[ESPHomeSupportedAudioFormat] = player.extra_data[ + "esphome_supported_audio_formats" + ] + # sort on purpose field, so we prefer the media pipeline + # but allows fallback to announcements pipeline if no media pipeline is available + supported_formats.sort(key=lambda x: x["purpose"]) + for supported_format in supported_formats: + codec = supported_format["format"] if supported_format["sample_rate"] not in supported_sample_rates: supported_sample_rates.append(supported_format["sample_rate"]) bit_depth = supported_format["sample_bytes"] * 8 if bit_depth not in supported_bit_depths: supported_bit_depths.append(bit_depth) - if not supports_flac: - base_entries = (*base_entries, CONF_ENTRY_OUTPUT_CODEC_ENFORCE_MP3) + return ( *base_entries, # New ESPHome mediaplayer (used in Voice PE) uses FLAC 48khz/16 bits CONF_ENTRY_FLOW_MODE_ENFORCED, CONF_ENTRY_HTTP_PROFILE_FORCED_2, - CONF_ENTRY_OUTPUT_CODEC_ENFORCE_FLAC, + create_output_codec_config_entry(True, codec), CONF_ENTRY_ENABLE_ICY_METADATA_HIDDEN, create_sample_rates_config_entry( supported_sample_rates=supported_sample_rates, -- 2.34.1