From 6cbbea554c50ff919964ebd0ee1c7900ae9c330a Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Tue, 4 Nov 2025 19:37:54 +0100 Subject: [PATCH] Fix AirPlay discovery info --- music_assistant/providers/airplay/player.py | 6 ++- .../providers/airplay/protocols/airplay2.py | 8 ++-- .../providers/airplay/protocols/raop.py | 14 +++--- music_assistant/providers/airplay/provider.py | 43 +++++++++++-------- 4 files changed, 41 insertions(+), 30 deletions(-) diff --git a/music_assistant/providers/airplay/player.py b/music_assistant/providers/airplay/player.py index fbbb009e..78486ba2 100644 --- a/music_assistant/providers/airplay/player.py +++ b/music_assistant/providers/airplay/player.py @@ -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 diff --git a/music_assistant/providers/airplay/protocols/airplay2.py b/music_assistant/providers/airplay/protocols/airplay2.py index 59475632..d38df424 100644 --- a/music_assistant/providers/airplay/protocols/airplay2.py +++ b/music_assistant/providers/airplay/protocols/airplay2.py @@ -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", diff --git a/music_assistant/providers/airplay/protocols/raop.py b/music_assistant/providers/airplay/protocols/raop.py index 954ad293..18a50b98 100644 --- a/music_assistant/providers/airplay/protocols/raop.py +++ b/music_assistant/providers/airplay/protocols/raop.py @@ -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( diff --git a/music_assistant/providers/airplay/provider.py b/music_assistant/providers/airplay/provider.py index f86355c3..0a9701f1 100644 --- a/music_assistant/providers/airplay/provider.py +++ b/music_assistant/providers/airplay/provider.py @@ -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 -- 2.34.1