diff --git a/beets/ui/__init__.py b/beets/ui/__init__.py index 4c93d66d8..ddd5ee98b 100644 --- a/beets/ui/__init__.py +++ b/beets/ui/__init__.py @@ -1048,6 +1048,18 @@ def print_newline_layout( FLOAT_EPSILON = 0.01 +def _multi_value_diff(field: str, oldset: set[str], newset: set[str]) -> str: + added = newset - oldset + removed = oldset - newset + + parts = [ + f"{field}:", + *(colorize("text_diff_removed", f" - {i}") for i in sorted(removed)), + *(colorize("text_diff_added", f" + {i}") for i in sorted(added)), + ] + return "\n".join(parts) + + def _field_diff( field: str, old: FormattedMapping, new: FormattedMapping ) -> str | None: @@ -1063,6 +1075,11 @@ def _field_diff( ): return None + if isinstance(oldval, list): + if (oldset := set(oldval)) != (newset := set(newval)): + return _multi_value_diff(field, oldset, newset) + return None + # Get formatted values for output. oldstr, newstr = old.get(field, ""), new.get(field, "") if field not in new: @@ -1071,8 +1088,7 @@ def _field_diff( if field not in old: return colorize("text_diff_added", f"{field}: {newstr}") - # For strings, highlight changes. For others, colorize the whole - # thing. + # For strings, highlight changes. For others, colorize the whole thing. if isinstance(oldval, str): oldstr, newstr = colordiff(oldstr, newstr) else: diff --git a/docs/changelog.rst b/docs/changelog.rst index eff2e2cc8..58ad3d58f 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -28,6 +28,7 @@ Other changes values: |semicolon_space|. For example ``beet modify albumtypes="album; ep"``. Previously, ``\␀`` was used as a separator. This applies to fields such as ``artists``, ``albumtypes`` etc. +- Improve highlighting of multi-valued fields changes. 2.6.2 (February 22, 2026) ------------------------- diff --git a/test/ui/test_field_diff.py b/test/ui/test_field_diff.py index 35f3c6ca7..8480d7d7c 100644 --- a/test/ui/test_field_diff.py +++ b/test/ui/test_field_diff.py @@ -1,3 +1,5 @@ +from textwrap import dedent + import pytest from beets.library import Item @@ -38,6 +40,19 @@ class TestFieldDiff: p({"mb_trackid": None}, {"mb_trackid": "1234"}, "mb_trackid", "mb_trackid: -> [text_diff_added]1234[/]", id="none_to_value"), p({}, {"new_flex": "foo"}, "new_flex", "[text_diff_added]new_flex: foo[/]", id="flex_field_added"), p({"old_flex": "foo"}, {}, "old_flex", "[text_diff_removed]old_flex: foo[/]", id="flex_field_removed"), + p({"albumtypes": ["album", "ep"]}, {"albumtypes": ["ep", "album"]}, "albumtypes", None, id="multi_value_unchanged"), + p( + {"albumtypes": ["ep"]}, + {"albumtypes": ["album", "compilation"]}, + "albumtypes", + dedent(""" + albumtypes: + [text_diff_removed] - ep[/] + [text_diff_added] + album[/] + [text_diff_added] + compilation[/] + """).strip(), + id="multi_value_changed" + ), ], ) # fmt: skip @pytest.mark.parametrize("color", [True], ids=["color_enabled"])