From cfbe1f1147755ee10f50df9ac0009c7537cd1f5e Mon Sep 17 00:00:00 2001 From: OzGav Date: Sat, 3 Jan 2026 00:38:25 +1000 Subject: [PATCH] Fix track name stripping too agressive (#2901) * 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 | 53 +++++++++++++++++------ tests/core/test_helpers.py | 77 +++++++++++++++++++++++++++++++++ 2 files changed, 116 insertions(+), 14 deletions(-) diff --git a/music_assistant/helpers/util.py b/music_assistant/helpers/util.py index bc14912b..7ea329ed 100644 --- a/music_assistant/helpers/util.py +++ b/music_assistant/helpers/util.py @@ -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 diff --git a/tests/core/test_helpers.py b/tests/core/test_helpers.py index ea40e65a..161f642c 100644 --- a/tests/core/test_helpers.py +++ b/tests/core/test_helpers.py @@ -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: -- 2.34.1