From 213592821b7de4d8ccb09cb4d74b517c8e63f02a Mon Sep 17 00:00:00 2001 From: BoulderBadgeDad Date: Thu, 18 Jun 2026 18:35:04 -0700 Subject: [PATCH] #890: strip leading track-number prefix from filename-derived titles MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Files named '01 - Sun It Rises.flac' with no embedded title tag leaked the stem, number and all, into tracks.title as '01 - Sun It Rises' — which never matches the canonical 'Sun It Rises', so the real track reads as a false 'missing' and albums sort wrong. New conservative strip_leading_track_number (paths.py): removes a clear track-number prefix (zero-padded number, OR a number followed by a real separator+space) while leaving titles that merely start with a number untouched — '7 Rings', '99 Luftballons', '50 Ways to Leave Your Lover', '1-800-273-8255', '1979' all preserved. Never reduces to empty/bare-number/punctuation. Applied at: - get_import_clean_title (context.py) — the universal resolver every import path funnels through, so the DB title AND the re-written embedded tag come out clean. - album_matching scorer — so '01 - Sun It Rises' scores against 'Sun It Rises' and the file matches its real track (inheriting the clean canonical name). 27 targeted tests + 772 imports/matching green. --- core/imports/album_matching.py | 5 +- core/imports/context.py | 7 ++- core/imports/paths.py | 27 +++++++++ tests/imports/test_track_number_strip.py | 77 ++++++++++++++++++++++++ 4 files changed, 114 insertions(+), 2 deletions(-) create mode 100644 tests/imports/test_track_number_strip.py diff --git a/core/imports/album_matching.py b/core/imports/album_matching.py index f29d6b56..fce847ad 100644 --- a/core/imports/album_matching.py +++ b/core/imports/album_matching.py @@ -156,8 +156,11 @@ def score_file_against_track( score = 0.0 # Title similarity (TITLE_WEIGHT). Falls back to filename stem when - # the file has no title tag. + # the file has no title tag — strip a leading track-number prefix off that + # stem (#890) so "01 - Sun It Rises" scores against "Sun It Rises". title = file_tags.get('title') or os.path.splitext(os.path.basename(file_path))[0] + from core.imports.paths import strip_leading_track_number + title = strip_leading_track_number(title) track_name = track.get('name', '') score += similarity(title, track_name) * TITLE_WEIGHT diff --git a/core/imports/context.py b/core/imports/context.py index 8f80d9f1..9ed2a909 100644 --- a/core/imports/context.py +++ b/core/imports/context.py @@ -183,7 +183,12 @@ def get_import_clean_title( if not title: track_info = get_import_track_info(context) title = _first_value(track_info, "name", "title", default="") - return str(title or default) + title = str(title or default) + # #890: strip a leading track-number prefix that leaked from a filename stem + # (e.g. "01 - Sun It Rises" → "Sun It Rises") so it matches the canonical title. + # Conservative — clean source titles ("7 Rings" etc.) pass through untouched. + from core.imports.paths import strip_leading_track_number + return strip_leading_track_number(title) def get_import_clean_album( diff --git a/core/imports/paths.py b/core/imports/paths.py index 69de9cc7..f65c6765 100644 --- a/core/imports/paths.py +++ b/core/imports/paths.py @@ -158,6 +158,33 @@ def clean_track_title(track_title: str, artist_name: str) -> str: return cleaned if cleaned else original +# A leading track-number prefix is EITHER a zero-padded number (01, 04, 099 — no +# real song title starts with one), OR a plain number followed by a real separator +# AND a space ("3 - ", "12. "). Deliberately NOT a bare "number space word", so it +# leaves "7 Rings", "99 Luftballons", "50 Ways to Leave Your Lover" and +# "1-800-273-8255" untouched. +_TRACK_NUM_PREFIX_RE = re.compile(r"^\s*(?:0\d{1,2}[\s._)\-]*|\d{1,3}\s*[._)\-]\s+)(?=\S)") + + +def strip_leading_track_number(title: str) -> str: + """Conservatively remove a leading track-number prefix from a track title. + + Fixes #890 — files named ``01 - Sun It Rises.flac`` whose stem leaks into the + title as ``01 - Sun It Rises``, which then never matches the canonical + ``Sun It Rises`` (false "missing"). Only strips an unambiguous track-number + prefix; a coincidental leading number that's part of the title is preserved, and + it never reduces a title to empty or a bare number.""" + s = (title or "").strip() + if not s: + return title or "" + stripped = _TRACK_NUM_PREFIX_RE.sub("", s, count=1).strip() + # Keep the original if stripping left nothing real — empty, a bare number, or + # only punctuation (e.g. "01 - " → "-"). A real title has a letter/digit. + if stripped.isdigit() or not re.search(r"[^\W_]", stripped): + return s + return stripped + + def get_album_type_display(raw_type, track_count) -> str: diff --git a/tests/imports/test_track_number_strip.py b/tests/imports/test_track_number_strip.py new file mode 100644 index 00000000..ece899f5 --- /dev/null +++ b/tests/imports/test_track_number_strip.py @@ -0,0 +1,77 @@ +"""#890: a leading track number leaking from a filename stem into the title +("01 - Sun It Rises") makes the track never match the canonical "Sun It Rises", +so it reads as a false "missing". strip_leading_track_number removes the prefix — +conservatively, so titles that merely START with a number are left alone. +""" + +from __future__ import annotations + +import pytest + +from core.imports.context import get_import_clean_title +from core.imports.paths import strip_leading_track_number + + +# ── the bug: track-number prefixes get stripped ─────────────────────────────── +@pytest.mark.parametrize("dirty,clean", [ + ("01 - Sun It Rises", "Sun It Rises"), # the screenshot + ("04 - Tiger Mountain Peasant Song", "Tiger Mountain Peasant Song"), + ("05 - Quiet Houses", "Quiet Houses"), + ("07 - Heard Them Stirring", "Heard Them Stirring"), + ("01 Sun It Rises", "Sun It Rises"), # zero-padded, no separator + ("3 - Title", "Title"), # plain number + separator + space + ("12. Some Song", "Some Song"), # dot separator + ("10 - Track Ten", "Track Ten"), + ("09) Closing Time", "Closing Time"), # paren separator + (" 02 - Spaced Out ", "Spaced Out"), # messy whitespace +]) +def test_strips_track_number_prefix(dirty, clean): + assert strip_leading_track_number(dirty) == clean + + +# ── the guard: titles that legitimately start with a number are UNTOUCHED ────── +@pytest.mark.parametrize("title", [ + "7 Rings", + "99 Luftballons", + "50 Ways to Leave Your Lover", + "1-800-273-8255", # number-with-dashes is part of the title + "1979", + "9 to 5", + "4 Minutes", + "8 Mile", + "21 Guns", + "24 Hour Party People", # no separator → not a track number + "0 to 100", + "Sun It Rises", # no leading number at all +]) +def test_preserves_real_titles(title): + assert strip_leading_track_number(title) == title + + +# ── degenerate inputs ───────────────────────────────────────────────────────── +def test_never_reduces_to_empty_or_bare_number(): + assert strip_leading_track_number("01") == "01" # bare number → keep + assert strip_leading_track_number("01 - ") == "01 -" # nothing left → keep original (trimmed) + assert strip_leading_track_number("") == "" + assert strip_leading_track_number(None) == "" + + +def test_only_strips_one_prefix(): + # A title that legitimately follows the number keeps its own leading number. + assert strip_leading_track_number("01 - 24 Hour Party People") == "24 Hour Party People" + + +# ── the chokepoint: every import path resolves its title through here ────────── +def test_get_import_clean_title_strips_filename_leak(): + # original_search['title'] came from the filename stem (no embedded tag). + ctx = {"original_search_result": {"title": "01 - Sun It Rises"}} + assert get_import_clean_title(ctx) == "Sun It Rises" + + +def test_get_import_clean_title_leaves_clean_source_title(): + ctx = {"original_search_result": {"title": "7 Rings"}} + assert get_import_clean_title(ctx) == "7 Rings" + + +def test_get_import_clean_title_default_untouched(): + assert get_import_clean_title({}, default="Unknown Track") == "Unknown Track"