Files
MoviePilot/tests/test_audio_metadata.py

361 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from pathlib import Path
from types import SimpleNamespace
from unittest.mock import Mock
from app.chain.media import MediaChain
from app.core.context import MusicInfo
from app.core.meta.metamusic import (
audio_quality_score,
audio_quality_tier,
format_audio_quality,
parse_audio_quality,
)
from app.helper.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.helper.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.core.meta 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.helper.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.helper.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.helper.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.helper.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.helper.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.helper.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.helper.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.helper.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.helper.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,
)