mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-08-15 19:14:01 +08:00
fix(music): 原声带电影名本体检索与合辑前缀拆分修复零命中
- musicbrainz: 原声带描述词(Original Motion Picture Soundtrack)剔除后 用电影名本体补充检索阶梯,候选比对支持本体与主标题弱匹配; 后缀定界防止 Ghost 类词内 ost 误剥,本体过短时不作独立检索目标 - musicbrainz: 尾部年份反复剥离,修复场景命名重复携带年份 (Live At Montreux 2011 2011)阻断精确匹配 - metamusic: Various Artists-Title 全拼合辑前缀拆分,未收录别名变体 统一归一为 Various Artists - 补充原声带/重复年份/合辑前缀测试案例
This commit is contained in:
@@ -251,10 +251,10 @@ _MUSIC_DATE_PREFIX_RE = re.compile(
|
||||
_MUSIC_ARTIST_SEPARATOR_RE = re.compile(r"\s*(?:&|,|,|、|/)\s*")
|
||||
# 合辑资源的 Various Artists 署名别名:VA 是场景命名常用缩写,
|
||||
# 归一为 MusicBrainz 规范署名 Various Artists 才能命中合辑条目
|
||||
_MUSIC_ARTIST_ALIASES = {"va": "Various Artists"}
|
||||
# VA-Title 无空格连字符前缀写法(场景命名),主拆分不适用需单独处理
|
||||
_MUSIC_ARTIST_ALIASES = {"va": "Various Artists", "various artists": "Various Artists"}
|
||||
# VA-Title / Various Artists-Title 无空格连字符前缀写法(场景命名),主拆分不适用需单独处理
|
||||
_MUSIC_ALIAS_PREFIX_RE = re.compile(
|
||||
r"^\s*(?P<alias>VA)\s*[-–—−-]\s*(?P<title>.+\S)\s*$",
|
||||
r"^\s*(?P<alias>VA|Various\.?\s*Artists)\s*[-–—−-]\s*(?P<title>.+\S)\s*$",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
# 艺术家与曲名的主分隔:半角/全角空格包裹的连字符(- – — − -)
|
||||
@@ -566,7 +566,9 @@ class MetaMusic(MetaBase):
|
||||
# 场景命名的 VA-Title 无空格连字符写法,主拆分不适用,按别名前缀单独拆分
|
||||
alias_match = None if self.artists else _MUSIC_ALIAS_PREFIX_RE.match(cleaned)
|
||||
if alias_match:
|
||||
self.artists = [_MUSIC_ARTIST_ALIASES[alias_match.group("alias").casefold()]]
|
||||
# 别名前缀均为合辑署名写法,未收录变体统一归一为 Various Artists
|
||||
self.artists = [_MUSIC_ARTIST_ALIASES.get(
|
||||
alias_match.group("alias").casefold(), "Various Artists")]
|
||||
self._finalize_title(self._clean_tail(alias_match.group("title")))
|
||||
else:
|
||||
# CJK 标题常见「专辑名-歌手」无空格连字符写法,主拆分未命中时兑底反向拆分
|
||||
|
||||
@@ -200,6 +200,23 @@ class MusicBrainzModule(_ModuleBase):
|
||||
"""剔除标题尾部的卷号后缀,返回专辑本体名。"""
|
||||
return cls._normalize_text(cls._VOLUME_SUFFIX_RE.sub("", str(value or "")))
|
||||
|
||||
# 原声带资源标题的通用描述词尾部(条目本体是电影名);
|
||||
# 尾部描述词保留时作为原声带形态标记参与弱匹配判定
|
||||
_SOUNDTRACK_SUFFIX_RE = re.compile(
|
||||
r"\s*(?:original\s+motion\s+picture|motion\s+picture)?\s*"
|
||||
r"(?<![A-Za-z0-9])(?:original\s+)?(?:soundtrack|score|ost)$",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _soundtrack_body(cls, value: Optional[str]) -> str:
|
||||
"""剔除原声带标题尾部的通用描述词,返回电影名本体;无描述词返回空串。"""
|
||||
text = str(value or "").strip()
|
||||
body = cls._SOUNDTRACK_SUFFIX_RE.sub("", text)
|
||||
if not body or body == text:
|
||||
return ""
|
||||
return cls._normalize_text(body)
|
||||
|
||||
# 演唱会资源的标题常带演出后缀(S.H.E十七音乐会),条目仅保留演出名本体
|
||||
_PERFORMANCE_SUFFIX_RE = re.compile(
|
||||
r"\s*(?:音乐会|音樂會|演唱会|演唱會|巡回|巡演|Live|Tour)$", re.IGNORECASE)
|
||||
@@ -261,6 +278,12 @@ class MusicBrainzModule(_ModuleBase):
|
||||
# 专辑名开头的艺术家署名前缀同样是命名习惯,用主体名检索
|
||||
title = cls._strip_artist_prefix(title, meta.artists)
|
||||
bare_title = cls._strip_artist_prefix(bare_title, meta.artists)
|
||||
# 原声带标题的通用描述词(Original Motion Picture Soundtrack)在条目中常省略,
|
||||
# 用电影名本体补充一级检索(The Hateful Eight / Pulp Fiction);
|
||||
# 本体过短(The Score 类)时不作为独立检索目标避免噪声
|
||||
soundtrack_body = cls._soundtrack_body(bare_title)
|
||||
if len(cls._match_text(soundtrack_body)) < 4:
|
||||
soundtrack_body = ""
|
||||
queries: list[str] = []
|
||||
for query in [
|
||||
f'releasegroup:{cls._query_phrase(title)} AND artist:"{cls._escape_query(artist)}"'
|
||||
@@ -268,6 +291,9 @@ class MusicBrainzModule(_ModuleBase):
|
||||
f"releasegroup:{cls._query_phrase(title)}" if title else None,
|
||||
f'releasegroup:{cls._query_phrase(bare_title)} AND artist:"{cls._escape_query(artist)}"'
|
||||
if artist and bare_title and bare_title != title else None,
|
||||
f'releasegroup:{cls._query_phrase(soundtrack_body)} AND artist:"{cls._escape_query(artist)}"'
|
||||
if artist and soundtrack_body else None,
|
||||
f"releasegroup:{cls._query_phrase(soundtrack_body)}" if soundtrack_body else None,
|
||||
# 署名变体兜底:仅按去注释专辑名检索,挑选阶段要求艺术家同时命中
|
||||
f"releasegroup:{cls._query_phrase(bare_title)}" if bare_title else None,
|
||||
]:
|
||||
@@ -739,6 +765,15 @@ class MusicBrainzModule(_ModuleBase):
|
||||
cls._performance_title(bare_title)
|
||||
and cls._same_text(cls._performance_title(bare_title), album_title)
|
||||
)
|
||||
# 原声带资源的描述词在条目中常省略(Pulp Fiction: Music From the…),
|
||||
# 电影名本体与条目标题或主标题一致视为弱匹配
|
||||
or (
|
||||
len(cls._match_text(cls._soundtrack_body(bare_title))) >= 4
|
||||
and (
|
||||
cls._same_text(cls._soundtrack_body(bare_title), album_title)
|
||||
or cls._same_text(cls._soundtrack_body(bare_title), cls._main_title(album_title))
|
||||
)
|
||||
)
|
||||
)
|
||||
):
|
||||
# 「我爱夜 (新歌+精选)」对位条目「我爱夜」这类注释差异
|
||||
@@ -844,8 +879,14 @@ class MusicBrainzModule(_ModuleBase):
|
||||
# 格式标记后紧跟的场景发布组标签(如 ALAC-HHWEB),整体剔除
|
||||
text = re.sub(r"[-–—]\s*[A-Z0-9]{3,}\s*$", "", text)
|
||||
# 曲名尾部独立年份是发行线索不是曲名一部分(解析阶段通常已提取),
|
||||
# 检索时剥离避免年份文本造成精确短语零命中
|
||||
text = re.sub(r"(?<!\d)\s+(?:19|20)\d{2}$", "", cls._normalize_text(text))
|
||||
# 反复剥离尾部年份:场景命名可能重复携带(Live At Montreux 2011 2011)
|
||||
text = cls._normalize_text(text)
|
||||
while True:
|
||||
# 仅剔除空白分隔的尾部年份,纯年份标题(1999)无前导空白不受影响
|
||||
stripped = re.sub(r"\s+(?:19|20)\d{2}$", "", text)
|
||||
if stripped == text:
|
||||
break
|
||||
text = stripped
|
||||
return cls._normalize_text(text)
|
||||
|
||||
def recognize_music(self, source: str, media_id: str) -> Optional[MusicInfo]:
|
||||
|
||||
@@ -171,6 +171,14 @@ def test_apply_title_strips_video_tokens():
|
||||
assert meta.year == 2018
|
||||
|
||||
|
||||
def test_apply_title_splits_various_artists_prefix():
|
||||
"""场景命名的 Various Artists-Title 无空格连字符前缀应拆分为合辑署名。"""
|
||||
meta = parse_title("Various.Artists-Reply.1988.OST")
|
||||
|
||||
assert meta.artists == ["Various Artists"]
|
||||
assert meta.title == "Reply 1988 OST"
|
||||
|
||||
|
||||
def test_apply_title_restores_letter_abbrev():
|
||||
"""单字母点号缩写还原不误伤合法缩写与单词。"""
|
||||
meta = parse_title("E.S.Posthumus - Ashielf Alpen FLAC")
|
||||
|
||||
@@ -720,6 +720,55 @@ def test_select_album_candidate_matches_contained_title():
|
||||
assert matched.media_id == "album-1"
|
||||
|
||||
|
||||
def test_soundtrack_body_extracts_movie_name():
|
||||
"""原声带描述词尾部剔除返回电影名本体,非原声带标题与词内后缀不受影响。"""
|
||||
body = MusicBrainzModule._soundtrack_body
|
||||
assert body("Pulp Fiction Original Motion Picture Soundtrack") == "Pulp Fiction"
|
||||
assert body("Reply 1988 OST") == "Reply 1988"
|
||||
# Ghost 尾部的 ost 是单词组成部分,不能剔除
|
||||
assert body("Ghost") == ""
|
||||
assert body("晴天") == ""
|
||||
|
||||
|
||||
def test_search_title_strips_repeated_trailing_year():
|
||||
"""场景命名重复携带的尾部年份应全部剥离,避免年份文本阻断精确匹配。"""
|
||||
assert MusicBrainzModule._search_title("Live At Montreux 2011 2011") == "Live At Montreux"
|
||||
# 纯年份标题无前导空白不受影响
|
||||
assert MusicBrainzModule._search_title("1999") == "1999"
|
||||
|
||||
|
||||
def test_album_queries_include_soundtrack_body():
|
||||
"""原声带专辑检索阶梯应包含电影名本体变体。"""
|
||||
queries = MusicBrainzModule._album_queries(
|
||||
MetaMusic(title="Pulp Fiction Original Motion Picture Soundtrack",
|
||||
artists=["Various Artists"], year=1994)
|
||||
)
|
||||
|
||||
assert 'releasegroup:"Pulp Fiction" AND artist:"Various Artists"' in queries
|
||||
|
||||
|
||||
def test_select_album_candidate_matches_soundtrack_body():
|
||||
"""条目以冒号副标题保留描述词时,电影名本体一致应弱匹配命中。"""
|
||||
meta = MetaMusic(
|
||||
title="Pulp Fiction Original Motion Picture Soundtrack",
|
||||
artists=["Various Artists"],
|
||||
year=1994,
|
||||
)
|
||||
album = MusicInfo(
|
||||
source="musicbrainz",
|
||||
music_type="album",
|
||||
media_id="album-1",
|
||||
title="Pulp Fiction: Music From the Motion Picture",
|
||||
artists=["Various Artists"],
|
||||
year=1994,
|
||||
)
|
||||
|
||||
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