From d47639fdf7ad326945d31669955234432979c26f Mon Sep 17 00:00:00 2001 From: sfortis Date: Sun, 21 Jun 2026 13:37:29 +0300 Subject: [PATCH 1/2] fix: handle unbound prefixes when xmlns:dlna is absent The strict=False recovery added in #35 anchored its namespace injection on an existing `xmlns:dlna` declaration. Some real-world devices (JBL Authentics, WiiM/LinkPlay) emit `` tags without declaring `xmlns:dlna`, so the injection was silently skipped and the unbound prefix remained, raising ParseError when otherwise recoverable. Anchor the injection on the DIDL-Lite root opening tag instead. That element is guaranteed to be present in any valid DIDL-Lite document, so the recovery works regardless of which namespace declarations the producer chose to include. Also batch all missing prefixes into a single regex substitution and add a regression test that exercises the no-dlna-namespace case. --- changes/52.bugfix.rst | 1 + didl_lite/didl_lite.py | 23 +++++++++++++++++------ tests/test_didl_lite.py | 29 +++++++++++++++++++++++++++++ 3 files changed, 47 insertions(+), 6 deletions(-) create mode 100644 changes/52.bugfix.rst diff --git a/changes/52.bugfix.rst b/changes/52.bugfix.rst new file mode 100644 index 0000000..8164c67 --- /dev/null +++ b/changes/52.bugfix.rst @@ -0,0 +1 @@ +Fix unbound prefix recovery when ``xmlns:dlna`` is absent. Devices that emit unbound prefixes (e.g. ````) without declaring ``xmlns:dlna`` are now handled correctly when ``strict=False``. diff --git a/didl_lite/didl_lite.py b/didl_lite/didl_lite.py index 116938b..b56f564 100644 --- a/didl_lite/didl_lite.py +++ b/didl_lite/didl_lite.py @@ -1063,12 +1063,23 @@ 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 `` without `xmlns:dlna`). + injections = " ".join( + f'xmlns:{prefix}="http://tempuri.org/{prefix}/"' + for prefix in sorted(missing_prefixes) + ) + xml_string = re.sub( + r" 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 `` tags + without declaring `xmlns:dlna`, leaving the unbound prefix in place + and breaking parsing even with strict=False. + """ + broken_xml = ( + '' + '' + "Test Title" + "Test Subtitle" + "object.item.audioItem.musicTrack" + "" + "" + ) + + 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( From 5828b5e51be563a6748481463d4f53759b3a9fc6 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 21 Jun 2026 10:48:59 +0000 Subject: [PATCH 2/2] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- didl_lite/didl_lite.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/didl_lite/didl_lite.py b/didl_lite/didl_lite.py index b56f564..f48c6b8 100644 --- a/didl_lite/didl_lite.py +++ b/didl_lite/didl_lite.py @@ -1071,8 +1071,7 @@ def from_xml_string(xml_string: str, strict: bool = True) -> List[Union[DidlObje # declaration entirely (observed on JBL Authentics and # WiiM/LinkPlay players sending `` without `xmlns:dlna`). injections = " ".join( - f'xmlns:{prefix}="http://tempuri.org/{prefix}/"' - for prefix in sorted(missing_prefixes) + f'xmlns:{prefix}="http://tempuri.org/{prefix}/"' for prefix in sorted(missing_prefixes) ) xml_string = re.sub( r"