Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changes/52.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fix unbound prefix recovery when ``xmlns:dlna`` is absent. Devices that emit unbound prefixes (e.g. ``<song:subTitle>``) without declaring ``xmlns:dlna`` are now handled correctly when ``strict=False``.
22 changes: 16 additions & 6 deletions didl_lite/didl_lite.py
Original file line number Diff line number Diff line change
Expand Up @@ -1063,12 +1063,22 @@ def from_xml_string(xml_string: str, strict: bool = True) -> List[Union[DidlObje
# Identify prefixes used but not defined.
missing_prefixes = used_prefixes - defined_prefixes - {"DIDL-Lite", "dc", "upnp", "dlna"}

# Remove the "if missing_prefixes:" line and just keep the for loop
for prefix in missing_prefixes:
dlna_ns = 'xmlns:dlna="urn:schemas-dlna-org:metadata-1-0/"'
if dlna_ns in xml_string:
replacement = f'{dlna_ns} xmlns:{prefix}="http://tempuri.org/{prefix}/"'
xml_string = xml_string.replace(dlna_ns, replacement)
if missing_prefixes:
# Inject temporary namespace declarations into the DIDL-Lite root
# opening tag. Anchoring on `<DIDL-Lite` (always present in valid
# DIDL-Lite documents) instead of an optional `xmlns:dlna` allows
# us to recover XML from devices that omit the dlna namespace
# declaration entirely (observed on JBL Authentics and
# WiiM/LinkPlay players sending `<song:*>` without `xmlns:dlna`).
injections = " ".join(
f'xmlns:{prefix}="http://tempuri.org/{prefix}/"' for prefix in sorted(missing_prefixes)
)
xml_string = re.sub(
r"<DIDL-Lite\b",
f"<DIDL-Lite {injections}",
xml_string,
count=1,
)

# Proceed with parsing using the (potentially) patched xml_string
xml_el = defusedxml.ElementTree.fromstring(xml_string)
Expand Down
29 changes: 29 additions & 0 deletions tests/test_didl_lite.py
Original file line number Diff line number Diff line change
Expand Up @@ -696,6 +696,35 @@ def test_from_xml_string_unbound_prefix(self) -> None:
assert objs[0].sub_title == "Test Subtitle"
assert isinstance(objs[0], didl_lite.MusicTrack)

def test_from_xml_string_unbound_prefix_without_dlna_namespace(self) -> None:
"""Test unbound prefix recovery when xmlns:dlna is absent.

Regression: the previous implementation anchored the namespace
injection on an existing `xmlns:dlna` declaration. Devices such as
JBL Authentics and WiiM/LinkPlay players emit `<song:*>` tags
without declaring `xmlns:dlna`, leaving the unbound prefix in place
and breaking parsing even with strict=False.
"""
broken_xml = (
'<DIDL-Lite xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/" '
'xmlns:dc="http://purl.org/dc/elements/1.1/" '
'xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/">'
'<item id="1" parentID="0" restricted="1">'
"<dc:title>Test Title</dc:title>"
"<song:subTitle>Test Subtitle</song:subTitle>"
"<upnp:class>object.item.audioItem.musicTrack</upnp:class>"
"</item>"
"</DIDL-Lite>"
)

objs = didl_lite.from_xml_string(broken_xml, strict=False)

assert len(objs) == 1
assert objs[0].title == "Test Title"
assert "sub_title" in objs[0].__dict__
assert objs[0].sub_title == "Test Subtitle"
assert isinstance(objs[0], didl_lite.MusicTrack)

def test_music_track_artist_and_genre(self) -> None:
"""Test MusicTrack artist and genre properties."""
track = didl_lite.MusicTrack(
Expand Down