diff --git a/app/domain/meta/metamusic.py b/app/domain/meta/metamusic.py index 11509e3d3..6838f3867 100644 --- a/app/domain/meta/metamusic.py +++ b/app/domain/meta/metamusic.py @@ -468,6 +468,11 @@ _MUSIC_DISC_DIR_RE = re.compile( r"^\s*(?:cd|disc|disk)\s*(?P\d{1,2})\s*$", re.IGNORECASE, ) +# 碟号后缀在目录名末尾的碟片目录:陈奕迅 - 十年经典 CD1、专辑名 Disc 2 +_MUSIC_DISC_DIR_SUFFIX_RE = re.compile( + r"^(?:.*[\s\-–—_.,、/\\])?(?:cd|disc|disk)\s*(?P\d{1,2})\s*$", + re.IGNORECASE, +) # 目录名中的年份:(2004)、[2004] _MUSIC_DIR_YEAR_RE = re.compile(r"[(\[]\s*(?P(?:19|20)\d{2})\s*[)\]]") _MUSIC_DIR_PREFIX_YEAR_RE = re.compile( @@ -2174,6 +2179,14 @@ class MetaMusic(MetaBase): match = _MUSIC_DISC_DIR_RE.match(str(name or "").strip()) return int(match.group("num")) if match and int(match.group("num")) > 0 else None + @classmethod + def parse_disc_dir_suffix(cls, name: str) -> Optional[int]: + """ + 识别把碟号后缀在目录名末尾的碟片目录并返回碟号,如 `陈奕迅 - 十年经典 CD1`。 + """ + match = _MUSIC_DISC_DIR_SUFFIX_RE.match(str(name or "").strip()) + return int(match.group("num")) if match and int(match.group("num")) > 0 else None + @staticmethod def _normalize_album_year(value: str) -> str: """ @@ -2292,6 +2305,10 @@ class MetaMusic(MetaBase): self.disc_number = parent_disc self.field_sources["disc_number"] = "directory" album_dir = parent.parent + elif self.disc_number is None and (suffix_disc := self.parse_disc_dir_suffix(parent.name)) is not None: + # 碟号写进专辑目录名末尾时只补碟号,该目录仍是专辑目录,不能上移专辑根。 + self.disc_number = suffix_disc + self.field_sources["disc_number"] = "directory" dir_info = self.parse_album_dir(album_dir.name) if dir_info: # 文件已明确歌手时,纯专辑名目录也可补足检索线索;通用监控根目录仍须排除。 diff --git a/tests/test_metamusic.py b/tests/test_metamusic.py index 5c469a842..276d70ee0 100644 --- a/tests/test_metamusic.py +++ b/tests/test_metamusic.py @@ -605,6 +605,41 @@ def test_parse_disc_dir(): assert MetaMusic.parse_disc_dir("无损音乐") is None +def test_parse_disc_dir_suffix(): + """ + 碟号后缀在专辑目录名末尾时识别碟号,且不得误读非碟片后缀的目录名。 + """ + assert MetaMusic.parse_disc_dir_suffix("陈奕迅 - 十年经典 CD1") == 1 + assert MetaMusic.parse_disc_dir_suffix("周杰伦 哎哟不错哦 Disc 2") == 2 + assert MetaMusic.parse_disc_dir_suffix("陈奕迅 DUO 演唱会 2CD") is None + assert MetaMusic.parse_disc_dir_suffix("2CD1") is None + assert MetaMusic.parse_disc_dir_suffix("某某专辑 CD100") is None + assert MetaMusic.parse_disc_dir_suffix("Discipline 1") is None + assert MetaMusic.parse_disc_dir_suffix("The Best disco") is None + + +def test_apply_path_context_reads_suffix_disc_without_moving_album_root(): + """ + 碟号后缀目录只补碟号,专辑线索仍来自该目录本身,多碟同曲序不会互相覆盖。 + """ + disc_two = MetaMusic( + org_string="01 你的背包.flac", + title="01 你的背包", + ).apply_path_context(Path("陈奕迅 - 十年经典 CD2/01 你的背包.flac")) + album_root = MetaMusic( + org_string="01 你的背包.flac", + title="01 你的背包", + ).apply_path_context(Path("陈奕迅 - 十年经典/CD2/01 你的背包.flac")) + + assert disc_two.disc_number == 2 + assert disc_two.track_number == 1 + assert disc_two.field_sources["disc_number"] == "directory" + assert disc_two.album == "十年经典 CD2" + # 独立碟片子目录仍把专辑根上移一级,两种布局的碟号证据都必须可用。 + assert album_root.disc_number == 2 + assert album_root.album == "十年经典" + + def test_parse_album_dir_extracts_artist_album_year(): """ 专辑目录名应提取歌手、专辑、年份和音质描述。 diff --git a/tests/test_music_download.py b/tests/test_music_download.py index 8d6a61b71..3869547a0 100644 --- a/tests/test_music_download.py +++ b/tests/test_music_download.py @@ -139,6 +139,36 @@ def test_album_resource_dedupes_same_track_in_different_formats(): assert context.confirmed_full_coverage is False +def test_album_resource_keeps_suffix_disc_directory_tracks_separate(): + """ + 碟号后缀在顶层目录名中的多碟专辑,两盘同曲序不能压成同一音轨键。 + """ + context = Context(media_info=_album_info(total_tracks=6)) + + DownloadChain._record_music_album_track_keys( + context, + [ + "陈奕迅 - 十年经典 CD1/01 K歌之王.flac", + "陈奕迅 - 十年经典 CD1/02 富士山下.flac", + "陈奕迅 - 十年经典 CD1/03 你的背包.flac", + "陈奕迅 - 十年经典 CD2/01 你的背包.flac", + "陈奕迅 - 十年经典 CD2/02 Shall We Talk.flac", + "陈奕迅 - 十年经典 CD2/03 不如不见.flac", + ], + ) + + assert context.music_track_keys == ["[1,1]", "[1,2]", "[1,3]", "[2,1]", "[2,2]", "[2,3]"] + + # 独立碟片子目录的既有布局必须保持同样的键集,修复不能改变已正确的解析。 + nested = Context(media_info=_album_info(total_tracks=6)) + DownloadChain._record_music_album_track_keys( + nested, + [f"十年经典/CD{disc}/{track:02d} 曲目.flac" for disc in (1, 2) for track in range(1, 4)], + ) + assert nested.music_track_keys == context.music_track_keys + assert context.confirmed_full_coverage is False + + def test_download_single_prepares_partial_album_for_client_submission(): """ 下载入口允许不完整专辑进入下载器,并把本轮音轨事实附着到上下文。