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, )