/
/
/
1"""Sync Group Player Provider implementation."""
2
3from __future__ import annotations
4
5from typing import TYPE_CHECKING
6
7import shortuuid
8from music_assistant_models.enums import PlayerType
9
10from music_assistant.constants import CONF_DYNAMIC_GROUP_MEMBERS, CONF_GROUP_MEMBERS
11from music_assistant.models.player_provider import PlayerProvider
12
13from .constants import SGP_PREFIX
14from .player import SyncGroupPlayer
15
16if TYPE_CHECKING:
17 from music_assistant_models.config_entries import ConfigEntry
18
19 from music_assistant.models.player import Player
20
21
22class SyncGroupProvider(PlayerProvider):
23 """Sync Group Player Provider."""
24
25 async def get_config_entries(self) -> tuple[ConfigEntry, ...]:
26 """Return Config entries to setup this provider."""
27 # nothing to configure (for now)
28 return ()
29
30 async def create_group_player(
31 self, name: str, members: list[str], dynamic: bool = True
32 ) -> Player:
33 """
34 Create new Sync Group Player.
35
36 :param name: Name of the group player.
37 :param members: List of player ids to add to the group.
38 :param dynamic: Whether the group is dynamic (members can change).
39 """
40 # validation to ensure all members are compatible (can_group_with check)
41 members = [x for x in members if x in [y.player_id for y in self.mass.players]]
42 final_members: list[str] = []
43 can_group_with: set[str] = set()
44 for member_id in members:
45 member = self.mass.players.get_player(member_id)
46 if member is None or not member.available:
47 continue
48 if not can_group_with:
49 # first member, add all its compatible players to the can_group_with set
50 can_group_with = set(member.state.can_group_with)
51 elif member_id not in can_group_with:
52 # member is not compatible with the current group, skip it
53 continue
54 final_members.append(member_id)
55 # generate a new player_id for the group player
56 player_id = f"{SGP_PREFIX}{shortuuid.random(8).lower()}"
57 self.mass.config.create_default_player_config(
58 player_id=player_id,
59 provider=self.instance_id,
60 player_type=PlayerType.GROUP,
61 name=name,
62 enabled=True,
63 values={
64 CONF_GROUP_MEMBERS: final_members,
65 CONF_DYNAMIC_GROUP_MEMBERS: dynamic,
66 },
67 )
68 return await self._register_player(player_id)
69
70 async def remove_group_player(self, player_id: str) -> None:
71 """
72 Remove a group player.
73
74 :param player_id: ID of the group player to remove.
75 """
76 # we simply permanently unregister the player and wipe its config
77 await self.mass.players.unregister(player_id, True)
78
79 async def discover_players(self) -> None:
80 """Discover players."""
81 for player_conf in await self.mass.config.get_player_configs(self.instance_id):
82 if player_conf.player_id.startswith(SGP_PREFIX):
83 await self._register_player(player_conf.player_id)
84
85 async def _register_player(self, player_id: str) -> Player:
86 """Register a sync group player."""
87 group = SyncGroupPlayer(self, player_id)
88 await self.mass.players.register_or_update(group)
89 return group
90