Various (very) small bugfixes and enhancements (#1992)
authorMarcel van der Veldt <m.vanderveldt@outlook.com>
Fri, 28 Feb 2025 17:43:32 +0000 (18:43 +0100)
committerGitHub <noreply@github.com>
Fri, 28 Feb 2025 17:43:32 +0000 (18:43 +0100)
* 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
music_assistant/controllers/config.py
music_assistant/controllers/players.py
music_assistant/controllers/streams.py
music_assistant/providers/filesystem_local/__init__.py
music_assistant/providers/hass_players/__init__.py

index fce17dac508a7799130c675716185df530976493..ebc24a6026714e5a7fa112e842acd5657467611a 100644 (file)
@@ -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)
index 92514eca37049608b9fb9eb95970f3429ab3baef..63e37d0050517f6d470b77f7d5979760f4d625a3 100644 (file)
@@ -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)
 
index 61307ea0a2749184962bf639e9a196ffa94419c8..16e1c9a38d889afb8b9c6286adde2ed6b6f3750e 100644 (file)
@@ -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(
index d45dc904cafbcaadbaf98fd6d6319488259a6131..acc0384fc905ced47f109f5b2e8a6024538f202b 100644 (file)
@@ -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,
index 1b8b4533c2fc8f215e6019ab577839f4ad4c9c60..be2dc8c7e10dc3a9816e510b68326bf93c4f33ef 100644 (file)
@@ -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
index 08dca19ef53f46c0c4b4b9bc5520c4a723806459..195436b58d73daf34735bf445cdaf03c70052547 100644 (file)
@@ -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,