A collection of small enhancements (#1130)
authorMarcel van der Veldt <m.vanderveldt@outlook.com>
Sun, 10 Mar 2024 22:09:12 +0000 (23:09 +0100)
committerGitHub <noreply@github.com>
Sun, 10 Mar 2024 22:09:12 +0000 (23:09 +0100)
.github/release-drafter.yml
.github/workflows/pr-labels.yaml
music_assistant/common/models/config_entries.py
music_assistant/constants.py
music_assistant/server/helpers/util.py
music_assistant/server/providers/airplay/__init__.py
music_assistant/server/providers/dlna/__init__.py
music_assistant/server/providers/snapcast/__init__.py

index 2c0fca59565060cbdec72cc464e4d0357f7ae241..797c1170f12f93b9329699f70af489b6f9c5d3b9 100644 (file)
@@ -22,6 +22,5 @@ version-resolver:
     labels:
       - 'new-feature'
       - 'new-provider'
-      - 'enhancement'
       - 'refactor'
   default: patch
index 995d1148bd5d90ea3dbdf609d759e2e2863e3d23..102d4c756f5fbb7e8bd710541b03cadcb233eb5f 100644 (file)
@@ -20,4 +20,4 @@ jobs:
         uses: ludeeus/action-require-labels@1.1.0
         with:
           labels: >-
-            breaking-change, bugfix, refactor, new-feature, maintenance, ci, dependencies, new-provider
+            breaking-change, bugfix, refactor, new-feature, maintenance, enhancement, ci, dependencies, new-provider
index 52f888883cfc036a9be8582399e31ca3a8238fb1..6250ef8e8fd87701708fb9a2bdacd74693545b5e 100644 (file)
@@ -15,6 +15,7 @@ from music_assistant.constants import (
     CONF_AUTO_PLAY,
     CONF_CROSSFADE,
     CONF_CROSSFADE_DURATION,
+    CONF_ENFORCE_MP3,
     CONF_EQ_BASS,
     CONF_EQ_MID,
     CONF_EQ_TREBLE,
@@ -408,3 +409,15 @@ CONF_ENTRY_HIDE_PLAYER = ConfigEntry(
     default_value=False,
     advanced=True,
 )
+
+CONF_ENTRY_ENFORCE_MP3 = ConfigEntry(
+    key=CONF_ENFORCE_MP3,
+    type=ConfigEntryType.BOOLEAN,
+    label="Enforce (lossy) mp3 stream",
+    default_value=False,
+    description="By default, Music Assistant sends lossless, high quality audio "
+    "to all players. Some players can not deal with that and require the stream to be packed "
+    "into a lossy mp3 codec. \n\n "
+    "Only enable when needed. Saves some bandwidth at the cost of audio quality.",
+    advanced=True,
+)
index ab5b81083e415d161882344fa64c7899f4bc25e0..1bde9e68c4ac459ac869f42b0d53c6af48bab30a 100644 (file)
@@ -53,6 +53,7 @@ CONF_GROUP_PLAYERS: Final[str] = "group_players"
 CONF_CROSSFADE: Final[str] = "crossfade"
 CONF_GROUP_MEMBERS: Final[str] = "group_members"
 CONF_HIDE_PLAYER: Final[str] = "hide_player"
+CONF_ENFORCE_MP3: Final[str] = "enforce_mp3"
 
 # config default values
 DEFAULT_HOST: Final[str] = "0.0.0.0"
index 33b9eacb4b9f442c8af709b5cc5b07245f65a6be..2c8c6b9c9453c787a1ba17112640fcb8abc076aa 100644 (file)
@@ -80,7 +80,7 @@ async def is_hass_supervisor() -> bool:
 
     def _check():
         try:
-            urllib.request.urlopen("http://supervisor/core")
+            urllib.request.urlopen("http://supervisor/core", timeout=1)
         except urllib.error.URLError as err:
             # this should return a 401 unauthorized if it exists
             return getattr(err, "code", 999) == 401
index 89e19c694eee2e981e7a3c4ab26978464b92b490..d9e8723aec40eb66e11e402c30a7ba0acae98ab5 100644 (file)
@@ -160,9 +160,15 @@ def get_model_from_am(am_property: str | None) -> tuple[str, str]:
 
 def get_primary_ip_address(discovery_info: AsyncServiceInfo) -> str | None:
     """Get primary IP address from zeroconf discovery info."""
-    return next(
-        (x for x in discovery_info.parsed_addresses(IPVersion.V4Only) if x != "127.0.0.1"), None
-    )
+    for address in discovery_info.parsed_addresses(IPVersion.V4Only):
+        if address.startswith("127"):
+            # filter out loopback address
+            continue
+        if address.startswith("169.254"):
+            # filter out APIPA address
+            continue
+        return address
+    return None
 
 
 class AirplayStreamJob:
@@ -357,12 +363,12 @@ class AirplayStreamJob:
                 # EOF chunk
                 break
             self._cliraop_proc.stdin.write(chunk)
-            with suppress(BrokenPipeError):
+            with suppress(BrokenPipeError, ConnectionResetError):
                 await self._cliraop_proc.stdin.drain()
         # send EOF
         if self._cliraop_proc.returncode is None and not self._cliraop_proc.stdin.is_closing():
             self._cliraop_proc.stdin.write_eof()
-            with suppress(BrokenPipeError):
+            with suppress(BrokenPipeError, ConnectionResetError):
                 await self._cliraop_proc.stdin.drain()
         logger.debug("Audio reader finished")
 
@@ -453,10 +459,10 @@ class AirplayProvider(PlayerProvider):
             if mass_player := self.mass.players.get(player_id):
                 cur_address = get_primary_ip_address(info)
                 if cur_address and cur_address != airplay_player.address:
-                    airplay_player.address = cur_address
                     airplay_player.logger.info(
                         "Address updated from %s to %s", airplay_player.address, cur_address
                     )
+                    airplay_player.address = cur_address
                     mass_player.device_info = DeviceInfo(
                         model=mass_player.device_info.model,
                         manufacturer=mass_player.device_info.manufacturer,
index d0b6ade9542eb35db7c46005c5fa975098e6fcf6..6e5d0244315fc7ca4fb10554928c5d1c44e61283 100644 (file)
@@ -25,6 +25,7 @@ from async_upnp_client.search import async_search
 
 from music_assistant.common.models.config_entries import (
     CONF_ENTRY_CROSSFADE_DURATION,
+    CONF_ENTRY_ENFORCE_MP3,
     CONF_ENTRY_FLOW_MODE,
     ConfigEntry,
     ConfigValueType,
@@ -38,7 +39,7 @@ from music_assistant.common.models.enums import (
 )
 from music_assistant.common.models.errors import PlayerUnavailableError
 from music_assistant.common.models.player import DeviceInfo, Player
-from music_assistant.constants import CONF_CROSSFADE, CONF_FLOW_MODE, CONF_PLAYERS
+from music_assistant.constants import CONF_CROSSFADE, CONF_ENFORCE_MP3, CONF_FLOW_MODE, CONF_PLAYERS
 from music_assistant.server.helpers.didl_lite import create_didl_metadata
 from music_assistant.server.models.player_provider import PlayerProvider
 
@@ -63,7 +64,7 @@ BASE_PLAYER_FEATURES = (
 )
 
 CONF_ENQUEUE_NEXT = "enqueue_next"
-CONF_ENFORCE_MP3 = "enforce_mp3"
+
 
 PLAYER_CONFIG_ENTRIES = (
     ConfigEntry(
@@ -88,17 +89,7 @@ PLAYER_CONFIG_ENTRIES = (
     ),
     CONF_ENTRY_FLOW_MODE,
     CONF_ENTRY_CROSSFADE_DURATION,
-    ConfigEntry(
-        key=CONF_ENFORCE_MP3,
-        type=ConfigEntryType.BOOLEAN,
-        label="Enforce (lossy) mp3 stream",
-        default_value=False,
-        description="By default, Music Assistant sends lossless, high quality audio "
-        "to all players. Some players can not deal with that and require the stream to be packed "
-        "into a lossy mp3 codec. \n\n "
-        "Only enable when needed. Saves some bandwidth at the cost of audio quality.",
-        advanced=True,
-    ),
+    CONF_ENTRY_ENFORCE_MP3,
 )
 
 CONF_NETWORK_SCAN = "network_scan"
index 13b0900e82f1b8e61809aedef17a9863febcd2a3..3e81f4818350710be526dc4ad1bd916c0e592855 100644 (file)
@@ -295,13 +295,18 @@ class SnapCastProvider(PlayerProvider):
                 ):
                     writer.write(pcm_chunk)
                     await writer.drain()
-
-            finally:
-                await self._snapserver.stream_remove_stream(stream.identifier)
+                # end of the stream reached
                 if writer.can_write_eof():
-                    writer.close()
+                    writer.write_eof()
+                    await writer.drain()
+                # we need to wait a bit before removing the stream to ensure
+                # that all snapclients have consumed the audio
+                # https://github.com/music-assistant/hass-music-assistant/issues/1962
+                await asyncio.sleep(30)
+            finally:
                 if not writer.is_closing():
                     writer.close()
+                await self._snapserver.stream_remove_stream(stream.identifier)
                 self.logger.debug("Closed connection to %s:%s", host, port)
 
         # start streaming the queue (pcm) audio in a background task
@@ -340,12 +345,18 @@ class SnapCastProvider(PlayerProvider):
                 async for pcm_chunk in stream_job.subscribe(player_id):
                     writer.write(pcm_chunk)
                     await writer.drain()
-            finally:
-                await self._snapserver.stream_remove_stream(stream.identifier)
+                # end of the stream reached
                 if writer.can_write_eof():
-                    writer.close()
+                    writer.write_eof()
+                    await writer.drain()
+                # we need to wait a bit before removing the stream to ensure
+                # that all snapclients have consumed the audio
+                # https://github.com/music-assistant/hass-music-assistant/issues/1962
+                await asyncio.sleep(30)
+            finally:
                 if not writer.is_closing():
                     writer.close()
+                await self._snapserver.stream_remove_stream(stream.identifier)
                 self.logger.debug("Closed connection to %s:%s", host, port)
 
         # start streaming the queue (pcm) audio in a background task