mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-08-14 02:05:13 +08:00
361 lines
12 KiB
Python
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.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,
|
||
)
|