|
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