fix(music): 艺术家-年份-专辑三明治结构拆分提取发行年份

This commit is contained in:
jxxghp
2026-08-11 11:55:12 +08:00
parent d0edcfa7bb
commit a11001cf8b
2 changed files with 68 additions and 3 deletions

View File

@@ -580,9 +580,18 @@ class MetaMusic(MetaBase):
self.artists = artists
self._finalize_title(self._clean_tail(title))
else:
title = self._clean_tail(cleaned)
# 无艺术家线索时剥离 CJK 标题尾部的「曲名-歌手」署名,候选比对阶段负责验证身份
self._finalize_title(self._strip_cjk_artist_suffix(title))
# 「艺术家 年份 专辑」三明治结构Leehom Wang 2010 The 18 Martial Arts
# 其他拆分均无艺术家线索时按中部年份拆分,年份提取为发行线索
artists, title, inner_year = self._split_year_sandwich(cleaned)
if artists:
self.artists = artists
if inner_year and not self.year:
self.year = inner_year
self._finalize_title(self._clean_tail(title))
else:
title = self._clean_tail(cleaned)
# 无艺术家线索时剥离 CJK 标题尾部的「曲名-歌手」署名,候选比对阶段负责验证身份
self._finalize_title(self._strip_cjk_artist_suffix(title))
else:
self.title = None
# 括号注释属于曲名的版本/出处说明,剥离的规格文本之后拼回展示
@@ -807,6 +816,37 @@ class MetaMusic(MetaBase):
return cls._split_artists(head), tail
return None, text
# 「艺术家 年份 专辑」三明治结构:艺术家段为不超过 4 个词的拉丁词组;
# 贪婪匹配艺术家段,避免懒惰量词把首个词当艺术家、剩余词混入专辑名
_YEAR_SANDWICH_RE = re.compile(
r"^(?P<artist>[A-Za-z][A-Za-z0-9&+.'\- ]*)\s+(?P<year>(?:19|20)\d{2})\s+(?P<rest>\S.*)$"
)
@classmethod
def _split_year_sandwich(cls, value: str) -> tuple[Optional[list[str]], str, Optional[int]]:
"""按中部独立年份拆分「艺术家 年份 专辑」Jacky Cheung 1987 Jacky
:return: (艺术家列表, 专辑名, 年份),不适用时 (None, 原文, None)
"""
match = cls._YEAR_SANDWICH_RE.match(str(value or "").strip())
if not match:
return None, str(value or ""), None
artist_text = match.group("artist").strip()
raw_rest = match.group("rest").strip()
rest = raw_rest.strip(" \t-–—−-")
# 艺术家段限 4 个词以内;年份后紧跟另一个年份时是年份区间不是三明治结构;
# 剩余段原文需以字母开头:「… 2014 2.0 -MINIBEL」数字开头、
# 「… 2013 -PTer」发布组标签连字符开头都不是专辑名拒绝拆分
if (
len(artist_text.split()) <= 4
and rest
and not re.match(r"(?:19|20)\d{2}\b", raw_rest)
and raw_rest[0].isalpha()
and any(char.isalpha() for char in rest)
):
return cls._split_artists(artist_text), rest, int(match.group("year"))
return None, str(value or ""), None
@staticmethod
def _compact_text(value: Any) -> str:
"""移除大小写、空白与标点,生成比对使用的紧凑文本。"""

View File

@@ -190,6 +190,31 @@ def test_apply_title_splits_various_artists_prefix():
assert meta.title == "Reply 1988 OST"
def test_apply_title_splits_year_sandwich():
"""「艺术家 年份 专辑」三明治结构按中部年份拆分并提取年份。"""
meta = parse_title("Leehom Wang 2010 The 18 Martial Arts")
assert meta.artists == ["Leehom Wang"]
assert meta.title == "The 18 Martial Arts"
assert meta.year == 2010
# 多艺术家分隔符与单词专辑名同样适用
meta = parse_title("ASKA&SENS 1993 YAH YAH YAH")
assert meta.artists == ["ASKA", "SENS"]
assert meta.title == "YAH YAH YAH"
assert meta.year == 1993
def test_apply_title_year_sandwich_guards():
"""三明治拆分的护栏:规格残留与长艺术家段不误拆。"""
# 剩余段数字开头是规格2.0)不是专辑名
assert parse_title("K3 Kan Het S02 2014 2.0 -MINIBEL").artists == []
# 剩余段连字符开头是发布组标签(-PTer不是专辑名
assert parse_title("Ashton Celebration 2013 -PTer").artists == []
# 艺术家段超过 4 个词时拒绝拆分(含曲名与年份的完整标题)
assert parse_title("Bee Gees One Night Only 1997 -ProfessorP").artists == []
def test_apply_title_restores_letter_abbrev():
"""单字母点号缩写还原不误伤合法缩写与单词。"""
meta = parse_title("E.S.Posthumus - Ashielf Alpen FLAC")