mirror of
https://github.com/beetbox/beets.git
synced 2025-12-06 16:42:42 +01:00
Append source to the lyrics
This commit is contained in:
parent
bdc564a573
commit
734bcc28a8
2 changed files with 33 additions and 15 deletions
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue