#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
|
score = 0.0
|
||||||
|
|
||||||
# Title similarity (TITLE_WEIGHT). Falls back to filename stem when
|
# 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]
|
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', '')
|
track_name = track.get('name', '')
|
||||||
score += similarity(title, track_name) * TITLE_WEIGHT
|
score += similarity(title, track_name) * TITLE_WEIGHT
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -183,7 +183,12 @@ def get_import_clean_title(
|
||||||
if not title:
|
if not title:
|
||||||
track_info = get_import_track_info(context)
|
track_info = get_import_track_info(context)
|
||||||
title = _first_value(track_info, "name", "title", default="")
|
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(
|
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
|
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:
|
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