mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-08-14 10:14:36 +08:00
fix(music): 缩写点号还原与包含/演出后缀弱匹配修复失配
- metamusic: 连续单字母空格序列还原为点号缩写(S H E -> S.H.E), 在场景点分之后执行避免点号再被压平 - metamusic: 规格段残留判定收紧,发布组短词仅在格式词无空格连字符 形态(FLAC-HHWEB)放行,修复自然语言曲名(Ashielf Alpen)被误剥 - musicbrainz: 专辑候选新增包含关系弱匹配(原声带条目带额外前缀) 与演出后缀弱匹配(S.H.E十七音乐会 -> 条目「十七」) - 补充 metamusic 缩写还原与 musicbrainz 弱匹配测试案例
This commit is contained in:
@@ -208,14 +208,14 @@ _MUSIC_QUALITY_TOKEN_RE = re.compile(
|
||||
# 演唱会/音乐视频种子的视频编码标记:分辨率、编码、容器与声道描述,
|
||||
# 不是音乐文本信息,不剔除会污染曲名并阻断艺术家/曲名拆分
|
||||
_MUSIC_VIDEO_TOKEN_RE = re.compile(rf"\b(?:{_MUSIC_VIDEO_TOKEN_ALT})\b", re.IGNORECASE)
|
||||
# 尾部规格段判定:整段仅由格式词、视频标记、位深采样、无损声明词与发布组标签组成,
|
||||
# 曲名含任何自然语言文本时判定失败,保证「曲名 (注释) - WEB-DL」不被误剥
|
||||
# 尾部规格段判定:整段仅由格式词、视频标记、位深采样与无损声明词组成,
|
||||
# 曲名含任何自然语言文本时判定失败,保证「曲名 (注释) - WEB-DL」不被误剥;
|
||||
# 发布组标签(HHWEB/FHDMv)不是固定词表,由 _strip_spec_segments 的短词规则另行放行
|
||||
_MUSIC_SPEC_SEGMENT_RE = re.compile(
|
||||
rf"^(?:\b(?:{_MUSIC_FORMAT_TOKEN_ALT}|{_MUSIC_VIDEO_TOKEN_ALT}|single|ep|album)\b"
|
||||
r"|\d{1,3}\s*-?\s*bits?|\d{2,4}(?:(?:[..]|\s)\d)?\s*k(?:hz|bps?)"
|
||||
r"|lossless|无损音质|无损|分轨|整轨|合集|精选"
|
||||
r"|[\s\-–—−-/+]+(?![\s\-–—−-/+])"
|
||||
r"|(?:(?=[A-Za-z0-9]*[A-Za-z])[A-Za-z0-9]{3,})"
|
||||
r"|[\s\-–—−-/+]"
|
||||
r")+$",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
@@ -364,6 +364,18 @@ def _normalize_scene_dots(value: str) -> str:
|
||||
return _SCENE_DOT_RE.sub(" ", value)
|
||||
|
||||
|
||||
# 连续单字母空格序列是缩写点号被全角归一/场景点分压平的结果(S.H.E -> S H E),
|
||||
# 还原为点号缩写(S.H.E)才能与 MusicBrainz 条目署名比对
|
||||
_LETTER_RUN_RE = re.compile(r"(?<![A-Za-z])((?:[A-Za-z] ){2,}[A-Za-z])(?![A-Za-z])")
|
||||
|
||||
|
||||
def _restore_letter_abbrev(value: str) -> str:
|
||||
"""把连续单字母空格序列还原为点号缩写(S H E -> S.H.E)。"""
|
||||
return _LETTER_RUN_RE.sub(
|
||||
lambda m: ".".join(m.group(1).split(" ")), str(value or "")
|
||||
)
|
||||
|
||||
|
||||
class MetaMusic(MetaBase):
|
||||
"""音乐文件名及音频标签解析结果,作为 MetaBase 的音乐分支实现。"""
|
||||
|
||||
@@ -598,6 +610,8 @@ class MetaMusic(MetaBase):
|
||||
text = _to_halfwidth(str(value or "")).replace("_", " ")
|
||||
# 场景命名用点号分隔单词(Shan.Ge.Liao.Zai.2023),归一为空格便于拆分检索
|
||||
text = _normalize_scene_dots(text)
|
||||
# 缩写点号被全角归一/场景点分压平成单字母空格序列,在点分之后还原(S H E -> S.H.E)
|
||||
text = _restore_letter_abbrev(text)
|
||||
return _MUSIC_SPACES_RE.sub(" ", text).strip()
|
||||
|
||||
@classmethod
|
||||
@@ -645,7 +659,7 @@ class MetaMusic(MetaBase):
|
||||
# 加号是 APE+CUE 类格式联合写法的分隔符,占位为空格后再判定
|
||||
probe = _MUSIC_VIDEO_TOKEN_RE.sub(" ", _MUSIC_QUALITY_TOKEN_RE.sub(" ", probe))
|
||||
residue = probe.replace("+", " ").strip()
|
||||
if residue and not _MUSIC_SPEC_SEGMENT_RE.fullmatch(residue):
|
||||
if residue and not cls._is_spec_residue(residue, bool(prefix)):
|
||||
return text
|
||||
# 不含任何规格词的纯标签段,仅在紧跟格式词的无空格形态下才是发布组标签;
|
||||
# 「艺术家 - 单词曲名」的曲名段不含规格词,必须保留
|
||||
@@ -653,6 +667,21 @@ class MetaMusic(MetaBase):
|
||||
return text
|
||||
text = text[: segment_match.start()].rstrip()
|
||||
|
||||
@classmethod
|
||||
def _is_spec_residue(cls, residue: str, has_prefix: bool) -> bool:
|
||||
"""判定规格词替换后的残留是否为发布组标签而非自然语言曲名。
|
||||
|
||||
发布组标签只出现在格式词无空格连字符形态(FLAC-HHWEB、AAC-FHDMv),
|
||||
此时短字母数字组合可放行;空白连字符分隔的段(艺术家 - 曲名 FLAC)
|
||||
残留可能是自然语言曲名,仅严格规格词表可过。
|
||||
"""
|
||||
if _MUSIC_SPEC_SEGMENT_RE.fullmatch(residue):
|
||||
return True
|
||||
tokens = residue.split()
|
||||
return has_prefix and bool(tokens) and all(
|
||||
len(token) <= 8 and re.fullmatch(r"[A-Za-z0-9]+", token) for token in tokens
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _strip_quality_tokens(cls, value: str) -> str:
|
||||
"""剥离音频格式、视频编码、规格参数与年份括号,保留有效的艺术家与曲名文本。"""
|
||||
|
||||
@@ -192,6 +192,15 @@ class MusicBrainzModule(_ModuleBase):
|
||||
text = str(value or "").strip()
|
||||
return cls._normalize_text(text.split(" ", 1)[0]) if text else ""
|
||||
|
||||
# 演唱会资源的标题常带演出后缀(S.H.E十七音乐会),条目仅保留演出名本体
|
||||
_PERFORMANCE_SUFFIX_RE = re.compile(
|
||||
r"\s*(?:音乐会|音樂會|演唱会|演唱會|巡回|巡演|Live|Tour)$", re.IGNORECASE)
|
||||
|
||||
@classmethod
|
||||
def _performance_title(cls, value: Optional[str]) -> str:
|
||||
"""剔除标题尾部的演出形态后缀,返回演出名本体。"""
|
||||
return cls._normalize_text(cls._PERFORMANCE_SUFFIX_RE.sub("", str(value or "")))
|
||||
|
||||
@classmethod
|
||||
def _strip_artist_prefix(cls, title: Optional[str], artists: Optional[list[str]]) -> str:
|
||||
"""剥离曲名开头的艺术家署名前缀(「许茹芸的爱情电影主题曲」)。
|
||||
@@ -635,6 +644,16 @@ class MusicBrainzModule(_ModuleBase):
|
||||
len(cls._match_text(bare_title)) >= 3
|
||||
and cls._same_text(bare_title, cls._lead_token(candidate.title))
|
||||
)
|
||||
# 条目带额外前缀/后缀完整包含资源主体名(好莱坞原声带类),长文本包含视为弱匹配
|
||||
or (
|
||||
len(cls._match_text(bare_title)) >= 6
|
||||
and cls._match_text(bare_title) in cls._match_text(candidate.title)
|
||||
)
|
||||
# 资源标题带演出后缀(S.H.E十七音乐会),条目本体一致视为弱匹配
|
||||
or (
|
||||
cls._performance_title(bare_title)
|
||||
and cls._same_text(cls._performance_title(bare_title), candidate.title)
|
||||
)
|
||||
)
|
||||
):
|
||||
score += 2
|
||||
@@ -701,6 +720,16 @@ class MusicBrainzModule(_ModuleBase):
|
||||
len(cls._match_text(bare_title)) >= 3
|
||||
and cls._same_text(bare_title, cls._lead_token(album_title))
|
||||
)
|
||||
# 条目带额外前缀/后缀完整包含资源主体名(好莱坞原声带类),长文本包含视为弱匹配
|
||||
or (
|
||||
len(cls._match_text(bare_title)) >= 6
|
||||
and cls._match_text(bare_title) in cls._match_text(album_title)
|
||||
)
|
||||
# 资源标题带演出后缀(S.H.E十七音乐会),条目本体一致视为弱匹配
|
||||
or (
|
||||
cls._performance_title(bare_title)
|
||||
and cls._same_text(cls._performance_title(bare_title), album_title)
|
||||
)
|
||||
)
|
||||
):
|
||||
# 「我爱夜 (新歌+精选)」对位条目「我爱夜」这类注释差异
|
||||
|
||||
@@ -164,12 +164,22 @@ 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十七音乐会"
|
||||
# 连续单字母空格序列是缩写点号被压平的结果,还原为 S.H.E 才能与条目署名比对
|
||||
assert meta.artists == ["S.H.E"]
|
||||
assert meta.title == "S.H.E十七音乐会"
|
||||
# 尾部年份提取为发行年份线索
|
||||
assert meta.year == 2018
|
||||
|
||||
|
||||
def test_apply_title_restores_letter_abbrev():
|
||||
"""单字母点号缩写还原不误伤合法缩写与单词。"""
|
||||
meta = parse_title("E.S.Posthumus - Ashielf Alpen FLAC")
|
||||
|
||||
# 点分少于 3 处的合法缩写不被场景点分/还原逻辑破坏
|
||||
assert meta.artists == ["E.S.Posthumus"]
|
||||
assert meta.title == "Ashielf Alpen"
|
||||
|
||||
|
||||
def test_apply_title_strips_release_group_tag():
|
||||
"""格式标记后的发布组标签(大小写混合)应整体剔除。"""
|
||||
meta = parse_title("某某乐队 - 星空 FLAC-FHDMv")
|
||||
|
||||
@@ -661,6 +661,47 @@ def test_select_album_candidate_matches_lead_token_structure():
|
||||
assert matched.media_id == "album-1"
|
||||
|
||||
|
||||
def test_select_album_candidate_matches_performance_suffix():
|
||||
"""资源标题带演出后缀(S.H.E十七音乐会)时,条目本体一致应弱匹配命中。"""
|
||||
meta = MetaMusic(title="S.H.E十七音乐会", artists=["S.H.E"], year=2018)
|
||||
album = MusicInfo(
|
||||
source="musicbrainz",
|
||||
music_type="album",
|
||||
media_id="album-1",
|
||||
title="十七",
|
||||
artists=["S.H.E"],
|
||||
year=2018,
|
||||
)
|
||||
|
||||
matched = MusicBrainzModule._select_album_candidate(meta, [album])
|
||||
|
||||
assert matched is not None
|
||||
assert matched.media_id == "album-1"
|
||||
|
||||
|
||||
def test_select_album_candidate_matches_contained_title():
|
||||
"""条目带额外前缀完整包含资源主体名(原声带类)时应弱匹配命中。"""
|
||||
meta = MetaMusic(
|
||||
title="Once Upon a Time in Hollywood Original Motion Picture Soundtrack",
|
||||
artists=["Various Artists"],
|
||||
year=2019,
|
||||
)
|
||||
album = MusicInfo(
|
||||
source="musicbrainz",
|
||||
music_type="album",
|
||||
media_id="album-1",
|
||||
title="Quentin Tarantino's Once Upon a Time in Hollywood: "
|
||||
"Original Motion Picture Soundtrack",
|
||||
artists=["Various Artists"],
|
||||
year=2019,
|
||||
)
|
||||
|
||||
matched = MusicBrainzModule._select_album_candidate(meta, [album])
|
||||
|
||||
assert matched is not None
|
||||
assert matched.media_id == "album-1"
|
||||
|
||||
|
||||
def test_select_candidate_rejects_wrong_artist_same_title():
|
||||
"""已知艺术家时,同名异曲的候选不能因标题相等被采信。"""
|
||||
meta = MetaMusic(title="因为有你", artists=["毛阿敏"])
|
||||
|
||||
Reference in New Issue
Block a user