Fix track name stripping too agressive (#2901)
authorOzGav <gavnosp@hotmail.com>
Fri, 2 Jan 2026 14:38:25 +0000 (00:38 +1000)
committerGitHub <noreply@github.com>
Fri, 2 Jan 2026 14:38:25 +0000 (15:38 +0100)
* Fix track name stripping too agressive

* Preserve capitalisation

* Optimise

* Adjust comment

* Simplify WITH_TITLE_WORDS

* Add tests

* Test for version words in song title

music_assistant/helpers/util.py
tests/core/test_helpers.py

index bc14912b22f255c38293608775490d315923eb77..7ea329edc896ecb06b6aaf6b1b75cbf93e23b986 100644 (file)
@@ -97,6 +97,15 @@ IGNORE_TITLE_PARTS = (
     "with ",
     "explicit",
 )
+WITH_TITLE_WORDS = (
+    # words that, when following "with", indicate this is part of the song title
+    # not a featuring credit.
+    "someone",
+    "the",
+    "u",
+    "you",
+    "no",
+)
 
 
 def filename_from_string(string: str) -> str:
@@ -146,23 +155,39 @@ def parse_title_and_version(title: str, track_version: str | None = None) -> tup
     version = track_version or ""
     for regex in (r"\(.*?\)", r"\[.*?\]", r" - .*"):
         for title_part in re.findall(regex, title):
+            # Extract the content without brackets/dashes for checking
+            clean_part = title_part.translate(str.maketrans("", "", "()[]-")).strip().lower()
+
+            # Check if this should be ignored (featuring/explicit parts)
+            should_ignore = False
             for ignore_str in IGNORE_TITLE_PARTS:
-                if ignore_str in title_part.lower():
+                if clean_part.startswith(ignore_str):
+                    # Special handling for "with " - check if followed by title words
+                    if ignore_str == "with ":
+                        # Extract the word after "with "
+                        after_with = (
+                            clean_part[len("with ") :].split()[0]
+                            if len(clean_part) > len("with ")
+                            else ""
+                        )
+                        if after_with in WITH_TITLE_WORDS:
+                            # This is part of the title (e.g., "with you"), don't ignore
+                            break
+                    # Remove this part from the title
                     title = title.replace(title_part, "").strip()
-                    continue
+                    should_ignore = True
+                    break
+
+            if should_ignore:
+                continue
+
+            # Check if this part is a version
             for version_str in VERSION_PARTS:
-                if version_str not in title_part.lower():
-                    continue
-                version = (
-                    title_part.replace("(", "")
-                    .replace(")", "")
-                    .replace("[", "")
-                    .replace("]", "")
-                    .replace("-", "")
-                    .strip()
-                )
-                title = title.replace(title_part, "").strip()
-                return (title, version)
+                if version_str in clean_part:
+                    # Preserve original casing for output
+                    version = title_part.strip("()[]- ").strip()
+                    title = title.replace(title_part, "").strip()
+                    return title, version
     return title, version
 
 
index ea40e65a4abd47b28c5a559fa0fd13c35c10a396..161f642cc01223061569af216edec1214b2aaf7b 100644 (file)
@@ -37,6 +37,83 @@ def test_version_extract() -> None:
     title, version = util.parse_title_and_version(test_str)
     assert title == "SuperSong"
     assert version == ""
+    # Version keywords in main title should NOT be stripped (only in parentheses)
+    test_str = "Great live unplugged song"
+    title, version = util.parse_title_and_version(test_str)
+    assert title == "Great live unplugged song"
+    assert version == ""
+    test_str = "I Do (featuring Sonny of P.O.D.) (Album Version)"
+    title, version = util.parse_title_and_version(test_str)
+    assert title == "I Do"
+    assert version == "Album Version"
+    test_str = "Get Up Stand Up (Phunk Investigation instrumental club mix)"
+    title, version = util.parse_title_and_version(test_str)
+    assert title == "Get Up Stand Up"
+    assert version == "Phunk Investigation instrumental club mix"
+    # Complex case: non-version part + version part with 'mix' keyword
+    test_str = "Lovin' You More (That Big Track) (Mosquito Chillout mix)"
+    title, version = util.parse_title_and_version(test_str)
+    assert title == "Lovin' You More (That Big Track)"
+    assert version == "Mosquito Chillout mix"
+
+
+def test_with_handling_in_titles() -> None:
+    """Test 'with' handling - preserved in title, stripped as featuring credit."""
+    # 'with you' (preserved as title word)
+    test_str = "CCF (I'm Gonna Stay with You)"
+    title, version = util.parse_title_and_version(test_str)
+    assert title == "CCF (I'm Gonna Stay with You)"
+    assert version == ""
+    # 'with someone' (preserved as title word)
+    test_str = "Ever Fallen in Love (With Someone You Shouldn't've)"
+    title, version = util.parse_title_and_version(test_str)
+    assert title == "Ever Fallen in Love (With Someone You Shouldn't've)"
+    assert version == ""
+    # 'with u' (preserved as title word)
+    test_str = "Dance (With U)"
+    title, version = util.parse_title_and_version(test_str)
+    assert title == "Dance (With U)"
+    assert version == ""
+    # 'with the' (preserved as title word)
+    test_str = "Girl (With the Patent Leather Face)"
+    title, version = util.parse_title_and_version(test_str)
+    assert title == "Girl (With the Patent Leather Face)"
+    assert version == ""
+    # 'with you' - different phrasing (preserved as title word)
+    test_str = "Rockin' Around (With You)"
+    title, version = util.parse_title_and_version(test_str)
+    assert title == "Rockin' Around (With You)"
+    assert version == ""
+    # 'with no' (preserved as title word)
+    test_str = "Ain't Gonna Bump No More (With No Big Fat Woman)"
+    title, version = util.parse_title_and_version(test_str)
+    assert title == "Ain't Gonna Bump No More (With No Big Fat Woman)"
+    assert version == ""
+    # 'with that' - not in WITH_TITLE_WORDS but not stripped because it doesn't start with "with "
+    test_str = "The Catastrophe (Good Luck with That Man)"
+    title, version = util.parse_title_and_version(test_str)
+    assert title == "The Catastrophe (Good Luck with That Man)"
+    assert version == ""
+    # 'with [artist name]' - should still be stripped (not a title word)
+    test_str = "Great Song (with John Smith)"
+    title, version = util.parse_title_and_version(test_str)
+    assert title == "Great Song"
+    assert version == ""
+    # 'with [artist name]' in brackets - should still be stripped
+    test_str = "Great Song [with Jane Doe]"
+    title, version = util.parse_title_and_version(test_str)
+    assert title == "Great Song"
+    assert version == ""
+    # Title word preserved + version extracted from dash notation
+    test_str = "CCF (I'm Gonna Stay with You) - Live Version"
+    title, version = util.parse_title_and_version(test_str)
+    assert title == "CCF (I'm Gonna Stay with You)"
+    assert version == "Live Version"
+    # Title word preserved + version extracted from brackets
+    test_str = "Dance (With U) [Remix]"
+    title, version = util.parse_title_and_version(test_str)
+    assert title == "Dance (With U)"
+    assert version == "Remix"
 
 
 async def test_uri_parsing() -> None: