Append source to the lyrics

This commit is contained in:
Šarūnas Nejus 2024-10-19 03:15:03 +01:00
parent bdc564a573
commit 734bcc28a8
No known key found for this signature in database
GPG key ID: DD28F6704DBE3435
2 changed files with 33 additions and 15 deletions

View file

@ -266,7 +266,7 @@ class Backend(RequestHandler, metaclass=BackendClass):
def fetch( def fetch(
self, artist: str, title: str, album: str, length: int self, artist: str, title: str, album: str, length: int
) -> str | None: ) -> tuple[str, str] | None:
raise NotImplementedError raise NotImplementedError
@ -277,6 +277,7 @@ class LRCLyrics:
DURATION_DIFF_TOLERANCE = 0.05 DURATION_DIFF_TOLERANCE = 0.05
target_duration: float target_duration: float
id: int
duration: float duration: float
instrumental: bool instrumental: bool
plain: str plain: str
@ -292,6 +293,7 @@ class LRCLyrics:
) -> LRCLyrics: ) -> LRCLyrics:
return cls( return cls(
target_duration, target_duration,
candidate["id"],
candidate["duration"] or 0.0, candidate["duration"] or 0.0,
candidate["instrumental"], candidate["instrumental"],
candidate["plainLyrics"], candidate["plainLyrics"],
@ -374,14 +376,15 @@ class LRCLib(Backend):
def fetch( def fetch(
self, artist: str, title: str, album: str, length: int self, artist: str, title: str, album: str, length: int
) -> str | None: ) -> tuple[str, str] | None:
"""Fetch lyrics text for the given song data.""" """Fetch lyrics text for the given song data."""
evaluate_item = partial(LRCLyrics.make, target_duration=length) evaluate_item = partial(LRCLyrics.make, target_duration=length)
for group in self.fetch_candidates(artist, title, album, length): for group in self.fetch_candidates(artist, title, album, length):
candidates = [evaluate_item(item) for item in group] candidates = [evaluate_item(item) for item in group]
if item := self.pick_best_match(candidates): if item := self.pick_best_match(candidates):
return item.get_text(self.config["synced"]) lyrics = item.get_text(self.config["synced"])
return lyrics, f"{self.GET_URL}/{item.id}"
return None return None
@ -420,7 +423,7 @@ class MusiXmatch(DirectBackend):
return quote(unidecode(text)) return quote(unidecode(text))
def fetch(self, artist: str, title: str, *_) -> str | None: def fetch(self, artist: str, title: str, *_) -> tuple[str, str] | None:
url = self.build_url(artist, title) url = self.build_url(artist, title)
html = self.fetch_text(url) html = self.fetch_text(url)
@ -442,7 +445,7 @@ class MusiXmatch(DirectBackend):
# sometimes there are non-existent lyrics with some content # sometimes there are non-existent lyrics with some content
if "Lyrics | Musixmatch" in lyrics: if "Lyrics | Musixmatch" in lyrics:
return None return None
return lyrics return lyrics, url
class Html: class Html:
@ -543,13 +546,13 @@ class SearchBackend(SoupMixin, Backend):
if check_match(candidate): if check_match(candidate):
yield candidate yield candidate
def fetch(self, artist: str, title: str, *_) -> str | None: def fetch(self, artist: str, title: str, *_) -> tuple[str, str] | None:
"""Fetch lyrics for the given artist and title.""" """Fetch lyrics for the given artist and title."""
for result in self.get_results(artist, title): for result in self.get_results(artist, title):
if (html := self.fetch_text(result.url)) and ( if (html := self.fetch_text(result.url)) and (
lyrics := self.scrape(html) lyrics := self.scrape(html)
): ):
return lyrics return lyrics, result.url
return None return None
@ -604,11 +607,15 @@ class Tekstowo(SoupMixin, DirectBackend):
def encode(cls, text: str) -> str: def encode(cls, text: str) -> str:
return cls.non_alpha_to_underscore(unidecode(text.lower())) return cls.non_alpha_to_underscore(unidecode(text.lower()))
def fetch(self, artist: str, title: str, *_) -> str | None: def fetch(self, artist: str, title: str, *_) -> tuple[str, str] | None:
url = self.build_url(artist, title)
# We are expecting to receive a 404 since we are guessing the URL. # We are expecting to receive a 404 since we are guessing the URL.
# Thus suppress the error so that it does not end up in the logs. # Thus suppress the error so that it does not end up in the logs.
with suppress(NotFoundError): with suppress(NotFoundError):
return self.scrape(self.fetch_text(self.build_url(artist, title))) if lyrics := self.scrape(self.fetch_text(url)):
return lyrics, url
return None
return None return None
@ -1014,8 +1021,9 @@ class LyricsPlugin(RequestHandler, plugins.BeetsPlugin):
self.info("Fetching lyrics for {} - {}", artist, title) self.info("Fetching lyrics for {} - {}", artist, title)
for backend in self.backends: for backend in self.backends:
with backend.handle_request(): with backend.handle_request():
if lyrics := backend.fetch(artist, title, *args): if lyrics_info := backend.fetch(artist, title, *args):
return lyrics lyrics, url = lyrics_info
return f"{lyrics}\n\nSource: {url}"
return None return None

View file

@ -279,11 +279,12 @@ class TestLyricsSources(LyricsBackendTest):
def test_backend_source(self, lyrics_plugin, lyrics_page: LyricsPage): def test_backend_source(self, lyrics_plugin, lyrics_page: LyricsPage):
"""Test parsed lyrics from each of the configured lyrics pages.""" """Test parsed lyrics from each of the configured lyrics pages."""
lyrics = lyrics_plugin.get_lyrics( lyrics_info = lyrics_plugin.get_lyrics(
lyrics_page.artist, lyrics_page.track_title, "", 186 lyrics_page.artist, lyrics_page.track_title, "", 186
) )
assert lyrics assert lyrics_info
lyrics, _ = lyrics_info.split("\n\nSource: ")
assert lyrics == lyrics_page.lyrics assert lyrics == lyrics_page.lyrics
@ -400,6 +401,7 @@ LYRICS_DURATION = 950
def lyrics_match(**overrides): def lyrics_match(**overrides):
return { return {
"id": 1,
"instrumental": False, "instrumental": False,
"duration": LYRICS_DURATION, "duration": LYRICS_DURATION,
"syncedLyrics": "synced", "syncedLyrics": "synced",
@ -428,7 +430,9 @@ class TestLRCLibLyrics(LyricsBackendTest):
[({"synced": True}, "synced"), ({"synced": False}, "plain")], [({"synced": True}, "synced"), ({"synced": False}, "plain")],
) )
def test_synced_config_option(self, fetch_lyrics, expected_lyrics): def test_synced_config_option(self, fetch_lyrics, expected_lyrics):
assert fetch_lyrics() == expected_lyrics lyrics, _ = fetch_lyrics()
assert lyrics == expected_lyrics
@pytest.mark.parametrize( @pytest.mark.parametrize(
"response_data, expected_lyrics", "response_data, expected_lyrics",
@ -490,4 +494,10 @@ class TestLRCLibLyrics(LyricsBackendTest):
) )
@pytest.mark.parametrize("plugin_config", [{"synced": True}]) @pytest.mark.parametrize("plugin_config", [{"synced": True}])
def test_fetch_lyrics(self, fetch_lyrics, expected_lyrics): def test_fetch_lyrics(self, fetch_lyrics, expected_lyrics):
assert fetch_lyrics() == expected_lyrics lyrics_info = fetch_lyrics()
if lyrics_info is None:
assert expected_lyrics is None
else:
lyrics, _ = fetch_lyrics()
assert lyrics == expected_lyrics