#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:
parent
8e218303be
commit
213592821b
4 changed files with 114 additions and 2 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
77
tests/imports/test_track_number_strip.py
Normal file
77
tests/imports/test_track_number_strip.py
Normal 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"
|
||||
Loading…
Reference in a new issue