pywiim 2.2.6__tar.gz → 2.2.7__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.6/pywiim.egg-info → pywiim-2.2.7}/PKG-INFO +1 -1
- {pywiim-2.2.6 → pywiim-2.2.7}/pyproject.toml +1 -1
- {pywiim-2.2.6 → pywiim-2.2.7}/pywiim/__init__.py +1 -1
- {pywiim-2.2.6 → pywiim-2.2.7}/pywiim/player/coverart.py +27 -21
- {pywiim-2.2.6 → pywiim-2.2.7}/pywiim/player/statemgr.py +1 -5
- {pywiim-2.2.6 → pywiim-2.2.7}/pywiim/upnp/client.py +5 -4
- {pywiim-2.2.6 → pywiim-2.2.7}/pywiim/upnp/metadata.py +4 -1
- {pywiim-2.2.6 → pywiim-2.2.7/pywiim.egg-info}/PKG-INFO +1 -1
- {pywiim-2.2.6 → pywiim-2.2.7}/LICENSE +0 -0
- {pywiim-2.2.6 → pywiim-2.2.7}/README.md +0 -0
- {pywiim-2.2.6 → pywiim-2.2.7}/pywiim/api/__init__.py +0 -0
- {pywiim-2.2.6 → pywiim-2.2.7}/pywiim/api/audio_pro.py +0 -0
- {pywiim-2.2.6 → pywiim-2.2.7}/pywiim/api/audio_settings.py +0 -0
- {pywiim-2.2.6 → pywiim-2.2.7}/pywiim/api/base.py +0 -0
- {pywiim-2.2.6 → pywiim-2.2.7}/pywiim/api/bluetooth.py +0 -0
- {pywiim-2.2.6 → pywiim-2.2.7}/pywiim/api/constants.py +0 -0
- {pywiim-2.2.6 → pywiim-2.2.7}/pywiim/api/device.py +0 -0
- {pywiim-2.2.6 → pywiim-2.2.7}/pywiim/api/diagnostics.py +0 -0
- {pywiim-2.2.6 → pywiim-2.2.7}/pywiim/api/endpoints.py +0 -0
- {pywiim-2.2.6 → pywiim-2.2.7}/pywiim/api/eq.py +0 -0
- {pywiim-2.2.6 → pywiim-2.2.7}/pywiim/api/firmware.py +0 -0
- {pywiim-2.2.6 → pywiim-2.2.7}/pywiim/api/group.py +0 -0
- {pywiim-2.2.6 → pywiim-2.2.7}/pywiim/api/lms.py +0 -0
- {pywiim-2.2.6 → pywiim-2.2.7}/pywiim/api/loop_mode.py +0 -0
- {pywiim-2.2.6 → pywiim-2.2.7}/pywiim/api/misc.py +0 -0
- {pywiim-2.2.6 → pywiim-2.2.7}/pywiim/api/parser.py +0 -0
- {pywiim-2.2.6 → pywiim-2.2.7}/pywiim/api/peq.py +0 -0
- {pywiim-2.2.6 → pywiim-2.2.7}/pywiim/api/playback.py +0 -0
- {pywiim-2.2.6 → pywiim-2.2.7}/pywiim/api/preset.py +0 -0
- {pywiim-2.2.6 → pywiim-2.2.7}/pywiim/api/ssl.py +0 -0
- {pywiim-2.2.6 → pywiim-2.2.7}/pywiim/api/subwoofer.py +0 -0
- {pywiim-2.2.6 → pywiim-2.2.7}/pywiim/api/timer.py +0 -0
- {pywiim-2.2.6 → pywiim-2.2.7}/pywiim/backoff.py +0 -0
- {pywiim-2.2.6 → pywiim-2.2.7}/pywiim/capabilities.py +0 -0
- {pywiim-2.2.6 → pywiim-2.2.7}/pywiim/cli/__init__.py +0 -0
- {pywiim-2.2.6 → pywiim-2.2.7}/pywiim/cli/diagnostics.py +0 -0
- {pywiim-2.2.6 → pywiim-2.2.7}/pywiim/cli/discovery_cli.py +0 -0
- {pywiim-2.2.6 → pywiim-2.2.7}/pywiim/cli/group_test_cli.py +0 -0
- {pywiim-2.2.6 → pywiim-2.2.7}/pywiim/cli/join_test_cli.py +0 -0
- {pywiim-2.2.6 → pywiim-2.2.7}/pywiim/cli/monitor_cli.py +0 -0
- {pywiim-2.2.6 → pywiim-2.2.7}/pywiim/cli/verify_cli.py +0 -0
- {pywiim-2.2.6 → pywiim-2.2.7}/pywiim/client.py +0 -0
- {pywiim-2.2.6 → pywiim-2.2.7}/pywiim/device_capabilities.py +0 -0
- {pywiim-2.2.6 → pywiim-2.2.7}/pywiim/discovery.py +0 -0
- {pywiim-2.2.6 → pywiim-2.2.7}/pywiim/exceptions.py +0 -0
- {pywiim-2.2.6 → pywiim-2.2.7}/pywiim/group.py +0 -0
- {pywiim-2.2.6 → pywiim-2.2.7}/pywiim/group_helpers.py +0 -0
- {pywiim-2.2.6 → pywiim-2.2.7}/pywiim/mcp/__init__.py +0 -0
- {pywiim-2.2.6 → pywiim-2.2.7}/pywiim/mcp/__main__.py +0 -0
- {pywiim-2.2.6 → pywiim-2.2.7}/pywiim/mcp/config.example.json +0 -0
- {pywiim-2.2.6 → pywiim-2.2.7}/pywiim/mcp/config.py +0 -0
- {pywiim-2.2.6 → pywiim-2.2.7}/pywiim/mcp/context.py +0 -0
- {pywiim-2.2.6 → pywiim-2.2.7}/pywiim/mcp/server.py +0 -0
- {pywiim-2.2.6 → pywiim-2.2.7}/pywiim/metadata.py +0 -0
- {pywiim-2.2.6 → pywiim-2.2.7}/pywiim/model_names.py +0 -0
- {pywiim-2.2.6 → pywiim-2.2.7}/pywiim/models.py +0 -0
- {pywiim-2.2.6 → pywiim-2.2.7}/pywiim/normalize.py +0 -0
- {pywiim-2.2.6 → pywiim-2.2.7}/pywiim/player/__init__.py +0 -0
- {pywiim-2.2.6 → pywiim-2.2.7}/pywiim/player/audio.py +0 -0
- {pywiim-2.2.6 → pywiim-2.2.7}/pywiim/player/base.py +0 -0
- {pywiim-2.2.6 → pywiim-2.2.7}/pywiim/player/bluetooth.py +0 -0
- {pywiim-2.2.6 → pywiim-2.2.7}/pywiim/player/debounce.py +0 -0
- {pywiim-2.2.6 → pywiim-2.2.7}/pywiim/player/diagnostics.py +0 -0
- {pywiim-2.2.6 → pywiim-2.2.7}/pywiim/player/groupops.py +0 -0
- {pywiim-2.2.6 → pywiim-2.2.7}/pywiim/player/media.py +0 -0
- {pywiim-2.2.6 → pywiim-2.2.7}/pywiim/player/playback.py +0 -0
- {pywiim-2.2.6 → pywiim-2.2.7}/pywiim/player/properties.py +0 -0
- {pywiim-2.2.6 → pywiim-2.2.7}/pywiim/player/source_capabilities.py +0 -0
- {pywiim-2.2.6 → pywiim-2.2.7}/pywiim/player/stream.py +0 -0
- {pywiim-2.2.6 → pywiim-2.2.7}/pywiim/player/stream_enricher.py +0 -0
- {pywiim-2.2.6 → pywiim-2.2.7}/pywiim/player/volume.py +0 -0
- {pywiim-2.2.6 → pywiim-2.2.7}/pywiim/polling.py +0 -0
- {pywiim-2.2.6 → pywiim-2.2.7}/pywiim/profiles.py +0 -0
- {pywiim-2.2.6 → pywiim-2.2.7}/pywiim/py.typed +0 -0
- {pywiim-2.2.6 → pywiim-2.2.7}/pywiim/role.py +0 -0
- {pywiim-2.2.6 → pywiim-2.2.7}/pywiim/state.py +0 -0
- {pywiim-2.2.6 → pywiim-2.2.7}/pywiim/upnp/__init__.py +0 -0
- {pywiim-2.2.6 → pywiim-2.2.7}/pywiim/upnp/eventer.py +0 -0
- {pywiim-2.2.6 → pywiim-2.2.7}/pywiim/upnp/health.py +0 -0
- {pywiim-2.2.6 → pywiim-2.2.7}/pywiim.egg-info/SOURCES.txt +0 -0
- {pywiim-2.2.6 → pywiim-2.2.7}/pywiim.egg-info/dependency_links.txt +0 -0
- {pywiim-2.2.6 → pywiim-2.2.7}/pywiim.egg-info/entry_points.txt +0 -0
- {pywiim-2.2.6 → pywiim-2.2.7}/pywiim.egg-info/requires.txt +0 -0
- {pywiim-2.2.6 → pywiim-2.2.7}/pywiim.egg-info/top_level.txt +0 -0
- {pywiim-2.2.6 → pywiim-2.2.7}/setup.cfg +0 -0
|
@@ -244,10 +244,14 @@ class CoverArtManager:
|
|
|
244
244
|
current_signature and self._last_track_signature and current_signature != self._last_track_signature
|
|
245
245
|
)
|
|
246
246
|
|
|
247
|
-
# Check if metadata needs enrichment (title/artist/album are Unknown)
|
|
247
|
+
# Check if metadata needs enrichment (title/artist/album are present but Unknown)
|
|
248
248
|
# This is common with Bluetooth AVRCP where getPlayerStatusEx returns "Unknown"
|
|
249
249
|
# but getMetaInfo has the actual track info
|
|
250
|
-
needs_metadata_enrichment =
|
|
250
|
+
needs_metadata_enrichment = (
|
|
251
|
+
("title" in merged_state and is_invalid_metadata(title))
|
|
252
|
+
or ("artist" in merged_state and is_invalid_metadata(artist))
|
|
253
|
+
or ("album" in merged_state and is_invalid_metadata(album))
|
|
254
|
+
)
|
|
251
255
|
|
|
252
256
|
if track_changed:
|
|
253
257
|
self._last_track_signature = current_signature
|
|
@@ -270,29 +274,30 @@ class CoverArtManager:
|
|
|
270
274
|
has_track_metadata = bool(title or artist or album)
|
|
271
275
|
needs_artwork_enrichment = not has_valid_artwork and has_track_metadata
|
|
272
276
|
should_fetch_metadata = (
|
|
273
|
-
(track_changed and not has_valid_artwork)
|
|
274
|
-
or needs_metadata_enrichment
|
|
275
|
-
or needs_artwork_enrichment
|
|
277
|
+
(track_changed and not has_valid_artwork) or needs_metadata_enrichment or needs_artwork_enrichment
|
|
276
278
|
)
|
|
277
279
|
|
|
278
280
|
if should_fetch_metadata:
|
|
279
281
|
# Cancel any existing fetch task
|
|
280
|
-
|
|
282
|
+
current_task = asyncio.current_task()
|
|
283
|
+
if (
|
|
284
|
+
self._artwork_fetch_task
|
|
285
|
+
and not self._artwork_fetch_task.done()
|
|
286
|
+
and self._artwork_fetch_task is not current_task
|
|
287
|
+
):
|
|
281
288
|
self._artwork_fetch_task.cancel()
|
|
282
289
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
# No event loop available (sync context) - will fetch on next poll
|
|
295
|
-
_LOGGER.debug("No event loop available, metadata will be fetched on next poll")
|
|
290
|
+
if current_task:
|
|
291
|
+
self._artwork_fetch_task = current_task
|
|
292
|
+
|
|
293
|
+
if needs_metadata_enrichment:
|
|
294
|
+
_LOGGER.debug("Metadata is Unknown, fetching enrichment sources")
|
|
295
|
+
elif needs_artwork_enrichment:
|
|
296
|
+
_LOGGER.debug("Artwork missing with track metadata, fetching enrichment sources")
|
|
297
|
+
else:
|
|
298
|
+
_LOGGER.debug("Track changed, fetching enrichment sources")
|
|
299
|
+
|
|
300
|
+
await self._fetch_artwork_from_metainfo(merged_state)
|
|
296
301
|
|
|
297
302
|
if not self._last_track_signature and current_signature:
|
|
298
303
|
# First track detected
|
|
@@ -347,6 +352,7 @@ class CoverArtManager:
|
|
|
347
352
|
encoded = quote(cache_key)
|
|
348
353
|
sep = "&" if "?" in str(artwork_url) else "?"
|
|
349
354
|
artwork_url = f"{artwork_url}{sep}cache={encoded}"
|
|
355
|
+
update["image_url"] = artwork_url
|
|
350
356
|
update["entity_picture"] = artwork_url
|
|
351
357
|
|
|
352
358
|
if update:
|
|
@@ -367,7 +373,7 @@ class CoverArtManager:
|
|
|
367
373
|
self.player._status_model.artist = merged.get("artist")
|
|
368
374
|
if "album" in update:
|
|
369
375
|
self.player._status_model.album = merged.get("album")
|
|
370
|
-
if "entity_picture" in update:
|
|
376
|
+
if "image_url" in update or "entity_picture" in update:
|
|
371
377
|
image_url = merged.get("image_url")
|
|
372
378
|
self.player._status_model.entity_picture = image_url
|
|
373
379
|
self.player._status_model.cover_url = image_url
|
|
@@ -427,7 +433,7 @@ class CoverArtManager:
|
|
|
427
433
|
if update:
|
|
428
434
|
self._apply_metadata_update(update, source_name="getMetaInfo")
|
|
429
435
|
|
|
430
|
-
if not update.get("entity_picture"):
|
|
436
|
+
if not (update.get("image_url") or update.get("entity_picture")):
|
|
431
437
|
await self._fetch_artwork_from_getinfoex(merged_state)
|
|
432
438
|
except asyncio.CancelledError:
|
|
433
439
|
# Task was cancelled (new track change detected)
|
|
@@ -983,11 +983,7 @@ class StateManager:
|
|
|
983
983
|
|
|
984
984
|
# HTTP polling path: enrich artwork/metadata when missing (e.g. Arylic GetInfoEx)
|
|
985
985
|
merged = self.player._state_synchronizer.get_merged_state()
|
|
986
|
-
|
|
987
|
-
loop = asyncio.get_event_loop()
|
|
988
|
-
loop.create_task(self.player._coverart_mgr.enrich_metadata_on_track_change(merged))
|
|
989
|
-
except RuntimeError:
|
|
990
|
-
_LOGGER.debug("No event loop available for metadata enrichment after refresh")
|
|
986
|
+
await self.player._coverart_mgr.enrich_metadata_on_track_change(merged)
|
|
991
987
|
|
|
992
988
|
# Notify callback
|
|
993
989
|
if self.player._on_state_changed:
|
|
@@ -9,6 +9,7 @@ from __future__ import annotations
|
|
|
9
9
|
import asyncio
|
|
10
10
|
import logging
|
|
11
11
|
import ssl
|
|
12
|
+
from collections.abc import Callable
|
|
12
13
|
from datetime import timedelta
|
|
13
14
|
from typing import Any, cast
|
|
14
15
|
|
|
@@ -818,9 +819,9 @@ class UpnpClient:
|
|
|
818
819
|
|
|
819
820
|
parsed.update(parse_didl_metadata(track_metadata))
|
|
820
821
|
_LOGGER.debug("GetInfoEx result for %s: has_artwork=%s", self.host, bool(parsed.get("image_url")))
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
822
|
+
if parsed.get("image_url"):
|
|
823
|
+
return parsed
|
|
824
|
+
_LOGGER.debug("GetInfoEx action for %s returned no artwork; trying raw SOAP", self.host)
|
|
824
825
|
except Exception as err:
|
|
825
826
|
_LOGGER.debug("GetInfoEx via async_upnp_client failed for %s: %s", self.host, err)
|
|
826
827
|
|
|
@@ -829,7 +830,7 @@ class UpnpClient:
|
|
|
829
830
|
async def _get_info_ex_raw(
|
|
830
831
|
self,
|
|
831
832
|
service_type: str,
|
|
832
|
-
parse_response: Any,
|
|
833
|
+
parse_response: Callable[[str], dict[str, Any]],
|
|
833
834
|
) -> dict[str, Any]:
|
|
834
835
|
"""Call LinkPlay GetInfoEx via raw SOAP when SCPD does not advertise it."""
|
|
835
836
|
control_url = getattr(self._av_transport_service, "control_url", None)
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import logging
|
|
6
|
+
import re
|
|
6
7
|
from html import unescape
|
|
7
8
|
from typing import Any
|
|
8
9
|
from urllib.parse import urlparse
|
|
@@ -10,6 +11,8 @@ from xml.etree import ElementTree as ET
|
|
|
10
11
|
|
|
11
12
|
_LOGGER = logging.getLogger(__name__)
|
|
12
13
|
|
|
14
|
+
_BARE_AMPERSAND_RE = re.compile(r"&(?!#\d+;|#x[0-9a-fA-F]+;|[A-Za-z][A-Za-z0-9]+;)")
|
|
15
|
+
|
|
13
16
|
|
|
14
17
|
def is_valid_image_url(url: str | None) -> bool:
|
|
15
18
|
"""Return True when value is a usable HTTP/HTTPS artwork URL."""
|
|
@@ -39,7 +42,7 @@ def parse_didl_metadata(didl_xml: str, *, allow_clear: bool = False) -> dict[str
|
|
|
39
42
|
return changes
|
|
40
43
|
|
|
41
44
|
try:
|
|
42
|
-
didl_xml = unescape(didl_xml)
|
|
45
|
+
didl_xml = _BARE_AMPERSAND_RE.sub("&", unescape(didl_xml))
|
|
43
46
|
root = ET.fromstring(didl_xml)
|
|
44
47
|
|
|
45
48
|
namespaces = {
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|