fix(music): align album tracks by exact title

This commit is contained in:
jxxghp
2026-08-26 23:11:38 +08:00
parent d3627f29c8
commit 9053db926d
2 changed files with 115 additions and 15 deletions
+63 -15
View File
@@ -1209,33 +1209,81 @@ class MediaChain(ChainBase, metaclass=Singleton):
return tuple(str(path.relative_to(directory)).casefold() for path in files)
@staticmethod
def _music_track_title_key(value: Optional[str]) -> str:
"""统一繁简、大小写和标点,生成专辑曲目精确对位键。"""
text = str(value or "")
try:
text = zhconv_convert(text, "zh-hans")
except Exception: # pylint: disable=broad-except
pass
return MetaMusic.compact_text(text)
@classmethod
def _align_music_album_tracks(
cls,
files: list[Path],
metas: list[MetaMusic],
tracks: list[MusicInfo],
) -> dict[Path, MusicInfo]:
"""优先按碟号和曲序把专辑曲目对位到本地文件。"""
"""优先按精确曲名、再按碟号和曲序把专辑曲目对位到本地文件。"""
matched: dict[Path, MusicInfo] = {}
used: set[tuple[int, int]] = set()
by_position = {
(track.disc_number or 1, track.track_number): track
for track in tracks if track.track_number
}
used: set[int] = set()
indexed_tracks = list(enumerate(tracks))
by_title: dict[str, list[int]] = {}
for index, track in indexed_tracks:
key = cls._music_track_title_key(track.title)
if key:
by_title.setdefault(key, []).append(index)
pending: list[tuple[Path, MetaMusic]] = []
for file, meta in zip(files, metas):
key = (meta.disc_number or 1, meta.track_number or 0)
track = by_position.get(key) if meta.track_number else None
if track and key not in used:
matched[file] = track
used.add(key)
candidates = [
index for index in by_title.get(cls._music_track_title_key(meta.title), [])
if index not in used
]
if candidates:
local_position = (meta.disc_number or 1, meta.track_number or 0)
index = next(
(
candidate for candidate in candidates
if (
tracks[candidate].disc_number or 1,
tracks[candidate].track_number or 0,
) == local_position
),
candidates[0],
)
matched[file] = tracks[index]
used.add(index)
else:
pending.append((file, meta))
by_position: dict[tuple[int, int], list[int]] = {}
for index, track in indexed_tracks:
if track.track_number:
by_position.setdefault(
(track.disc_number or 1, track.track_number), []
).append(index)
unresolved: list[tuple[Path, MetaMusic]] = []
for file, meta in pending:
key = (meta.disc_number or 1, meta.track_number or 0)
candidates = [
index for index in by_position.get(key, [])
if index not in used
] if meta.track_number else []
if candidates:
index = candidates[0]
matched[file] = tracks[index]
used.add(index)
else:
unresolved.append((file, meta))
remaining = [
track for track in tracks
if (track.disc_number or 1, track.track_number or 0) not in used
track for index, track in indexed_tracks if index not in used
]
pending.sort(key=lambda item: (item[1].disc_number or 1, item[0].name.casefold()))
for (file, _), track in zip(pending, remaining):
unresolved.sort(
key=lambda item: (item[1].disc_number or 1, item[0].name.casefold())
)
for (file, _), track in zip(unresolved, remaining):
matched[file] = track
return matched
+52
View File
@@ -1,4 +1,5 @@
import asyncio
from pathlib import Path
from unittest.mock import AsyncMock, Mock
import pytest
@@ -165,6 +166,57 @@ def test_recognize_album_directory_maps_files(tmp_path, media_chain, monkeypatch
source_chain.match_music_album.assert_called_once()
def test_align_album_tracks_prefers_exact_titles_over_conflicting_positions():
"""本地曲序与发行版本冲突时,精确曲名必须优先,避免整张专辑错位。"""
files = [
Path("费玉清-真的好想你.flac"),
Path("费玉清-冬之夜.flac"),
Path("费玉清-愛是一個圓.flac"),
Path("04.flac"),
]
metas = [
MetaMusic(title="真的好想你", track_number=1),
MetaMusic(title="冬之夜", track_number=2),
MetaMusic(title="愛是一個圓", track_number=3),
MetaMusic(title="04", track_number=4),
]
tracks = [
MusicInfo(
media_source="musicbrainz",
media_id="winter",
title="冬之夜",
track_number=1,
),
MusicInfo(
media_source="musicbrainz",
media_id="circle",
title="爱是一个圆",
track_number=2,
),
MusicInfo(
media_source="musicbrainz",
media_id="miss",
title="真的好想你",
track_number=3,
),
MusicInfo(
media_source="musicbrainz",
media_id="fallback",
title="一生的朋友",
track_number=4,
),
]
matched = MediaChain._align_music_album_tracks(files, metas, tracks)
assert [matched[file].media_id for file in files] == [
"miss",
"winter",
"circle",
"fallback",
]
def test_async_recognize_album_directory_calls_async_module(
tmp_path,
media_chain,