From 1185e77e2d9c72d0cd5b21d0561f245cd857b4a9 Mon Sep 17 00:00:00 2001 From: OzGav Date: Mon, 2 Feb 2026 20:23:33 +1100 Subject: [PATCH] Fix players/get_by_name not always returning a result (#2945) * fix: Improve player name matching in get_by_name API The players/get_by_name API was failing to find some players because it only performed exact matching on the player's name property. This commit enhances the matching logic to: - Match against both player.name and player.display_name (which may differ if users have customized the player name in config) - First attempt exact match (case-sensitive) - Fall back to case-insensitive, whitespace-trimmed matching if exact match fails This fixes the issue where players like "Alfresco", "Sonos Roaming", and "pCP-Airplay Test" were returning NULL from the API despite being available in the players/all endpoint. * Improve duplicate player name handling in get_by_name Enhances the get_player_by_name method to handle the edge case where multiple players might have the same display_name: - Collects all matching players instead of returning first match immediately - Logs a warning when multiple players match the same name, including the player_ids of all matches - Guides users to use player_id for unambiguous lookups - Maintains backward compatibility by returning the first match This improves debuggability when users have players with duplicate names (e.g., multiple identical devices with default names or custom config that creates name collisions). * Clarify warning message for duplicate player names Improves the warning message when multiple players match the same name: - Now shows which player_id was actually returned (first match) - Explicitly mentions "players/get API" instead of vague "use player_id" - Makes it clear users should use a different API endpoint for unambiguous lookups, not try to pass player_id to get_by_name This addresses confusion about how to resolve duplicate name lookups. * Include API endpoint name in duplicate player warning Prefixes the warning message with "players/get_by_name:" to make it immediately clear which API call triggered the warning. This helps users debug their automations and integrations when they see the warning in logs. Warning now reads: "players/get_by_name: Multiple players found with name 'X': [...] - returning first match (Y). Consider using the players/get API..." * Simplify to single-pass case-insensitive matching Removes the two-pass approach (exact match first, then case-insensitive) in favor of a simpler single-pass case-insensitive search. Benefits: - Single iteration through players (better performance) - Simpler, more readable code - More predictable behavior (always case-insensitive) - Still handles all real-world use cases The two-pass approach was protecting against the extremely unlikely scenario of having players "Kitchen" and "kitchen" simultaneously, which would generate a duplicate warning anyway. * Remove redundant text from docstring return statement * Fix line length to comply with E501 linting rule * Use player.state.name for matching instead of multiple properties Simplifies the matching logic to check only player.state.name instead of checking both player.name and player.display_name. The state.name already contains the final player name as visible in clients and the API, making it the single source of truth for player name matching. As suggested by head dev review. --------- Co-authored-by: Claude --- .../controllers/players/player_controller.py | 28 ++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/music_assistant/controllers/players/player_controller.py b/music_assistant/controllers/players/player_controller.py index 793b3187..fbc1364a 100644 --- a/music_assistant/controllers/players/player_controller.py +++ b/music_assistant/controllers/players/player_controller.py @@ -371,10 +371,36 @@ class PlayerController(CoreController): """ Return Player by name. + Performs case-insensitive matching against the player's state name + (the final name visible in clients and API). + If multiple players match, logs a warning and returns the first match. + :param name: Name of the player. :return: Player object or None. """ - return next((x for x in self._players.values() if x.name == name), None) + name_normalized = name.strip().lower() + matches: list[Player] = [] + + for player in self._players.values(): + if player.state.name.strip().lower() == name_normalized: + matches.append(player) + + if not matches: + return None + + if len(matches) > 1: + player_ids = [p.player_id for p in matches] + self.logger.warning( + "players/get_by_name: Multiple players found with name '%s': %s - " + "returning first match (%s). " + "Consider using the players/get API with player_id instead " + "for unambiguous lookups.", + name, + player_ids, + matches[0].player_id, + ) + + return matches[0] @api_command("players/get_by_name") def get_player_state_by_name(self, name: str) -> PlayerState | None: -- 2.34.1