Skip to content

Populate track metadata from polled GetInfoEx responses - #20

Open
markfrancisonly wants to merge 8 commits into
Linkplay2020:mainfrom
markfrancisonly:fix/track-metadata-from-getinfoex
Open

Populate track metadata from polled GetInfoEx responses#20
markfrancisonly wants to merge 8 commits into
Linkplay2020:mainfrom
markfrancisonly:fix/track-metadata-from-getinfoex

Conversation

@markfrancisonly

@markfrancisonly markfrancisonly commented Jul 29, 2026

Copy link
Copy Markdown

What this enables: track title, artist, album and artwork show up on devices whose UPnP eventing never delivers — the metadata is in every poll the SDK already makes, it just wasn't read. Devices with working eventing behave exactly as before.

Problem_current_track_info, the only source for title/artist/album/art, is written solely by the AVTransport event path, so when events don't arrive metadata stays empty forever. Every GetInfoEx poll already carries the same DIDL-Lite under TrackMetaData; only its capabilities and queue fields are read. Reported in home-assistant/core#168047 and home-assistant/core#177517; #6 is an open PR against the same event path.

Fix

  • Extract the DIDL/JSON normalisation into _parse_track_metadata, so both paths produce identical shapes. Event path otherwise unchanged.
  • Apply it to the TrackMetaData already in the GetInfoEx reply — no extra request.
  • Only unescape payloads that arrive escaped: polled ones are already decoded, and unescaping them corrupts a literal &. Found on a live WiiM Pro.
  • Two guards, so polling can only add metadata — skip an empty parse, and skip a reply an event superseded mid-request (_current_track_info identity compared across the await).

Also: if item_elem: relied on Element.__bool__, so an attribute-only <item> took the "no item" branch. Now is not None, matching the check above it.

Tests — 9 cases: both paths, four empty-payload flavours, the mid-request race, both escaping variants, present-vs-absent <item>. 60 passed.

Media metadata was only ever written by the AVTransport eventing path, keyed
on AVTransportURIMetaData or CurrentTrackMetaData. Devices that never emit
those in a LastChange event therefore report no title, artist, album or album
art at all, even though GetInfoEx already returns the same DIDL-Lite payload
under TrackMetaData on every poll.

Extract the DIDL-Lite/JSON normalisation out of
_update_state_from_av_transport_event_data into _parse_track_metadata so both
paths produce identically shaped data, and apply it to the GetInfoEx response
already fetched by async_get_transport_capabilities. No extra request is made.

The polled path only overwrites _current_track_info when the payload yields at
least one usable field, so a device returning empty or unparseable
TrackMetaData cannot clobber metadata delivered by eventing. Devices whose
eventing works are unaffected.

Also fix the DIDL item check, which used Element truthiness. That evaluates to
len(el) != 0, so an <item> carrying only attributes was silently parsed as an
empty dict; ElementTree also deprecates testing an element's truth value. Use
`is not None`, matching the check already applied to the same element when
resolving its <res> child.
Event payloads arrive XML-escaped inside LastChange, but the polled
GetInfoEx TrackMetaData arrives already decoded. Unconditionally
unescaping a decoded payload corrupts any literal &amp; in it -- a URL
query string or an ampersand in a title -- and the subsequent parse fails
with "not well-formed", leaving the metadata empty.

Unescape only when the payload does not already start like markup or
JSON, and retry a failed XML parse once with unescaping as a fallback.
Found by running the polled path against a live WiiM Pro whose current
track carried an ampersand in its metadata.
@markfrancisonly

Copy link
Copy Markdown
Author

Pushed a follow-up commit after running this against a live WiiM Pro: the polled TrackMetaData arrives already decoded (unlike event payloads, which are escaped inside LastChange), so the parser's unconditional unescape() corrupted any literal &amp; — a URL query string was enough — and the parse failed. The parser now only unescapes payloads that arrive escaped, with a parse-retry fallback, and there are regression tests for both variants.

The polled path parses TrackMetaData on every refresh, so what was a rare
event-path condition became routine and filled the log with warnings for
a successful operation. Parse failures still log at error level.
Comment thread src/wiim/wiim_device.py Outdated
async_get_transport_capabilities awaits the MEDIA_INFO request, and the
AVTransport event handler is synchronous, so an event can replace the track
info while that request is still outstanding. The reply then describes the
track that was playing when it was issued, and it was applied
unconditionally, overwriting newer event metadata with older polled
metadata.

The poll is itself scheduled from the event path, so the window opens
exactly when metadata is changing, and the caller's in-flight guard means an
event arriving mid-request queues no follow-up poll to correct it. The stale
title, artist and album art therefore stand until the next unrelated event.

Read the track info before issuing the request and apply the reply only if it
is still the same object. Every write to _current_track_info binds a new
dict, so identity is a sufficient change token, and it also covers the
disconnect reset without special-casing it.
@markfrancisonly
markfrancisonly force-pushed the fix/track-metadata-from-getinfoex branch from 954039b to a51e9eb Compare July 30, 2026 15:24
The mock fires an AVTransport event from inside the request it is standing in
for, which is what makes the interleaving deterministic. Without a note the
call reads as arbitrary.
@markfrancisonly

markfrancisonly commented Jul 30, 2026

Copy link
Copy Markdown
Author

@Linkplay2020 I wasn't receiving artwork in my ha setup after buying a wiim pro, so that prompted me to troubleshoot and fix these issues. I went ahead and submitted PR home-assistant/core#177667

Ultimately I was able to workaround the upnp issue by disabling all but one home assistant one network interface from the several exposed to the core home assistant container by my docker config. This workaround isn't desirable and I want to bring my secondary network interfaces back online cross vlan usage

Comment thread tests/wiim/test_wiim_device.py Outdated
Comment thread tests/wiim/test_wiim_device.py Outdated
test_parse_track_metadata_reads_item_without_child_elements only checked
the key set of the returned dict, which _parse_track_metadata fills in
unconditionally, so the `if item_elem:` it was added for passed it too.

Parse a present-but-childless item and a genuinely absent one in the same
test and compare the results: the first yields empty fields and no warning,
the second leaves them unset and reports the missing item. The old
truthiness check collapses the two and fails on the first assertion.
test_polled_media_info_applies_when_no_event_intervenes seeded the later
track through the event path and then had the poll return the earlier one,
so the behaviour it pins read as a stale polled reply replacing newer event
metadata — the opposite of what the identity guard is for.

Seed the earlier track and poll the later one, standing in for the track
advancing with no event to report it. The guard is exercised the same way
and no metadata rollback is implied. _DIDL_LITE_NEXT_TRACK is no longer
specific to the interleaving test, so its comment no longer says so.
@markfrancisonly

Copy link
Copy Markdown
Author

Updated PR body text and handing off

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants