pywiim 2.2.7__tar.gz → 2.2.9__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. {pywiim-2.2.7/pywiim.egg-info → pywiim-2.2.9}/PKG-INFO +1 -1
  2. {pywiim-2.2.7 → pywiim-2.2.9}/pyproject.toml +1 -1
  3. {pywiim-2.2.7 → pywiim-2.2.9}/pywiim/__init__.py +3 -1
  4. {pywiim-2.2.7 → pywiim-2.2.9}/pywiim/discovery.py +28 -0
  5. {pywiim-2.2.7 → pywiim-2.2.9}/pywiim/player/coverart.py +21 -3
  6. {pywiim-2.2.7 → pywiim-2.2.9}/pywiim/state.py +10 -2
  7. {pywiim-2.2.7 → pywiim-2.2.9/pywiim.egg-info}/PKG-INFO +1 -1
  8. {pywiim-2.2.7 → pywiim-2.2.9}/LICENSE +0 -0
  9. {pywiim-2.2.7 → pywiim-2.2.9}/README.md +0 -0
  10. {pywiim-2.2.7 → pywiim-2.2.9}/pywiim/api/__init__.py +0 -0
  11. {pywiim-2.2.7 → pywiim-2.2.9}/pywiim/api/audio_pro.py +0 -0
  12. {pywiim-2.2.7 → pywiim-2.2.9}/pywiim/api/audio_settings.py +0 -0
  13. {pywiim-2.2.7 → pywiim-2.2.9}/pywiim/api/base.py +0 -0
  14. {pywiim-2.2.7 → pywiim-2.2.9}/pywiim/api/bluetooth.py +0 -0
  15. {pywiim-2.2.7 → pywiim-2.2.9}/pywiim/api/constants.py +0 -0
  16. {pywiim-2.2.7 → pywiim-2.2.9}/pywiim/api/device.py +0 -0
  17. {pywiim-2.2.7 → pywiim-2.2.9}/pywiim/api/diagnostics.py +0 -0
  18. {pywiim-2.2.7 → pywiim-2.2.9}/pywiim/api/endpoints.py +0 -0
  19. {pywiim-2.2.7 → pywiim-2.2.9}/pywiim/api/eq.py +0 -0
  20. {pywiim-2.2.7 → pywiim-2.2.9}/pywiim/api/firmware.py +0 -0
  21. {pywiim-2.2.7 → pywiim-2.2.9}/pywiim/api/group.py +0 -0
  22. {pywiim-2.2.7 → pywiim-2.2.9}/pywiim/api/lms.py +0 -0
  23. {pywiim-2.2.7 → pywiim-2.2.9}/pywiim/api/loop_mode.py +0 -0
  24. {pywiim-2.2.7 → pywiim-2.2.9}/pywiim/api/misc.py +0 -0
  25. {pywiim-2.2.7 → pywiim-2.2.9}/pywiim/api/parser.py +0 -0
  26. {pywiim-2.2.7 → pywiim-2.2.9}/pywiim/api/peq.py +0 -0
  27. {pywiim-2.2.7 → pywiim-2.2.9}/pywiim/api/playback.py +0 -0
  28. {pywiim-2.2.7 → pywiim-2.2.9}/pywiim/api/preset.py +0 -0
  29. {pywiim-2.2.7 → pywiim-2.2.9}/pywiim/api/ssl.py +0 -0
  30. {pywiim-2.2.7 → pywiim-2.2.9}/pywiim/api/subwoofer.py +0 -0
  31. {pywiim-2.2.7 → pywiim-2.2.9}/pywiim/api/timer.py +0 -0
  32. {pywiim-2.2.7 → pywiim-2.2.9}/pywiim/backoff.py +0 -0
  33. {pywiim-2.2.7 → pywiim-2.2.9}/pywiim/capabilities.py +0 -0
  34. {pywiim-2.2.7 → pywiim-2.2.9}/pywiim/cli/__init__.py +0 -0
  35. {pywiim-2.2.7 → pywiim-2.2.9}/pywiim/cli/diagnostics.py +0 -0
  36. {pywiim-2.2.7 → pywiim-2.2.9}/pywiim/cli/discovery_cli.py +0 -0
  37. {pywiim-2.2.7 → pywiim-2.2.9}/pywiim/cli/group_test_cli.py +0 -0
  38. {pywiim-2.2.7 → pywiim-2.2.9}/pywiim/cli/join_test_cli.py +0 -0
  39. {pywiim-2.2.7 → pywiim-2.2.9}/pywiim/cli/monitor_cli.py +0 -0
  40. {pywiim-2.2.7 → pywiim-2.2.9}/pywiim/cli/verify_cli.py +0 -0
  41. {pywiim-2.2.7 → pywiim-2.2.9}/pywiim/client.py +0 -0
  42. {pywiim-2.2.7 → pywiim-2.2.9}/pywiim/device_capabilities.py +0 -0
  43. {pywiim-2.2.7 → pywiim-2.2.9}/pywiim/exceptions.py +0 -0
  44. {pywiim-2.2.7 → pywiim-2.2.9}/pywiim/group.py +0 -0
  45. {pywiim-2.2.7 → pywiim-2.2.9}/pywiim/group_helpers.py +0 -0
  46. {pywiim-2.2.7 → pywiim-2.2.9}/pywiim/mcp/__init__.py +0 -0
  47. {pywiim-2.2.7 → pywiim-2.2.9}/pywiim/mcp/__main__.py +0 -0
  48. {pywiim-2.2.7 → pywiim-2.2.9}/pywiim/mcp/config.example.json +0 -0
  49. {pywiim-2.2.7 → pywiim-2.2.9}/pywiim/mcp/config.py +0 -0
  50. {pywiim-2.2.7 → pywiim-2.2.9}/pywiim/mcp/context.py +0 -0
  51. {pywiim-2.2.7 → pywiim-2.2.9}/pywiim/mcp/server.py +0 -0
  52. {pywiim-2.2.7 → pywiim-2.2.9}/pywiim/metadata.py +0 -0
  53. {pywiim-2.2.7 → pywiim-2.2.9}/pywiim/model_names.py +0 -0
  54. {pywiim-2.2.7 → pywiim-2.2.9}/pywiim/models.py +0 -0
  55. {pywiim-2.2.7 → pywiim-2.2.9}/pywiim/normalize.py +0 -0
  56. {pywiim-2.2.7 → pywiim-2.2.9}/pywiim/player/__init__.py +0 -0
  57. {pywiim-2.2.7 → pywiim-2.2.9}/pywiim/player/audio.py +0 -0
  58. {pywiim-2.2.7 → pywiim-2.2.9}/pywiim/player/base.py +0 -0
  59. {pywiim-2.2.7 → pywiim-2.2.9}/pywiim/player/bluetooth.py +0 -0
  60. {pywiim-2.2.7 → pywiim-2.2.9}/pywiim/player/debounce.py +0 -0
  61. {pywiim-2.2.7 → pywiim-2.2.9}/pywiim/player/diagnostics.py +0 -0
  62. {pywiim-2.2.7 → pywiim-2.2.9}/pywiim/player/groupops.py +0 -0
  63. {pywiim-2.2.7 → pywiim-2.2.9}/pywiim/player/media.py +0 -0
  64. {pywiim-2.2.7 → pywiim-2.2.9}/pywiim/player/playback.py +0 -0
  65. {pywiim-2.2.7 → pywiim-2.2.9}/pywiim/player/properties.py +0 -0
  66. {pywiim-2.2.7 → pywiim-2.2.9}/pywiim/player/source_capabilities.py +0 -0
  67. {pywiim-2.2.7 → pywiim-2.2.9}/pywiim/player/statemgr.py +0 -0
  68. {pywiim-2.2.7 → pywiim-2.2.9}/pywiim/player/stream.py +0 -0
  69. {pywiim-2.2.7 → pywiim-2.2.9}/pywiim/player/stream_enricher.py +0 -0
  70. {pywiim-2.2.7 → pywiim-2.2.9}/pywiim/player/volume.py +0 -0
  71. {pywiim-2.2.7 → pywiim-2.2.9}/pywiim/polling.py +0 -0
  72. {pywiim-2.2.7 → pywiim-2.2.9}/pywiim/profiles.py +0 -0
  73. {pywiim-2.2.7 → pywiim-2.2.9}/pywiim/py.typed +0 -0
  74. {pywiim-2.2.7 → pywiim-2.2.9}/pywiim/role.py +0 -0
  75. {pywiim-2.2.7 → pywiim-2.2.9}/pywiim/upnp/__init__.py +0 -0
  76. {pywiim-2.2.7 → pywiim-2.2.9}/pywiim/upnp/client.py +0 -0
  77. {pywiim-2.2.7 → pywiim-2.2.9}/pywiim/upnp/eventer.py +0 -0
  78. {pywiim-2.2.7 → pywiim-2.2.9}/pywiim/upnp/health.py +0 -0
  79. {pywiim-2.2.7 → pywiim-2.2.9}/pywiim/upnp/metadata.py +0 -0
  80. {pywiim-2.2.7 → pywiim-2.2.9}/pywiim.egg-info/SOURCES.txt +0 -0
  81. {pywiim-2.2.7 → pywiim-2.2.9}/pywiim.egg-info/dependency_links.txt +0 -0
  82. {pywiim-2.2.7 → pywiim-2.2.9}/pywiim.egg-info/entry_points.txt +0 -0
  83. {pywiim-2.2.7 → pywiim-2.2.9}/pywiim.egg-info/requires.txt +0 -0
  84. {pywiim-2.2.7 → pywiim-2.2.9}/pywiim.egg-info/top_level.txt +0 -0
  85. {pywiim-2.2.7 → pywiim-2.2.9}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pywiim
3
- Version: 2.2.7
3
+ Version: 2.2.9
4
4
  Summary: Python library for WiiM/LinkPlay device communication
5
5
  Author-email: Michael Cumming <mjcumming@gmail.com>
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "pywiim"
7
- version = "2.2.7"
7
+ version = "2.2.9"
8
8
  description = "Python library for WiiM/LinkPlay device communication"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -62,6 +62,7 @@ from .discovery import (
62
62
  discover_devices,
63
63
  discover_via_ssdp,
64
64
  validate_device,
65
+ validate_device_strict,
65
66
  )
66
67
  from .exceptions import (
67
68
  WiiMConnectionError,
@@ -87,7 +88,7 @@ from .profiles import (
87
88
  from .role import RoleDetectionResult, detect_role
88
89
  from .state import GroupStateSynchronizer, StateSynchronizer
89
90
 
90
- __version__ = "2.2.7"
91
+ __version__ = "2.2.9"
91
92
  __all__ = [
92
93
  # Main client
93
94
  "WiiMClient",
@@ -111,6 +112,7 @@ __all__ = [
111
112
  "discover_devices",
112
113
  "discover_via_ssdp",
113
114
  "validate_device",
115
+ "validate_device_strict",
114
116
  # Backoff
115
117
  "BackoffController",
116
118
  # Normalization
@@ -22,6 +22,7 @@ except ImportError:
22
22
  async_search = None # type: ignore[assignment]
23
23
 
24
24
  from .client import WiiMClient
25
+ from .exceptions import WiiMConnectionError
25
26
 
26
27
  _LOGGER = logging.getLogger(__name__)
27
28
 
@@ -32,6 +33,7 @@ __all__ = [
32
33
  "is_known_linkplay",
33
34
  "is_linkplay_device",
34
35
  "validate_device",
36
+ "validate_device_strict",
35
37
  ]
36
38
 
37
39
  # Known LinkPlay/WiiM server patterns (devices we're CERTAIN are LinkPlay)
@@ -485,6 +487,32 @@ async def validate_device(device: DiscoveredDevice) -> DiscoveredDevice:
485
487
  return device
486
488
 
487
489
 
490
+ async def validate_device_strict(device: DiscoveredDevice) -> DiscoveredDevice:
491
+ """Validate a discovered device and raise if it is not usable.
492
+
493
+ ``validate_device()`` is intentionally soft-fail for bulk discovery: it
494
+ returns the input device with ``validated=False`` when the host does not
495
+ pass the LinkPlay/WiiM probe. Use this helper for manual setup flows or
496
+ other code paths where an unvalidated device is an error.
497
+
498
+ Args:
499
+ device: Device to validate.
500
+
501
+ Returns:
502
+ Updated device with full information.
503
+
504
+ Raises:
505
+ WiiMConnectionError: If the host cannot be validated as LinkPlay/WiiM.
506
+ """
507
+ validated_device = await validate_device(device)
508
+ if not validated_device.validated:
509
+ raise WiiMConnectionError(
510
+ f"Device at {device.ip} did not validate as a LinkPlay/WiiM device",
511
+ operation_context="device_validation",
512
+ )
513
+ return validated_device
514
+
515
+
488
516
  async def discover_devices(
489
517
  methods: list[str] | None = None,
490
518
  validate: bool = True,
@@ -359,12 +359,30 @@ class CoverArtManager:
359
359
  _LOGGER.debug("Built metadata update from %s: %s", source_name, update)
360
360
  return update
361
361
 
362
- def _apply_metadata_update(self, update: dict[str, Any], *, source_name: str) -> None:
362
+ def _apply_metadata_update(
363
+ self,
364
+ update: dict[str, Any],
365
+ *,
366
+ source_name: str,
367
+ state_source: str = "http",
368
+ ) -> None:
363
369
  """Apply enrichment metadata to synchronizer, status model, and callbacks."""
364
370
  if not update:
365
371
  return
366
372
 
367
- self.player._state_synchronizer.update_from_http(update, timestamp=time.time())
373
+ timestamp = time.time()
374
+ if state_source == "upnp":
375
+ self.player._state_synchronizer.update_from_upnp(
376
+ update,
377
+ timestamp=timestamp,
378
+ force_metadata_update=True,
379
+ )
380
+ else:
381
+ self.player._state_synchronizer.update_from_http(
382
+ update,
383
+ timestamp=timestamp,
384
+ force_metadata_update=True,
385
+ )
368
386
  merged = self.player._state_synchronizer.get_merged_state()
369
387
  if self.player._status_model:
370
388
  if "title" in update:
@@ -406,7 +424,7 @@ class CoverArtManager:
406
424
  self.player._getinfoex_supported = True
407
425
  update = self._build_metadata_update(merged_state, info, source_name="GetInfoEx")
408
426
  if update:
409
- self._apply_metadata_update(update, source_name="GetInfoEx")
427
+ self._apply_metadata_update(update, source_name="GetInfoEx", state_source="upnp")
410
428
  return update
411
429
 
412
430
  async def _fetch_artwork_from_metainfo(self, merged_state: dict[str, Any]) -> None:
@@ -310,6 +310,7 @@ class StateSynchronizer:
310
310
  data: dict[str, Any],
311
311
  timestamp: float | None = None,
312
312
  source: str = "http",
313
+ force_metadata_update: bool = False,
313
314
  ) -> None:
314
315
  """Update state from HTTP polling data.
315
316
 
@@ -319,6 +320,9 @@ class StateSynchronizer:
319
320
  source: Source identifier for the update. Defaults to "http" for normal
320
321
  HTTP polling. Use "propagated" when state is propagated from master
321
322
  to slave in a group.
323
+ force_metadata_update: Apply metadata fields even when transport is paused/idle.
324
+ Use for explicit enrichment probes that return valid metadata for the current
325
+ paused track; normal status polling should leave this False.
322
326
  """
323
327
  ts = timestamp or time.time()
324
328
 
@@ -374,7 +378,7 @@ class StateSynchronizer:
374
378
  )
375
379
 
376
380
  # Extract metadata (preserve if playing)
377
- if not self._should_clear_metadata():
381
+ if force_metadata_update or not self._should_clear_metadata():
378
382
  for field_name in ["title", "artist", "album", "image_url"]:
379
383
  if field_name in data:
380
384
  value = data.get(field_name)
@@ -395,12 +399,16 @@ class StateSynchronizer:
395
399
  self,
396
400
  data: dict[str, Any],
397
401
  timestamp: float | None = None,
402
+ force_metadata_update: bool = False,
398
403
  ) -> None:
399
404
  """Update state from UPnP event data.
400
405
 
401
406
  Args:
402
407
  data: Dictionary with state fields from UPnP event
403
408
  timestamp: Timestamp of the update (defaults to now)
409
+ force_metadata_update: Apply metadata fields even when transport is paused/idle.
410
+ Use for explicit enrichment probes like GetInfoEx; ordinary UPnP events should
411
+ leave this False so metadata-free stop/idle updates do not clear current fields.
404
412
  """
405
413
  ts = timestamp or time.time()
406
414
 
@@ -453,7 +461,7 @@ class StateSynchronizer:
453
461
  )
454
462
 
455
463
  # Extract metadata (preserve if playing)
456
- if not self._should_clear_metadata():
464
+ if force_metadata_update or not self._should_clear_metadata():
457
465
  for field_name in ["title", "artist", "album", "image_url"]:
458
466
  if field_name in data:
459
467
  value = data.get(field_name)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pywiim
3
- Version: 2.2.7
3
+ Version: 2.2.9
4
4
  Summary: Python library for WiiM/LinkPlay device communication
5
5
  Author-email: Michael Cumming <mjcumming@gmail.com>
6
6
  License: MIT
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes