211 lines
7.3 KiB
Python
211 lines
7.3 KiB
Python
#!/usr/bin/env python3
|
|
|
|
import unittest
|
|
from unittest import mock
|
|
import urllib.error
|
|
|
|
import lyric_sources
|
|
|
|
|
|
class LrclibAdapterTest(unittest.TestCase):
|
|
def setUp(self):
|
|
self.track = {
|
|
"title": "Can't Stop",
|
|
"artist": "Red Hot Chili Peppers",
|
|
"album": "By the Way",
|
|
"duration": 269_000_000,
|
|
}
|
|
self.results = [
|
|
{
|
|
"id": 10,
|
|
"trackName": "Can't Stop",
|
|
"artistName": "Red Hot Chili Peppers",
|
|
"albumName": "By the Way",
|
|
"duration": 269,
|
|
"plainLyrics": "Plain line",
|
|
"syncedLyrics": "",
|
|
},
|
|
{
|
|
"id": 20,
|
|
"trackName": "Can't Stop",
|
|
"artistName": "Red Hot Chili Peppers",
|
|
"albumName": "By the Way",
|
|
"duration": 269,
|
|
"plainLyrics": "Synced line",
|
|
"syncedLyrics": "[00:01.00]Synced line",
|
|
},
|
|
]
|
|
|
|
@mock.patch("lyric_sources.itunes_cover", return_value="")
|
|
@mock.patch("lyric_sources.request_json")
|
|
def test_prefers_synced_candidate_and_returns_metadata(self, request_json, _itunes_cover):
|
|
request_json.return_value = self.results
|
|
|
|
result = lyric_sources.adapter_lrclib(self.track, {}, {})
|
|
|
|
self.assertEqual(result["selected_candidate_id"], "20")
|
|
self.assertEqual(result["lines"][0]["time"], 1000)
|
|
self.assertEqual([item["id"] for item in result["candidates"]], ["20", "10"])
|
|
self.assertNotIn("plainLyrics", result["candidates"][0])
|
|
self.assertTrue(result["candidates"][0]["synced"])
|
|
|
|
@mock.patch("lyric_sources.itunes_cover", return_value="")
|
|
@mock.patch("lyric_sources.request_json")
|
|
def test_honors_requested_candidate(self, request_json, _itunes_cover):
|
|
request_json.return_value = self.results
|
|
|
|
result = lyric_sources.adapter_lrclib(
|
|
self.track, {}, {"lyrics_candidate_id": "10"}
|
|
)
|
|
|
|
self.assertEqual(result["selected_candidate_id"], "10")
|
|
self.assertEqual(result["lines"][0]["time"], -1)
|
|
self.assertEqual(result["lines"][0]["text"], "Plain line")
|
|
|
|
@mock.patch("lyric_sources.request_json")
|
|
def test_rejects_stale_requested_candidate(self, request_json):
|
|
request_json.return_value = self.results
|
|
|
|
result = lyric_sources.adapter_lrclib(
|
|
self.track, {}, {"lyrics_candidate_id": "missing"}
|
|
)
|
|
|
|
self.assertEqual(result["type"], "none")
|
|
self.assertEqual(result["diag"], ["lrclib: requested match unavailable"])
|
|
|
|
def test_rejects_wrong_artist_even_when_synced(self):
|
|
results = [
|
|
{
|
|
"id": 30,
|
|
"trackName": "Can't Stop",
|
|
"artistName": "Unrelated Artist",
|
|
"duration": 269,
|
|
"syncedLyrics": "[00:01.00]Wrong",
|
|
},
|
|
self.results[0],
|
|
]
|
|
|
|
ranked = lyric_sources.lrclib_candidates(results, self.track)
|
|
|
|
self.assertEqual([item["id"] for item in ranked], [10])
|
|
|
|
def test_prefers_duration_bucket_before_sync_status(self):
|
|
results = [
|
|
{
|
|
"id": 30,
|
|
"trackName": "Can't Stop",
|
|
"artistName": "Red Hot Chili Peppers",
|
|
"albumName": "By the Way",
|
|
"duration": 400,
|
|
"syncedLyrics": "[00:01.00]Wrong version",
|
|
},
|
|
self.results[0],
|
|
]
|
|
|
|
ranked = lyric_sources.lrclib_candidates(results, self.track)
|
|
|
|
self.assertEqual([item["id"] for item in ranked], [10, 30])
|
|
|
|
def test_filters_missing_and_duplicate_candidate_ids(self):
|
|
duplicate = dict(self.results[1])
|
|
duplicate["plainLyrics"] = "Duplicate"
|
|
missing = dict(self.results[0])
|
|
missing.pop("id")
|
|
|
|
ranked = lyric_sources.lrclib_candidates(
|
|
[missing, self.results[1], duplicate], self.track
|
|
)
|
|
|
|
self.assertEqual([item["id"] for item in ranked], [20])
|
|
|
|
|
|
class SPlayerLinesTest(unittest.TestCase):
|
|
def test_preserves_timing_layers_and_markers(self):
|
|
lines = lyric_sources.splayer_transmitted_lines({
|
|
"duration": 5000,
|
|
"yrcData": [{
|
|
"startTime": 1000,
|
|
"endTime": 3000,
|
|
"translatedLyric": "Hello",
|
|
"isBG": True,
|
|
"isDuet": True,
|
|
"words": [
|
|
{"word": "A", "startTime": 1000, "endTime": 1500, "romanWord": "ay"},
|
|
{"word": "B", "startTime": 1500, "endTime": 2000, "romanWord": "bee"},
|
|
],
|
|
}],
|
|
})
|
|
|
|
self.assertEqual(lines[0]["text"], "AB")
|
|
self.assertEqual(lines[0]["translation"], "Hello")
|
|
self.assertEqual(lines[0]["romanization"], "ay bee")
|
|
self.assertEqual(lines[0]["chars"], [1000, 1500])
|
|
self.assertTrue(lines[0]["is_background"])
|
|
self.assertTrue(lines[0]["is_duet"])
|
|
self.assertEqual(lines[0]["words"][1]["end"], 2000)
|
|
|
|
def test_falls_back_to_lrc_when_yrc_is_invalid(self):
|
|
lines = lyric_sources.splayer_transmitted_lines({
|
|
"yrcData": [{"unexpected": "value"}],
|
|
"lrcData": [{"startTime": 2000, "endTime": 3000, "text": "fallback"}],
|
|
})
|
|
|
|
self.assertEqual(len(lines), 1)
|
|
self.assertEqual(lines[0]["text"], "fallback")
|
|
|
|
def test_marks_stretched_single_word_as_inferred(self):
|
|
lines = lyric_sources.splayer_transmitted_lines({
|
|
"yrcData": [
|
|
{
|
|
"startTime": 1000,
|
|
"endTime": 9000,
|
|
"words": [{"word": "line", "startTime": 1000, "endTime": 9000}],
|
|
},
|
|
{"startTime": 9000, "endTime": 10000, "text": "next"},
|
|
],
|
|
})
|
|
|
|
self.assertTrue(lines[0]["duration_inferred"])
|
|
self.assertEqual(lines[0]["chars"], [])
|
|
|
|
|
|
class SPlayerAdapterTest(unittest.TestCase):
|
|
@mock.patch("lyric_sources.time.sleep")
|
|
@mock.patch("lyric_sources.request_json")
|
|
def test_unavailable_api_uses_bounded_retries(self, request_json, sleep):
|
|
request_json.side_effect = urllib.error.URLError("offline")
|
|
|
|
result = lyric_sources.adapter_splayer(
|
|
{"title": "Song", "artist": "Artist"},
|
|
{"splayer_api_url": "http://127.0.0.1:25884"},
|
|
{},
|
|
)
|
|
|
|
self.assertEqual(result["type"], "none")
|
|
self.assertEqual(result["diag"], ["splayer: API unavailable"])
|
|
self.assertEqual(request_json.call_count, 3)
|
|
request_json.assert_called_with(
|
|
"http://127.0.0.1:25884/api/control/song-info", timeout=1
|
|
)
|
|
self.assertEqual(sleep.call_count, 2)
|
|
|
|
@mock.patch("lyric_sources.request_json")
|
|
def test_matches_title_suffix_and_artist_list(self, request_json):
|
|
request_json.return_value = {"data": {
|
|
"name": "Song (Live)",
|
|
"artists": [{"name": "Artist"}],
|
|
"lrcData": [{"startTime": 0, "endTime": 1000, "text": "line"}],
|
|
}}
|
|
|
|
result = lyric_sources.adapter_splayer(
|
|
{"title": "Song", "artist": "Artist"},
|
|
{"splayer_api_url": "http://127.0.0.1:25884"},
|
|
{},
|
|
)
|
|
|
|
self.assertEqual(result["type"], "lyrics")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|