diff --git a/beetsplug/lyrics.py b/beetsplug/lyrics.py index 8bef1e5ee..58fb95565 100644 --- a/beetsplug/lyrics.py +++ b/beetsplug/lyrics.py @@ -266,7 +266,7 @@ class Backend(RequestHandler, metaclass=BackendClass): def fetch( self, artist: str, title: str, album: str, length: int - ) -> str | None: + ) -> tuple[str, str] | None: raise NotImplementedError @@ -277,6 +277,7 @@ class LRCLyrics: DURATION_DIFF_TOLERANCE = 0.05 target_duration: float + id: int duration: float instrumental: bool plain: str @@ -292,6 +293,7 @@ class LRCLyrics: ) -> LRCLyrics: return cls( target_duration, + candidate["id"], candidate["duration"] or 0.0, candidate["instrumental"], candidate["plainLyrics"], @@ -374,14 +376,15 @@ class LRCLib(Backend): def fetch( self, artist: str, title: str, album: str, length: int - ) -> str | None: + ) -> tuple[str, str] | None: """Fetch lyrics text for the given song data.""" evaluate_item = partial(LRCLyrics.make, target_duration=length) for group in self.fetch_candidates(artist, title, album, length): candidates = [evaluate_item(item) for item in group] 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 @@ -420,7 +423,7 @@ class MusiXmatch(DirectBackend): 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) html = self.fetch_text(url) @@ -442,7 +445,7 @@ class MusiXmatch(DirectBackend): # sometimes there are non-existent lyrics with some content if "Lyrics | Musixmatch" in lyrics: return None - return lyrics + return lyrics, url class Html: @@ -543,13 +546,13 @@ class SearchBackend(SoupMixin, Backend): if check_match(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.""" for result in self.get_results(artist, title): if (html := self.fetch_text(result.url)) and ( lyrics := self.scrape(html) ): - return lyrics + return lyrics, result.url return None @@ -604,11 +607,15 @@ class Tekstowo(SoupMixin, DirectBackend): def encode(cls, text: str) -> str: 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. # Thus suppress the error so that it does not end up in the logs. 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 @@ -1014,8 +1021,9 @@ class LyricsPlugin(RequestHandler, plugins.BeetsPlugin): self.info("Fetching lyrics for {} - {}", artist, title) for backend in self.backends: with backend.handle_request(): - if lyrics := backend.fetch(artist, title, *args): - return lyrics + if lyrics_info := backend.fetch(artist, title, *args): + lyrics, url = lyrics_info + return f"{lyrics}\n\nSource: {url}" return None diff --git a/test/plugins/test_lyrics.py b/test/plugins/test_lyrics.py index 3d65b3ee6..24a870a91 100644 --- a/test/plugins/test_lyrics.py +++ b/test/plugins/test_lyrics.py @@ -279,11 +279,12 @@ class TestLyricsSources(LyricsBackendTest): def test_backend_source(self, lyrics_plugin, lyrics_page: LyricsPage): """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 ) - assert lyrics + assert lyrics_info + lyrics, _ = lyrics_info.split("\n\nSource: ") assert lyrics == lyrics_page.lyrics @@ -400,6 +401,7 @@ LYRICS_DURATION = 950 def lyrics_match(**overrides): return { + "id": 1, "instrumental": False, "duration": LYRICS_DURATION, "syncedLyrics": "synced", @@ -428,7 +430,9 @@ class TestLRCLibLyrics(LyricsBackendTest): [({"synced": True}, "synced"), ({"synced": False}, "plain")], ) 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( "response_data, expected_lyrics", @@ -490,4 +494,10 @@ class TestLRCLibLyrics(LyricsBackendTest): ) @pytest.mark.parametrize("plugin_config", [{"synced": True}]) 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