/
/
1"""Helpers for player behavior."""
2
3from typing import TYPE_CHECKING
4
5from music_assistant_models.enums import PlayerType, ProviderFeature
6from music_assistant_models.media_items import AudioSource
7
8from music_assistant.models.plugin import PluginProvider
9
10if TYPE_CHECKING:
11 from music_assistant_models.player_queue import PlayerQueue
12
13 from music_assistant.mass import MusicAssistant
14
15_DEFAULT_ICONS_BY_PROVIDER = {
16 "airplay": "airplay",
17 "chromecast": "cast",
18 "fully_kiosk": "tablet",
19 "msx_bridge": "tv",
20 "roku_media_assistant": "tv",
21 "sonos": "sonos",
22 "sonos_s1": "sonos",
23 "wiim": "wiim",
24}
25
26_DEFAULT_ICONS_BY_PLAYER_TYPE = {
27 PlayerType.DISPLAY: "monitor",
28 PlayerType.LIGHT: "sun",
29 PlayerType.VISUALIZER: "monitor",
30}
31
32_DEFAULT_ICON_MATCHES = (
33 ("homepod-mini", ("homepod",)),
34 ("apple-tv", ("apple tv", "appletv")),
35 (
36 "google-nest",
37 ("google home", "google nest", "nest audio", "nest hub", "nest mini"),
38 ),
39 ("voice-pe", ("home assistant voice", "voice pe", "voice preview edition")),
40 ("sonos", ("sonos",)),
41 ("wiim", ("wiim",)),
42 ("soundbar", ("sound bar", "soundbar")),
43 ("tv", (" tv", "smarttv", "television")),
44 ("monitor", ("web browser",)),
45 ("laptop", ("laptop", "notebook")),
46 ("smartphone", ("iphone", "mobile application", "smartphone")),
47 ("tablet", ("ipad", "tablet")),
48 ("headphones", ("earbud", "headphone", "headset")),
49 ("bluetooth", ("bluetooth",)),
50 ("radio", ("radio",)),
51 ("car", ("carplay",)),
52)
53
54
55def get_default_player_icon(
56 player_type: PlayerType,
57 provider_domain: str,
58 manufacturer: str | None,
59 model: str | None,
60) -> str:
61 """
62 Return the most appropriate default icon for a player.
63
64 :param player_type: The player's functional type.
65 :param provider_domain: The domain of the player provider.
66 :param manufacturer: The device manufacturer reported by the provider.
67 :param model: The device model reported by the provider.
68 """
69 if player_type in (PlayerType.GROUP, PlayerType.STEREO_PAIR):
70 return "speakers"
71 if player_type_icon := _DEFAULT_ICONS_BY_PLAYER_TYPE.get(player_type):
72 return player_type_icon
73
74 manufacturer_name = (manufacturer or "").casefold()
75 model_name = (model or "").casefold()
76 if manufacturer_name.startswith("apple") and model_name.startswith(("imac", "mac")):
77 return "mac"
78
79 device_description = f"{manufacturer_name} {model_name}"
80 for icon, matches in _DEFAULT_ICON_MATCHES:
81 if any(match in device_description for match in matches):
82 return icon
83
84 if provider_icon := _DEFAULT_ICONS_BY_PROVIDER.get(provider_domain):
85 return provider_icon
86 return "speaker"
87
88
89def get_queue_audio_source(
90 mass: MusicAssistant, queue: PlayerQueue | None
91) -> tuple[AudioSource, PluginProvider] | None:
92 """
93 Return the AudioSource current on the given queue and its owning PluginProvider.
94
95 Returns None when the queue's current item is not a MediaType.AUDIO_SOURCE
96 or when the owning plugin provider is no longer available.
97
98 :param mass: The MusicAssistant instance.
99 :param queue: The queue whose current item to inspect (None allowed).
100 """
101 if queue is None:
102 return None
103 current_item = queue.current_item
104 if current_item is None or current_item.media_item is None:
105 return None
106 media_item = current_item.media_item
107 # isinstance check defends against a non-AudioSource subclass that
108 # somehow has media_type=AUDIO_SOURCE set (mutated or constructed wrong)
109 # â the media_type guard alone would let it through and crash later.
110 if not isinstance(media_item, AudioSource):
111 return None
112 provider = mass.get_provider(media_item.provider)
113 if not isinstance(provider, PluginProvider):
114 return None
115 # Belt-and-suspenders: a queue item carrying media_type=AUDIO_SOURCE
116 # can only have come from a provider that declared the feature, but
117 # a feature flag flipped off at runtime (provider reload, config
118 # change) would leave on_source_control / on_volume_change raising
119 # NotImplementedError. Skip cleanly.
120 if ProviderFeature.AUDIO_SOURCE not in provider.supported_features:
121 return None
122 return media_item, provider
123