From db0730f65fe4e7c110d72aa7c87f92423442874c Mon Sep 17 00:00:00 2001 From: jxxghp Date: Mon, 9 Nov 2026 18:41:43 +0800 Subject: [PATCH] fix(music): accept multi-file album release names --- app/domain/meta/metamusic.py | 11 ++++++++++- app/domain/music.py | 20 +++++++++++++++++++- tests/test_music_resource_names.py | 20 ++++++++++++++++++++ 3 files changed, 49 insertions(+), 2 deletions(-) diff --git a/app/domain/meta/metamusic.py b/app/domain/meta/metamusic.py index f9a3bb088..d1189a83e 100644 --- a/app/domain/meta/metamusic.py +++ b/app/domain/meta/metamusic.py @@ -321,9 +321,15 @@ _MUSIC_RIP_METHOD_RE = re.compile(r"(?:分轨|分軌|整轨|整軌|原抓|自抓 _MUSIC_AUDIO_RELEASE_TAIL_RE = re.compile( rf"(?(?:19|20)\d{{2}})\s*[-–—−-]\s*" rf"(?:{_MUSIC_FORMAT_TOKEN_ALT})(?:\s*(?:分[轨軌]|整[轨軌]|原抓|自抓|自扫|自掃))*" + r"(?:\s+multi[\s_-]*file)?" r"(?:\s*[-–—−-]\s*[^\s\-–—−-]+){0,4}\s*$", re.IGNORECASE, ) +# OpenCD 等站点用 ``格式 Multi File`` 表示分轨资源;保留格式词,去掉文件形态尾标。 +_MUSIC_MULTI_FILE_SUFFIX_RE = re.compile( + rf"\s+(?P{_MUSIC_FORMAT_TOKEN_ALT})\s+multi[\s_-]*file" + r"(?:\s*[-–—−-]\s*[A-Za-z0-9][A-Za-z0-9@._-]{1,20})?\s*$", re.IGNORECASE, +) # 圆括号中的联合位深声明(24/48bit)及规格剔除后留下的空括号。 _MUSIC_PAREN_SPEC_RE = re.compile( r"[\((]\s*\d{1,3}\s*/\s*\d{1,3}\s*-?\s*bits?\s*[\))]", @@ -1499,7 +1505,8 @@ class MetaMusic(MetaBase): 为两种解析引擎统一清理有格式证据的发行尾标与音频文件曲序。 点分发行类型只在音频规格前生效;版本括号保留语义,介质和目录编号 - 不进入检索。斜杠规格尾链整段剥离并保留年份,正文中的斜杠仍保留。 + 不进入检索。斜杠规格尾链和 ``FLAC Multi File`` 分轨尾标整段剥离并保留年份, + 正文中的斜杠仍保留。 文件曲序先于艺人拆分,避免 ``01. Artist`` 成为艺人名。 :param value: 原始音乐资源标题或文件名 @@ -1507,6 +1514,8 @@ class MetaMusic(MetaBase): """ text, file_title = self._prepare_filename_input(value) text = _MUSIC_SLASH_SPEC_TAIL_RE.sub(r" \g", text) + text = _MUSIC_AUDIO_RELEASE_TAIL_RE.sub(r" \g", text) + text = _MUSIC_MULTI_FILE_SUFFIX_RE.sub(r" \g", text) text = _MUSIC_EDITION_BRACKET_RE.sub(r"(\1)", text) text = _MUSIC_CATALOG_SPEC_RE.sub("", text) if re.search(r"(?:19|20)\d{2}", text) and len(_SCENE_DOT_RE.findall(value)) >= 3: diff --git a/app/domain/music.py b/app/domain/music.py index f8626d2ff..b5e12f191 100644 --- a/app/domain/music.py +++ b/app/domain/music.py @@ -915,12 +915,30 @@ def _music_title_key(value: Optional[str], *, preserve_editions: bool = False) - return re.sub(r"\s+", "", base).casefold() +def _music_album_main_title(value: Optional[str], *, preserve_editions: bool = False) -> str: + """ + 提取带冒号说明的专辑主标题,兼容资源名省略说明性副标题的常见命名。 + """ + text = normalize("NFKC", str(value or "")).strip() + if not re.search(r"\s*[::]\s*", text): + return "" + main = re.split(r"\s*[::]\s*", text, maxsplit=1)[0] + key = _music_title_key(main, preserve_editions=preserve_editions) + return key if key != _music_title_key(text, preserve_editions=preserve_editions) else "" + + def music_title_matches(music: MusicInfo, title: Optional[str], *, preserve_editions: bool = False) -> bool: """ - 比较完整名称及可信双语别名,保留纯符号名称、未知括号和发行版的身份约束。 + 比较完整名称、可信别名及专辑主标题,保留纯符号名称和发行版的身份约束。 """ key = partial(_music_title_key, preserve_editions=preserve_editions) names = {key(name) for name in music_titles(music)} + if music.music_type == MUSIC_ENTITY_ALBUM: + names.update( + main + for name in music_titles(music, album=True) + if (main := _music_album_main_title(name, preserve_editions=preserve_editions)) + ) expected = _music_name_key(title, names, key) return bool(expected and expected in names) diff --git a/tests/test_music_resource_names.py b/tests/test_music_resource_names.py index d6316be63..62dead678 100644 --- a/tests/test_music_resource_names.py +++ b/tests/test_music_resource_names.py @@ -128,6 +128,26 @@ def test_release_names_without_group_or_cjk_credit(title, artists): assert (meta.title, meta.artists, meta.year, meta.audio_format) == ("Album", artists, 2022, "FLAC") +@pytest.mark.usefixtures("music_engine") +def test_multi_file_release_suffix_matches_album_main_title(): + """ + 分轨资源常用的 ``FLAC Multi File`` 尾标不应进入专辑标题,专辑副标题省略时仍应命中主标题。 + """ + resource = "Various Artists - The Secret Life Of Walter Mitty 2013 - FLAC Multi File" + meta = MetaMusic.parse_resource(resource) + target = MusicInfo( + music_type="album", + title="The Secret Life of Walter Mitty: Music From and Inspired by the Motion Picture", + artists=["Various Artists"], + year=2013, + ) + + assert (meta.title, meta.year, meta.artists) == ( + "The Secret Life Of Walter Mitty", 2013, ["Various Artists"], + ) + assert match_music_resource(target, resource).status == "exact" + + @pytest.mark.usefixtures("music_engine") @pytest.mark.parametrize("title,expected,year", [ (