mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-11-01 04:47:03 +08:00
fix(music): preserve album directory year and version evidence
This commit is contained in:
@@ -391,7 +391,12 @@ _MUSIC_DIR_YEAR_RE = re.compile(r"[(\[]\s*(?P<year>(?:19|20)\d{2})\s*[)\]]")
|
||||
_MUSIC_DIR_PREFIX_YEAR_RE = re.compile(
|
||||
r"^(?P<year>(?:19|20)\d{2})\s*[-._\s]+(?!(?:19|20)\d{2}\b)"
|
||||
)
|
||||
# 目录名中的括号补充说明(格式、音质、厂牌等),如 [FLAC 24bit-96kHz]
|
||||
# 独立中间年份不能与数字专辑名混淆,必须同时存在艺人、年份及作品三个部分。
|
||||
_MUSIC_ALBUM_YEAR_RE = re.compile(
|
||||
r"^(?P<artist>.+?)\s*[-–—−-]\s*(?P<year>(?:19|20)\d{2})"
|
||||
r"\s*[-–—−-]\s*(?P<title>\S.*)$"
|
||||
)
|
||||
# 目录括号既可能是规格,也可能是作品或发行版本的一部分,需要按内容区分。
|
||||
_MUSIC_BRACKET_RE = re.compile(r"\[[^\]]*\]|【[^】]*】|\([^)]*\)")
|
||||
_MUSIC_RECORDING_VERSION_RE = re.compile(
|
||||
r"[\[((【]([^\]))】]*(?:\blive\b|\bremix\b|\binstrumental\b|\bacoustic\b|"
|
||||
@@ -1002,6 +1007,7 @@ class MetaMusic(MetaBase):
|
||||
if artist or directory.get("artist") else [],
|
||||
album_artist=artist or directory.get("artist"),
|
||||
year=directory.get("year"),
|
||||
version=directory.get("version"),
|
||||
)
|
||||
|
||||
@property
|
||||
@@ -1060,7 +1066,7 @@ class MetaMusic(MetaBase):
|
||||
公共层先完成字符归一、音质与干扰信息剔除;随后由注册中心依次匹配
|
||||
命名模式和对应解析器,最后统一回填结构化字段并提取曲序前缀。
|
||||
"""
|
||||
raw = str(value or "")
|
||||
raw = self._normalize_album_year(str(value or ""))
|
||||
if not self.version:
|
||||
self.version = self._resource_version(raw, None)
|
||||
accelerator = get_metainfo_accelerator()
|
||||
@@ -1665,15 +1671,35 @@ class MetaMusic(MetaBase):
|
||||
match = _MUSIC_DISC_DIR_RE.match(str(name or "").strip())
|
||||
return int(match.group("num")) if match else None
|
||||
|
||||
@staticmethod
|
||||
def _normalize_album_year(value: str) -> str:
|
||||
"""在 Python/Rust 分流前统一艺人-年份-作品,保留数字作品及年份区间。"""
|
||||
if _MUSIC_YEAR_RANGE_DETECT_RE.search(value) or _MUSIC_AUDIO_RELEASE_TAIL_RE.search(value):
|
||||
return value
|
||||
match = _MUSIC_ALBUM_YEAR_RE.fullmatch(value.strip())
|
||||
if not match or match.group("artist").strip().isdigit():
|
||||
return value
|
||||
artist, year, title = match.group("artist", "year", "title")
|
||||
return f"{artist.strip()} - {title.strip()} ({year})"
|
||||
|
||||
@staticmethod
|
||||
def _clean_directory_bracket(match: re.Match[str]) -> str:
|
||||
"""只删除已知规格/内容评级,未知括号及版本名称保留作为作品证据。"""
|
||||
content = match.group()[1:-1].strip()
|
||||
if _MUSIC_SPEC_SEGMENT_RE.fullmatch(content) or content.casefold() in {"explicit", "clean"}:
|
||||
return " "
|
||||
return match.group()
|
||||
|
||||
@classmethod
|
||||
def parse_album_dir(cls, name: str) -> dict[str, Any]:
|
||||
"""解析专辑目录名,提取歌手、专辑名、年份和音质描述。
|
||||
|
||||
支持 `歌手 - 专辑 (2004) [FLAC 24bit-96kHz]` 等常见命名。
|
||||
"""
|
||||
text = cls._clean_text(name)
|
||||
text = cls._normalize_album_year(cls._normalize_text(name))
|
||||
if not text:
|
||||
return {}
|
||||
quality_text = text
|
||||
year = None
|
||||
prefixed_year = False
|
||||
year_match = _MUSIC_DIR_YEAR_RE.search(text)
|
||||
@@ -1686,9 +1712,10 @@ class MetaMusic(MetaBase):
|
||||
year = int(prefix_year_match.group("year"))
|
||||
text = text[prefix_year_match.end():]
|
||||
prefixed_year = True
|
||||
# 括号内的格式/音质描述先剥离出专辑名,但仍可用于音质解析
|
||||
brackets = " ".join(fragment for fragment in _MUSIC_BRACKET_RE.findall(text))
|
||||
album_text = cls._clean_text(_MUSIC_BRACKET_RE.sub(" ", text))
|
||||
# Live/Deluxe 以及未知副标题均参与身份确认,不能像音质标签一样整体删除。
|
||||
album_text = cls._clean_text(cls._strip_spec_segments(
|
||||
_MUSIC_BRACKET_RE.sub(cls._clean_directory_bracket, text)
|
||||
))
|
||||
if not album_text:
|
||||
return {}
|
||||
artist, album = (
|
||||
@@ -1700,7 +1727,8 @@ class MetaMusic(MetaBase):
|
||||
"artist": artist,
|
||||
"album": album,
|
||||
"year": year,
|
||||
"quality_text": cls._clean_text(f"{album_text} {brackets}"),
|
||||
"version": cls._resource_version(album, None),
|
||||
"quality_text": quality_text,
|
||||
}
|
||||
|
||||
def apply_path_context(self, path: "str | Path") -> "MetaMusic":
|
||||
@@ -1771,6 +1799,9 @@ class MetaMusic(MetaBase):
|
||||
self.album_artist = dir_info["artist"]
|
||||
if self.year is None and dir_info.get("year"):
|
||||
self.year = dir_info["year"]
|
||||
if title_from_name and not self.version and dir_info.get("version"):
|
||||
# 目录版本可补无标签曲目,真实歌曲标签不能被整张专辑的版本推测覆盖。
|
||||
self.version = dir_info["version"]
|
||||
# 目录名里的格式、位深、采样率可补齐本地标签未声明的音质参数
|
||||
if dir_info.get("quality_text"):
|
||||
self.apply_audio_quality(dir_info["quality_text"])
|
||||
|
||||
@@ -577,7 +577,7 @@ def test_parse_album_dir_with_prefix_year_does_not_invent_artist():
|
||||
)
|
||||
|
||||
assert info["artist"] is None
|
||||
assert info["album"] == "All Too Well - Recorded at Long Pond Studios"
|
||||
assert info["album"] == "All Too Well (Sad Girl Autumn Version) - Recorded at Long Pond Studios"
|
||||
assert info["year"] == 2021
|
||||
|
||||
|
||||
|
||||
91
tests/test_music_directory_evidence.py
Normal file
91
tests/test_music_directory_evidence.py
Normal file
@@ -0,0 +1,91 @@
|
||||
"""PT 音乐目录名称在两种解析引擎下保留作品、年份和版本证据。"""
|
||||
|
||||
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
|
||||
|
||||
|
||||
@pytest.fixture(params=["python", "rust"])
|
||||
def parser_engine(request, monkeypatch):
|
||||
"""为实际文件名与目录上下文切换解析引擎,Rust 不可用时明确跳过。"""
|
||||
if request.param == "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 request.param == "rust" else None)
|
||||
return request.param
|
||||
|
||||
|
||||
@pytest.mark.parametrize("name", [
|
||||
"周杰伦 - 2001 - 范特西 [FLAC]", "周杰伦-2001-范特西 [FLAC]",
|
||||
"Daft Punk - 2001 - Discovery [FLAC]",
|
||||
])
|
||||
def test_middle_year_is_shared_by_resource_and_directory(name, parser_engine):
|
||||
"""有三个明确部分时,各入口应使用相同艺人、专辑和年份。"""
|
||||
resource = MetaMusic.parse_resource(name)
|
||||
directory = MetaMusic.parse_album_dir(name)
|
||||
path = MetaMusic().apply_path_context(Path("/music") / name / "01.flac")
|
||||
|
||||
assert resource.year == directory["year"] == path.year == 2001
|
||||
assert resource.title == directory["album"] == path.album
|
||||
assert resource.artists == path.artists == [directory["artist"]]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("suffix", ["(Live)", "(Deluxe Edition)", "[Live]", "(Unknown Subtitle)", "(现场版)"])
|
||||
def test_directory_keeps_identity_qualifiers(suffix, parser_engine):
|
||||
"""括号中的版本及未知作品副标题不能作为规格噪声丢弃。"""
|
||||
name = f"Artist - Record {suffix} (2001) [FLAC 24bit-96kHz]"
|
||||
meta = MetaMusic().apply_path_context(Path("/music") / name / "03.flac")
|
||||
|
||||
assert suffix.replace("(", "(").replace(")", ")") in meta.album
|
||||
assert meta.year == 2001
|
||||
assert meta.artists == ["Artist"]
|
||||
assert meta.track_number == 3
|
||||
assert meta.bit_depth == 24
|
||||
assert meta.sample_rate == 96000
|
||||
if suffix in {"(Live)", "[Live]"}:
|
||||
assert meta.version == "Live"
|
||||
|
||||
|
||||
def test_tagged_track_version_is_not_inferred_from_album_directory():
|
||||
"""现场专辑中的有标签曲目也可能是录音室附赠版,不能强写录音版本。"""
|
||||
meta = MetaMusic(title="Studio Bonus", artists=["Artist"], album="Record")
|
||||
|
||||
meta.apply_path_context("/music/Artist - Record (Live) (2001)/03.flac")
|
||||
|
||||
assert meta.title == "Studio Bonus"
|
||||
assert meta.album == "Record"
|
||||
assert meta.version is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize("name,album,year", [
|
||||
("Taylor Swift - 1989 [FLAC]", "1989", None),
|
||||
("2020 - Record (Live) [FLAC]", "Record (Live)", 2020),
|
||||
("Artist - Best Album (2001) [FLAC]", "Best Album", 2001),
|
||||
])
|
||||
def test_directory_does_not_guess_numeric_names(name, album, year):
|
||||
"""数字专辑名、年份前缀和自然标题保持原本语义。"""
|
||||
result = MetaMusic.parse_album_dir(name)
|
||||
|
||||
assert result["album"] == album
|
||||
assert result["year"] == year
|
||||
if name.startswith("2020"):
|
||||
assert result["artist"] is None
|
||||
|
||||
|
||||
def test_album_context_retains_directory_version():
|
||||
"""整专匹配同样接收目录录音版本,不能只在单文件入口保留。"""
|
||||
meta = MetaMusic.from_album_context("Artist - Record (Live) (2001) [FLAC]", [])
|
||||
|
||||
assert meta.title == "Record (Live)"
|
||||
assert meta.version == "Live"
|
||||
Reference in New Issue
Block a user