pywiim 2.3.1__tar.gz → 2.3.2__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.3.1/pywiim.egg-info → pywiim-2.3.2}/PKG-INFO +1 -1
- {pywiim-2.3.1 → pywiim-2.3.2}/pyproject.toml +1 -1
- {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/__init__.py +1 -1
- {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/api/base.py +9 -1
- {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/player/properties.py +25 -7
- {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/profiles.py +4 -21
- {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/state.py +8 -1
- {pywiim-2.3.1 → pywiim-2.3.2/pywiim.egg-info}/PKG-INFO +1 -1
- {pywiim-2.3.1 → pywiim-2.3.2}/LICENSE +0 -0
- {pywiim-2.3.1 → pywiim-2.3.2}/README.md +0 -0
- {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/api/__init__.py +0 -0
- {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/api/audio_pro.py +0 -0
- {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/api/audio_settings.py +0 -0
- {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/api/bluetooth.py +0 -0
- {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/api/constants.py +0 -0
- {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/api/device.py +0 -0
- {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/api/diagnostics.py +0 -0
- {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/api/endpoints.py +0 -0
- {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/api/eq.py +0 -0
- {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/api/firmware.py +0 -0
- {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/api/group.py +0 -0
- {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/api/lms.py +0 -0
- {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/api/loop_mode.py +0 -0
- {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/api/misc.py +0 -0
- {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/api/parser.py +0 -0
- {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/api/peq.py +0 -0
- {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/api/playback.py +0 -0
- {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/api/preset.py +0 -0
- {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/api/ssl.py +0 -0
- {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/api/subwoofer.py +0 -0
- {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/api/timer.py +0 -0
- {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/backoff.py +0 -0
- {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/capabilities.py +0 -0
- {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/cli/__init__.py +0 -0
- {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/cli/diagnostics.py +0 -0
- {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/cli/discovery_cli.py +0 -0
- {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/cli/group_test_cli.py +0 -0
- {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/cli/join_test_cli.py +0 -0
- {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/cli/monitor_cli.py +0 -0
- {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/cli/verify_cli.py +0 -0
- {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/client.py +0 -0
- {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/device_capabilities.py +0 -0
- {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/discovery.py +0 -0
- {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/exceptions.py +0 -0
- {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/group.py +0 -0
- {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/group_helpers.py +0 -0
- {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/mcp/__init__.py +0 -0
- {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/mcp/__main__.py +0 -0
- {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/mcp/config.example.json +0 -0
- {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/mcp/config.py +0 -0
- {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/mcp/context.py +0 -0
- {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/mcp/server.py +0 -0
- {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/metadata.py +0 -0
- {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/model_names.py +0 -0
- {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/models.py +0 -0
- {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/normalize.py +0 -0
- {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/player/__init__.py +0 -0
- {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/player/audio.py +0 -0
- {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/player/base.py +0 -0
- {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/player/bluetooth.py +0 -0
- {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/player/coverart.py +0 -0
- {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/player/debounce.py +0 -0
- {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/player/diagnostics.py +0 -0
- {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/player/groupops.py +0 -0
- {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/player/media.py +0 -0
- {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/player/playback.py +0 -0
- {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/player/source_capabilities.py +0 -0
- {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/player/statemgr.py +0 -0
- {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/player/stream.py +0 -0
- {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/player/stream_enricher.py +0 -0
- {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/player/volume.py +0 -0
- {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/polling.py +0 -0
- {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/py.typed +0 -0
- {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/role.py +0 -0
- {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/upnp/__init__.py +0 -0
- {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/upnp/client.py +0 -0
- {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/upnp/eventer.py +0 -0
- {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/upnp/health.py +0 -0
- {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/upnp/metadata.py +0 -0
- {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/upnp/playqueue.py +0 -0
- {pywiim-2.3.1 → pywiim-2.3.2}/pywiim.egg-info/SOURCES.txt +0 -0
- {pywiim-2.3.1 → pywiim-2.3.2}/pywiim.egg-info/dependency_links.txt +0 -0
- {pywiim-2.3.1 → pywiim-2.3.2}/pywiim.egg-info/entry_points.txt +0 -0
- {pywiim-2.3.1 → pywiim-2.3.2}/pywiim.egg-info/requires.txt +0 -0
- {pywiim-2.3.1 → pywiim-2.3.2}/pywiim.egg-info/top_level.txt +0 -0
- {pywiim-2.3.1 → pywiim-2.3.2}/setup.cfg +0 -0
|
@@ -489,7 +489,15 @@ class BaseWiiMClient:
|
|
|
489
489
|
except Exception: # noqa: BLE001
|
|
490
490
|
pass # Device info not available, continue without it
|
|
491
491
|
|
|
492
|
-
|
|
492
|
+
error_cls = (
|
|
493
|
+
WiiMConnectionError
|
|
494
|
+
if isinstance(
|
|
495
|
+
err,
|
|
496
|
+
(aiohttp.ClientConnectorError, aiohttp.ServerDisconnectedError, WiiMConnectionError),
|
|
497
|
+
)
|
|
498
|
+
else WiiMRequestError
|
|
499
|
+
)
|
|
500
|
+
raise error_cls(
|
|
493
501
|
f"Request failed after {retry_count} attempts: {err}",
|
|
494
502
|
endpoint=endpoint,
|
|
495
503
|
attempts=retry_count,
|
|
@@ -238,11 +238,22 @@ class PlayerProperties:
|
|
|
238
238
|
# === Media Metadata ===
|
|
239
239
|
|
|
240
240
|
def _status_field(self, *names: str) -> str | None:
|
|
241
|
-
"""Return the first non-empty attribute from merged state or cached status.
|
|
242
|
-
|
|
243
|
-
|
|
241
|
+
"""Return the first non-empty attribute from merged state or cached status.
|
|
242
|
+
|
|
243
|
+
Only fall back to the raw cached status for names the synchronizer has
|
|
244
|
+
never resolved. If it resolved a name to no value (live stream, source
|
|
245
|
+
change), that decision must not be overridden by leftover firmware
|
|
246
|
+
fields still sitting on ``_status_model`` (wiim #263).
|
|
247
|
+
"""
|
|
248
|
+
state_obj = self.player._state_synchronizer.get_state_object()
|
|
249
|
+
names_unresolved_by_synchronizer: list[str] = []
|
|
244
250
|
for n in names:
|
|
245
|
-
|
|
251
|
+
field = getattr(state_obj, n, None)
|
|
252
|
+
if field is None:
|
|
253
|
+
names_unresolved_by_synchronizer.append(n)
|
|
254
|
+
continue
|
|
255
|
+
|
|
256
|
+
val = field.value
|
|
246
257
|
if val is not None:
|
|
247
258
|
# Field-aware filtering: artwork fields must be valid URLs,
|
|
248
259
|
# other metadata must be non-placeholder values.
|
|
@@ -269,11 +280,18 @@ class PlayerProperties:
|
|
|
269
280
|
if val not in (None, ""):
|
|
270
281
|
return str(val) if val is not None else None
|
|
271
282
|
|
|
272
|
-
# Fallback to cached status
|
|
273
|
-
|
|
283
|
+
# Fallback to cached status only before the first merge. After that the
|
|
284
|
+
# raw status model is a leftover firmware snapshot and must not reopen
|
|
285
|
+
# fields the synchronizer already decided are empty (wiim #263).
|
|
286
|
+
if (
|
|
287
|
+
self.player._status_model is None
|
|
288
|
+
or not names_unresolved_by_synchronizer
|
|
289
|
+
or state_obj.http_last_update is not None
|
|
290
|
+
or state_obj.upnp_last_update is not None
|
|
291
|
+
):
|
|
274
292
|
return None
|
|
275
293
|
|
|
276
|
-
for n in
|
|
294
|
+
for n in names_unresolved_by_synchronizer:
|
|
277
295
|
if hasattr(self.player._status_model, n):
|
|
278
296
|
val = getattr(self.player._status_model, n)
|
|
279
297
|
if n in {"image_url", "entity_picture", "cover_url"}:
|
|
@@ -520,18 +520,7 @@ def get_device_profile(device_info: DeviceInfo) -> DeviceProfile:
|
|
|
520
520
|
device_info.firmware,
|
|
521
521
|
)
|
|
522
522
|
# Return a modified profile with WiFi Direct enabled
|
|
523
|
-
return
|
|
524
|
-
vendor=profile.vendor,
|
|
525
|
-
generation=profile.generation,
|
|
526
|
-
display_name=profile.display_name,
|
|
527
|
-
loop_mode_scheme=profile.loop_mode_scheme,
|
|
528
|
-
state_sources=profile.state_sources,
|
|
529
|
-
connection=profile.connection,
|
|
530
|
-
endpoints=profile.endpoints,
|
|
531
|
-
grouping=GroupingConfig(
|
|
532
|
-
uses_wifi_direct=True,
|
|
533
|
-
),
|
|
534
|
-
)
|
|
523
|
+
return replace(profile, grouping=GroupingConfig(uses_wifi_direct=True))
|
|
535
524
|
|
|
536
525
|
return profile
|
|
537
526
|
|
|
@@ -553,17 +542,11 @@ def get_device_profile(device_info: DeviceInfo) -> DeviceProfile:
|
|
|
553
542
|
device_info.wmrm_version,
|
|
554
543
|
device_info.firmware,
|
|
555
544
|
)
|
|
556
|
-
return
|
|
557
|
-
|
|
545
|
+
return replace(
|
|
546
|
+
profile,
|
|
558
547
|
generation="gen1",
|
|
559
548
|
display_name=f"{profile.display_name} (Gen1)",
|
|
560
|
-
|
|
561
|
-
state_sources=profile.state_sources,
|
|
562
|
-
connection=profile.connection,
|
|
563
|
-
endpoints=profile.endpoints,
|
|
564
|
-
grouping=GroupingConfig(
|
|
565
|
-
uses_wifi_direct=True,
|
|
566
|
-
),
|
|
549
|
+
grouping=GroupingConfig(uses_wifi_direct=True),
|
|
567
550
|
)
|
|
568
551
|
|
|
569
552
|
return profile
|
|
@@ -951,10 +951,17 @@ class StateSynchronizer:
|
|
|
951
951
|
Firmware often keeps the previous source's metadata in getPlayerStatusEx
|
|
952
952
|
(Spotify → network stream). Clearing here prevents a permanently stale track.
|
|
953
953
|
"""
|
|
954
|
+
now = time.time()
|
|
954
955
|
for field_name in ("title", "artist", "album", "image_url", "duration"):
|
|
955
956
|
self._http_state.pop(field_name, None)
|
|
956
957
|
self._upnp_state.pop(field_name, None)
|
|
957
|
-
|
|
958
|
+
# Keep a resolved-to-None field so Player._status_field does not
|
|
959
|
+
# fall back to leftover firmware values on _status_model (#263).
|
|
960
|
+
setattr(
|
|
961
|
+
self._merged_state,
|
|
962
|
+
field_name,
|
|
963
|
+
TimestampedField(value=None, source="http", timestamp=now),
|
|
964
|
+
)
|
|
958
965
|
|
|
959
966
|
def _should_clear_metadata(self) -> bool:
|
|
960
967
|
"""Determine if metadata should be cleared.
|
|
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
|