mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-05 23:47:41 +08:00
refactor(music): 音乐识别核心下沉 MetaMusic 并优化 MusicBrainz 检索
- 标题解析核心从 app/helper/music.py 迁移到 MetaMusic(apply_title), 删除 music helper,metainfo/chain 调用点切换 - 解析增强:全角归一、场景点分命名、年份区间双模式、日期前缀、 视频标记剔除、VA 合辑署名归一、尾部年份提取、CJK 连字符拆分 - MusicBrainz 性能:全局 Session keep-alive 复用(6.3s→0.35s/请求), 无艺术家线索时跳过专辑回退检索 - 候选挑选支持冒号副标题弱匹配,检索式剥离尾部年份减少无效检索 - 测试:test_metamusic 32 案例、musicbrainz 弱匹配/检索式新用例
This commit is contained in:
@@ -0,0 +1,323 @@
|
||||
from app.core.meta import MetaMusic
|
||||
|
||||
|
||||
def parse_title(title: str) -> MetaMusic:
|
||||
"""构造种子/文件名标题解析结果,供识别断言复用。"""
|
||||
return MetaMusic(org_string=title, title=title, parse_title=True)
|
||||
|
||||
|
||||
def test_strip_track_prefix_handles_dot_separator():
|
||||
"""曲序前缀 01. 应剥离并返回曲名。"""
|
||||
track, disc, title = MetaMusic.split_track_prefix("01.晴天")
|
||||
|
||||
assert (track, disc, title) == (1, None, "晴天")
|
||||
|
||||
|
||||
def test_strip_track_prefix_handles_dash_and_space():
|
||||
"""常见 rip 命名 01 - 曲名 和 01 曲名 都应识别曲序。"""
|
||||
assert MetaMusic.split_track_prefix("03 - 七里香") == (3, None, "七里香")
|
||||
assert MetaMusic.split_track_prefix("05 借口") == (5, None, "借口")
|
||||
|
||||
|
||||
def test_strip_track_prefix_handles_disc_track_number():
|
||||
"""碟号-曲序前缀 1-02 应同时提取碟号和曲序。"""
|
||||
track, disc, title = MetaMusic.split_track_prefix("1-02 半岛铁盒")
|
||||
|
||||
assert (track, disc, title) == (2, 1, "半岛铁盒")
|
||||
|
||||
|
||||
def test_strip_track_prefix_handles_number_only_name():
|
||||
"""纯数字文件名只能得到曲序,曲名返回 None 由调用方兜底。"""
|
||||
track, disc, title = MetaMusic.split_track_prefix("07")
|
||||
|
||||
assert (track, disc, title) == (7, None, None)
|
||||
|
||||
|
||||
def test_strip_track_prefix_keeps_normal_title():
|
||||
"""普通曲名不应被误判为曲序前缀。"""
|
||||
assert MetaMusic.split_track_prefix("晴天") == (None, None, None)
|
||||
assert MetaMusic.split_track_prefix("2002") == (None, None, None)
|
||||
|
||||
|
||||
def test_split_artist_title():
|
||||
"""歌手 - 曲名结构应拆分,无分隔符时原文作为标题。"""
|
||||
artist, title = MetaMusic.split_artist_title("周杰伦 - 晴天")
|
||||
|
||||
assert artist == "周杰伦"
|
||||
assert title == "晴天"
|
||||
assert MetaMusic.split_artist_title("晴天") == (None, "晴天")
|
||||
|
||||
|
||||
def test_parse_disc_dir():
|
||||
"""CD1、Disc 2 等碟片目录应识别碟号。"""
|
||||
assert MetaMusic.parse_disc_dir("CD1") == 1
|
||||
assert MetaMusic.parse_disc_dir("Disc 2") == 2
|
||||
assert MetaMusic.parse_disc_dir("disk03") == 3
|
||||
assert MetaMusic.parse_disc_dir("无损音乐") is None
|
||||
|
||||
|
||||
def test_parse_album_dir_extracts_artist_album_year():
|
||||
"""专辑目录名应提取歌手、专辑、年份和音质描述。"""
|
||||
info = MetaMusic.parse_album_dir("周杰伦 - 七里香 (2004) [FLAC 24bit-96kHz]")
|
||||
|
||||
assert info["artist"] == "周杰伦"
|
||||
assert info["album"] == "七里香"
|
||||
assert info["year"] == 2004
|
||||
assert "FLAC" in info["quality_text"]
|
||||
|
||||
|
||||
def test_parse_album_dir_without_artist():
|
||||
"""没有歌手分隔的目录名整体作为专辑名。"""
|
||||
info = MetaMusic.parse_album_dir("Random Access Memories (2013)")
|
||||
|
||||
assert info["artist"] is None
|
||||
assert info["album"] == "Random Access Memories"
|
||||
assert info["year"] == 2013
|
||||
|
||||
|
||||
def test_apply_path_context_fills_wav_meta(tmp_path):
|
||||
"""无标签 WAV 应从文件名和目录结构补齐曲序、专辑和歌手。"""
|
||||
album_dir = tmp_path / "周杰伦 - 七里香 (2004) [FLAC]"
|
||||
album_dir.mkdir()
|
||||
wav_file = album_dir / "01.我的地盘.wav"
|
||||
wav_file.write_bytes(b"RIFF")
|
||||
|
||||
meta = MetaMusic(org_string=wav_file.name, title=wav_file.stem, audio_format="WAV")
|
||||
meta.apply_path_context(wav_file)
|
||||
|
||||
assert meta.track_number == 1
|
||||
assert meta.title == "我的地盘"
|
||||
assert meta.album == "七里香"
|
||||
assert meta.artists == ["周杰伦"]
|
||||
assert meta.album_artist == "周杰伦"
|
||||
assert meta.year == 2004
|
||||
|
||||
|
||||
def test_apply_path_context_uses_disc_subdir(tmp_path):
|
||||
"""CD1 子目录内的文件应继承碟号并向上找到专辑目录。"""
|
||||
album_dir = tmp_path / "Daft Punk - Discovery (2001)"
|
||||
disc_dir = album_dir / "CD1"
|
||||
disc_dir.mkdir(parents=True)
|
||||
wav_file = disc_dir / "01 - One More Time.flac"
|
||||
wav_file.write_bytes(b"fLaC")
|
||||
|
||||
meta = MetaMusic(org_string=wav_file.name, title=wav_file.stem, audio_format="FLAC")
|
||||
meta.apply_path_context(wav_file)
|
||||
|
||||
assert meta.disc_number == 1
|
||||
assert meta.track_number == 1
|
||||
assert meta.title == "One More Time"
|
||||
assert meta.album == "Discovery"
|
||||
assert meta.artists == ["Daft Punk"]
|
||||
|
||||
|
||||
def test_apply_path_context_keeps_existing_tags(tmp_path):
|
||||
"""已有标签字段不应被目录猜测覆盖。"""
|
||||
album_dir = tmp_path / "周杰伦 - 七里香 (2004)"
|
||||
album_dir.mkdir()
|
||||
audio_file = album_dir / "01.我的地盘.mp3"
|
||||
audio_file.write_bytes(b"")
|
||||
|
||||
meta = MetaMusic(
|
||||
org_string=audio_file.name,
|
||||
title="我的地盘",
|
||||
artists=["周杰伦"],
|
||||
album="七里香",
|
||||
year=2004,
|
||||
track_number=1,
|
||||
audio_format="MP3",
|
||||
)
|
||||
meta.apply_path_context(audio_file)
|
||||
|
||||
assert meta.title == "我的地盘"
|
||||
assert meta.artists == ["周杰伦"]
|
||||
assert meta.album == "七里香"
|
||||
assert meta.year == 2004
|
||||
|
||||
|
||||
def test_apply_title_splits_artist_and_track():
|
||||
"""标准「歌手 - 曲名」种子标题应拆分艺术家与曲名。"""
|
||||
meta = parse_title("周杰伦 - 晴天")
|
||||
|
||||
assert meta.artists == ["周杰伦"]
|
||||
assert meta.title == "晴天"
|
||||
|
||||
|
||||
def test_apply_title_splits_multi_artists():
|
||||
"""多艺术家 & 联名写法应拆分为列表保留顺序。"""
|
||||
meta = parse_title("章子怡 & 周深 - 灯火里的中国")
|
||||
|
||||
assert meta.artists == ["章子怡", "周深"]
|
||||
assert meta.title == "灯火里的中国"
|
||||
|
||||
|
||||
def test_apply_title_strips_quality_tokens():
|
||||
"""格式、位深采样与发行标记不应进入曲名。"""
|
||||
meta = parse_title("[250917] SARD UNDERGROUND - 故障した車 FLAC")
|
||||
|
||||
assert meta.artists == ["SARD UNDERGROUND"]
|
||||
assert meta.title == "故障した車"
|
||||
assert meta.audio_format == "FLAC"
|
||||
|
||||
|
||||
def test_apply_title_strips_video_tokens():
|
||||
"""演唱会视频种子的分辨率与编码标记不应进入曲名。"""
|
||||
meta = parse_title("S H E - S H E十七音乐会 2018 WEB-DL 1080P AVC AAC-FHDMv")
|
||||
|
||||
assert meta.artists == ["S H E"]
|
||||
assert meta.title == "S H E十七音乐会"
|
||||
# 尾部年份提取为发行年份线索
|
||||
assert meta.year == 2018
|
||||
|
||||
|
||||
def test_apply_title_strips_release_group_tag():
|
||||
"""格式标记后的发布组标签(大小写混合)应整体剔除。"""
|
||||
meta = parse_title("某某乐队 - 星空 FLAC-FHDMv")
|
||||
|
||||
assert meta.title == "星空"
|
||||
|
||||
|
||||
def test_apply_title_strips_date_prefix():
|
||||
"""电视录制标题开头的日期前缀应剔除。"""
|
||||
meta = parse_title("2018.01.10 藤田麻衣子 思い続ければ FLAC")
|
||||
|
||||
assert meta.title == "藤田麻衣子 思い続ければ"
|
||||
assert meta.audio_format == "FLAC"
|
||||
|
||||
|
||||
def test_apply_title_date_prefix_with_time():
|
||||
"""带时分秒的录制前缀(下划线分隔)同样应剔除。"""
|
||||
meta = parse_title("2024-01-27_20-00_WOWOWプライム_松任谷由実 50th Anniversary")
|
||||
|
||||
assert "2024" not in (meta.title or "")
|
||||
assert "松任谷由実" in (meta.title or "")
|
||||
assert "50th" in (meta.title or "")
|
||||
|
||||
|
||||
def test_apply_title_keeps_song_named_with_year():
|
||||
"""曲名自带的年份数字(非日期结构)不应被误剔除。"""
|
||||
meta = parse_title("2002年的第一场雪")
|
||||
|
||||
assert meta.title == "2002年的第一场雪"
|
||||
|
||||
|
||||
def test_apply_title_year_range_as_year():
|
||||
"""全集标题尾部的年份区间应提取结束年并剥离出标题。"""
|
||||
meta = parse_title("天国的情人-邓丽君作品全集1967-1995")
|
||||
|
||||
assert meta.title == "天国的情人"
|
||||
assert meta.artists == ["邓丽君"]
|
||||
assert meta.year == 1995
|
||||
|
||||
|
||||
def test_apply_title_year_range_in_title_kept():
|
||||
"""年份区间后随内容文字时属于标题本身,只提取年份不剥离。"""
|
||||
meta = parse_title("许茹芸 - 许茹芸1995-2000年光华真纪录 (2001)")
|
||||
|
||||
assert meta.artists == ["许茹芸"]
|
||||
assert meta.title == "许茹芸1995-2000年光华真纪录"
|
||||
# 括号年份优先于区间提取的结束年
|
||||
assert meta.year == 2001
|
||||
|
||||
|
||||
def test_apply_title_scene_dot_naming():
|
||||
"""场景点分命名的点号应归一为空格,发布组标签剔除。"""
|
||||
meta = parse_title("Shan.Ge.Liao.Zai.2023.WEB-DL.FLAC-CMCTA")
|
||||
|
||||
assert meta.title == "Shan Ge Liao Zai"
|
||||
assert meta.year == 2023
|
||||
assert meta.audio_format == "FLAC"
|
||||
|
||||
|
||||
def test_apply_title_scene_dot_with_symbols():
|
||||
"""点分命名中环绕符号的点(.&.、.-.)也应归一并支持艺术家拆分。"""
|
||||
meta = parse_title(
|
||||
"Deep.Purple.&.Orchestra.-.Live.At.Montreux.1999.2022.1080p.BluRay.AVC.DTS-HD.MA5.1"
|
||||
)
|
||||
|
||||
assert meta.artists == ["Deep Purple", "Orchestra"]
|
||||
assert meta.title == "Live At Montreux 1999 2022"
|
||||
|
||||
|
||||
def test_apply_title_keeps_artist_abbreviation_dots():
|
||||
"""点分隔少于 3 处的艺术家缩写点号不应被归一。"""
|
||||
meta = parse_title("E.S.Posthumus - Maraboot")
|
||||
|
||||
assert meta.artists == ["E.S.Posthumus"]
|
||||
assert meta.title == "Maraboot"
|
||||
|
||||
|
||||
def test_apply_title_va_alias_artist():
|
||||
"""合辑署名 VA 应归一为 MusicBrainz 规范署名 Various Artists。"""
|
||||
meta = parse_title("VA - Funky Jazz Saxophone 2024 FLAC")
|
||||
|
||||
assert meta.artists == ["Various Artists"]
|
||||
assert meta.title == "Funky Jazz Saxophone"
|
||||
assert meta.year == 2024
|
||||
|
||||
|
||||
def test_apply_title_va_scene_prefix():
|
||||
"""场景命名 VA-Title 无空格连字符写法应按别名前缀拆分。"""
|
||||
meta = parse_title(
|
||||
"VA-Once.Upon.a.Time.in.Hollywood.Original.Motion.Picture.Soundtrack.2019.FLAC.24bit.96kHz"
|
||||
)
|
||||
|
||||
assert meta.artists == ["Various Artists"]
|
||||
assert meta.title == "Once Upon a Time in Hollywood Original Motion Picture Soundtrack"
|
||||
assert meta.year == 2019
|
||||
|
||||
|
||||
def test_apply_title_keeps_single_word_title():
|
||||
"""无音质标记时「艺术家 - 单词曲名」的曲名不应被当发布组标签剔除。"""
|
||||
meta = parse_title("E.S.Posthumus - Maraboot")
|
||||
|
||||
assert meta.title == "Maraboot"
|
||||
|
||||
|
||||
def test_apply_title_cjk_hyphen_artist_suffix():
|
||||
"""CJK「曲名-歌手」无空格连字符写法应反向拆分艺术家。"""
|
||||
meta = parse_title("因为有你-毛阿敏")
|
||||
|
||||
assert meta.artists == ["毛阿敏"]
|
||||
assert meta.title == "因为有你"
|
||||
|
||||
|
||||
def test_apply_title_double_em_dash_split():
|
||||
"""双破折号分隔的「主题——歌手」写法应拆分艺术家。"""
|
||||
meta = parse_title("为你盛开——许巍 无尽光芒巡回演唱会 2025")
|
||||
|
||||
assert meta.artists == ["许巍"]
|
||||
assert meta.title.startswith("为你盛开")
|
||||
|
||||
|
||||
def test_apply_title_keeps_english_hyphen_title():
|
||||
"""英文曲名中的连字符是标题组成部分,不做艺术家拆分。"""
|
||||
meta = parse_title("Dire Straits - Alchemy-Live 1983")
|
||||
|
||||
assert meta.artists == ["Dire Straits"]
|
||||
assert meta.title == "Alchemy-Live"
|
||||
assert meta.year == 1983
|
||||
|
||||
|
||||
def test_apply_title_lossless_declaration():
|
||||
"""「无损」声明词应剥离出标题并推断无损音质。"""
|
||||
meta = parse_title("某某 - 晴天 FLAC 无损")
|
||||
|
||||
assert meta.title == "晴天"
|
||||
assert meta.audio_lossless is True
|
||||
|
||||
|
||||
def test_apply_title_track_prefix_in_title():
|
||||
"""标题中的曲序前缀应提取为曲序并还原曲名。"""
|
||||
meta = parse_title("01.晴天")
|
||||
|
||||
assert meta.track_number == 1
|
||||
assert meta.title == "晴天"
|
||||
|
||||
|
||||
def test_apply_title_bracket_date_prefix():
|
||||
"""发行日期方括号前缀应整体剔除。"""
|
||||
meta = parse_title("[250917] 某歌手 - 某曲名")
|
||||
|
||||
assert meta.artists == ["某歌手"]
|
||||
assert meta.title == "某曲名"
|
||||
Reference in New Issue
Block a user