pywiim 2.3.3__tar.gz → 2.3.4__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.3/pywiim.egg-info → pywiim-2.3.4}/PKG-INFO +1 -1
- {pywiim-2.3.3 → pywiim-2.3.4}/pyproject.toml +1 -1
- {pywiim-2.3.3 → pywiim-2.3.4}/pywiim/__init__.py +1 -1
- {pywiim-2.3.3 → pywiim-2.3.4}/pywiim/player/statemgr.py +48 -6
- {pywiim-2.3.3 → pywiim-2.3.4}/pywiim/state.py +77 -86
- {pywiim-2.3.3 → pywiim-2.3.4/pywiim.egg-info}/PKG-INFO +1 -1
- {pywiim-2.3.3 → pywiim-2.3.4}/LICENSE +0 -0
- {pywiim-2.3.3 → pywiim-2.3.4}/README.md +0 -0
- {pywiim-2.3.3 → pywiim-2.3.4}/pywiim/api/__init__.py +0 -0
- {pywiim-2.3.3 → pywiim-2.3.4}/pywiim/api/audio_pro.py +0 -0
- {pywiim-2.3.3 → pywiim-2.3.4}/pywiim/api/audio_settings.py +0 -0
- {pywiim-2.3.3 → pywiim-2.3.4}/pywiim/api/base.py +0 -0
- {pywiim-2.3.3 → pywiim-2.3.4}/pywiim/api/bluetooth.py +0 -0
- {pywiim-2.3.3 → pywiim-2.3.4}/pywiim/api/constants.py +0 -0
- {pywiim-2.3.3 → pywiim-2.3.4}/pywiim/api/device.py +0 -0
- {pywiim-2.3.3 → pywiim-2.3.4}/pywiim/api/diagnostics.py +0 -0
- {pywiim-2.3.3 → pywiim-2.3.4}/pywiim/api/endpoints.py +0 -0
- {pywiim-2.3.3 → pywiim-2.3.4}/pywiim/api/eq.py +0 -0
- {pywiim-2.3.3 → pywiim-2.3.4}/pywiim/api/firmware.py +0 -0
- {pywiim-2.3.3 → pywiim-2.3.4}/pywiim/api/group.py +0 -0
- {pywiim-2.3.3 → pywiim-2.3.4}/pywiim/api/lms.py +0 -0
- {pywiim-2.3.3 → pywiim-2.3.4}/pywiim/api/loop_mode.py +0 -0
- {pywiim-2.3.3 → pywiim-2.3.4}/pywiim/api/misc.py +0 -0
- {pywiim-2.3.3 → pywiim-2.3.4}/pywiim/api/parser.py +0 -0
- {pywiim-2.3.3 → pywiim-2.3.4}/pywiim/api/peq.py +0 -0
- {pywiim-2.3.3 → pywiim-2.3.4}/pywiim/api/playback.py +0 -0
- {pywiim-2.3.3 → pywiim-2.3.4}/pywiim/api/preset.py +0 -0
- {pywiim-2.3.3 → pywiim-2.3.4}/pywiim/api/ssl.py +0 -0
- {pywiim-2.3.3 → pywiim-2.3.4}/pywiim/api/subwoofer.py +0 -0
- {pywiim-2.3.3 → pywiim-2.3.4}/pywiim/api/timer.py +0 -0
- {pywiim-2.3.3 → pywiim-2.3.4}/pywiim/backoff.py +0 -0
- {pywiim-2.3.3 → pywiim-2.3.4}/pywiim/capabilities.py +0 -0
- {pywiim-2.3.3 → pywiim-2.3.4}/pywiim/cli/__init__.py +0 -0
- {pywiim-2.3.3 → pywiim-2.3.4}/pywiim/cli/diagnostics.py +0 -0
- {pywiim-2.3.3 → pywiim-2.3.4}/pywiim/cli/discovery_cli.py +0 -0
- {pywiim-2.3.3 → pywiim-2.3.4}/pywiim/cli/group_test_cli.py +0 -0
- {pywiim-2.3.3 → pywiim-2.3.4}/pywiim/cli/join_test_cli.py +0 -0
- {pywiim-2.3.3 → pywiim-2.3.4}/pywiim/cli/monitor_cli.py +0 -0
- {pywiim-2.3.3 → pywiim-2.3.4}/pywiim/cli/verify_cli.py +0 -0
- {pywiim-2.3.3 → pywiim-2.3.4}/pywiim/client.py +0 -0
- {pywiim-2.3.3 → pywiim-2.3.4}/pywiim/device_capabilities.py +0 -0
- {pywiim-2.3.3 → pywiim-2.3.4}/pywiim/discovery.py +0 -0
- {pywiim-2.3.3 → pywiim-2.3.4}/pywiim/exceptions.py +0 -0
- {pywiim-2.3.3 → pywiim-2.3.4}/pywiim/group.py +0 -0
- {pywiim-2.3.3 → pywiim-2.3.4}/pywiim/group_helpers.py +0 -0
- {pywiim-2.3.3 → pywiim-2.3.4}/pywiim/mcp/__init__.py +0 -0
- {pywiim-2.3.3 → pywiim-2.3.4}/pywiim/mcp/__main__.py +0 -0
- {pywiim-2.3.3 → pywiim-2.3.4}/pywiim/mcp/config.example.json +0 -0
- {pywiim-2.3.3 → pywiim-2.3.4}/pywiim/mcp/config.py +0 -0
- {pywiim-2.3.3 → pywiim-2.3.4}/pywiim/mcp/context.py +0 -0
- {pywiim-2.3.3 → pywiim-2.3.4}/pywiim/mcp/server.py +0 -0
- {pywiim-2.3.3 → pywiim-2.3.4}/pywiim/metadata.py +0 -0
- {pywiim-2.3.3 → pywiim-2.3.4}/pywiim/model_names.py +0 -0
- {pywiim-2.3.3 → pywiim-2.3.4}/pywiim/models.py +0 -0
- {pywiim-2.3.3 → pywiim-2.3.4}/pywiim/normalize.py +0 -0
- {pywiim-2.3.3 → pywiim-2.3.4}/pywiim/player/__init__.py +0 -0
- {pywiim-2.3.3 → pywiim-2.3.4}/pywiim/player/audio.py +0 -0
- {pywiim-2.3.3 → pywiim-2.3.4}/pywiim/player/base.py +0 -0
- {pywiim-2.3.3 → pywiim-2.3.4}/pywiim/player/bluetooth.py +0 -0
- {pywiim-2.3.3 → pywiim-2.3.4}/pywiim/player/coverart.py +0 -0
- {pywiim-2.3.3 → pywiim-2.3.4}/pywiim/player/debounce.py +0 -0
- {pywiim-2.3.3 → pywiim-2.3.4}/pywiim/player/diagnostics.py +0 -0
- {pywiim-2.3.3 → pywiim-2.3.4}/pywiim/player/groupops.py +0 -0
- {pywiim-2.3.3 → pywiim-2.3.4}/pywiim/player/media.py +0 -0
- {pywiim-2.3.3 → pywiim-2.3.4}/pywiim/player/playback.py +0 -0
- {pywiim-2.3.3 → pywiim-2.3.4}/pywiim/player/properties.py +0 -0
- {pywiim-2.3.3 → pywiim-2.3.4}/pywiim/player/source_capabilities.py +0 -0
- {pywiim-2.3.3 → pywiim-2.3.4}/pywiim/player/stream.py +0 -0
- {pywiim-2.3.3 → pywiim-2.3.4}/pywiim/player/stream_enricher.py +0 -0
- {pywiim-2.3.3 → pywiim-2.3.4}/pywiim/player/volume.py +0 -0
- {pywiim-2.3.3 → pywiim-2.3.4}/pywiim/polling.py +0 -0
- {pywiim-2.3.3 → pywiim-2.3.4}/pywiim/profiles.py +0 -0
- {pywiim-2.3.3 → pywiim-2.3.4}/pywiim/py.typed +0 -0
- {pywiim-2.3.3 → pywiim-2.3.4}/pywiim/role.py +0 -0
- {pywiim-2.3.3 → pywiim-2.3.4}/pywiim/upnp/__init__.py +0 -0
- {pywiim-2.3.3 → pywiim-2.3.4}/pywiim/upnp/client.py +0 -0
- {pywiim-2.3.3 → pywiim-2.3.4}/pywiim/upnp/eventer.py +0 -0
- {pywiim-2.3.3 → pywiim-2.3.4}/pywiim/upnp/health.py +0 -0
- {pywiim-2.3.3 → pywiim-2.3.4}/pywiim/upnp/metadata.py +0 -0
- {pywiim-2.3.3 → pywiim-2.3.4}/pywiim/upnp/playqueue.py +0 -0
- {pywiim-2.3.3 → pywiim-2.3.4}/pywiim.egg-info/SOURCES.txt +0 -0
- {pywiim-2.3.3 → pywiim-2.3.4}/pywiim.egg-info/dependency_links.txt +0 -0
- {pywiim-2.3.3 → pywiim-2.3.4}/pywiim.egg-info/entry_points.txt +0 -0
- {pywiim-2.3.3 → pywiim-2.3.4}/pywiim.egg-info/requires.txt +0 -0
- {pywiim-2.3.3 → pywiim-2.3.4}/pywiim.egg-info/top_level.txt +0 -0
- {pywiim-2.3.3 → pywiim-2.3.4}/setup.cfg +0 -0
|
@@ -37,6 +37,19 @@ _LOGGER = logging.getLogger(__name__)
|
|
|
37
37
|
# UPnP retry cooldown - wait this many seconds between failed creation attempts
|
|
38
38
|
UPNP_RETRY_COOLDOWN = 60.0
|
|
39
39
|
|
|
40
|
+
# Sources where play_url() filename fallback is the intended title.
|
|
41
|
+
# WiiM reports URL playback as custompushurl (display name "URL Stream").
|
|
42
|
+
_URL_PLAYBACK_SOURCES = frozenset({"wifi", "network", "ethernet", "usb", "udisk", "http", "custompushurl"})
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _is_url_playback_source(source: Any) -> bool:
|
|
46
|
+
"""Return True for URL/network playback sources (keep play_url filename)."""
|
|
47
|
+
if source is None:
|
|
48
|
+
return False
|
|
49
|
+
key = str(source).strip().lower().replace("-", "_").replace(" ", "_")
|
|
50
|
+
return key in _URL_PLAYBACK_SOURCES or key in {"wi_fi"}
|
|
51
|
+
|
|
52
|
+
|
|
40
53
|
# Standard sources that shouldn't be cleared when checking for multiroom/master names
|
|
41
54
|
STANDARD_SOURCES = {
|
|
42
55
|
"spotify",
|
|
@@ -340,9 +353,11 @@ class StateManager:
|
|
|
340
353
|
async def _refresh_core_status(self) -> PlayerStatus:
|
|
341
354
|
"""Fetch core player status and update state synchronizer.
|
|
342
355
|
|
|
343
|
-
For slaves in multi-room groups, we
|
|
344
|
-
|
|
345
|
-
via propagation.
|
|
356
|
+
For slaves in multi-room groups, we poll the capability-configured status
|
|
357
|
+
endpoint for volume/mute/group only. Playback state (track, position, etc.)
|
|
358
|
+
comes from the master via propagation. Volume and mute are written to the
|
|
359
|
+
state synchronizer so ``Player.volume_level`` / ``is_muted`` reflect device
|
|
360
|
+
changes (physical buttons, app) on the next poll.
|
|
346
361
|
|
|
347
362
|
Returns:
|
|
348
363
|
PlayerStatus model from device.
|
|
@@ -380,12 +395,18 @@ class StateManager:
|
|
|
380
395
|
status = PlayerStatus.model_construct() # Empty status, fields populated below
|
|
381
396
|
|
|
382
397
|
# Update volume/mute/group from device status (these are slave-specific)
|
|
398
|
+
observed_volume = False
|
|
399
|
+
observed_mute = False
|
|
383
400
|
if "volume" in status_dict:
|
|
384
401
|
vol = status_dict.get("volume")
|
|
385
402
|
if vol is not None:
|
|
386
403
|
status.volume = int(vol) if not isinstance(vol, int) else vol
|
|
404
|
+
observed_volume = True
|
|
387
405
|
if "mute" in status_dict:
|
|
388
|
-
|
|
406
|
+
mute_val = status_dict.get("mute")
|
|
407
|
+
if mute_val is not None:
|
|
408
|
+
status.mute = mute_val
|
|
409
|
+
observed_mute = True
|
|
389
410
|
if "group" in status_dict:
|
|
390
411
|
status.group = status_dict.get("group")
|
|
391
412
|
|
|
@@ -402,12 +423,30 @@ class StateManager:
|
|
|
402
423
|
mute_from_upnp = await self.player._upnp_client.get_mute()
|
|
403
424
|
if volume_from_upnp is not None:
|
|
404
425
|
status.volume = volume_from_upnp
|
|
426
|
+
observed_volume = True
|
|
405
427
|
if mute_from_upnp is not None:
|
|
406
428
|
status.mute = mute_from_upnp
|
|
429
|
+
observed_mute = True
|
|
407
430
|
except Exception as err:
|
|
408
431
|
_LOGGER.debug("UPnP GetVolume failed for slave %s: %s", self.player.host, err)
|
|
409
432
|
|
|
410
433
|
self.player._status_model = status
|
|
434
|
+
|
|
435
|
+
# volume_level / is_muted read the synchronizer first. Physical-button
|
|
436
|
+
# and app changes on a slave only arrive via this poll (HA does not
|
|
437
|
+
# subscribe to UPnP events), so we must push slave-local volume/mute
|
|
438
|
+
# without playback/metadata (those stay master-propagated).
|
|
439
|
+
# Only stamp fields observed this cycle so a missing reading cannot
|
|
440
|
+
# refresh a stale cache timestamp over a newer UPnP event.
|
|
441
|
+
# See: https://github.com/mjcumming/wiim/issues/269
|
|
442
|
+
http_update: dict[str, Any] = {}
|
|
443
|
+
if observed_volume and status.volume is not None:
|
|
444
|
+
http_update["volume"] = status.volume
|
|
445
|
+
if observed_mute and status.mute is not None:
|
|
446
|
+
http_update["muted"] = status.mute
|
|
447
|
+
if http_update:
|
|
448
|
+
self.player._state_synchronizer.update_from_http(http_update)
|
|
449
|
+
|
|
411
450
|
self.player._last_refresh = time.time()
|
|
412
451
|
self.player._available = True
|
|
413
452
|
return status
|
|
@@ -670,8 +709,11 @@ class StateManager:
|
|
|
670
709
|
self._last_source = current_source
|
|
671
710
|
elif source_changed:
|
|
672
711
|
self._last_source = current_source
|
|
673
|
-
#
|
|
674
|
-
|
|
712
|
+
# Filename fallback is for URL/network playback. Keep it when
|
|
713
|
+
# switching *to* wifi/network (play_url just changed the source).
|
|
714
|
+
# Clear it for physical inputs and other sources (#263).
|
|
715
|
+
if not _is_url_playback_source(current_source):
|
|
716
|
+
self.player._last_played_url = None
|
|
675
717
|
if status:
|
|
676
718
|
status.title = None
|
|
677
719
|
status.artist = None
|
|
@@ -347,13 +347,6 @@ class StateSynchronizer:
|
|
|
347
347
|
timestamp=ts,
|
|
348
348
|
)
|
|
349
349
|
|
|
350
|
-
if "duration" in data and not self._is_leftover_after_source_change("duration", data.get("duration")):
|
|
351
|
-
self._http_state["duration"] = TimestampedField(
|
|
352
|
-
value=data.get("duration"),
|
|
353
|
-
source=source,
|
|
354
|
-
timestamp=ts,
|
|
355
|
-
)
|
|
356
|
-
|
|
357
350
|
# Extract volume and mute
|
|
358
351
|
# Only update if value is not None (preserve existing values when API doesn't return volume)
|
|
359
352
|
# This prevents clearing volume when some devices (e.g., Audio Pro) don't return volume
|
|
@@ -372,40 +365,14 @@ class StateSynchronizer:
|
|
|
372
365
|
timestamp=ts,
|
|
373
366
|
)
|
|
374
367
|
|
|
375
|
-
#
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
self._remember_stale_metadata(data)
|
|
384
|
-
self._clear_track_metadata()
|
|
385
|
-
self._http_state["source"] = TimestampedField(
|
|
386
|
-
value=new_source,
|
|
387
|
-
source=source,
|
|
388
|
-
timestamp=ts,
|
|
389
|
-
)
|
|
390
|
-
|
|
391
|
-
self._release_stale_guard_if_fresh_track(data)
|
|
392
|
-
|
|
393
|
-
# Extract metadata (preserve if playing)
|
|
394
|
-
if not source_changed and (force_metadata_update or not self._should_clear_metadata()):
|
|
395
|
-
for field_name in ["title", "artist", "album", "image_url"]:
|
|
396
|
-
if field_name in data:
|
|
397
|
-
value = data.get(field_name)
|
|
398
|
-
if self._is_leftover_after_source_change(field_name, value):
|
|
399
|
-
continue
|
|
400
|
-
# Always update if field is present in data (even if None)
|
|
401
|
-
# This ensures metadata gets populated when available
|
|
402
|
-
# If value is None/empty, we still update to track that HTTP doesn't have it
|
|
403
|
-
# (but existing metadata from other sources will be preserved via merge)
|
|
404
|
-
self._http_state[field_name] = TimestampedField(
|
|
405
|
-
value=value,
|
|
406
|
-
source=source,
|
|
407
|
-
timestamp=ts,
|
|
408
|
-
)
|
|
368
|
+
# Source first so leftover-guard is set before duration/title are applied.
|
|
369
|
+
self._apply_source_and_track_fields(
|
|
370
|
+
data,
|
|
371
|
+
self._http_state,
|
|
372
|
+
source,
|
|
373
|
+
ts,
|
|
374
|
+
force_metadata_update=force_metadata_update,
|
|
375
|
+
)
|
|
409
376
|
|
|
410
377
|
self._merged_state.http_last_update = ts
|
|
411
378
|
self._merge_state()
|
|
@@ -445,13 +412,6 @@ class StateSynchronizer:
|
|
|
445
412
|
timestamp=ts,
|
|
446
413
|
)
|
|
447
414
|
|
|
448
|
-
if "duration" in data and not self._is_leftover_after_source_change("duration", data.get("duration")):
|
|
449
|
-
self._upnp_state["duration"] = TimestampedField(
|
|
450
|
-
value=data.get("duration"),
|
|
451
|
-
source="upnp",
|
|
452
|
-
timestamp=ts,
|
|
453
|
-
)
|
|
454
|
-
|
|
455
415
|
# Extract volume and mute
|
|
456
416
|
if "volume" in data:
|
|
457
417
|
self._upnp_state["volume"] = TimestampedField(
|
|
@@ -467,40 +427,13 @@ class StateSynchronizer:
|
|
|
467
427
|
timestamp=ts,
|
|
468
428
|
)
|
|
469
429
|
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
if source_changed:
|
|
478
|
-
self._remember_stale_metadata(data)
|
|
479
|
-
self._clear_track_metadata()
|
|
480
|
-
self._upnp_state["source"] = TimestampedField(
|
|
481
|
-
value=new_source,
|
|
482
|
-
source="upnp",
|
|
483
|
-
timestamp=ts,
|
|
484
|
-
)
|
|
485
|
-
|
|
486
|
-
self._release_stale_guard_if_fresh_track(data)
|
|
487
|
-
|
|
488
|
-
# Extract metadata (preserve if playing)
|
|
489
|
-
if not source_changed and (force_metadata_update or not self._should_clear_metadata()):
|
|
490
|
-
for field_name in ["title", "artist", "album", "image_url"]:
|
|
491
|
-
if field_name in data:
|
|
492
|
-
value = data.get(field_name)
|
|
493
|
-
if self._is_leftover_after_source_change(field_name, value):
|
|
494
|
-
continue
|
|
495
|
-
# Always update if field is present in data (even if None)
|
|
496
|
-
# This ensures metadata gets populated when available
|
|
497
|
-
# If value is None/empty, we still update to track that UPnP doesn't have it
|
|
498
|
-
# (but existing metadata from other sources will be preserved via merge)
|
|
499
|
-
self._upnp_state[field_name] = TimestampedField(
|
|
500
|
-
value=value,
|
|
501
|
-
source="upnp",
|
|
502
|
-
timestamp=ts,
|
|
503
|
-
)
|
|
430
|
+
self._apply_source_and_track_fields(
|
|
431
|
+
data,
|
|
432
|
+
self._upnp_state,
|
|
433
|
+
"upnp",
|
|
434
|
+
ts,
|
|
435
|
+
force_metadata_update=force_metadata_update,
|
|
436
|
+
)
|
|
504
437
|
|
|
505
438
|
self._merged_state.upnp_last_update = ts
|
|
506
439
|
self._merge_state()
|
|
@@ -914,14 +847,72 @@ class StateSynchronizer:
|
|
|
914
847
|
new_n = str(new).strip().lower().replace("_", "-").replace(" ", "-")
|
|
915
848
|
return bool(old_n and new_n and old_n != new_n)
|
|
916
849
|
|
|
917
|
-
def
|
|
918
|
-
|
|
850
|
+
def _apply_source_and_track_fields(
|
|
851
|
+
self,
|
|
852
|
+
data: dict[str, Any],
|
|
853
|
+
dest: dict[str, TimestampedField],
|
|
854
|
+
source: str,
|
|
855
|
+
ts: float,
|
|
856
|
+
*,
|
|
857
|
+
force_metadata_update: bool,
|
|
858
|
+
) -> None:
|
|
859
|
+
"""Apply source, then duration/title so leftover-guard is already set.
|
|
860
|
+
|
|
861
|
+
On a source change, snapshot only the *pre-switch* merged track and then
|
|
862
|
+
still accept incoming fields that are not that leftover. Skipping the
|
|
863
|
+
transition poll (and snapshotting incoming) hid Spotify title/artist
|
|
864
|
+
until the next track (wiim #263).
|
|
865
|
+
"""
|
|
866
|
+
source_changed = False
|
|
867
|
+
if "source" in data:
|
|
868
|
+
new_source = data.get("source")
|
|
869
|
+
old_field = self._merged_state.source
|
|
870
|
+
old_source = old_field.value if old_field is not None else None
|
|
871
|
+
source_changed = self._sources_differ(old_source, new_source)
|
|
872
|
+
if source_changed:
|
|
873
|
+
self._remember_stale_metadata()
|
|
874
|
+
self._clear_track_metadata()
|
|
875
|
+
dest["source"] = TimestampedField(
|
|
876
|
+
value=new_source,
|
|
877
|
+
source=source,
|
|
878
|
+
timestamp=ts,
|
|
879
|
+
)
|
|
880
|
+
|
|
881
|
+
self._release_stale_guard_if_fresh_track(data)
|
|
882
|
+
|
|
883
|
+
if "duration" in data and not self._is_leftover_after_source_change("duration", data.get("duration")):
|
|
884
|
+
dest["duration"] = TimestampedField(
|
|
885
|
+
value=data.get("duration"),
|
|
886
|
+
source=source,
|
|
887
|
+
timestamp=ts,
|
|
888
|
+
)
|
|
889
|
+
|
|
890
|
+
apply_metadata = force_metadata_update or source_changed or not self._should_clear_metadata()
|
|
891
|
+
if not apply_metadata:
|
|
892
|
+
return
|
|
893
|
+
for field_name in ("title", "artist", "album", "image_url"):
|
|
894
|
+
if field_name not in data:
|
|
895
|
+
continue
|
|
896
|
+
value = data.get(field_name)
|
|
897
|
+
if self._is_leftover_after_source_change(field_name, value):
|
|
898
|
+
continue
|
|
899
|
+
dest[field_name] = TimestampedField(
|
|
900
|
+
value=value,
|
|
901
|
+
source=source,
|
|
902
|
+
timestamp=ts,
|
|
903
|
+
)
|
|
904
|
+
|
|
905
|
+
def _remember_stale_metadata(self) -> None:
|
|
906
|
+
"""Snapshot pre-switch merged metadata so later polls cannot re-apply it.
|
|
907
|
+
|
|
908
|
+
Incoming values on the same poll may already be the new track
|
|
909
|
+
(network stream → Spotify) and must not be added to the leftover set.
|
|
910
|
+
"""
|
|
919
911
|
stale: dict[str, set[Any]] = {}
|
|
920
912
|
for field_name in ("title", "artist", "album", "image_url", "duration"):
|
|
921
|
-
incoming = data.get(field_name)
|
|
922
913
|
merged_field = getattr(self._merged_state, field_name, None)
|
|
923
914
|
merged_val = merged_field.value if merged_field is not None else None
|
|
924
|
-
values = {v for v in (
|
|
915
|
+
values = {v for v in (merged_val,) if v not in (None, "")}
|
|
925
916
|
stale[field_name] = values
|
|
926
917
|
self._stale_metadata_after_source = stale
|
|
927
918
|
|
|
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
|
|
File without changes
|
|
File without changes
|