mirror of
https://github.com/beetbox/beets.git
synced 2026-01-04 06:53:27 +01:00
Add LRCLIB as a provider for the lyrics plugin
This commit is contained in:
parent
b5ff061c72
commit
e14982cad7
4 changed files with 134 additions and 11 deletions
|
|
@ -290,10 +290,31 @@ class Backend:
|
|||
self._log.debug("failed to fetch: {0} ({1})", url, r.status_code)
|
||||
return None
|
||||
|
||||
def fetch(self, artist, title):
|
||||
def fetch(self, artist, title, album=None, length=None):
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class LRCLib(Backend):
|
||||
base_url = "https://lrclib.net/api/get"
|
||||
|
||||
def fetch(self, artist, title, album=None, length=None):
|
||||
params = {
|
||||
"artist_name": artist,
|
||||
"track_name": title,
|
||||
"album_name": album,
|
||||
"duration": length,
|
||||
}
|
||||
|
||||
try:
|
||||
response = requests.get(self.base_url, params=params)
|
||||
data = response.json()
|
||||
except (requests.RequestException, json.decoder.JSONDecodeError) as exc:
|
||||
self._log.debug("LRCLib API request failed: {0}", exc)
|
||||
return None
|
||||
|
||||
return data.get("syncedLyrics") or data.get("plainLyrics")
|
||||
|
||||
|
||||
class MusiXmatch(Backend):
|
||||
REPLACEMENTS = {
|
||||
r"\s+": "-",
|
||||
|
|
@ -313,7 +334,7 @@ class MusiXmatch(Backend):
|
|||
|
||||
return super()._encode(s)
|
||||
|
||||
def fetch(self, artist, title):
|
||||
def fetch(self, artist, title, album=None, length=None):
|
||||
url = self.build_url(artist, title)
|
||||
|
||||
html = self.fetch_url(url)
|
||||
|
|
@ -361,7 +382,7 @@ class Genius(Backend):
|
|||
"User-Agent": USER_AGENT,
|
||||
}
|
||||
|
||||
def fetch(self, artist, title):
|
||||
def fetch(self, artist, title, album=None, length=None):
|
||||
"""Fetch lyrics from genius.com
|
||||
|
||||
Because genius doesn't allow accessing lyrics via the api,
|
||||
|
|
@ -473,7 +494,7 @@ class Tekstowo(Backend):
|
|||
BASE_URL = "http://www.tekstowo.pl"
|
||||
URL_PATTERN = BASE_URL + "/wyszukaj.html?search-title=%s&search-artist=%s"
|
||||
|
||||
def fetch(self, artist, title):
|
||||
def fetch(self, artist, title, album=None, length=None):
|
||||
url = self.build_url(title, artist)
|
||||
search_results = self.fetch_url(url)
|
||||
if not search_results:
|
||||
|
|
@ -706,7 +727,7 @@ class Google(Backend):
|
|||
ratio = difflib.SequenceMatcher(None, song_title, title).ratio()
|
||||
return ratio >= typo_ratio
|
||||
|
||||
def fetch(self, artist, title):
|
||||
def fetch(self, artist, title, album=None, length=None):
|
||||
query = f"{artist} {title}"
|
||||
url = "https://www.googleapis.com/customsearch/v1?key=%s&cx=%s&q=%s" % (
|
||||
self.api_key,
|
||||
|
|
@ -750,12 +771,13 @@ class Google(Backend):
|
|||
|
||||
|
||||
class LyricsPlugin(plugins.BeetsPlugin):
|
||||
SOURCES = ["google", "musixmatch", "genius", "tekstowo"]
|
||||
SOURCES = ["google", "musixmatch", "genius", "tekstowo", "lrclib"]
|
||||
SOURCE_BACKENDS = {
|
||||
"google": Google,
|
||||
"musixmatch": MusiXmatch,
|
||||
"genius": Genius,
|
||||
"tekstowo": Tekstowo,
|
||||
"lrclib": LRCLib,
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
|
|
@ -1019,8 +1041,13 @@ class LyricsPlugin(plugins.BeetsPlugin):
|
|||
return
|
||||
|
||||
lyrics = None
|
||||
album = item.album
|
||||
length = round(item.length)
|
||||
for artist, titles in search_pairs(item):
|
||||
lyrics = [self.get_lyrics(artist, title) for title in titles]
|
||||
lyrics = [
|
||||
self.get_lyrics(artist, title, album=album, length=length)
|
||||
for title in titles
|
||||
]
|
||||
if any(lyrics):
|
||||
break
|
||||
|
||||
|
|
@ -1049,12 +1076,12 @@ class LyricsPlugin(plugins.BeetsPlugin):
|
|||
item.try_write()
|
||||
item.store()
|
||||
|
||||
def get_lyrics(self, artist, title):
|
||||
def get_lyrics(self, artist, title, album=None, length=None):
|
||||
"""Fetch lyrics, trying each source in turn. Return a string or
|
||||
None if no lyrics were found.
|
||||
"""
|
||||
for backend in self.backends:
|
||||
lyrics = backend.fetch(artist, title)
|
||||
lyrics = backend.fetch(artist, title, album=album, length=length)
|
||||
if lyrics:
|
||||
self._log.debug(
|
||||
"got lyrics from backend: {0}", backend.__class__.__name__
|
||||
|
|
|
|||
|
|
@ -141,6 +141,7 @@ New features:
|
|||
but no thumbnail is provided by CAA. We now fallback to the raw image.
|
||||
* :doc:`/plugins/advancedrewrite`: Add an advanced version of the `rewrite`
|
||||
plugin which allows to replace fields based on a given library query.
|
||||
* :doc:`/plugins/lyrics`: Add LRCLIB as a new lyrics provider.
|
||||
|
||||
Bug fixes:
|
||||
|
||||
|
|
|
|||
|
|
@ -2,11 +2,12 @@ Lyrics Plugin
|
|||
=============
|
||||
|
||||
The ``lyrics`` plugin fetches and stores song lyrics from databases on the Web.
|
||||
Namely, the current version of the plugin uses `Genius.com`_, `Tekstowo.pl`_,
|
||||
Namely, the current version of the plugin uses `Genius.com`_, `Tekstowo.pl`_, `LRCLIB`_
|
||||
and, optionally, the Google custom search API.
|
||||
|
||||
.. _Genius.com: https://genius.com/
|
||||
.. _Tekstowo.pl: https://www.tekstowo.pl/
|
||||
.. _LRCLIB: https://lrclib.net/
|
||||
|
||||
|
||||
Fetch Lyrics During Import
|
||||
|
|
@ -58,7 +59,7 @@ configuration file. The available options are:
|
|||
sources known to be scrapeable.
|
||||
- **sources**: List of sources to search for lyrics. An asterisk ``*`` expands
|
||||
to all available sources.
|
||||
Default: ``google genius tekstowo``, i.e., all the available sources. The
|
||||
Default: ``google genius tekstowo lrclib``, i.e., all the available sources. The
|
||||
``google`` source will be automatically deactivated if no ``google_API_key``
|
||||
is setup.
|
||||
The ``google``, ``genius``, and ``tekstowo`` sources will only be enabled if
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ from test import _common
|
|||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import confuse
|
||||
import requests
|
||||
|
||||
from beets import logging
|
||||
from beets.library import Item
|
||||
|
|
@ -34,6 +35,7 @@ raw_backend = lyrics.Backend({}, log)
|
|||
google = lyrics.Google(MagicMock(), log)
|
||||
genius = lyrics.Genius(MagicMock(), log)
|
||||
tekstowo = lyrics.Tekstowo(MagicMock(), log)
|
||||
lrclib = lyrics.LRCLib(MagicMock(), log)
|
||||
|
||||
|
||||
class LyricsPluginTest(unittest.TestCase):
|
||||
|
|
@ -677,6 +679,98 @@ class TekstowoIntegrationTest(TekstowoBaseTest, LyricsAssertions):
|
|||
self.assertEqual(lyrics, None)
|
||||
|
||||
|
||||
# test LRCLib backend
|
||||
|
||||
|
||||
class LRCLibLyricsTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.plugin = lyrics.LyricsPlugin()
|
||||
lrclib.config = self.plugin.config
|
||||
|
||||
@patch("beetsplug.lyrics.requests.get")
|
||||
def test_fetch_synced_lyrics(self, mock_get):
|
||||
mock_response = {
|
||||
"syncedLyrics": "[00:00.00] la la la",
|
||||
"plainLyrics": "la la la",
|
||||
}
|
||||
mock_get.return_value.json.return_value = mock_response
|
||||
mock_get.return_value.status_code = 200
|
||||
|
||||
lyrics = lrclib.fetch("la", "la", "la", 999)
|
||||
|
||||
self.assertEqual(lyrics, mock_response["syncedLyrics"])
|
||||
|
||||
@patch("beetsplug.lyrics.requests.get")
|
||||
def test_fetch_plain_lyrics(self, mock_get):
|
||||
mock_response = {
|
||||
"syncedLyrics": "",
|
||||
"plainLyrics": "la la la",
|
||||
}
|
||||
mock_get.return_value.json.return_value = mock_response
|
||||
mock_get.return_value.status_code = 200
|
||||
|
||||
lyrics = lrclib.fetch("la", "la", "la", 999)
|
||||
|
||||
self.assertEqual(lyrics, mock_response["plainLyrics"])
|
||||
|
||||
@patch("beetsplug.lyrics.requests.get")
|
||||
def test_fetch_not_found(self, mock_get):
|
||||
mock_response = {
|
||||
"statusCode": 404,
|
||||
"error": "Not Found",
|
||||
"message": "Failed to find specified track",
|
||||
}
|
||||
mock_get.return_value.json.return_value = mock_response
|
||||
mock_get.return_value.status_code = 404
|
||||
|
||||
lyrics = lrclib.fetch("la", "la", "la", 999)
|
||||
|
||||
self.assertIsNone(lyrics)
|
||||
|
||||
@patch("beetsplug.lyrics.requests.get")
|
||||
def test_fetch_exception(self, mock_get):
|
||||
mock_get.side_effect = requests.RequestException
|
||||
|
||||
lyrics = lrclib.fetch("la", "la", "la", 999)
|
||||
|
||||
self.assertIsNone(lyrics)
|
||||
|
||||
|
||||
class LRCLibIntegrationTest(LyricsAssertions):
|
||||
def setUp(self):
|
||||
self.plugin = lyrics.LyricsPlugin()
|
||||
lrclib.config = self.plugin.config
|
||||
|
||||
@unittest.skipUnless(
|
||||
os.environ.get("INTEGRATION_TEST", "0") == "1",
|
||||
"integration testing not enabled",
|
||||
)
|
||||
def test_track_with_lyrics(self):
|
||||
lyrics = lrclib.fetch("Boy in Space", "u n eye", "Live EP", 160)
|
||||
self.assertLyricsContentOk("u n eye", lyrics)
|
||||
|
||||
@unittest.skipUnless(
|
||||
os.environ.get("INTEGRATION_TEST", "0") == "1",
|
||||
"integration testing not enabled",
|
||||
)
|
||||
def test_instrumental_track(self):
|
||||
lyrics = lrclib.fetch(
|
||||
"Kelly Bailey",
|
||||
"Black Mesa Inbound",
|
||||
"Half Life 2 Soundtrack",
|
||||
134,
|
||||
)
|
||||
self.assertIsNone(lyrics)
|
||||
|
||||
@unittest.skipUnless(
|
||||
os.environ.get("INTEGRATION_TEST", "0") == "1",
|
||||
"integration testing not enabled",
|
||||
)
|
||||
def test_nonexistent_track(self):
|
||||
lyrics = lrclib.fetch("blah", "blah", "blah", 999)
|
||||
self.assertIsNone(lyrics)
|
||||
|
||||
|
||||
# test utilities
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue