#890: strip leading track-number prefix from filename-derived titles

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.
This commit is contained in:
BoulderBadgeDad 2026-06-18 18:35:04 -07:00
parent 8e218303be
commit 213592821b
4 changed files with 114 additions and 2 deletions

View file

@ -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

View file

@ -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(

View file

@ -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:

View file

@ -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"