Fix AirPlay discovery info
authorMarcel van der Veldt <m.vanderveldt@outlook.com>
Tue, 4 Nov 2025 18:37:54 +0000 (19:37 +0100)
committerMarcel van der Veldt <m.vanderveldt@outlook.com>
Tue, 4 Nov 2025 18:37:54 +0000 (19:37 +0100)
music_assistant/providers/airplay/player.py
music_assistant/providers/airplay/protocols/airplay2.py
music_assistant/providers/airplay/protocols/raop.py
music_assistant/providers/airplay/provider.py

index fbbb009ea89f419f0237265836f09b7044180010..78486ba220fb7d48dcd8d3eed89ca7871b1c73c4 100644 (file)
@@ -67,7 +67,8 @@ class AirPlayPlayer(Player):
         self,
         provider: AirPlayProvider,
         player_id: str,
-        discovery_info: AsyncServiceInfo | None,
+        raop_discovery_info: AsyncServiceInfo | None,
+        airplay_discovery_info: AsyncServiceInfo | None,
         address: str,
         display_name: str,
         manufacturer: str,
@@ -76,7 +77,8 @@ class AirPlayPlayer(Player):
     ) -> None:
         """Initialize AirPlayPlayer."""
         super().__init__(provider, player_id)
-        self.discovery_info = discovery_info
+        self.raop_discovery_info = raop_discovery_info
+        self.airplay_discovery_info = airplay_discovery_info
         self.address = address
         self.stream: RaopStream | AirPlay2Stream | None = None
         self.last_command_sent = 0.0
index 594756327b5879c8eda8807c59795b16d5d80076..d38df424f683059f49387c3a45fc15bcb62f7590 100644 (file)
@@ -68,7 +68,7 @@ class AirPlay2Stream(AirPlayProtocol):
     async def start(self, start_ntp: int, skip: int = 0) -> None:
         """Initialize CLI process for a player."""
         cli_binary = await get_cli_binary(self.player.protocol)
-        assert self.player.discovery_info is not None
+        assert self.player.airplay_discovery_info is not None
 
         player_id = self.player.player_id
         sync_adjust = self.mass.config.get_raw_player_config_value(player_id, CONF_SYNC_ADJUST, 0)
@@ -78,7 +78,7 @@ class AirPlay2Stream(AirPlayProtocol):
         )
 
         txt_kv: str = ""
-        for key, value in self.player.discovery_info.decoded_properties.items():
+        for key, value in self.player.airplay_discovery_info.decoded_properties.items():
             txt_kv += f'"{key}={value}" '
 
         # Note: skip parameter is accepted for API compatibility with base class
@@ -94,11 +94,11 @@ class AirPlay2Stream(AirPlayProtocol):
             "--name",
             self.player.display_name,
             "--hostname",
-            str(self.player.discovery_info.server),
+            str(self.player.airplay_discovery_info.server),
             "--address",
             str(self.player.address),
             "--port",
-            str(self.player.discovery_info.port),
+            str(self.player.airplay_discovery_info.port),
             "--txt",
             txt_kv,
             "--ntpstart",
index 954ad293bf3edf704c58d30b9a8670b8c55a1df7..18a50b98b9a714e55609fe0f231b6d5348171b99 100644 (file)
@@ -39,7 +39,7 @@ class RaopStream(AirPlayProtocol):
 
     async def start(self, start_ntp: int) -> None:
         """Initialize CLIRaop process for a player."""
-        assert self.player.discovery_info is not None  # for type checker
+        assert self.player.raop_discovery_info is not None  # for type checker
         cli_binary = await get_cli_binary(self.player.protocol)
         extra_args: list[str] = []
         player_id = self.player.player_id
@@ -49,7 +49,7 @@ class RaopStream(AirPlayProtocol):
         if self.player.config.get_value(CONF_ALAC_ENCODE, True):
             extra_args += ["-alac"]
         for prop in ("et", "md", "am", "pk", "pw"):
-            if prop_value := self.player.discovery_info.decoded_properties.get(prop):
+            if prop_value := self.player.raop_discovery_info.decoded_properties.get(prop):
                 extra_args += [f"-{prop}", prop_value]
         sync_adjust = self.player.config.get_value(CONF_SYNC_ADJUST, 0)
         assert isinstance(sync_adjust, int)
@@ -79,7 +79,7 @@ class RaopStream(AirPlayProtocol):
             "-ntpstart",
             str(start_ntp),
             "-port",
-            str(self.player.discovery_info.port),
+            str(self.player.raop_discovery_info.port),
             "-latency",
             str(read_ahead),
             "-volume",
@@ -92,7 +92,7 @@ class RaopStream(AirPlayProtocol):
             "-cmdpipe",
             self.commands_pipe.path,
             "-udn",
-            self.player.discovery_info.name,
+            self.player.raop_discovery_info.name,
             self.player.address,
             self.audio_pipe.path,
         ]
@@ -119,7 +119,7 @@ class RaopStream(AirPlayProtocol):
 
     async def start_pairing(self) -> None:
         """Start pairing process for this protocol (if supported)."""
-        assert self.player.discovery_info is not None  # for type checker
+        assert self.player.raop_discovery_info is not None  # for type checker
         cli_binary = await get_cli_binary(self.player.protocol)
 
         cliraop_args = [
@@ -128,9 +128,9 @@ class RaopStream(AirPlayProtocol):
             "-if",
             self.mass.streams.bind_ip,
             "-port",
-            str(self.player.discovery_info.port),
+            str(self.player.raop_discovery_info.port),
             "-udn",
-            self.player.discovery_info.name,
+            self.player.raop_discovery_info.name,
             self.player.address,
         ]
         self.player.logger.debug(
index f86355c36503168517195b13a281802f5d8c249b..0a9701f1a034bd3e7b22359e9aa8b80dfc023049 100644 (file)
@@ -20,10 +20,12 @@ from music_assistant.helpers.util import (
 from music_assistant.models.player_provider import PlayerProvider
 
 from .constants import (
+    AIRPLAY_DISCOVERY_TYPE,
     CACHE_CATEGORY_PREV_VOLUME,
     CONF_IGNORE_VOLUME,
     DACP_DISCOVERY_TYPE,
     FALLBACK_VOLUME,
+    RAOP_DISCOVERY_TYPE,
 )
 from .helpers import convert_airplay_volume, get_model_info
 from .player import AirPlayPlayer
@@ -116,24 +118,31 @@ class AirPlayProvider(PlayerProvider):
         self, player_id: str, display_name: str, discovery_info: AsyncServiceInfo
     ) -> None:
         """Handle setup of a new player that is discovered using mdns."""
-        if discovery_info.type == "_raop._tcp.local.":
+        raop_discovery_info: AsyncServiceInfo | None = None
+        airplay_discovery_info: AsyncServiceInfo | None = None
+        if discovery_info.type == RAOP_DISCOVERY_TYPE:
             # RAOP service discovered
+            raop_discovery_info = discovery_info
             self.logger.debug("Discovered RAOP service for %s", display_name)
             # always prefer airplay mdns info as it has more details
             # fallback to raop info if airplay info is not available,
             # (old device only announcing raop)
-            airplay_info = AsyncServiceInfo(
-                "_airplay._tcp.local.",
+            airplay_discovery_info = AsyncServiceInfo(
+                AIRPLAY_DISCOVERY_TYPE,
                 discovery_info.name.split("@")[-1].replace("_raop", "_airplay"),
             )
+            await airplay_discovery_info.async_request(self.mass.aiozc.zeroconf, 3000)
         else:
             # AirPlay service discovered
             self.logger.debug("Discovered AirPlay service for %s", display_name)
-            airplay_info = discovery_info
-        if await airplay_info.async_request(self.mass.aiozc.zeroconf, 3000):
-            manufacturer, model = get_model_info(airplay_info)
+            airplay_discovery_info = discovery_info
+
+        if airplay_discovery_info:
+            manufacturer, model = get_model_info(airplay_discovery_info)
+        elif raop_discovery_info:
+            manufacturer, model = get_model_info(raop_discovery_info)
         else:
-            manufacturer, model = get_model_info(discovery_info)
+            manufacturer, model = "Unknown", "Unknown"
 
         if not self.mass.config.get_raw_player_config_value(player_id, "enabled", True):
             self.logger.debug("Ignoring %s in discovery as it is disabled.", display_name)
@@ -162,6 +171,14 @@ class AirPlayProvider(PlayerProvider):
         if not is_apple and "airplay" not in display_name.lower():
             display_name += " (AirPlay)"
 
+        # Final check before registration to handle race conditions
+        # (multiple MDNS events processed in parallel for same device)
+        if self.mass.players.get(player_id):
+            self.logger.debug(
+                "Player %s already registered during setup, skipping registration", player_id
+            )
+            return
+
         self.logger.debug(
             "Setting up player %s: manufacturer=%s, model=%s",
             display_name,
@@ -174,22 +191,14 @@ class AirPlayProvider(PlayerProvider):
         player = AirPlayPlayer(
             provider=self,
             player_id=player_id,
-            discovery_info=discovery_info,
+            raop_discovery_info=raop_discovery_info,
+            airplay_discovery_info=airplay_discovery_info,
             address=address,
             display_name=display_name,
             manufacturer=manufacturer,
             model=model,
             initial_volume=volume,
         )
-
-        # Final check before registration to handle race conditions
-        # (multiple MDNS events processed in parallel for same device)
-        if self.mass.players.get(player_id):
-            self.logger.debug(
-                "Player %s already registered during setup, skipping registration", player_id
-            )
-            return
-
         await self.mass.players.register(player)
 
     async def _handle_dacp_request(  # noqa: PLR0915