mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-08-28 19:47:41 +08:00
fix(music): align album tracks by exact title
This commit is contained in:
+63
-15
@@ -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
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user