mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-08-14 18:24:42 +08:00
fix(music): 署名前缀剥离与候选首段弱匹配修复失配
- 曲名开头的艺术家署名前缀(许茹芸的爱情电影主题曲)是命名习惯, 检索式与候选比对统一用剥离后的主体名,署名身份由艺术家要求保证 - 条目「主体名 补充说明」结构(愛情電影主題曲 雲且留住)首段一致视为弱匹配 - 采样率支持空格写法(44 1khz),合集/精选纳入发行形态标记剔除 - 规格剔除后仅剩悬空分隔符时仍拆出艺术家(周杰伦 - 合集 2000-2022) - 修正合集修饰词正则中的杂空格
This commit is contained in:
@@ -199,9 +199,10 @@ _MUSIC_VIDEO_TOKEN_ALT = (
|
||||
_MUSIC_QUALITY_TOKEN_RE = re.compile(
|
||||
r"\[[^\]]*\]|\((?:19|20)\d{2}\)|"
|
||||
rf"\b(?:{_MUSIC_FORMAT_TOKEN_ALT})\b|"
|
||||
r"\b\d{1,3}\s*-?\s*bits?\b|\b\d{2,4}(?:\.\d)?\s*k(?:hz|bps?)\b|"
|
||||
# 无损声明词、 ripping 方式与流媒体发行实体标记(- Single / - EP),不是曲名的一部分
|
||||
r"\b(?:single|ep|album)\b|(?<![A-Za-z0-9])(?:lossless|无损音质|无损|分轨|整轨)(?![A-Za-z0-9])",
|
||||
r"\b\d{1,3}\s*-?\s*bits?\b|\b\d{2,4}(?:(?:[..]|\s)\d)?\s*k(?:hz|bps?)\b|"
|
||||
# 无损声明词、 ripping 方式与流媒体发行实体标记(- Single / - EP),不是曲名的一部分;
|
||||
# 合集/精选是发行形态标记,CJK 词用非字母数字定界(\b 对中文不可靠)
|
||||
r"\b(?:single|ep|album)\b|(?<![A-Za-z0-9])(?:lossless|无损音质|无损|分轨|整轨|合集|精选)(?![A-Za-z0-9])",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
# 演唱会/音乐视频种子的视频编码标记:分辨率、编码、容器与声道描述,
|
||||
@@ -211,8 +212,8 @@ _MUSIC_VIDEO_TOKEN_RE = re.compile(rf"\b(?:{_MUSIC_VIDEO_TOKEN_ALT})\b", re.IGNO
|
||||
# 曲名含任何自然语言文本时判定失败,保证「曲名 (注释) - WEB-DL」不被误剥
|
||||
_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}(?:\.\d)?\s*k(?:hz|bps?)"
|
||||
r"|lossless|无损音质|无损|分轨|整轨"
|
||||
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")+$",
|
||||
@@ -498,6 +499,14 @@ class MetaMusic(MetaBase):
|
||||
if comment_match:
|
||||
comment = comment_match.group("comment").strip()
|
||||
cleaned = cleaned[: comment_match.start()].strip()
|
||||
# 规格剔除后曲名侧无剩余文本时,尾部悬空分隔符仍是艺术家署名结构
|
||||
# (「周杰伦 - 合集 2000-2022 - FLAC 16bit 44 1khz」剔除后仅剩「周杰伦 -」)
|
||||
dangling = re.fullmatch(r"(?P<artist>.+?)\s+[\-–—−-]+", cleaned) if cleaned else None
|
||||
if dangling and not self.artists:
|
||||
self.artists = self._split_artists(dangling.group("artist"))
|
||||
self.title = None
|
||||
self._apply_track_prefix()
|
||||
return
|
||||
# CJK「歌手《专辑名》」书名号命名优先于连字符拆分,提取艺术家与专辑实体
|
||||
marker = None if self.artists else _MUSIC_ALBUM_MARKER_RE.match(cleaned)
|
||||
if marker:
|
||||
|
||||
@@ -151,6 +151,9 @@ class MusicBrainzModule(_ModuleBase):
|
||||
artist = meta.artists[0] if meta.artists else None
|
||||
# 括号内的影视 tie-in、版本说明多为半角,与条目全角写法不一致,准备去注释曲名兜底
|
||||
bare_title = cls._strip_parenthetical(title)
|
||||
# 曲名开头的艺术家署名前缀是命名习惯不是曲名内容,用主体名检索
|
||||
title = cls._strip_artist_prefix(title, meta.artists)
|
||||
bare_title = cls._strip_artist_prefix(bare_title, meta.artists)
|
||||
queries: list[str] = []
|
||||
for query in [
|
||||
cls._build_query(meta),
|
||||
@@ -183,6 +186,31 @@ class MusicBrainzModule(_ModuleBase):
|
||||
text = re.split(r"\s*[-–—−-::]\s*|\s*《", str(value or ""), maxsplit=1)[0]
|
||||
return cls._normalize_text(text)
|
||||
|
||||
@classmethod
|
||||
def _lead_token(cls, value: Optional[str]) -> str:
|
||||
"""提取候选标题首个空白分隔段(「愛情電影主題曲 雲且留住」的主体名)。"""
|
||||
text = str(value or "").strip()
|
||||
return cls._normalize_text(text.split(" ", 1)[0]) if text else ""
|
||||
|
||||
@classmethod
|
||||
def _strip_artist_prefix(cls, title: Optional[str], artists: Optional[list[str]]) -> str:
|
||||
"""剥离曲名开头的艺术家署名前缀(「许茹芸的爱情电影主题曲」)。
|
||||
|
||||
资源命名习惯把署名放在曲名前,条目不含该前缀;署名身份由
|
||||
候选挑选阶段的艺术家要求保证,不会产生错误归属。前缀剥离后
|
||||
无剩余文本时保留原标题(「合集 - 花开」类短标题保护)。
|
||||
"""
|
||||
text = str(title or "").strip()
|
||||
for artist in artists or []:
|
||||
artist = str(artist or "").strip()
|
||||
if len(artist) < 2:
|
||||
continue
|
||||
if text.startswith(artist):
|
||||
remainder = re.sub(r"^[的之]\s*", "", text[len(artist):]).strip()
|
||||
if remainder:
|
||||
return remainder
|
||||
return text
|
||||
|
||||
def _search_albums(self, meta: MetaMusic, limit: int) -> list[MusicInfo]:
|
||||
"""按标题和可选艺术家搜索 Release Group 专辑候选,检索式同样逐级放宽。"""
|
||||
for query in self._album_queries(meta):
|
||||
@@ -214,6 +242,9 @@ class MusicBrainzModule(_ModuleBase):
|
||||
bare_title = cls._strip_parenthetical(title)
|
||||
bare_title = re.sub(r",?\s*vol\.?\s*\d+$", "", bare_title, flags=re.IGNORECASE)
|
||||
bare_title = cls._normalize_text(bare_title)
|
||||
# 专辑名开头的艺术家署名前缀同样是命名习惯,用主体名检索
|
||||
title = cls._strip_artist_prefix(title, meta.artists)
|
||||
bare_title = cls._strip_artist_prefix(bare_title, meta.artists)
|
||||
queries: list[str] = []
|
||||
for query in [
|
||||
f'releasegroup:{cls._query_phrase(title)} AND artist:"{cls._escape_query(artist)}"'
|
||||
@@ -569,8 +600,9 @@ class MusicBrainzModule(_ModuleBase):
|
||||
def _select_candidate(cls, meta: MetaMusic, candidates: Iterable[MusicInfo], source: str) -> Optional[MusicInfo]:
|
||||
"""按标题、艺术家和专辑匹配度选择最可信的搜索候选。"""
|
||||
normalized_source = cls._normalize_text(source).casefold()
|
||||
# 资源标题携带的音质标记先剥离,再与候选曲名比对
|
||||
clean_title = cls._search_title(meta.title)
|
||||
# 资源标题携带的音质标记先剥离,再与候选曲名比对;
|
||||
# 曲名开头的艺术家署名前缀是命名习惯,用主体名比对
|
||||
clean_title = cls._strip_artist_prefix(cls._search_title(meta.title), meta.artists)
|
||||
# 条目的影视 tie-in 注释多为全角括号,与资源半角注释无法精确相等,
|
||||
# 去括号后的主体曲名一致视为弱匹配,且需艺术家同时命中才采信
|
||||
bare_title = cls._strip_parenthetical(clean_title)
|
||||
@@ -598,6 +630,11 @@ class MusicBrainzModule(_ModuleBase):
|
||||
or cls._same_text(bare_title, cls._main_title(candidate.title))
|
||||
# 条目「为你盛开-许巍《无尽光芒》…」这类连字符前置命名,首段曲名一致视为弱匹配
|
||||
or cls._same_text(bare_title, cls._head_title(candidate.title))
|
||||
# 条目「愛情電影主題曲 雲且留住」这类「主体名 补充说明」结构,首段一致视为弱匹配
|
||||
or (
|
||||
len(cls._match_text(bare_title)) >= 3
|
||||
and cls._same_text(bare_title, cls._lead_token(candidate.title))
|
||||
)
|
||||
)
|
||||
):
|
||||
score += 2
|
||||
@@ -632,7 +669,8 @@ class MusicBrainzModule(_ModuleBase):
|
||||
专辑重名多,要求标题(含去括号弱匹配)与艺术家同时命中才返回,
|
||||
避免把音轨身份安到错误专辑上。
|
||||
"""
|
||||
clean_title = cls._search_title(meta.album or meta.title)
|
||||
clean_title = cls._strip_artist_prefix(
|
||||
cls._search_title(meta.album or meta.title), meta.artists)
|
||||
if not clean_title:
|
||||
return None
|
||||
bare_title = cls._strip_parenthetical(clean_title)
|
||||
@@ -658,6 +696,11 @@ class MusicBrainzModule(_ModuleBase):
|
||||
or cls._same_text(bare_title, cls._main_title(album_title))
|
||||
# 条目「为你盛开-许巍《无尽光芒》…」这类连字符前置命名,首段曲名一致视为弱匹配
|
||||
or cls._same_text(bare_title, cls._head_title(album_title))
|
||||
# 条目「愛情電影主題曲 雲且留住」这类「主体名 补充说明」结构,首段一致视为弱匹配
|
||||
or (
|
||||
len(cls._match_text(bare_title)) >= 3
|
||||
and cls._same_text(bare_title, cls._lead_token(album_title))
|
||||
)
|
||||
)
|
||||
):
|
||||
# 「我爱夜 (新歌+精选)」对位条目「我爱夜」这类注释差异
|
||||
|
||||
@@ -390,3 +390,14 @@ def test_apply_title_keeps_single_paren_disambiguation():
|
||||
|
||||
assert meta.artists == ["周杰伦"]
|
||||
assert meta.title == "晴天 (电影版)"
|
||||
|
||||
|
||||
def test_apply_title_collection_with_space_sample_rate():
|
||||
"""合集/精选是发行形态标记,空格写法的采样率(44 1khz)也应剔除;
|
||||
剔除后仅剩悬空分隔符时艺术家仍需拆出。"""
|
||||
meta = parse_title("周杰伦 - 合集 2000-2022 - FLAC 16bit 44 1khz")
|
||||
|
||||
assert meta.artists == ["周杰伦"]
|
||||
assert meta.title is None
|
||||
assert meta.year == 2022
|
||||
assert meta.audio_format == "FLAC"
|
||||
|
||||
@@ -634,6 +634,33 @@ def test_same_text_normalizes_cjk_numerals():
|
||||
assert not MusicBrainzModule._same_text("茹此精彩十三首", "茹此精彩14首")
|
||||
|
||||
|
||||
def test_strip_artist_prefix_removes_signature_prefix():
|
||||
"""曲名开头的艺术家署名前缀是命名习惯,检索与比对应使用主体名。"""
|
||||
assert MusicBrainzModule._strip_artist_prefix(
|
||||
"许茹芸的爱情电影主题曲", ["许茹芸"]) == "爱情电影主题曲"
|
||||
# 剥离后无剩余时保留原标题,短标题不受影响
|
||||
assert MusicBrainzModule._strip_artist_prefix("许茹芸", ["许茹芸"]) == "许茹芸"
|
||||
assert MusicBrainzModule._strip_artist_prefix("晴天", ["周杰伦"]) == "晴天"
|
||||
|
||||
|
||||
def test_select_album_candidate_matches_lead_token_structure():
|
||||
"""条目「主体名 补充说明」结构与资源主体名首段一致时应弱匹配命中。"""
|
||||
meta = MetaMusic(title="许茹芸的爱情电影主题曲", artists=["许茹芸"], year=2003)
|
||||
album = MusicInfo(
|
||||
source="musicbrainz",
|
||||
music_type="album",
|
||||
media_id="album-1",
|
||||
title="愛情電影主題曲 雲且留住",
|
||||
artists=["許茹芸"],
|
||||
year=2003,
|
||||
)
|
||||
|
||||
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