Skip to content

Commit fc59070

Browse files
committed
Make album, duration required for LyricsPlugin.fetch
Since at least one Backend requires album` and `duration` arguments (`LRCLib`), the caller (`LyricsPlugin.fetch_item_lyrics`) must always provide them. Since they need to provided, we need to enforce this by defining them as positional arguments. Why is this important? I found that integrated `LRCLib` tests have been passing, but they called `LRCLib.fetch` with values for `artist` and `title` fields only, while the actual functionality *always* provides values for `album` and `duration` fields too. When I adjusted the test to provide values for the missing fields, I found that it failed. This makes sense: Lib `album` and `duration` filters are strict on LRCLib, so I was not surprised the lyrics could not be found. Thus I adjusted `LRCLib` backend implementation to only filter by each of these fields when their values are truthy.
1 parent 8e9e43a commit fc59070

File tree

2 files changed

+41
-35
lines changed

2 files changed

+41
-35
lines changed

beetsplug/lyrics.py

+39-33
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,19 @@
2626
import unicodedata
2727
import warnings
2828
from functools import partial
29-
from typing import ClassVar
29+
from typing import TYPE_CHECKING, ClassVar
3030
from urllib.parse import quote, urlencode
3131

3232
import requests
3333
from unidecode import unidecode
3434

35+
import beets
36+
from beets import plugins, ui
37+
38+
if TYPE_CHECKING:
39+
from beets.importer import ImportTask
40+
from beets.library import Item
41+
3542
try:
3643
import bs4
3744
from bs4 import SoupStrainer
@@ -47,10 +54,6 @@
4754
except ImportError:
4855
HAS_LANGDETECT = False
4956

50-
51-
import beets
52-
from beets import plugins, ui
53-
5457
DIV_RE = re.compile(r"<(/?)div>?", re.I)
5558
COMMENT_RE = re.compile(r"<!--.*-->", re.S)
5659
TAG_RE = re.compile(r"<[^>]*>")
@@ -256,20 +259,27 @@ def fetch_url(self, url):
256259
self._log.debug("failed to fetch: {0} ({1})", url, r.status_code)
257260
return None
258261

259-
def fetch(self, artist, title, album=None, length=None):
260-
raise NotImplementedError()
262+
def fetch(
263+
self, artist: str, title: str, album: str, length: int
264+
) -> str | None:
265+
raise NotImplementedError
261266

262267

263268
class LRCLib(Backend):
264269
base_url = "https://lrclib.net/api/get"
265270

266-
def fetch(self, artist, title, album=None, length=None):
267-
params = {
271+
def fetch(
272+
self, artist: str, title: str, album: str, length: int
273+
) -> str | None:
274+
params: dict[str, str | int] = {
268275
"artist_name": artist,
269276
"track_name": title,
270-
"album_name": album,
271-
"duration": length,
272277
}
278+
if album:
279+
params["album_name"] = album
280+
281+
if length:
282+
params["duration"] = length
273283

274284
try:
275285
response = requests.get(
@@ -322,7 +332,7 @@ def encode(cls, text: str) -> str:
322332

323333
return quote(unidecode(text))
324334

325-
def fetch(self, artist, title, album=None, length=None):
335+
def fetch(self, artist: str, title: str, *_) -> str | None:
326336
url = self.build_url(artist, title)
327337

328338
html = self.fetch_url(url)
@@ -370,7 +380,7 @@ def __init__(self, config, log):
370380
"User-Agent": USER_AGENT,
371381
}
372382

373-
def fetch(self, artist, title, album=None, length=None):
383+
def fetch(self, artist: str, title: str, *_) -> str | None:
374384
"""Fetch lyrics from genius.com
375385
376386
Because genius doesn't allow accessing lyrics via the api,
@@ -501,7 +511,7 @@ class Tekstowo(DirectBackend):
501511
def encode(cls, text: str) -> str:
502512
return cls.non_alpha_to_underscore(unidecode(text.lower()))
503513

504-
def fetch(self, artist, title, album=None, length=None):
514+
def fetch(self, artist: str, title: str, *_) -> str | None:
505515
if html := self.fetch_url(self.build_url(artist, title)):
506516
return self.extract_lyrics(html)
507517

@@ -675,7 +685,7 @@ def is_page_candidate(self, url_link, url_title, title, artist):
675685
ratio = difflib.SequenceMatcher(None, song_title, title).ratio()
676686
return ratio >= typo_ratio
677687

678-
def fetch(self, artist, title, album=None, length=None):
688+
def fetch(self, artist: str, title: str, *_) -> str | None:
679689
query = f"{artist} {title}"
680690
url = "https://www.googleapis.com/customsearch/v1?key=%s&cx=%s&q=%s" % (
681691
self.api_key,
@@ -885,10 +895,7 @@ def func(lib, opts, args):
885895
for item in items:
886896
if not opts.local_only and not self.config["local"]:
887897
self.fetch_item_lyrics(
888-
lib,
889-
item,
890-
write,
891-
opts.force_refetch or self.config["force"],
898+
item, write, opts.force_refetch or self.config["force"]
892899
)
893900
if item.lyrics:
894901
if opts.printlyr:
@@ -974,15 +981,13 @@ def writerest_indexes(self, directory):
974981
with open(conffile, "w") as output:
975982
output.write(REST_CONF_TEMPLATE)
976983

977-
def imported(self, session, task):
984+
def imported(self, _, task: ImportTask) -> None:
978985
"""Import hook for fetching lyrics automatically."""
979986
if self.config["auto"]:
980987
for item in task.imported_items():
981-
self.fetch_item_lyrics(
982-
session.lib, item, False, self.config["force"]
983-
)
988+
self.fetch_item_lyrics(item, False, self.config["force"])
984989

985-
def fetch_item_lyrics(self, lib, item, write, force):
990+
def fetch_item_lyrics(self, item: Item, write: bool, force: bool) -> None:
986991
"""Fetch and store lyrics for a single item. If ``write``, then the
987992
lyrics will also be written to the file itself.
988993
"""
@@ -991,18 +996,17 @@ def fetch_item_lyrics(self, lib, item, write, force):
991996
self._log.info("lyrics already present: {0}", item)
992997
return
993998

994-
lyrics = None
995-
album = item.album
996-
length = round(item.length)
999+
lyrics_matches = []
1000+
album, length = item.album, round(item.length)
9971001
for artist, titles in search_pairs(item):
998-
lyrics = [
999-
self.get_lyrics(artist, title, album=album, length=length)
1002+
lyrics_matches = [
1003+
self.get_lyrics(artist, title, album, length)
10001004
for title in titles
10011005
]
1002-
if any(lyrics):
1006+
if any(lyrics_matches):
10031007
break
10041008

1005-
lyrics = "\n\n---\n\n".join(filter(None, lyrics))
1009+
lyrics = "\n\n---\n\n".join(filter(None, lyrics_matches))
10061010

10071011
if lyrics:
10081012
self._log.info("fetched lyrics: {0}", item)
@@ -1027,18 +1031,20 @@ def fetch_item_lyrics(self, lib, item, write, force):
10271031
item.try_write()
10281032
item.store()
10291033

1030-
def get_lyrics(self, artist, title, album=None, length=None):
1034+
def get_lyrics(self, artist: str, title: str, *args) -> str | None:
10311035
"""Fetch lyrics, trying each source in turn. Return a string or
10321036
None if no lyrics were found.
10331037
"""
10341038
for backend in self.backends:
1035-
lyrics = backend.fetch(artist, title, album=album, length=length)
1039+
lyrics = backend.fetch(artist, title, *args)
10361040
if lyrics:
10371041
self._log.debug(
10381042
"got lyrics from backend: {0}", backend.__class__.__name__
10391043
)
10401044
return _scrape_strip_cruft(lyrics, True)
10411045

1046+
return None
1047+
10421048
def append_translation(self, text, to_lang):
10431049
from xml.etree import ElementTree
10441050

test/plugins/test_lyrics.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,7 @@ def test_backend_source(self, backend):
201201
"""
202202
title = "Lady Madonna"
203203

204-
lyrics = backend.fetch("The Beatles", title)
204+
lyrics = backend.fetch("The Beatles", title, "", 0)
205205

206206
assert lyrics
207207
assert PHRASE_BY_TITLE[title] in lyrics.lower()
@@ -366,7 +366,7 @@ def backend_name(self):
366366
def fetch_lyrics(self, backend, requests_mock, response_data):
367367
requests_mock.get(lyrics.LRCLib.base_url, json=response_data)
368368

369-
return partial(backend.fetch, "la", "la", "la")
369+
return partial(backend.fetch, "la", "la", "la", 0)
370370

371371
@pytest.mark.parametrize(
372372
"response_data",

0 commit comments

Comments
 (0)