Skip to content

Commit 67cd5f9

Browse files
committed
More fixes for protocol linking
1 parent 53df337 commit 67cd5f9

5 files changed

Lines changed: 426 additions & 24 deletions

File tree

music_assistant/controllers/players/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ The `PlayerState` is a dataclass representing the final state of the player. It:
4646

4747
```
4848
┌─────────────────────────────────────────────────────────────────┐
49-
│ Player (Internal)
49+
│ Player (Internal) │
5050
│ - Provider-specific implementation │
5151
│ - Control methods (play, pause, volume_set, etc.) │
5252
│ - Raw state (_attr_volume_level, _attr_playback_state, etc.) │

music_assistant/controllers/players/controller.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2456,10 +2456,14 @@ async def _handle_set_members_with_protocols(
24562456
# Forward native members to parent player's set_members
24572457
if native_members_to_add or native_members_to_remove:
24582458
filtered_native_add = self._filter_native_members(native_members_to_add, parent_player)
2459+
# For removal, allow protocol players if they're actually in the parent's group_members
2460+
# This handles native protocol players (e.g., native AirPlay) where group_members
2461+
# contains protocol player IDs
24592462
filtered_native_remove = [
24602463
pid
24612464
for pid in native_members_to_remove
2462-
if (p := self.get_player(pid)) and p.type != PlayerType.PROTOCOL
2465+
if (p := self.get_player(pid))
2466+
and (p.type != PlayerType.PROTOCOL or pid in parent_player.group_members)
24632467
]
24642468
self.logger.debug(
24652469
"Native grouping on %s: filtered_add=%s, filtered_remove=%s",

music_assistant/controllers/players/protocol_linking.py

Lines changed: 72 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1066,7 +1066,23 @@ def _translate_members_to_remove_for_protocols(
10661066
protocol_members.append(child_protocol.output_protocol_id)
10671067
continue
10681068

1069-
native_members.append(child_player_id)
1069+
# Check if child's protocol player is in parent's native group_members
1070+
# This handles native protocol players (e.g., native AirPlay player like Apple TV)
1071+
# where the parent itself contains protocol player IDs in its group_members
1072+
translated = False
1073+
for linked in child_player.linked_output_protocols:
1074+
if linked.output_protocol_id in parent_player.group_members:
1075+
self.logger.debug(
1076+
"Translating removal (native parent): %s -> protocol %s",
1077+
child_player_id,
1078+
linked.output_protocol_id,
1079+
)
1080+
native_members.append(linked.output_protocol_id)
1081+
translated = True
1082+
break
1083+
1084+
if not translated:
1085+
native_members.append(child_player_id)
10701086

10711087
return protocol_members, native_members
10721088

@@ -1119,13 +1135,15 @@ def _try_child_preferred_protocol(
11191135
if not child_protocol or not child_protocol.available:
11201136
return None, None
11211137

1122-
# Check if parent supports this protocol
1123-
parent_protocol = parent_player.get_linked_protocol(child_protocol.protocol_domain)
1138+
# Check if parent supports this protocol (including native protocol)
1139+
parent_protocol = parent_player.get_output_protocol_by_domain(
1140+
child_protocol.protocol_domain
1141+
)
11241142
if not parent_protocol or not parent_protocol.available:
11251143
return None, None
11261144

11271145
# Check if this protocol supports set_members
1128-
protocol_player = self.get_player(parent_protocol.output_protocol_id)
1146+
protocol_player = parent_player.get_protocol_player(parent_protocol.output_protocol_id)
11291147
if (
11301148
not protocol_player
11311149
or PlayerFeature.SET_MEMBERS not in protocol_player.state.supported_features
@@ -1165,7 +1183,9 @@ def _try_find_common_protocol(
11651183
)
11661184
if not child_protocol or not child_protocol.available:
11671185
continue
1168-
protocol_player = self.get_player(parent_output_protocol.output_protocol_id)
1186+
protocol_player = parent_player.get_protocol_player(
1187+
parent_output_protocol.output_protocol_id
1188+
)
11691189
if (
11701190
protocol_player
11711191
and PlayerFeature.SET_MEMBERS in protocol_player.state.supported_features
@@ -1231,9 +1251,11 @@ def _translate_members_for_protocols(
12311251
and (not parent_protocol_domain or protocol_domain == parent_protocol_domain)
12321252
):
12331253
if not parent_protocol_player or parent_protocol_domain != protocol_domain:
1234-
parent_protocol = parent_player.get_linked_protocol(protocol_domain)
1254+
parent_protocol = parent_player.get_output_protocol_by_domain(protocol_domain)
12351255
if parent_protocol:
1236-
parent_protocol_player = self.get_player(parent_protocol.output_protocol_id)
1256+
parent_protocol_player = parent_player.get_protocol_player(
1257+
parent_protocol.output_protocol_id
1258+
)
12371259
parent_protocol_domain = protocol_domain
12381260
protocol_members.append(child_protocol_id)
12391261
self.logger.log(
@@ -1290,7 +1312,9 @@ def _translate_members_for_protocols(
12901312
not parent_protocol_player
12911313
or parent_protocol_domain != parent_protocol.protocol_domain
12921314
):
1293-
parent_protocol_player = self.get_player(parent_protocol.output_protocol_id)
1315+
parent_protocol_player = parent_player.get_protocol_player(
1316+
parent_protocol.output_protocol_id
1317+
)
12941318
if parent_protocol_player:
12951319
parent_protocol_domain = parent_protocol_player.provider.domain
12961320
protocol_members.append(child_protocol.output_protocol_id)
@@ -1365,28 +1389,55 @@ async def _forward_protocol_set_members(
13651389
player_ids_to_remove=filtered_protocol_remove or None,
13661390
)
13671391

1392+
# Set active output protocol on added child players
1393+
if filtered_protocol_add:
1394+
for child_protocol_id in filtered_protocol_add:
1395+
if child_protocol := self.get_player(child_protocol_id):
1396+
if child_protocol.protocol_parent_id:
1397+
if child_player := self.get_player(child_protocol.protocol_parent_id):
1398+
if child_player.active_output_protocol != child_protocol_id:
1399+
self.logger.debug(
1400+
"Setting active output protocol on child %s to %s",
1401+
child_player.state.name,
1402+
child_protocol_id,
1403+
)
1404+
child_player.set_active_output_protocol(child_protocol_id)
1405+
13681406
# If we added members via this protocol, set it as the active output protocol
1369-
# and restart playback if currently playing
1370-
if (
1371-
filtered_protocol_add
1372-
and parent_player.active_output_protocol != parent_protocol_player.player_id
1373-
):
1407+
# and restart playback if currently playing AND we're switching protocols
1408+
if filtered_protocol_add:
13741409
previous_protocol = parent_player.active_output_protocol
13751410
was_playing = parent_player.state.playback_state == PlaybackState.PLAYING
13761411

1412+
# Determine if we're switching protocols (which requires restart)
1413+
# Native protocol: parent_protocol_player is the same as parent_player
1414+
is_native_protocol = parent_protocol_player.player_id == parent_player.player_id
1415+
already_using_native = previous_protocol in (None, "native")
1416+
already_using_this_protocol = previous_protocol == parent_protocol_player.player_id
1417+
1418+
# Only restart if we're actually switching to a different protocol
1419+
switching_protocols = not (
1420+
(is_native_protocol and already_using_native) or already_using_this_protocol
1421+
)
1422+
13771423
self.logger.debug(
1378-
"Setting active output protocol to %s after grouping members "
1379-
"(previous: %s, was_playing: %s)",
1380-
parent_protocol_player.player_id,
1381-
previous_protocol,
1424+
"Protocol grouping: is_native=%s, already_native=%s, already_this=%s, "
1425+
"switching=%s, was_playing=%s",
1426+
is_native_protocol,
1427+
already_using_native,
1428+
already_using_this_protocol,
1429+
switching_protocols,
13821430
was_playing,
13831431
)
1384-
parent_player.set_active_output_protocol(parent_protocol_player.player_id)
13851432

1386-
# Restart playback on the new protocol if we were playing
1387-
if was_playing:
1433+
# Update active output protocol if not already using native
1434+
if not (is_native_protocol and already_using_native):
1435+
parent_player.set_active_output_protocol(parent_protocol_player.player_id)
1436+
1437+
# Restart playback only if we're switching protocols
1438+
if was_playing and switching_protocols:
13881439
self.logger.info(
1389-
"Restarting playback on %s via %s protocol after grouping members",
1440+
"Restarting playback on %s via %s protocol after switching protocols",
13901441
parent_player.state.name,
13911442
parent_protocol_player.provider.domain,
13921443
)

music_assistant/models/player.py

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1070,6 +1070,21 @@ def get_linked_protocol(self, protocol_domain: str) -> OutputProtocol | None:
10701070
)
10711071
return None
10721072

1073+
@final
1074+
def get_output_protocol_by_domain(self, protocol_domain: str) -> OutputProtocol | None:
1075+
"""
1076+
Get an output protocol by domain, including native protocol.
1077+
1078+
Unlike get_linked_protocol, this also checks if the player's native protocol
1079+
matches the requested domain.
1080+
1081+
:param protocol_domain: The protocol domain to search for (e.g., "airplay", "sonos").
1082+
"""
1083+
for output_protocol in self.output_protocols:
1084+
if output_protocol.protocol_domain == protocol_domain:
1085+
return output_protocol
1086+
return None
1087+
10731088
@final
10741089
def get_protocol_player(self, player_id: str) -> Player | None:
10751090
"""Get the protocol Player for a given player_id."""
@@ -1613,7 +1628,15 @@ def __final_group_members(self) -> list[str]:
16131628
# If player is synced to another player, it has no group members itself
16141629
return []
16151630

1616-
members = self.group_members.copy()
1631+
# Start by translating native group_members to visible player IDs
1632+
# This handles cases where a native player (e.g., native AirPlay) has grouped
1633+
# protocol players (e.g., Sonos AirPlay protocol players) that need translation
1634+
members: list[str] = []
1635+
translated_members = self._translate_protocol_ids_to_visible(set(self.group_members))
1636+
for member in translated_members:
1637+
if member.player_id not in members:
1638+
members.append(member.player_id)
1639+
16171640
# If there's an active linked protocol, include its group members (translated)
16181641
if self.__attr_active_output_protocol and self.__attr_active_output_protocol != "native":
16191642
if protocol_player := self.mass.players.get_player(self.__attr_active_output_protocol):
@@ -1653,9 +1676,13 @@ def __final_synced_to(self) -> str | None:
16531676
if protocol_player.synced_to:
16541677
# Protocol player is synced, translate to visible player
16551678
if proto_sync_parent := self.mass.players.get_player(protocol_player.synced_to):
1679+
if proto_sync_parent.type != PlayerType.PROTOCOL:
1680+
# Sync parent is already a visible player (e.g., native AirPlay player)
1681+
return proto_sync_parent.player_id
16561682
if proto_sync_parent.protocol_parent_id and (
16571683
parent := self.mass.players.get_player(proto_sync_parent.protocol_parent_id)
16581684
):
1685+
# Sync parent is a protocol player, return its visible parent
16591686
return parent.player_id
16601687

16611688
return None

0 commit comments

Comments
 (0)