mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-04 23:17:20 +08:00
160 lines
5.9 KiB
Python
160 lines
5.9 KiB
Python
import asyncio
|
|
from types import SimpleNamespace
|
|
from unittest.mock import AsyncMock, Mock
|
|
|
|
from mutagen.id3 import SYLT, USLT
|
|
|
|
from app.application.audio import AudioMetadataHelper
|
|
from app.chain.lyrics import LyricsChain
|
|
from app.chain.scraping import ScrapingChain
|
|
from app.domain.context import MusicInfo, MusicLyrics
|
|
from app.domain.meta.metamusic import MetaMusic
|
|
from app.schemas.workflow import FileItem
|
|
|
|
|
|
def test_lyricsfile_derives_lrc_plain_language_and_word_quality() -> None:
|
|
"""Lyricsfile 应保留原文,并派生播放器兼容内容和逐字同步质量。"""
|
|
lyrics = MusicLyrics(
|
|
provider="lrclib",
|
|
lyricsfile="""
|
|
version: '1.0'
|
|
metadata:
|
|
title: 晴天
|
|
artist: 周杰伦
|
|
language: zh
|
|
lines:
|
|
- start_ms: 1250
|
|
text: 晴天
|
|
words:
|
|
- {start_ms: 1250, text: 晴}
|
|
- {start_ms: 1500, text: 天}
|
|
""",
|
|
)
|
|
|
|
assert lyrics.language == "zh"
|
|
assert lyrics.synced_lyrics == "[00:01.25]晴天"
|
|
assert lyrics.plain_lyrics == "晴天"
|
|
assert lyrics.quality_rank == 4
|
|
|
|
|
|
def test_lyricsfile_rejects_yaml_aliases() -> None:
|
|
"""外部 Lyricsfile 不得通过 YAML 锚点别名制造共享或膨胀结构。"""
|
|
lyrics = MusicLyrics(
|
|
provider="lrclib",
|
|
lyricsfile="""
|
|
version: '1.0'
|
|
metadata: {title: Track, artist: Artist}
|
|
base: &line {start: 1000, text: unsafe}
|
|
lines: [*line]
|
|
""",
|
|
)
|
|
|
|
assert lyrics.content is None
|
|
assert lyrics.quality_rank == 0
|
|
|
|
|
|
def test_lyrics_chain_prefers_synced_candidate_over_exact_plain_fallback(monkeypatch) -> None:
|
|
"""可信候选中应优先同步质量,再以匹配度和来源优先级打破平局。"""
|
|
chain = LyricsChain()
|
|
responses = iter([
|
|
MusicLyrics(provider="legacy", plain_lyrics="plain", match_score=100),
|
|
[MusicLyrics(provider="licensed", synced_lyrics="[00:01]sync", match_score=95)],
|
|
])
|
|
monkeypatch.setattr(chain, "run_module", lambda *_args, **_kwargs: next(responses))
|
|
|
|
result = chain.get_music_lyrics(MusicInfo(title="Track", artists=["Artist"]))
|
|
|
|
assert result is not None
|
|
assert result.provider == "licensed"
|
|
|
|
|
|
def test_lyrics_chain_selects_async_candidates(monkeypatch) -> None:
|
|
"""通用歌词链异步入口应返回候选中的最优结果。"""
|
|
chain = LyricsChain()
|
|
run_module = AsyncMock(side_effect=[
|
|
MusicLyrics(provider="legacy", plain_lyrics="plain", match_score=100),
|
|
[MusicLyrics(provider="lrclib", synced_lyrics="[00:01]sync", match_score=100)],
|
|
])
|
|
monkeypatch.setattr(chain, "async_run_module", run_module)
|
|
|
|
result = asyncio.run(chain.async_get_music_lyrics(
|
|
MusicInfo(title="Track", artists=["Artist"])
|
|
))
|
|
|
|
assert result is not None
|
|
assert result.synced_lyrics == "[00:01]sync"
|
|
|
|
|
|
def test_audio_helper_reads_id3_synced_and_plain_lyrics(monkeypatch, tmp_path) -> None:
|
|
"""ID3 SYLT 和 USLT 应转换为本地高置信歌词候选。"""
|
|
tags = Mock()
|
|
tags.getall.side_effect = lambda name: {
|
|
"SYLT": [SYLT(encoding=3, lang="zho", format=2, type=1, text=[("晴天", 1250)])],
|
|
"USLT": [USLT(encoding=3, lang="zho", text="晴天")],
|
|
}[name]
|
|
tags.items.return_value = []
|
|
monkeypatch.setattr("app.application.audio.MutagenFile", lambda *_args, **_kwargs: SimpleNamespace(tags=tags))
|
|
|
|
lyrics = AudioMetadataHelper.read_lyrics(tmp_path / "track.mp3")
|
|
|
|
assert lyrics is not None
|
|
assert lyrics.provider == "embedded"
|
|
assert lyrics.synced_lyrics == "[00:01.25]晴天"
|
|
assert lyrics.plain_lyrics == "晴天"
|
|
|
|
|
|
def test_scrape_never_downgrades_existing_lrc_to_plain_text(monkeypatch, tmp_path) -> None:
|
|
"""即使调用方要求覆盖,也不能用纯文本替换已有同步歌词。"""
|
|
chain = object.__new__(ScrapingChain)
|
|
chain.storagechain = Mock()
|
|
existing = FileItem(storage="local", path=(tmp_path / "track.lrc").as_posix(), type="file")
|
|
chain.storagechain.get_file_item.side_effect = lambda storage, path: (
|
|
existing if str(path).endswith(".lrc") else None
|
|
)
|
|
lyrics_chain = Mock()
|
|
lyrics_chain.budget_exceeded = False
|
|
lyrics_chain.get_music_lyrics.return_value = MusicLyrics(
|
|
provider="theaudiodb",
|
|
plain_lyrics="plain",
|
|
match_score=100,
|
|
)
|
|
monkeypatch.setattr(AudioMetadataHelper, "read_lyrics", lambda _path: None)
|
|
write = Mock(return_value=True)
|
|
monkeypatch.setattr(chain, "_write_music_lyrics_sidecar", write)
|
|
|
|
status = chain._scrape_music_lyrics(
|
|
fileitem=FileItem(storage="local", path=(tmp_path / "track.flac").as_posix(), type="file"),
|
|
local_path=tmp_path / "track.flac",
|
|
scrape_info=MetaMusic(title="Track", artists=["Artist"]),
|
|
lyrics_option=SimpleNamespace(is_skip=False, is_upgrade=False),
|
|
overwrite=True,
|
|
lyrics_chain=lyrics_chain,
|
|
album_info=None,
|
|
)
|
|
|
|
assert status == "protected"
|
|
write.assert_not_called()
|
|
|
|
|
|
def test_music_lyrics_sidecars_match_only_same_stem_audio() -> None:
|
|
"""整理链只关联同目录同主干名歌词,Lyricsfile 双扩展名也应正确剥离。"""
|
|
audio = FileItem(storage="local", path="/music/Track.flac", name="Track.flac", type="file", extension="flac")
|
|
lrc = FileItem(storage="local", path="/music/Track.lrc", name="Track.lrc", type="file", extension="lrc")
|
|
lyricsfile = FileItem(
|
|
storage="local",
|
|
path="/music/Track.lyricsfile.yaml",
|
|
name="Track.lyricsfile.yaml",
|
|
type="file",
|
|
extension="yaml",
|
|
)
|
|
other = FileItem(storage="local", path="/music/Notes.txt", name="Notes.txt", type="file", extension="txt")
|
|
|
|
from app.chain.transfer import TransferChain
|
|
|
|
transfer_chain = object.__new__(TransferChain)
|
|
transfer_chain._subtitle_exts = ()
|
|
transfer_chain._audio_exts = (".flac",)
|
|
assert transfer_chain._get_related_main_file_key(lrc, [audio]) == ("local", "/music/Track.flac")
|
|
assert transfer_chain._get_related_main_file_key(lyricsfile, [audio]) == ("local", "/music/Track.flac")
|
|
assert transfer_chain._get_related_main_file_key(other, [audio]) is None
|