mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-08 17:08:35 +08:00
fix(music): recognize real site titles and prefer native subtitle names
This commit is contained in:
+139
-20
@@ -1,10 +1,13 @@
|
||||
import logging
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
from functools import lru_cache
|
||||
from pathlib import Path
|
||||
from threading import RLock
|
||||
from typing import Any, Callable, Optional
|
||||
|
||||
from Pinyin2Hanzi import DefaultHmmParams, is_pinyin
|
||||
|
||||
from app.domain.meta.metabase import MetaBase
|
||||
from app.domain.meta.runtime import get_metainfo_accelerator
|
||||
from app.schemas.media import resolve_media_identity
|
||||
@@ -15,9 +18,9 @@ _AUDIO_FORMAT_PATTERN = re.compile(
|
||||
r"MP3|AAC|M4A|OGG|VORBIS|OPUS|WMA)(?![A-Z])",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
_BIT_DEPTH_PATTERN = re.compile(r"(?<!\d)(?P<value>16|20|24|32)\s*(?:-?bit|bits?|位)(?!\w)", re.IGNORECASE)
|
||||
_BIT_DEPTH_PATTERN = re.compile(r"(?<!\d)(?P<value>16|20|24|32)\s*(?:-?bit|bits?|位|B)(?!\w)", re.IGNORECASE)
|
||||
_SAMPLE_RATE_PATTERN = re.compile(
|
||||
r"(?<!\d)(?P<value>44(?:\.1)?|48|88(?:\.2)?|96|176(?:\.4)?|192|352(?:\.8)?|384|705(?:\.6)?|768)"
|
||||
r"(?<!\d)(?P<value>44(?:[. ]1)?|48|88(?:[. ]2)?|96|176(?:[. ]4)?|192|352(?:[. ]8)?|384|705(?:[. ]6)?|768)"
|
||||
r"\s*k(?:hz)?(?!\w)",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
@@ -78,7 +81,7 @@ def parse_audio_quality(value: Any) -> dict[str, Any]:
|
||||
audio_format = normalize_audio_format(format_match.group("format")) if format_match else None
|
||||
bit_depth = int(bit_depth_match.group("value")) if bit_depth_match else None
|
||||
sample_rate = (
|
||||
int(float(sample_rate_match.group("value")) * 1000)
|
||||
int(float(sample_rate_match.group("value").replace(" ", ".")) * 1000)
|
||||
if sample_rate_match
|
||||
else None
|
||||
)
|
||||
@@ -277,11 +280,19 @@ _MUSIC_PAREN_SPEC_RE = re.compile(
|
||||
_MUSIC_EMPTY_BRACKET_RE = re.compile(r"[\((\[]\s*(?:[/+,\-]\s*)*[\))\]]")
|
||||
# 尾部花括号通常是唱片目录号或发布标记,仅在末尾剔除,保护正文中的花括号文本。
|
||||
_MUSIC_TRAILING_CATALOG_RE = re.compile(r"\s*\{[A-Za-z0-9][^{}]{0,40}\}\s*$")
|
||||
# 年份括号:(2000)(2000)【2000】形式的发行年份,作为候选消歧线索
|
||||
_MUSIC_YEAR_RE = re.compile(r"[\(\[(【]((?:19|20)\d{2})[\)\])】]")
|
||||
# 年份及完整发行日期括号提供候选消歧线索,规格或目录号不作为日期。
|
||||
_MUSIC_YEAR_RE = re.compile(
|
||||
r"[\(\[(【]((?:19|20)\d{2})"
|
||||
r"(?:[. /-](?:0?[1-9]|1[0-2])[. /-](?:0?[1-9]|[12]\d|3[01]))?[\)\])】]"
|
||||
)
|
||||
# 标题尾部独立年份:「xxx音乐会 2018」「Funky Jazz Saxophone 2024」「系列-2007」,
|
||||
# 提取为发行年份线索并从曲名剥离,避免年份文本进入检索式造成零命中
|
||||
_MUSIC_TRAILING_YEAR_RE = re.compile(r"(?<!\d)[\s\-–—]+((?:19|20)\d{2})\s*$")
|
||||
_MUSIC_TRAILING_YEAR_RE = re.compile(r"[\s\-–—]+((?:19|20)\d{2})\s*$")
|
||||
# 仅清理独立发行类型尾段或紧随年份的类型,保留 Best Album 等自然标题。
|
||||
_MUSIC_RELEASE_TYPE_RE = re.compile(
|
||||
r"(?:\s+[-–—−-]+\s*(?:single|ep|album)|"
|
||||
r"(?P<year>(?:19|20)\d{2})\s+(?:single|ep|album))\s*$", re.IGNORECASE,
|
||||
)
|
||||
# 无括号年份区间:全集/精选标题尾部的 1967-1995、2015-16,取结束年作为发行年份线索;
|
||||
# CJK 字符属于 \w,不能用 \b 定界,改用数字负向断言;
|
||||
# 短年右侧禁止再跟数字,避免把 2024-01-27 这类日期的 2024-01 误当区间;
|
||||
@@ -313,6 +324,11 @@ _MUSIC_ALIAS_PREFIX_RE = re.compile(
|
||||
_MUSIC_ARTIST_TITLE_RE = re.compile(
|
||||
r"^\s*(?P<artist>.+?)\s+[\-–—−-]+\s+(?P<title>.+?)\s*$"
|
||||
)
|
||||
_MUSIC_ARTIST_TITLE_SEPARATOR_RE = re.compile(r"\s+[-–—−-]+(?:\s+|$)")
|
||||
_MUSIC_RESOURCE_ARTIST_LABELS = r"艺术家|藝術家|藝人|歌手|演唱|专辑艺人|專輯藝人|artist|performer"
|
||||
_MUSIC_FEATURED_ARTIST_RE = re.compile(
|
||||
r"\((?:featuring\s+|(?:feat|ft)(?:\.\s*|\s+))(?P<artist>[^()]+)\)", re.IGNORECASE,
|
||||
)
|
||||
# 曲名后含书名号的括号注释(影视原声说明等):「等得到 (电影《如影随心》主题曲 独唱版)」,
|
||||
# 注释内的《》会抢先触发专辑书名号判定,需在结构解析前提取;
|
||||
# 单层括号注释(电影版/Live)是 MusicBrainz 条目的消歧后缀,不提取
|
||||
@@ -651,6 +667,12 @@ class MusicNameRegistry:
|
||||
)
|
||||
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def _music_pinyin_parameters() -> DefaultHmmParams:
|
||||
"""按需复用已有拼音库的字音模型,仅校验副标题原文,不生成猜测名称。"""
|
||||
return DefaultHmmParams()
|
||||
|
||||
|
||||
class MetaMusic(MetaBase):
|
||||
"""音乐文件名及音频标签解析结果,作为 MetaBase 的音乐分支实现。"""
|
||||
|
||||
@@ -733,29 +755,112 @@ class MetaMusic(MetaBase):
|
||||
meta.album, number, meta.title = track.groups()
|
||||
meta.track_number = int(number)
|
||||
if subtitle:
|
||||
secondary = cls.parse_query(subtitle)
|
||||
artist = re.search(
|
||||
r"(?:^|[;;\n])\s*(?:艺术家|藝術家|藝人|歌手|演唱|专辑艺人|專輯藝人|artist|performer)"
|
||||
r"\s*[::]\s*([^;;\n]+)", subtitle, re.I,
|
||||
# 管道分隔字段包含平台、规格与发行类型,不属于作品名。
|
||||
secondary_text = re.split(r"[||]", subtitle, maxsplit=1)[0].strip()
|
||||
secondary = cls.parse_query(secondary_text)
|
||||
artist = cls._resource_label(
|
||||
subtitle, _MUSIC_RESOURCE_ARTIST_LABELS,
|
||||
)
|
||||
if not meta.artists:
|
||||
meta.artists = cls._split_artists(artist.group(1)) if artist else list(secondary.artists)
|
||||
album = re.search(
|
||||
r"(?:^|[;;\n])\s*(?:专辑(?:名|名称)?|專輯(?:名|名稱)?|album)\s*[::]\s*([^;;\n]+)",
|
||||
subtitle, re.I,
|
||||
if artist:
|
||||
meta.artists = cls._split_artists(cls._strip_quality_tokens(cls._strip_spec_segments(artist)))
|
||||
elif (
|
||||
_MUSIC_ARTIST_TITLE_RE.match(cls._normalize_text(secondary_text))
|
||||
or _MUSIC_ALBUM_MARKER_RE.match(cls._normalize_text(secondary_text))
|
||||
):
|
||||
# 不把厂牌目录号、营销文案中的无空格连字符当作艺术家署名。
|
||||
if any(re.search(r"[^\W\d_]", item) for item in secondary.artists):
|
||||
meta.artists = list(secondary.artists)
|
||||
album = cls._resource_label(
|
||||
subtitle, r"专辑(?:名|名称)?|專輯(?:名|名稱)?|album",
|
||||
)
|
||||
if album and not meta.album:
|
||||
meta.album = album.group(1).strip()
|
||||
elif not meta.album and secondary.artists and secondary.title != meta.title:
|
||||
if {cls.compact_text(item) for item in meta.artists} & {cls.compact_text(item) for item in secondary.artists}:
|
||||
meta.album = secondary.title
|
||||
meta.album = album
|
||||
elif not meta.album and secondary.album:
|
||||
primary_artists = {cls.compact_text(item) for item in meta.artists}
|
||||
if primary_artists & {cls.compact_text(item) for item in secondary.artists}:
|
||||
meta.album = secondary.album
|
||||
if not meta.year:
|
||||
meta.year = secondary.year
|
||||
if not meta.year and secondary_text != subtitle:
|
||||
# 日期可能独立位于后续字段;仅补年份,不采用整段文本的作品名。
|
||||
meta.year = cls.parse_query(subtitle).year
|
||||
meta.apply_audio_quality(subtitle)
|
||||
if not meta.album and meta.title and not meta.track_number and cls._resource_is_album(title, subtitle):
|
||||
meta.album = meta.title
|
||||
if subtitle:
|
||||
native_title = cls._native_resource_title(meta.title, subtitle, secondary)
|
||||
if native_title:
|
||||
if meta.album == meta.title:
|
||||
meta.album = native_title
|
||||
meta.title = native_title
|
||||
if not meta.version:
|
||||
meta.version = cls._resource_version(title, subtitle)
|
||||
return meta
|
||||
|
||||
@classmethod
|
||||
def _native_resource_title(
|
||||
cls, title: Optional[str], subtitle: str, secondary: "MetaMusic",
|
||||
) -> Optional[str]:
|
||||
"""拼音主标题与副标题中文字音逐字一致时优先原文,保留普通外文名称。"""
|
||||
if not title or not re.fullmatch(r"[A-Za-z ._-]+", title):
|
||||
return None
|
||||
syllables = re.sub(r"(?<=[a-z])(?=[A-Z])", " ", title).lower()
|
||||
parts = re.split(r"[ ._-]+", syllables.strip())
|
||||
if not parts or not all(is_pinyin(part) for part in parts):
|
||||
return None
|
||||
candidates = [
|
||||
cls._resource_label(subtitle, r"曲名|歌曲(?:名|名称)?|标题|標題|专辑(?:名)?|專輯(?:名)?|title|album"),
|
||||
secondary.title,
|
||||
]
|
||||
for field in re.split(r"[||;;\n]", subtitle):
|
||||
# 「山歌廖哉 - 歌手:刀郎」的字段前缀本身就是明确的作品名。
|
||||
candidate = re.split(
|
||||
rf"\s+[-–—−-]+\s*(?:{_MUSIC_RESOURCE_ARTIST_LABELS})\s*[::]",
|
||||
field, maxsplit=1, flags=re.I,
|
||||
)[0]
|
||||
candidates.append(candidate)
|
||||
for candidate in candidates:
|
||||
if not candidate:
|
||||
continue
|
||||
candidate = cls._normalize_text(candidate).strip(" 《》「」『』[]")
|
||||
characters = cls.compact_text(candidate)
|
||||
if len(characters) != len(parts) or not re.fullmatch(r"[\u3400-\u9fff]+", characters):
|
||||
continue
|
||||
try:
|
||||
if all(char in _music_pinyin_parameters().get_states(part) for char, part in zip(characters, parts)):
|
||||
return candidate
|
||||
except KeyError:
|
||||
# 字音模型不覆盖的音节不提供名称替换证据。
|
||||
continue
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _resource_label(subtitle: str, labels: str) -> Optional[str]:
|
||||
"""读取明确的副标题字段,允许管道、中文逗号或独立连字符作为字段边界。"""
|
||||
match = re.search(
|
||||
rf"(?:^|[;;\n||,]|\s+[-–—−-]+\s+)\s*(?:{labels})"
|
||||
r"\s*[::]\s*([^;;\n||,]+?)"
|
||||
r"(?=\s+[-–—−-]+\s+[^::;;\n||,]+[::]|[;;\n||,]|$)", subtitle, re.I,
|
||||
)
|
||||
return match.group(1).strip() if match else None
|
||||
|
||||
@classmethod
|
||||
def _resource_is_album(cls, title: str, subtitle: Optional[str]) -> bool:
|
||||
"""只采信明确专辑标签,不由音频格式或不同语言的副标题推断所属专辑。"""
|
||||
normalized = cls._normalize_text(title)
|
||||
if re.search(r"\[\s*(?:album|专辑|專輯)\s*\]", normalized, re.I):
|
||||
return True
|
||||
if re.search(r"\b(?:19|20)\d{2}\s+album\s*$", normalized, re.I):
|
||||
return True
|
||||
return any(
|
||||
field.strip().strip("[]【】()()").strip().casefold() in {
|
||||
"album", "专辑", "專輯", "音乐专辑", "音樂專輯",
|
||||
"录音室专辑", "錄音室專輯", "单曲专辑", "單曲專輯",
|
||||
}
|
||||
for field in re.split(r"[||;;\n]", subtitle or "")
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _resource_version(title: str, subtitle: Optional[str]) -> Optional[str]:
|
||||
"""优先保留标题版本,副标题仅接受明确版本字段或独立版本标签,避免误读艺名。"""
|
||||
@@ -984,6 +1089,14 @@ class MetaMusic(MetaBase):
|
||||
self.title = f"{self.title} ({context.comment})"
|
||||
if parsed.artists is not None:
|
||||
self.artists = list(parsed.artists)
|
||||
if self.artists and not context.artists:
|
||||
# 客串署名是明确艺术家证据,曲名中的原始版本说明仍完整保留。
|
||||
seen = {self.compact_text(artist) for artist in self.artists}
|
||||
for featured in _MUSIC_FEATURED_ARTIST_RE.findall(context.text):
|
||||
for artist in self._split_artists(featured):
|
||||
if self.compact_text(artist) not in seen:
|
||||
self.artists.append(artist)
|
||||
seen.add(self.compact_text(artist))
|
||||
if parsed.album is not None:
|
||||
self.album = parsed.album
|
||||
if self.year is None:
|
||||
@@ -1240,7 +1353,8 @@ class MetaMusic(MetaBase):
|
||||
text = _MUSIC_TRAILING_CATALOG_RE.sub(" ", text)
|
||||
text = _MUSIC_EMPTY_BRACKET_RE.sub(" ", text)
|
||||
# 规格剥离后可能残留悬空分隔符(含 APE+CUE 类格式联合写法残留的加号),统一修剪
|
||||
return cls._normalize_text(re.sub(r"^[\s\-–—−-/+]+|[\s\-–—−-/+]+$", "", text))
|
||||
text = cls._normalize_text(re.sub(r"^[\s\-–—−-/+]+|[\s\-–—−-/+]+$", "", text))
|
||||
return _MUSIC_RELEASE_TYPE_RE.sub(lambda match: match.group("year") or "", text).strip()
|
||||
|
||||
@classmethod
|
||||
def _strip_artist_suffix(cls, value: str, artists: list[str]) -> str:
|
||||
@@ -1731,7 +1845,12 @@ def _match_album_marker(context: MusicNameContext) -> Optional[Any]:
|
||||
"""匹配 CJK 书名号专辑命名。"""
|
||||
if context.artists:
|
||||
return None
|
||||
return _MUSIC_ALBUM_MARKER_RE.match(context.text)
|
||||
matched = _MUSIC_ALBUM_MARKER_RE.match(context.text)
|
||||
if matched and _MUSIC_ARTIST_TITLE_RE.match(context.text):
|
||||
# 标准 artist - title 的作品名可含《片名》;双语双分隔前缀仍走专辑模式。
|
||||
if len(_MUSIC_ARTIST_TITLE_SEPARATOR_RE.findall(matched.group("artist"))) == 1:
|
||||
return None
|
||||
return matched
|
||||
|
||||
|
||||
def _parse_album_marker(
|
||||
|
||||
+209
@@ -0,0 +1,209 @@
|
||||
{
|
||||
"sampled_on": "2026-09-06",
|
||||
"provenance": "Read-only authenticated music listing samples; only original titles and subtitles retained. Expected fields are manually annotated from naming evidence, not external catalog guesses.",
|
||||
"samples": [
|
||||
{
|
||||
"id": "wintersakura-pinyin-prefers-native-title",
|
||||
"site": "wintersakura",
|
||||
"title": "Yisa Yu 2019 SanShiErLi FLAC",
|
||||
"subtitle": "郁可唯 - 三十而慄 2019 - FLAC 分軌",
|
||||
"expected": {"artists": ["Yisa Yu"], "title": "三十而慄", "album": null, "year": 2019}
|
||||
},
|
||||
{
|
||||
"id": "ptsbao-explicit-subtitle-album-marker",
|
||||
"site": "ptsbao",
|
||||
"title": "Wagner: Das Rheingold 2010 1080i Blu-ray AVC DTS 5.1",
|
||||
"subtitle": "瓦格纳《莱茵的黄金》 / 大都会歌剧院高清转播系列 / Wagner: Das Rheingold | 类别:音乐 歌剧",
|
||||
"expected": {"artists": ["瓦格纳"], "title": "Wagner: Das Rheingold", "album": "莱茵的黄金", "year": 2010}
|
||||
},
|
||||
{
|
||||
"id": "hdhome-catalog-year-is-not-artist",
|
||||
"site": "HDHome",
|
||||
"title": "Emiri Miyamoto-Renaissance",
|
||||
"subtitle": "2012 - Sony Records Int'l / SICC 10115 / SACD - DSF / Lossless / 频谱图",
|
||||
"expected": {"artists": [], "album": null}
|
||||
},
|
||||
{
|
||||
"id": "hdhome-description-is-not-artist",
|
||||
"site": "HDHome",
|
||||
"title": "Headphone Acoustics Reference CD [金耳朵]录音监察员专用测试天碟 编号:STS digital 611143",
|
||||
"subtitle": "欧洲权威电器硬件生产制造商\"PHILIPS\"授权荷兰发烧录音制作品牌STS-Digital命名及合作录制的首张专业测试音乐CD,立体动感..",
|
||||
"expected": {"artists": [], "album": null, "year": null}
|
||||
},
|
||||
{
|
||||
"id": "hhan-single-pipe-fields",
|
||||
"site": "hhanclub",
|
||||
"title": "李佳薇 - 词不达意 (2020) - WEB-DL - 24bit ALAC-HHWEB",
|
||||
"subtitle": "李佳薇 - 词不达意 | ALAC | 苹果音乐 | 单曲专辑",
|
||||
"expected": {"artists": ["李佳薇"], "title": "词不达意", "album": "词不达意", "year": 2020, "audio_format": "ALAC", "bit_depth": 24}
|
||||
},
|
||||
{
|
||||
"id": "hhan-featured-artist",
|
||||
"site": "hhanclub",
|
||||
"title": "黄明志 - 一起飙高音 (feat. 李佳薇) (2018) - WEB-DL - 24bit ALAC-HHWEB",
|
||||
"subtitle": "黄明志 - 一起飙高音 (feat. 李佳薇) | ALAC | 苹果音乐 | 单曲专辑",
|
||||
"expected": {"artists": ["黄明志", "李佳薇"], "title": "一起飙高音 (feat. 李佳薇)", "album": "一起飙高音 (feat. 李佳薇)", "year": 2018}
|
||||
},
|
||||
{
|
||||
"id": "hhan-featured-compact",
|
||||
"site": "hhanclub",
|
||||
"title": "李佳薇 - 追心者2.0 (feat.舒灏) (2023) - WEB-DL - 16bit ALAC-HHWEB",
|
||||
"subtitle": "李佳薇 - 追心者2.0 (feat.舒灏) | ALAC | 苹果音乐 | 录音室专辑",
|
||||
"expected": {"artists": ["李佳薇", "舒灏"], "title": "追心者2.0 (feat.舒灏)", "album": "追心者2.0 (feat.舒灏)", "year": 2023}
|
||||
},
|
||||
{
|
||||
"id": "hhan-soundtrack-book-title",
|
||||
"site": "hhanclub",
|
||||
"title": "群星 - 电视剧《如果奔跑是我的人生》原声带 (2024) - WEB-DL - 24bit ALAC-HHWEB",
|
||||
"subtitle": "周深& 李佳薇& 金玟岐 & PMP Music - 电视剧《如果奔跑是我的人生》原声带 | ALAC | 苹果音乐 | 录音室专辑",
|
||||
"expected": {"artists": ["群星"], "title": "电视剧《如果奔跑是我的人生》原声带", "album": "电视剧《如果奔跑是我的人生》原声带", "year": 2024}
|
||||
},
|
||||
{
|
||||
"id": "hhan-soundtrack-leading-book-title",
|
||||
"site": "hhanclub",
|
||||
"title": "群星 - 《破事精英2》影视剧原声带 (2023) - WEB-DL - 24bit ALAC-HHWEB",
|
||||
"subtitle": "李佳薇& 王弦& 池约翰C.J & 常佳宁 - 《破事精英2》影视剧原声带 | ALAC | 苹果音乐 | 录音室专辑",
|
||||
"expected": {"artists": ["群星"], "title": "《破事精英2》影视剧原声带", "album": "《破事精英2》影视剧原声带", "year": 2023}
|
||||
},
|
||||
{
|
||||
"id": "hhan-release-type-bit-abbreviation",
|
||||
"site": "hhanclub",
|
||||
"title": "李佳薇 - 大火 (Reborn) - Single(2024) - ALAC [16B-44.1kHz]",
|
||||
"subtitle": "李佳薇 - 大火 (Reborn) | ALAC | 苹果音乐 | 录音室专辑",
|
||||
"expected": {"artists": ["李佳薇"], "title": "大火 (Reborn)", "album": "大火 (Reborn)", "year": 2024, "bit_depth": 16, "sample_rate": 44100}
|
||||
},
|
||||
{
|
||||
"id": "hhan-book-title-in-comment",
|
||||
"site": "hhanclub",
|
||||
"title": "李佳薇 - 黎明所愿 (电视剧《暗夜与黎明》插曲) (2024) - WEB-DL - 24bit ALAC-HHWEB",
|
||||
"subtitle": "李佳薇 - 黎明所愿 (电视剧《暗夜与黎明》插曲) | ALAC | 苹果音乐 | 录音室专辑",
|
||||
"expected": {"artists": ["李佳薇"], "title": "黎明所愿 (电视剧《暗夜与黎明》插曲)", "album": "黎明所愿 (电视剧《暗夜与黎明》插曲)", "year": 2024}
|
||||
},
|
||||
{
|
||||
"id": "hhan-primary-artist-preserved",
|
||||
"site": "hhanclub",
|
||||
"title": "群星 - 风过留痕 影视原声带 (2026) - WEB-DL - 16bit ALAC-HHWEB",
|
||||
"subtitle": "李佳薇& 杨宝心& 刘宇宁 & 诗和远方 - 风过留痕 影视原声带 | ALAC | 苹果音乐 | 录音室专辑",
|
||||
"expected": {"artists": ["群星"], "title": "风过留痕 影视原声带", "album": "风过留痕 影视原声带", "year": 2026}
|
||||
},
|
||||
{
|
||||
"id": "ptsbao-album-full-date",
|
||||
"site": "ptsbao",
|
||||
"title": "[Album] Aoi Teshima - Tokyo [2017.11.22]",
|
||||
"subtitle": "[Album] Aoi Teshima - Tokyo [2017.11.22]",
|
||||
"expected": {"artists": ["Aoi Teshima"], "title": "Tokyo", "album": "Tokyo", "year": 2017}
|
||||
},
|
||||
{
|
||||
"id": "ptsbao-spaced-release-date",
|
||||
"site": "ptsbao",
|
||||
"title": "[2021 10 20]手嶌葵(Aoi Teshima) - Highlights from Simple is best Vol 2 [24bit/96kHz] (Flac)",
|
||||
"subtitle": "手嶌葵(Aoi Teshima) - Highlights from Simple is best Vol 2",
|
||||
"expected": {"artists": ["手嶌葵(Aoi Teshima)"], "title": "Highlights from Simple is best Vol 2", "album": null, "year": 2021, "bit_depth": 24, "sample_rate": 96000}
|
||||
},
|
||||
{
|
||||
"id": "ssd-explicit-subtitle-artist",
|
||||
"site": "ssd",
|
||||
"title": "Shan.Ge.Liao.Zai.2023.WEB-DL.FLAC-CMCTA",
|
||||
"subtitle": "音乐专辑 | 山歌廖哉 - 歌手:刀郎 - FLAC分轨[~1600kbps]",
|
||||
"expected": {"artists": ["刀郎"], "title": "山歌廖哉", "album": "山歌廖哉", "year": 2023, "audio_format": "FLAC"}
|
||||
},
|
||||
{
|
||||
"id": "ssd-various-artists-compact-alias",
|
||||
"site": "ssd",
|
||||
"title": "VariousArtists-Top.100.Classical.Music.1994.Flac.16bit.44.1khz",
|
||||
"subtitle": "群星-古典音乐1685年-1928年10CD|Delta",
|
||||
"expected": {"artists": ["Various Artists"], "title": "Top 100 Classical Music", "album": null, "year": 1994, "sample_rate": 44100}
|
||||
},
|
||||
{
|
||||
"id": "ssd-various-artists-dotted-alias",
|
||||
"site": "ssd",
|
||||
"title": "Various.Artists-Sci-Trance.2014-Redacted",
|
||||
"subtitle": "Smiling Corpse / SCAA001 / CD / Log (100%) / Cue",
|
||||
"expected": {"artists": ["Various Artists"], "album": null}
|
||||
},
|
||||
{
|
||||
"id": "wintersakura-year-album-suffix",
|
||||
"site": "wintersakura",
|
||||
"title": "Sophie Zelmani-Sophie Zelmani 1995 Album",
|
||||
"subtitle": "FLAC / Lossless / Log (100%) / Cue",
|
||||
"expected": {"artists": ["Sophie Zelmani"], "title": "Sophie Zelmani", "album": "Sophie Zelmani", "year": 1995, "audio_format": "FLAC"}
|
||||
},
|
||||
{
|
||||
"id": "wintersakura-year-album-second",
|
||||
"site": "wintersakura",
|
||||
"title": "Sophie Zelmani-Sing and Dance 2002 Album",
|
||||
"subtitle": "FLAC / Lossless / Log (100%) / Cue",
|
||||
"expected": {"artists": ["Sophie Zelmani"], "title": "Sing and Dance", "album": "Sing and Dance", "year": 2002}
|
||||
},
|
||||
{
|
||||
"id": "wintersakura-spaced-sample-rate",
|
||||
"site": "wintersakura",
|
||||
"title": "Gene Clark-White Light 1971 - FLAC 16bit 44 1khz",
|
||||
"subtitle": "吉恩·克拉克-白光 | 专辑 | 转自RED",
|
||||
"expected": {"artists": ["Gene Clark"], "title": "White Light", "album": "White Light", "year": 1971, "sample_rate": 44100}
|
||||
},
|
||||
{
|
||||
"id": "wintersakura-collection-not-recording",
|
||||
"site": "wintersakura",
|
||||
"title": "周杰伦 - 合集 2000-2022 - FLAC 16bit 44 1khz",
|
||||
"subtitle": "周杰伦 音乐作品合集 2000-2022 | 专辑合集",
|
||||
"expected": {"artists": ["周杰伦"], "title": null, "album": null, "year": 2022, "sample_rate": 44100}
|
||||
},
|
||||
{
|
||||
"id": "0ff-spaced-sample-rate",
|
||||
"site": "0ff",
|
||||
"title": "Aimer - A World Where the Sun Never Rises 2025-FLAC 16bit 44 1khz-Mmx",
|
||||
"subtitle": "",
|
||||
"expected": {"artists": ["Aimer"], "title": "A World Where the Sun Never Rises", "album": null, "year": 2025, "sample_rate": 44100}
|
||||
},
|
||||
{
|
||||
"id": "0ff-multiple-artists",
|
||||
"site": "0ff",
|
||||
"title": "Eric W Brown & Yasunori Mitsuda - Sea of Stars: Original Soundtrack 2023-FLAC 16bit 44 1khz-Mmx",
|
||||
"subtitle": "",
|
||||
"expected": {"artists": ["Eric W Brown", "Yasunori Mitsuda"], "title": "Sea of Stars: Original Soundtrack", "album": null, "year": 2023, "sample_rate": 44100}
|
||||
},
|
||||
{
|
||||
"id": "0ff-hyphenated-artist",
|
||||
"site": "0ff",
|
||||
"title": "A-Lin - 出道十周年情歌精选 2016 FLAC 16bit 44 1kHz",
|
||||
"subtitle": "",
|
||||
"expected": {"artists": ["A-Lin"], "title": "出道十周年情歌精选", "album": null, "year": 2016, "sample_rate": 44100}
|
||||
},
|
||||
{
|
||||
"id": "hdfans-volume-before-year",
|
||||
"site": "hdfans",
|
||||
"title": "VA - Bar Groove Analog 17 2026 FLAC",
|
||||
"subtitle": "",
|
||||
"expected": {"artists": ["Various Artists"], "title": "Bar Groove Analog 17", "album": null, "year": 2026}
|
||||
},
|
||||
{
|
||||
"id": "btschool-known-good-title",
|
||||
"site": "btschool",
|
||||
"title": "Bonnie Tyler - The Very Best Of 2001 FLAC",
|
||||
"subtitle": "()",
|
||||
"expected": {"artists": ["Bonnie Tyler"], "title": "The Very Best Of", "album": null, "year": 2001}
|
||||
},
|
||||
{
|
||||
"id": "hdsky-hyphenated-artist",
|
||||
"site": "hdsky",
|
||||
"title": "Anti-Flag - American Reckoning - 2009 - FLAC分轨",
|
||||
"subtitle": "",
|
||||
"expected": {"artists": ["Anti-Flag"], "title": "American Reckoning", "album": null, "year": 2009}
|
||||
},
|
||||
{
|
||||
"id": "audiences-classical-composer",
|
||||
"site": "audiences",
|
||||
"title": "德沃夏克 - 第三、第七交响曲【1995】【CD】【FLAC分轨】",
|
||||
"subtitle": "德沃夏克:第三、第七交响曲 郑明勋(1995) [维也纳爱乐乐团175周年纪念套装cd_14]",
|
||||
"expected": {"artists": ["德沃夏克"], "title": "第三、第七交响曲", "album": null, "year": 1995}
|
||||
},
|
||||
{
|
||||
"id": "hdhome-subtitle-translation-not-album",
|
||||
"site": "HDHome",
|
||||
"title": "罗文 & 甄妮 – 射雕英雄传 Roman Tam & Jenny Tseng – Legends of the Condor Heroes 1983 [APE整轨+CUE]",
|
||||
"subtitle": "羅文 & 甄妮 – 射雕英雄傳 (粤语) [APE整轨+CUE]",
|
||||
"expected": {"artists": ["罗文", "甄妮"], "title": "射雕英雄传 Roman Tam & Jenny Tseng – Legends of the Condor Heroes", "album": null, "year": 1983}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -325,7 +325,7 @@ def test_metainfo_path_uses_rust_once_and_keeps_python_directory_context(
|
||||
"[CD][FLAC+CUE+LOG+BK][KDSD-01049]",
|
||||
["中恵光城"],
|
||||
"SELENiTE -Mitsuki Nakae Works Best Album",
|
||||
None,
|
||||
2022,
|
||||
"FLAC",
|
||||
),
|
||||
(
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
"""真实站点主副标题的离线音乐识别回归,不包含站点凭据或网络访问。"""
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from app.adapters.system import rust
|
||||
from app.domain.meta import runtime
|
||||
from app.domain.meta.metamusic import MetaMusic
|
||||
from app.domain.metainfo import MetaInfo
|
||||
from app.schemas.types import MediaType
|
||||
|
||||
SAMPLES = json.loads(
|
||||
(Path(__file__).parent / "fixtures" / "music_metainfo_samples.json").read_text(encoding="utf-8")
|
||||
)["samples"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("engine", ["python", "rust"])
|
||||
@pytest.mark.parametrize("sample", SAMPLES, ids=lambda sample: sample["id"])
|
||||
def test_real_music_titles_and_subtitles(sample, engine, monkeypatch):
|
||||
"""两条真实 MetaInfo 路径须提取相同的名称证据,不能用 Python 回退冒充 Rust。"""
|
||||
if engine == "rust":
|
||||
if not rust.is_available():
|
||||
pytest.skip("moviepilot_rust 扩展未安装")
|
||||
monkeypatch.setattr(rust, "is_enabled", lambda: True)
|
||||
|
||||
def reject_python_fallback(*_args, **_kwargs):
|
||||
"""Rust 样本解析一旦回退 Python 就显式失败。"""
|
||||
raise AssertionError("真实音乐样本不应从 Rust 回退 Python")
|
||||
|
||||
monkeypatch.setattr(MetaMusic, "_prepare_name_context", reject_python_fallback)
|
||||
monkeypatch.setattr(runtime, "_metainfo_accelerator", rust if engine == "rust" else None)
|
||||
|
||||
meta = MetaInfo(sample["title"], sample["subtitle"], mtype=MediaType.MUSIC)
|
||||
|
||||
assert meta.type == MediaType.MUSIC
|
||||
assert meta.org_string == sample["title"]
|
||||
for field, expected in sample["expected"].items():
|
||||
assert getattr(meta, field) == expected, field
|
||||
|
||||
|
||||
@pytest.mark.parametrize("engine", ["python", "rust"])
|
||||
@pytest.mark.parametrize("title, expected", [
|
||||
("Artist - Best Album FLAC", "Best Album"),
|
||||
("Artist - Single Ladies FLAC", "Single Ladies"),
|
||||
("Artist - Live At Montreux 1999 2022 FLAC", "Live At Montreux 1999 2022"),
|
||||
])
|
||||
def test_release_cleanup_preserves_natural_titles(title, expected, engine, monkeypatch):
|
||||
"""发行类型和年份规则不能删除作品名中的自然单词或连续年份。"""
|
||||
if engine == "rust" and not rust.is_available():
|
||||
pytest.skip("moviepilot_rust 扩展未安装")
|
||||
monkeypatch.setattr(rust, "is_enabled", lambda: True)
|
||||
monkeypatch.setattr(runtime, "_metainfo_accelerator", rust if engine == "rust" else None)
|
||||
|
||||
meta = MetaInfo(title, mtype=MediaType.MUSIC)
|
||||
|
||||
assert meta.title == expected
|
||||
assert meta.album is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize("engine", ["python", "rust"])
|
||||
@pytest.mark.parametrize("subtitle", [
|
||||
"歌手:周杰伦 - 专辑:叶惠美 | FLAC | [2003]",
|
||||
"歌手:周杰伦;专辑:叶惠美;[2003] FLAC",
|
||||
"歌手:周杰伦,专辑:叶惠美 | [2003] FLAC",
|
||||
])
|
||||
def test_subtitle_labels_keep_album_and_year_boundaries(subtitle, engine, monkeypatch):
|
||||
"""明确字段可补全缺失信息,平台分隔与后续字段不能污染署名或专辑名。"""
|
||||
if engine == "rust" and not rust.is_available():
|
||||
pytest.skip("moviepilot_rust 扩展未安装")
|
||||
monkeypatch.setattr(rust, "is_enabled", lambda: True)
|
||||
monkeypatch.setattr(runtime, "_metainfo_accelerator", rust if engine == "rust" else None)
|
||||
|
||||
meta = MetaInfo("晴天", subtitle, mtype=MediaType.MUSIC)
|
||||
|
||||
assert (meta.artists, meta.title, meta.album, meta.year) == (["周杰伦"], "晴天", "叶惠美", 2003)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("engine", ["python", "rust"])
|
||||
@pytest.mark.parametrize("suffix, artists", [
|
||||
("(featuring Guest)", ["Artist", "Guest"]),
|
||||
("(feat. Artist & Guest)", ["Artist", "Guest"]),
|
||||
("(feature presentation)", ["Artist"]),
|
||||
])
|
||||
def test_featured_credits_require_explicit_marker(suffix, artists, engine, monkeypatch):
|
||||
"""仅提取明确的客串署名,保留原始曲名,去重且不误读普通注释。"""
|
||||
if engine == "rust" and not rust.is_available():
|
||||
pytest.skip("moviepilot_rust 扩展未安装")
|
||||
monkeypatch.setattr(rust, "is_enabled", lambda: True)
|
||||
monkeypatch.setattr(runtime, "_metainfo_accelerator", rust if engine == "rust" else None)
|
||||
|
||||
meta = MetaInfo(f"Artist - Song {suffix} FLAC", mtype=MediaType.MUSIC)
|
||||
|
||||
assert meta.artists == artists
|
||||
assert meta.title == f"Song {suffix}"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("engine", ["python", "rust"])
|
||||
@pytest.mark.parametrize("title, subtitle, expected", [
|
||||
("Shan.Ge.Liao.Zai.2023.FLAC", "音乐专辑 | 山歌廖哉 - 歌手:刀郎", "山歌廖哉"),
|
||||
("Yisa Yu 2019 SanShiErLi FLAC", "郁可唯 - 三十而慄 2019 - FLAC 分軌", "三十而慄"),
|
||||
("Shan Ge Liao Zai FLAC", "曲名:山高水长", "Shan Ge Liao Zai"),
|
||||
("Artist - White Light 1971 FLAC", "歌手:歌手;曲名:白光", "White Light"),
|
||||
("山歌廖哉 FLAC", "曲名:其他专辑", "山歌廖哉"),
|
||||
])
|
||||
def test_pinyin_title_prefers_verified_native_subtitle(title, subtitle, expected, engine, monkeypatch):
|
||||
"""中文原文须与拼音逐字对应;同字数无关文案及正常外文名不能触发替换。"""
|
||||
if engine == "rust" and not rust.is_available():
|
||||
pytest.skip("moviepilot_rust 扩展未安装")
|
||||
monkeypatch.setattr(rust, "is_enabled", lambda: True)
|
||||
monkeypatch.setattr(runtime, "_metainfo_accelerator", rust if engine == "rust" else None)
|
||||
|
||||
meta = MetaInfo(title, subtitle, mtype=MediaType.MUSIC)
|
||||
|
||||
assert meta.title == expected
|
||||
assert meta.org_string == title
|
||||
Reference in New Issue
Block a user