Files
MoviePilot/tests/test_audio_metadata.py
T

361 lines
12 KiB
Python

from pathlib import Path
from types import SimpleNamespace
from unittest.mock import Mock
from app.chain.media import MediaChain
from app.domain.context import MusicInfo
from app.domain.meta.metamusic import (
audio_quality_score,
audio_quality_tier,
format_audio_quality,
parse_audio_quality,
)
from app.application.audio import AudioMetadataHelper
from app.schemas.types import MUSIC_ENTITY_ALBUM
RECORDING_ID = "38035858-f990-4fbb-b3b2-f2f8b958eeba"
def test_read_audio_metadata_maps_easy_tags(monkeypatch):
"""音频标签和技术参数应映射为 MetaMusic。"""
audio = SimpleNamespace(
tags={
"title": ["Get Lucky"],
"artist": ["Daft Punk", "Pharrell Williams"],
"album": ["Random Access Memories"],
"albumartist": ["Daft Punk"],
"date": ["2013-05-17"],
"tracknumber": ["8/13"],
"discnumber": ["1/1"],
"isrc": ["USQX91300105"],
"musicbrainz_trackid": [RECORDING_ID],
},
info=SimpleNamespace(
length=369.4,
bitrate=1411200,
bits_per_sample=16,
sample_rate=44100,
),
)
monkeypatch.setattr("app.application.audio.MutagenFile", lambda *_args, **_kwargs: audio)
meta = AudioMetadataHelper.read(Path("/music/08 - Get Lucky.flac"))
assert meta.title == "Get Lucky"
assert meta.artists == ["Daft Punk", "Pharrell Williams"]
assert meta.album == "Random Access Memories"
assert meta.year == 2013
assert meta.track_number == 8
assert meta.total_tracks == 13
assert meta.duration == 369
assert meta.audio_format == "FLAC"
assert meta.audio_lossless is True
assert meta.audio_quality == "lossless"
assert meta.audio_specs == "FLAC · 16-bit · 44.1 kHz · 1,411 kbps"
assert meta.media_source == "musicbrainz"
assert meta.media_id == RECORDING_ID
def test_parse_declared_hires_audio_quality_from_resource_title():
"""站点资源标题中的格式、位深和采样率应形成可筛选的统一音质参数。"""
specs = parse_audio_quality("周杰伦 - 叶惠美 FLAC 24bit 96kHz Hi-Res")
assert specs == {
"audio_format": "FLAC",
"audio_lossless": True,
"bit_depth": 24,
"sample_rate": 96000,
"bitrate": None,
}
assert audio_quality_tier(**specs) == "hires"
assert audio_quality_score(**specs) == 96
assert format_audio_quality(**specs) == "FLAC · 24-bit · 96 kHz"
def test_audio_quality_score_orders_lossy_lossless_and_terminal_hires():
"""音乐洗版分数必须稳定满足有损、无损、顶级 Hi-Res 的递增关系。"""
mp3_score = audio_quality_score("MP3", bitrate=320000)
flac_score = audio_quality_score("FLAC", bit_depth=16, sample_rate=44100)
hires_score = audio_quality_score("FLAC", bit_depth=24, sample_rate=192000)
assert 0 < mp3_score < flac_score < hires_score
assert hires_score == 100
def test_music_info_serialization_exposes_derived_audio_quality():
"""音乐 REST 序列化应同时返回原始技术参数和规范化音质展示字段。"""
payload = MusicInfo(
title="晴天",
audio_format="FLAC",
bit_depth=24,
sample_rate=96_000,
bitrate=2_304_000,
).to_dict()
assert payload["audio_quality"] == "hires"
assert payload["audio_quality_score"] == 96
assert payload["audio_specs"] == "FLAC · 24-bit · 96 kHz · 2,304 kbps"
def test_music_info_from_meta_preserves_track_and_audio_evidence():
"""核心元数据转换应保留整理、刮削和通知依赖的曲序与实际音频参数。"""
from app.domain.meta.metamusic import MetaMusic
info = MusicInfo.from_meta(MetaMusic(
title="Get Lucky",
artists=["Daft Punk"],
album="Random Access Memories",
disc_number=1,
track_number=8,
total_tracks=13,
audio_format="FLAC",
bit_depth=24,
sample_rate=96_000,
bitrate=2_304_000,
duration=369,
isrc="USQX91300105",
))
assert info.track_number == 8
assert info.total_tracks == 13
assert info.audio_format == "FLAC"
assert info.audio_lossless is True
assert info.bit_depth == 24
assert info.sample_rate == 96_000
assert info.bitrate == 2_304_000
assert info.duration == 369
assert info.isrc == "USQX91300105"
def test_parse_compact_audio_quality_tokens_without_false_sample_bitrate():
"""紧凑资源命名中的 FLAC24bit 和 320K 应可识别,96kHz 不得误判为码率。"""
lossless = parse_audio_quality("Album.FLAC24bit.96kHz")
lossy = parse_audio_quality("Album.MP3.320K")
assert lossless["audio_format"] == "FLAC"
assert lossless["bit_depth"] == 24
assert lossless["sample_rate"] == 96000
assert lossless["bitrate"] is None
assert lossy["bitrate"] == 320000
def test_read_audio_metadata_falls_back_to_filename(monkeypatch):
"""无法读取标签时应保留可用于手动整理的文件名元数据。"""
monkeypatch.setattr("app.application.audio.MutagenFile", lambda *_args, **_kwargs: None)
meta = AudioMetadataHelper.read(Path("/music/Unknown Track.mp3"))
assert meta.title == "Unknown Track"
assert meta.audio_format == "MP3"
def test_read_audio_tags_does_not_fill_from_filename(monkeypatch):
"""标签识别层应只使用标签证据,避免把文件名线索提前混入。"""
audio = SimpleNamespace(
tags={"title": ["Tagged Title"]},
info=SimpleNamespace(length=180),
)
monkeypatch.setattr("app.application.audio.MutagenFile", lambda *_args, **_kwargs: audio)
meta = AudioMetadataHelper.read_tags(Path("/music/Daft Punk - Get Lucky 2013.flac"))
assert meta.title == "Tagged Title"
assert meta.artists == []
assert meta.year is None
def test_read_audio_tags_ignores_invalid_musicbrainz_id(monkeypatch):
"""异常 MusicBrainz 标签不得进入 ID 详情路径。"""
audio = SimpleNamespace(
tags={
"title": ["Tagged Title"],
"musicbrainz_trackid": ["../../unexpected"],
},
info=SimpleNamespace(length=180),
)
monkeypatch.setattr("app.application.audio.MutagenFile", lambda *_args, **_kwargs: audio)
meta = AudioMetadataHelper.read_tags(Path("/music/track.flac"))
assert meta.media_source is None
assert meta.media_id is None
def test_remote_path_meta_parses_track_prefix_once(tmp_path):
"""远程或尚未落盘的音频路径应先剥离曲序,不能把 08 误识别成艺术家。"""
audio_path = tmp_path / "Daft Punk - Random Access Memories (2013)" / "08 - Get Lucky.flac"
music_meta = MediaChain.read_path_meta(audio_path)
assert music_meta.title == "Get Lucky"
assert music_meta.artists == ["Daft Punk"]
assert music_meta.album == "Random Access Memories"
assert music_meta.track_number == 8
assert music_meta.audio_format == "FLAC"
def test_read_audio_metadata_fallback_uses_dynamic_filename_parser(tmp_path, monkeypatch):
"""标签不可读时应直接使用完整动态模式解析复杂音乐文件名。"""
audio_path = tmp_path / (
"S H E - S H E十七音乐会 2018 WEB-DL 1080P AVC AAC-FHDMv.flac"
)
audio_path.write_bytes(b"fake-flac")
monkeypatch.setattr("app.application.audio.MutagenFile", lambda *_args, **_kwargs: None)
meta = AudioMetadataHelper.read(audio_path)
assert meta.artists == ["S.H.E"]
assert meta.title == "S.H.E十七音乐会"
assert meta.year == 2018
assert meta.audio_format == "FLAC"
def test_read_audio_metadata_partial_tags_use_filename_for_missing_fields(
tmp_path,
monkeypatch,
):
"""真实标签优先,缺失的艺术家和年份由完整文件名解析补齐。"""
audio_path = tmp_path / "Daft Punk - Get Lucky 2013 FLAC.flac"
audio_path.write_bytes(b"fake-flac")
audio = SimpleNamespace(
tags={"title": ["Tagged Title"]},
info=SimpleNamespace(
length=369.4,
bitrate=1411200,
bits_per_sample=16,
sample_rate=44100,
),
)
monkeypatch.setattr("app.application.audio.MutagenFile", lambda *_args, **_kwargs: audio)
meta = AudioMetadataHelper.read(audio_path)
assert meta.title == "Tagged Title"
assert meta.artists == ["Daft Punk"]
assert meta.year == 2013
assert meta.duration == 369
assert meta.sample_rate == 44100
def test_read_audio_metadata_distinguishes_alac_inside_m4a(monkeypatch):
"""M4A 容器应依据实际流编码区分 ALAC 与 AAC,避免把无损音频降级。"""
audio = SimpleNamespace(
tags={"title": ["Lossless Track"]},
info=SimpleNamespace(
codec="alac",
codec_description="Apple Lossless Audio Codec",
length=180,
bitrate=900000,
bits_per_sample=24,
sample_rate=96000,
),
)
monkeypatch.setattr("app.application.audio.MutagenFile", lambda *_args, **_kwargs: audio)
meta = AudioMetadataHelper.read(Path("/music/Lossless Track.m4a"))
assert meta.audio_format == "ALAC"
assert meta.audio_lossless is True
assert meta.audio_quality == "hires"
def test_write_audio_metadata_maps_music_info_to_easy_tags(monkeypatch):
"""音乐刮削应把标准歌曲、专辑和曲序字段写回音频标签。"""
class FakeAudio:
"""记录 Mutagen Easy 标签写入结果。"""
def __init__(self):
self.tags = {}
self.saved = False
def __setitem__(self, key, value):
self.tags[key] = value
def save(self):
self.saved = True
audio = FakeAudio()
monkeypatch.setattr("app.application.audio.MutagenFile", lambda *_args, **_kwargs: audio)
success = AudioMetadataHelper.write(
Path("/music/08 - Get Lucky.flac"),
MusicInfo(
media_source="musicbrainz",
media_id=RECORDING_ID,
title="Get Lucky",
artists=["Daft Punk", "Pharrell Williams"],
album="Random Access Memories",
album_artist="Daft Punk",
year=2013,
track_number=8,
total_tracks=13,
isrc="USQX91300105",
),
)
assert success is True
assert audio.saved is True
assert audio.tags["title"] == ["Get Lucky"]
assert audio.tags["artist"] == ["Daft Punk", "Pharrell Williams"]
assert audio.tags["tracknumber"] == ["8/13"]
assert audio.tags["musicbrainz_trackid"] == [RECORDING_ID]
def test_write_audio_metadata_does_not_write_album_id_as_recording_tag(monkeypatch):
"""MusicBrainz 专辑身份不得写入只接受 recording ID 的曲目标签。"""
class FakeAudio:
"""记录专辑元数据写入结果。"""
def __init__(self):
"""初始化空标签容器。"""
self.tags = {}
def __setitem__(self, key, value):
"""记录 Easy 标签赋值。"""
self.tags[key] = value
def save(self):
"""模拟 Mutagen 保存。"""
audio = FakeAudio()
monkeypatch.setattr("app.application.audio.MutagenFile", lambda *_args, **_kwargs: audio)
success = AudioMetadataHelper.write(
Path("/music/Random Access Memories.flac"),
MusicInfo(
media_source="musicbrainz",
media_id="release-group-1",
music_type=MUSIC_ENTITY_ALBUM,
title="Random Access Memories",
),
)
assert success is True
assert "musicbrainz_trackid" not in audio.tags
def test_write_audio_metadata_can_embed_cover_without_rewriting_tags(monkeypatch):
"""音乐封面策略应能在标签策略关闭时独立执行。"""
audio = SimpleNamespace(tags={"title": ["Original"]})
monkeypatch.setattr("app.application.audio.MutagenFile", lambda *_args, **_kwargs: audio)
write_cover = Mock()
monkeypatch.setattr(AudioMetadataHelper, "_write_cover", write_cover)
success = AudioMetadataHelper.write(
Path("/music/track.flac"),
MusicInfo(title="Changed"),
cover_data=b"cover",
write_tags=False,
cover_overwrite=False,
)
assert success is True
assert audio.tags == {"title": ["Original"]}
write_cover.assert_called_once_with(
path=Path("/music/track.flac"),
cover_data=b"cover",
cover_mime="image/jpeg",
overwrite=False,
)