| 
24 | 24 | from starlette.requests import Request  | 
25 | 25 | from starlette.responses import JSONResponse, PlainTextResponse, Response  | 
26 | 26 | 
 
  | 
 | 27 | +from rapidfuzz import fuzz, process  | 
 | 28 | + | 
27 | 29 | from .cache import MediaCache  | 
28 | 30 | from .config import Settings  | 
29 | 31 | 
 
  | 
@@ -293,9 +295,9 @@ def _load_clients() -> list[Any]:  | 
293 | 295 |         def _collect_alias(identifier: str | None) -> None:  | 
294 | 296 |             if not identifier:  | 
295 | 297 |                 return  | 
296 |  | -            alias = aliases.get(identifier)  | 
297 |  | -            if alias and alias not in friendly_names:  | 
298 |  | -                friendly_names.append(alias)  | 
 | 298 | +            for alias in aliases.get(identifier, []):  | 
 | 299 | +                if alias and alias not in friendly_names:  | 
 | 300 | +                    friendly_names.append(alias)  | 
299 | 301 | 
 
  | 
300 | 302 |         _collect_alias(machine_id)  | 
301 | 303 |         _collect_alias(client_id)  | 
@@ -330,26 +332,59 @@ def _collect_alias(identifier: str | None) -> None:  | 
330 | 332 |     return players  | 
331 | 333 | 
 
  | 
332 | 334 | 
 
  | 
 | 335 | +_FUZZY_MATCH_THRESHOLD = 70  | 
 | 336 | + | 
 | 337 | + | 
333 | 338 | def _match_player(query: str, players: Sequence[dict[str, Any]]) -> dict[str, Any]:  | 
334 | 339 |     """Locate a Plex player by friendly name or identifier."""  | 
335 | 340 | 
 
  | 
336 |  | -    normalized = query.strip().lower()  | 
 | 341 | +    normalized_query = query.strip()  | 
 | 342 | +    normalized = normalized_query.lower()  | 
 | 343 | +    if not normalized_query:  | 
 | 344 | +        raise ValueError(f"Player '{query}' not found")  | 
 | 345 | + | 
 | 346 | +    candidate_entries: list[tuple[str, str, dict[str, Any]]] = []  | 
337 | 347 |     for player in players:  | 
338 |  | -        candidates = {  | 
 | 348 | +        candidate_strings = {  | 
339 | 349 |             player.get("display_name"),  | 
340 | 350 |             player.get("name"),  | 
341 | 351 |             player.get("product"),  | 
342 | 352 |             player.get("machine_identifier"),  | 
343 | 353 |             player.get("client_identifier"),  | 
344 | 354 |         }  | 
345 |  | -        candidates.update(player.get("friendly_names", []))  | 
 | 355 | +        candidate_strings.update(player.get("friendly_names", []))  | 
346 | 356 |         machine_id = player.get("machine_identifier")  | 
347 | 357 |         client_id = player.get("client_identifier")  | 
348 | 358 |         if machine_id and client_id:  | 
349 |  | -            candidates.add(f"{machine_id}:{client_id}")  | 
350 |  | -        for candidate in candidates:  | 
351 |  | -            if candidate and candidate.lower() == normalized:  | 
 | 359 | +            candidate_strings.add(f"{machine_id}:{client_id}")  | 
 | 360 | +        for candidate in candidate_strings:  | 
 | 361 | +            if not candidate:  | 
 | 362 | +                continue  | 
 | 363 | +            candidate_str = str(candidate).strip()  | 
 | 364 | +            if not candidate_str:  | 
 | 365 | +                continue  | 
 | 366 | +            candidate_lower = candidate_str.lower()  | 
 | 367 | +            candidate_entries.append((candidate_str, candidate_lower, player))  | 
 | 368 | +            if candidate_lower == normalized:  | 
352 | 369 |                 return player  | 
 | 370 | +    def _process_choice(  | 
 | 371 | +        choice: str | tuple[str, str, dict[str, Any]]  | 
 | 372 | +    ) -> str:  | 
 | 373 | +        if isinstance(choice, tuple):  | 
 | 374 | +            return choice[1]  | 
 | 375 | +        return str(choice).strip().lower()  | 
 | 376 | + | 
 | 377 | +    match = process.extractOne(  | 
 | 378 | +        normalized_query,  | 
 | 379 | +        candidate_entries,  | 
 | 380 | +        scorer=fuzz.WRatio,  | 
 | 381 | +        processor=_process_choice,  | 
 | 382 | +        score_cutoff=_FUZZY_MATCH_THRESHOLD,  | 
 | 383 | +    )  | 
 | 384 | +    if match:  | 
 | 385 | +        choice, _, _ = match  | 
 | 386 | +        if choice is not None:  | 
 | 387 | +            return choice[2]  | 
353 | 388 |     raise ValueError(f"Player '{query}' not found")  | 
354 | 389 | 
 
  | 
355 | 390 | 
 
  | 
 | 
0 commit comments