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.
- {pywiim-2.2.7/pywiim.egg-info → pywiim-2.2.9}/PKG-INFO +1 -1
- {pywiim-2.2.7 → pywiim-2.2.9}/pyproject.toml +1 -1
- {pywiim-2.2.7 → pywiim-2.2.9}/pywiim/__init__.py +3 -1
- {pywiim-2.2.7 → pywiim-2.2.9}/pywiim/discovery.py +28 -0
- {pywiim-2.2.7 → pywiim-2.2.9}/pywiim/player/coverart.py +21 -3
- {pywiim-2.2.7 → pywiim-2.2.9}/pywiim/state.py +10 -2
- {pywiim-2.2.7 → pywiim-2.2.9/pywiim.egg-info}/PKG-INFO +1 -1
- {pywiim-2.2.7 → pywiim-2.2.9}/LICENSE +0 -0
- {pywiim-2.2.7 → pywiim-2.2.9}/README.md +0 -0
- {pywiim-2.2.7 → pywiim-2.2.9}/pywiim/api/__init__.py +0 -0
- {pywiim-2.2.7 → pywiim-2.2.9}/pywiim/api/audio_pro.py +0 -0
- {pywiim-2.2.7 → pywiim-2.2.9}/pywiim/api/audio_settings.py +0 -0
- {pywiim-2.2.7 → pywiim-2.2.9}/pywiim/api/base.py +0 -0
- {pywiim-2.2.7 → pywiim-2.2.9}/pywiim/api/bluetooth.py +0 -0
- {pywiim-2.2.7 → pywiim-2.2.9}/pywiim/api/constants.py +0 -0
- {pywiim-2.2.7 → pywiim-2.2.9}/pywiim/api/device.py +0 -0
- {pywiim-2.2.7 → pywiim-2.2.9}/pywiim/api/diagnostics.py +0 -0
- {pywiim-2.2.7 → pywiim-2.2.9}/pywiim/api/endpoints.py +0 -0
- {pywiim-2.2.7 → pywiim-2.2.9}/pywiim/api/eq.py +0 -0
- {pywiim-2.2.7 → pywiim-2.2.9}/pywiim/api/firmware.py +0 -0
- {pywiim-2.2.7 → pywiim-2.2.9}/pywiim/api/group.py +0 -0
- {pywiim-2.2.7 → pywiim-2.2.9}/pywiim/api/lms.py +0 -0
- {pywiim-2.2.7 → pywiim-2.2.9}/pywiim/api/loop_mode.py +0 -0
- {pywiim-2.2.7 → pywiim-2.2.9}/pywiim/api/misc.py +0 -0
- {pywiim-2.2.7 → pywiim-2.2.9}/pywiim/api/parser.py +0 -0
- {pywiim-2.2.7 → pywiim-2.2.9}/pywiim/api/peq.py +0 -0
- {pywiim-2.2.7 → pywiim-2.2.9}/pywiim/api/playback.py +0 -0
- {pywiim-2.2.7 → pywiim-2.2.9}/pywiim/api/preset.py +0 -0
- {pywiim-2.2.7 → pywiim-2.2.9}/pywiim/api/ssl.py +0 -0
- {pywiim-2.2.7 → pywiim-2.2.9}/pywiim/api/subwoofer.py +0 -0
- {pywiim-2.2.7 → pywiim-2.2.9}/pywiim/api/timer.py +0 -0
- {pywiim-2.2.7 → pywiim-2.2.9}/pywiim/backoff.py +0 -0
- {pywiim-2.2.7 → pywiim-2.2.9}/pywiim/capabilities.py +0 -0
- {pywiim-2.2.7 → pywiim-2.2.9}/pywiim/cli/__init__.py +0 -0
- {pywiim-2.2.7 → pywiim-2.2.9}/pywiim/cli/diagnostics.py +0 -0
- {pywiim-2.2.7 → pywiim-2.2.9}/pywiim/cli/discovery_cli.py +0 -0
- {pywiim-2.2.7 → pywiim-2.2.9}/pywiim/cli/group_test_cli.py +0 -0
- {pywiim-2.2.7 → pywiim-2.2.9}/pywiim/cli/join_test_cli.py +0 -0
- {pywiim-2.2.7 → pywiim-2.2.9}/pywiim/cli/monitor_cli.py +0 -0
- {pywiim-2.2.7 → pywiim-2.2.9}/pywiim/cli/verify_cli.py +0 -0
- {pywiim-2.2.7 → pywiim-2.2.9}/pywiim/client.py +0 -0
- {pywiim-2.2.7 → pywiim-2.2.9}/pywiim/device_capabilities.py +0 -0
- {pywiim-2.2.7 → pywiim-2.2.9}/pywiim/exceptions.py +0 -0
- {pywiim-2.2.7 → pywiim-2.2.9}/pywiim/group.py +0 -0
- {pywiim-2.2.7 → pywiim-2.2.9}/pywiim/group_helpers.py +0 -0
- {pywiim-2.2.7 → pywiim-2.2.9}/pywiim/mcp/__init__.py +0 -0
- {pywiim-2.2.7 → pywiim-2.2.9}/pywiim/mcp/__main__.py +0 -0
- {pywiim-2.2.7 → pywiim-2.2.9}/pywiim/mcp/config.example.json +0 -0
- {pywiim-2.2.7 → pywiim-2.2.9}/pywiim/mcp/config.py +0 -0
- {pywiim-2.2.7 → pywiim-2.2.9}/pywiim/mcp/context.py +0 -0
- {pywiim-2.2.7 → pywiim-2.2.9}/pywiim/mcp/server.py +0 -0
- {pywiim-2.2.7 → pywiim-2.2.9}/pywiim/metadata.py +0 -0
- {pywiim-2.2.7 → pywiim-2.2.9}/pywiim/model_names.py +0 -0
- {pywiim-2.2.7 → pywiim-2.2.9}/pywiim/models.py +0 -0
- {pywiim-2.2.7 → pywiim-2.2.9}/pywiim/normalize.py +0 -0
- {pywiim-2.2.7 → pywiim-2.2.9}/pywiim/player/__init__.py +0 -0
- {pywiim-2.2.7 → pywiim-2.2.9}/pywiim/player/audio.py +0 -0
- {pywiim-2.2.7 → pywiim-2.2.9}/pywiim/player/base.py +0 -0
- {pywiim-2.2.7 → pywiim-2.2.9}/pywiim/player/bluetooth.py +0 -0
- {pywiim-2.2.7 → pywiim-2.2.9}/pywiim/player/debounce.py +0 -0
- {pywiim-2.2.7 → pywiim-2.2.9}/pywiim/player/diagnostics.py +0 -0
- {pywiim-2.2.7 → pywiim-2.2.9}/pywiim/player/groupops.py +0 -0
- {pywiim-2.2.7 → pywiim-2.2.9}/pywiim/player/media.py +0 -0
- {pywiim-2.2.7 → pywiim-2.2.9}/pywiim/player/playback.py +0 -0
- {pywiim-2.2.7 → pywiim-2.2.9}/pywiim/player/properties.py +0 -0
- {pywiim-2.2.7 → pywiim-2.2.9}/pywiim/player/source_capabilities.py +0 -0
- {pywiim-2.2.7 → pywiim-2.2.9}/pywiim/player/statemgr.py +0 -0
- {pywiim-2.2.7 → pywiim-2.2.9}/pywiim/player/stream.py +0 -0
- {pywiim-2.2.7 → pywiim-2.2.9}/pywiim/player/stream_enricher.py +0 -0
- {pywiim-2.2.7 → pywiim-2.2.9}/pywiim/player/volume.py +0 -0
- {pywiim-2.2.7 → pywiim-2.2.9}/pywiim/polling.py +0 -0
- {pywiim-2.2.7 → pywiim-2.2.9}/pywiim/profiles.py +0 -0
- {pywiim-2.2.7 → pywiim-2.2.9}/pywiim/py.typed +0 -0
- {pywiim-2.2.7 → pywiim-2.2.9}/pywiim/role.py +0 -0
- {pywiim-2.2.7 → pywiim-2.2.9}/pywiim/upnp/__init__.py +0 -0
- {pywiim-2.2.7 → pywiim-2.2.9}/pywiim/upnp/client.py +0 -0
- {pywiim-2.2.7 → pywiim-2.2.9}/pywiim/upnp/eventer.py +0 -0
- {pywiim-2.2.7 → pywiim-2.2.9}/pywiim/upnp/health.py +0 -0
- {pywiim-2.2.7 → pywiim-2.2.9}/pywiim/upnp/metadata.py +0 -0
- {pywiim-2.2.7 → pywiim-2.2.9}/pywiim.egg-info/SOURCES.txt +0 -0
- {pywiim-2.2.7 → pywiim-2.2.9}/pywiim.egg-info/dependency_links.txt +0 -0
- {pywiim-2.2.7 → pywiim-2.2.9}/pywiim.egg-info/entry_points.txt +0 -0
- {pywiim-2.2.7 → pywiim-2.2.9}/pywiim.egg-info/requires.txt +0 -0
- {pywiim-2.2.7 → pywiim-2.2.9}/pywiim.egg-info/top_level.txt +0 -0
- {pywiim-2.2.7 → pywiim-2.2.9}/setup.cfg +0 -0
|
@@ -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.
|
|
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(
|
|
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
|
-
|
|
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)
|
|
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
|
|
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
|