Date: Wed, 2 Oct 2024 23:40:21 +0100
Subject: [PATCH 08/15] Refactor test_slug to pytest
---
test/plugins/test_lyrics.py | 57 +++++++++++++------------------------
1 file changed, 19 insertions(+), 38 deletions(-)
diff --git a/test/plugins/test_lyrics.py b/test/plugins/test_lyrics.py
index 9acd2382e8..8a7ff46d89 100644
--- a/test/plugins/test_lyrics.py
+++ b/test/plugins/test_lyrics.py
@@ -14,9 +14,7 @@
"""Tests for the 'lyrics' plugin."""
-import itertools
import os
-import unittest
from functools import partial
import pytest
@@ -42,11 +40,7 @@ def xfail_on_ci(msg: str) -> pytest.MarkDecorator:
)
-class LyricsPluginTest(unittest.TestCase):
- def setUp(self):
- """Set up configuration."""
- lyrics.LyricsPlugin()
-
+class TestLyricsUtils:
def test_search_artist(self):
item = Item(artist="Alice ft. Bob", title="song")
assert ("Alice ft. Bob", ["song"]) in lyrics.search_pairs(item)
@@ -171,6 +165,24 @@ def test_scrape_merge_paragraphs(self):
text = "one two
three"
assert lyrics._scrape_merge_paragraphs(text) == "one\ntwo\nthree"
+ @pytest.mark.parametrize(
+ "text, expected",
+ [
+ ("test", "test"),
+ ("Mørdag", "mordag"),
+ ("l'été c'est fait pour jouer", "l-ete-c-est-fait-pour-jouer"),
+ ("\xe7afe au lait (boisson)", "cafe-au-lait-boisson"),
+ ("Multiple spaces -- and symbols! -- merged", "multiple-spaces-and-symbols-merged"), # noqa: E501
+ ("\u200bno-width-space", "no-width-space"),
+ ("El\u002dp", "el-p"),
+ ("\u200bblackbear", "blackbear"),
+ ("\u200d", ""),
+ ("\u2010", ""),
+ ],
+ ) # fmt: skip
+ def test_slug(self, text, expected):
+ assert lyrics.slug(text) == expected
+
@pytest.fixture(scope="module")
def lyrics_root_dir(pytestconfig: pytest.Config):
@@ -414,34 +426,3 @@ def test_synced_config_option(self, fetch_lyrics, expected_lyrics):
)
def test_fetch_lyrics(self, fetch_lyrics, expected_lyrics):
assert fetch_lyrics() == expected_lyrics
-
-
-# test utilities
-
-
-class SlugTests(unittest.TestCase):
- def test_slug(self):
- # plain ascii passthrough
- text = "test"
- assert lyrics.slug(text) == "test"
-
- # german unicode and capitals
- text = "Mørdag"
- assert lyrics.slug(text) == "mordag"
-
- # more accents and quotes
- text = "l'été c'est fait pour jouer"
- assert lyrics.slug(text) == "l-ete-c-est-fait-pour-jouer"
-
- # accents, parens and spaces
- text = "\xe7afe au lait (boisson)"
- assert lyrics.slug(text) == "cafe-au-lait-boisson"
- text = "Multiple spaces -- and symbols! -- merged"
- assert lyrics.slug(text) == "multiple-spaces-and-symbols-merged"
- text = "\u200bno-width-space"
- assert lyrics.slug(text) == "no-width-space"
-
- # variations of dashes should get standardized
- dashes = ["\u200d", "\u2010"]
- for dash1, dash2 in itertools.combinations(dashes, 2):
- assert lyrics.slug(dash1) == lyrics.slug(dash2)
From f32b03412fb99830930b25ba4a07c3941caf4852 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0ar=C5=ABnas=20Nejus?=
Date: Thu, 3 Oct 2024 08:13:00 +0100
Subject: [PATCH 09/15] Refactor search_pairs tests to use pytest parametrize
- Consolidated multiple test cases into parameterized tests for better
readability and maintainability.
- Simplified assertions by comparing lists of actual and expected
artists/titles.
- Added `unexpected_empty_artist` marker to handle cases which
unexpectedly return an empty artist. This seems to be happen when
`artist_sort` field is empty.
---
test/plugins/test_lyrics.py | 123 +++++++++++++-----------------------
1 file changed, 44 insertions(+), 79 deletions(-)
diff --git a/test/plugins/test_lyrics.py b/test/plugins/test_lyrics.py
index 8a7ff46d89..95c6426528 100644
--- a/test/plugins/test_lyrics.py
+++ b/test/plugins/test_lyrics.py
@@ -41,92 +41,57 @@ def xfail_on_ci(msg: str) -> pytest.MarkDecorator:
class TestLyricsUtils:
- def test_search_artist(self):
- item = Item(artist="Alice ft. Bob", title="song")
- assert ("Alice ft. Bob", ["song"]) in lyrics.search_pairs(item)
- assert ("Alice", ["song"]) in lyrics.search_pairs(item)
-
- item = Item(artist="Alice feat Bob", title="song")
- assert ("Alice feat Bob", ["song"]) in lyrics.search_pairs(item)
- assert ("Alice", ["song"]) in lyrics.search_pairs(item)
-
- item = Item(artist="Alice feat. Bob", title="song")
- assert ("Alice feat. Bob", ["song"]) in lyrics.search_pairs(item)
- assert ("Alice", ["song"]) in lyrics.search_pairs(item)
-
- item = Item(artist="Alice feats Bob", title="song")
- assert ("Alice feats Bob", ["song"]) in lyrics.search_pairs(item)
- assert ("Alice", ["song"]) not in lyrics.search_pairs(item)
-
- item = Item(artist="Alice featuring Bob", title="song")
- assert ("Alice featuring Bob", ["song"]) in lyrics.search_pairs(item)
- assert ("Alice", ["song"]) in lyrics.search_pairs(item)
-
- item = Item(artist="Alice & Bob", title="song")
- assert ("Alice & Bob", ["song"]) in lyrics.search_pairs(item)
- assert ("Alice", ["song"]) in lyrics.search_pairs(item)
-
- item = Item(artist="Alice and Bob", title="song")
- assert ("Alice and Bob", ["song"]) in lyrics.search_pairs(item)
- assert ("Alice", ["song"]) in lyrics.search_pairs(item)
-
- item = Item(artist="Alice and Bob", title="song")
- assert ("Alice and Bob", ["song"]) == list(lyrics.search_pairs(item))[0]
-
- def test_search_artist_sort(self):
- item = Item(artist="CHVRCHΞS", title="song", artist_sort="CHVRCHES")
- assert ("CHVRCHΞS", ["song"]) in lyrics.search_pairs(item)
- assert ("CHVRCHES", ["song"]) in lyrics.search_pairs(item)
+ unexpected_empty_artist = pytest.mark.xfail(
+ reason="Empty artist '' should not be present"
+ )
- # Make sure that the original artist name is still the first entry
- assert ("CHVRCHΞS", ["song"]) == list(lyrics.search_pairs(item))[0]
+ @pytest.mark.parametrize(
+ "artist, artist_sort, expected_extra_artists",
+ [
+ _p("Alice ft. Bob", "", ["Alice"], marks=unexpected_empty_artist),
+ _p("Alice feat Bob", "", ["Alice"], marks=unexpected_empty_artist),
+ _p("Alice feat. Bob", "", ["Alice"], marks=unexpected_empty_artist),
+ _p("Alice feats Bob", "", [], marks=unexpected_empty_artist),
+ _p("Alice featuring Bob", "", ["Alice"], marks=unexpected_empty_artist),
+ _p("Alice & Bob", "", ["Alice"], marks=unexpected_empty_artist),
+ _p("Alice and Bob", "", ["Alice"], marks=unexpected_empty_artist),
+ _p("Alice", "", [], marks=unexpected_empty_artist),
+ ("CHVRCHΞS", "CHVRCHES", ["CHVRCHES"]),
+ ("横山克", "Masaru Yokoyama", ["Masaru Yokoyama"]),
+ ],
+ )
+ def test_search_pairs_artists(
+ self, artist, artist_sort, expected_extra_artists
+ ):
+ item = Item(artist=artist, artist_sort=artist_sort, title="song")
- item = Item(
- artist="横山克", title="song", artist_sort="Masaru Yokoyama"
- )
- assert ("横山克", ["song"]) in lyrics.search_pairs(item)
- assert ("Masaru Yokoyama", ["song"]) in lyrics.search_pairs(item)
+ actual_artists = [a for a, _ in lyrics.search_pairs(item)]
# Make sure that the original artist name is still the first entry
- assert ("横山克", ["song"]) == list(lyrics.search_pairs(item))[0]
-
- def test_search_pairs_multi_titles(self):
- item = Item(title="1 / 2", artist="A")
- assert ("A", ["1 / 2"]) in lyrics.search_pairs(item)
- assert ("A", ["1", "2"]) in lyrics.search_pairs(item)
-
- item = Item(title="1/2", artist="A")
- assert ("A", ["1/2"]) in lyrics.search_pairs(item)
- assert ("A", ["1", "2"]) in lyrics.search_pairs(item)
-
- def test_search_pairs_titles(self):
- item = Item(title="Song (live)", artist="A")
- assert ("A", ["Song"]) in lyrics.search_pairs(item)
- assert ("A", ["Song (live)"]) in lyrics.search_pairs(item)
-
- item = Item(title="Song (live) (new)", artist="A")
- assert ("A", ["Song"]) in lyrics.search_pairs(item)
- assert ("A", ["Song (live) (new)"]) in lyrics.search_pairs(item)
+ assert actual_artists == [artist, *expected_extra_artists]
- item = Item(title="Song (live (new))", artist="A")
- assert ("A", ["Song"]) in lyrics.search_pairs(item)
- assert ("A", ["Song (live (new))"]) in lyrics.search_pairs(item)
-
- item = Item(title="Song ft. B", artist="A")
- assert ("A", ["Song"]) in lyrics.search_pairs(item)
- assert ("A", ["Song ft. B"]) in lyrics.search_pairs(item)
-
- item = Item(title="Song featuring B", artist="A")
- assert ("A", ["Song"]) in lyrics.search_pairs(item)
- assert ("A", ["Song featuring B"]) in lyrics.search_pairs(item)
+ @pytest.mark.parametrize(
+ "title, expected_extra_titles",
+ [
+ ("1/2", ["1", "2"]),
+ ("1 / 2", ["1", "2"]),
+ ("Song (live)", ["Song"]),
+ ("Song (live) (new)", ["Song"]),
+ ("Song (live (new))", ["Song"]),
+ ("Song ft. B", ["Song"]),
+ ("Song featuring B", ["Song"]),
+ ("Song and B", []),
+ ("Song: B", ["Song"]),
+ ],
+ )
+ def test_search_pairs_titles(self, title, expected_extra_titles):
+ item = Item(title=title, artist="A")
- item = Item(title="Song and B", artist="A")
- assert ("A", ["Song and B"]) in lyrics.search_pairs(item)
- assert ("A", ["Song"]) not in lyrics.search_pairs(item)
+ actual_titles = {
+ t: None for _, tit in lyrics.search_pairs(item) for t in tit
+ }
- item = Item(title="Song: B", artist="A")
- assert ("A", ["Song"]) in lyrics.search_pairs(item)
- assert ("A", ["Song: B"]) in lyrics.search_pairs(item)
+ assert list(actual_titles) == [title, *expected_extra_titles]
def test_remove_credits(self):
assert (
From cbcf98030474d73edc7386be8a552963af9e1529 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0ar=C5=ABnas=20Nejus?=
Date: Thu, 3 Oct 2024 10:02:51 +0100
Subject: [PATCH 10/15] Refactor utils test cases to use
pytest.mark.parametrize
---
test/plugins/test_lyrics.py | 58 ++++++++++++++++++++-----------------
1 file changed, 31 insertions(+), 27 deletions(-)
diff --git a/test/plugins/test_lyrics.py b/test/plugins/test_lyrics.py
index 95c6426528..0007a5b511 100644
--- a/test/plugins/test_lyrics.py
+++ b/test/plugins/test_lyrics.py
@@ -93,38 +93,42 @@ def test_search_pairs_titles(self, title, expected_extra_titles):
assert list(actual_titles) == [title, *expected_extra_titles]
- def test_remove_credits(self):
- assert (
- lyrics.remove_credits(
- """It's close to midnight
- Lyrics brought by example.com"""
- )
- == "It's close to midnight"
- )
- assert lyrics.remove_credits("""Lyrics brought by example.com""") == ""
-
- # don't remove 2nd verse for the only reason it contains 'lyrics' word
- text = """Look at all the shit that i done bought her
- See lyrics ain't nothin
- if the beat aint crackin"""
- assert lyrics.remove_credits(text) == text
+ @pytest.mark.parametrize(
+ "initial_lyrics, expected",
+ [
+ ("Verse\nLyrics credit in the last line", "Verse"),
+ ("Lyrics credit in the first line\nVerse", "Verse"),
+ (
+ """Verse
+ Lyrics mentioned somewhere in the middle
+ Verse""",
+ """Verse
+ Lyrics mentioned somewhere in the middle
+ Verse""",
+ ),
+ ],
+ )
+ def test_remove_credits(self, initial_lyrics, expected):
+ assert lyrics.remove_credits(initial_lyrics) == expected
- def test_scrape_strip_cruft(self):
- text = """
+ @pytest.mark.parametrize(
+ "initial_text, expected",
+ [
+ (
+ """
one
two !
- """
- assert lyrics._scrape_strip_cruft(text, True) == "one\ntwo !\n\nfour"
-
- def test_scrape_strip_scripts(self):
- text = """foobaz"""
- assert lyrics._scrape_strip_cruft(text, True) == "foobaz"
-
- def test_scrape_strip_tag_in_comment(self):
- text = """fooqux"""
- assert lyrics._scrape_strip_cruft(text, True) == "fooqux"
+ """,
+ "one\ntwo !\n\nfour",
+ ),
+ ("foobaz", "foobaz"),
+ ("fooqux", "fooqux"),
+ ],
+ )
+ def test_scrape_strip_cruft(self, initial_text, expected):
+ assert lyrics._scrape_strip_cruft(initial_text, True) == expected
def test_scrape_merge_paragraphs(self):
text = "one
two
three"
From d0c8ff122dd55b5935cb782a20646974f5de2932 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0ar=C5=ABnas=20Nejus?=
Date: Thu, 3 Oct 2024 14:15:56 +0100
Subject: [PATCH 11/15] Do not attempt to fetch lyrics with empty data
Modified `search_pairs` function in `lyrics.py` to:
* Firstly strip each of `artist`, `artist_sort` and `title` fields
* Only generate alternatives if both `artist` and `title` are not empty
* Ensure that `artist_sort` is not empty and not equal to artist (ignoring
case) before appending it to the artists
Extended tests to cover the changes.
---
beetsplug/lyrics.py | 10 ++++++++--
docs/changelog.rst | 3 +++
test/plugins/test_lyrics.py | 35 +++++++++++++++++++++++++----------
3 files changed, 36 insertions(+), 12 deletions(-)
diff --git a/beetsplug/lyrics.py b/beetsplug/lyrics.py
index a5edc2de13..82532d1a91 100644
--- a/beetsplug/lyrics.py
+++ b/beetsplug/lyrics.py
@@ -152,7 +152,13 @@ def generate_alternatives(string, patterns):
alternatives.append(match.group(1))
return alternatives
- title, artist, artist_sort = item.title, item.artist, item.artist_sort
+ title, artist, artist_sort = (
+ item.title.strip(),
+ item.artist.strip(),
+ item.artist_sort.strip(),
+ )
+ if not title or not artist:
+ return ()
patterns = [
# Remove any featuring artists from the artists name
@@ -161,7 +167,7 @@ def generate_alternatives(string, patterns):
artists = generate_alternatives(artist, patterns)
# Use the artist_sort as fallback only if it differs from artist to avoid
# repeated remote requests with the same search terms
- if artist != artist_sort:
+ if artist_sort and artist.lower() != artist_sort.lower():
artists.append(artist_sort)
patterns = [
diff --git a/docs/changelog.rst b/docs/changelog.rst
index 2602b8fe28..5e4e3c5bcc 100644
--- a/docs/changelog.rst
+++ b/docs/changelog.rst
@@ -18,6 +18,9 @@ Bug fixes:
configuration for each test case. This fixes the issue where some tests
failed because they read developer's local lyrics configuration.
:bug:`5133`
+* :doc:`plugins/lyrics`: Do not attempt to search for lyrics if either the
+ artist or title is missing and ignore ``artist_sort`` value if it is empty.
+ :bug:`2635`
For packagers:
diff --git a/test/plugins/test_lyrics.py b/test/plugins/test_lyrics.py
index 0007a5b511..634dea0f4a 100644
--- a/test/plugins/test_lyrics.py
+++ b/test/plugins/test_lyrics.py
@@ -41,21 +41,36 @@ def xfail_on_ci(msg: str) -> pytest.MarkDecorator:
class TestLyricsUtils:
- unexpected_empty_artist = pytest.mark.xfail(
- reason="Empty artist '' should not be present"
+ @pytest.mark.parametrize(
+ "artist, title",
+ [
+ ("Artist", ""),
+ ("", "Title"),
+ (" ", ""),
+ ("", " "),
+ ("", ""),
+ ],
)
+ def test_search_empty(self, artist, title):
+ actual_pairs = lyrics.search_pairs(Item(artist=artist, title=title))
+
+ assert not list(actual_pairs)
@pytest.mark.parametrize(
"artist, artist_sort, expected_extra_artists",
[
- _p("Alice ft. Bob", "", ["Alice"], marks=unexpected_empty_artist),
- _p("Alice feat Bob", "", ["Alice"], marks=unexpected_empty_artist),
- _p("Alice feat. Bob", "", ["Alice"], marks=unexpected_empty_artist),
- _p("Alice feats Bob", "", [], marks=unexpected_empty_artist),
- _p("Alice featuring Bob", "", ["Alice"], marks=unexpected_empty_artist),
- _p("Alice & Bob", "", ["Alice"], marks=unexpected_empty_artist),
- _p("Alice and Bob", "", ["Alice"], marks=unexpected_empty_artist),
- _p("Alice", "", [], marks=unexpected_empty_artist),
+ ("Alice ft. Bob", "", ["Alice"]),
+ ("Alice feat Bob", "", ["Alice"]),
+ ("Alice feat. Bob", "", ["Alice"]),
+ ("Alice feats Bob", "", []),
+ ("Alice featuring Bob", "", ["Alice"]),
+ ("Alice & Bob", "", ["Alice"]),
+ ("Alice and Bob", "", ["Alice"]),
+ ("Alice", "", []),
+ ("Alice", "Alice", []),
+ ("Alice", "alice", []),
+ ("Alice", "alice ", []),
+ ("Alice", "Alice A", ["Alice A"]),
("CHVRCHΞS", "CHVRCHES", ["CHVRCHES"]),
("横山克", "Masaru Yokoyama", ["Masaru Yokoyama"]),
],
From dd6ee957d206b8691f5fb24baaacfdca857d13f7 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0ar=C5=ABnas=20Nejus?=
Date: Sat, 12 Oct 2024 23:07:56 +0100
Subject: [PATCH 12/15] Remove pytest.param alias _p
---
test/plugins/test_lyrics.py | 8 +++-----
1 file changed, 3 insertions(+), 5 deletions(-)
diff --git a/test/plugins/test_lyrics.py b/test/plugins/test_lyrics.py
index 634dea0f4a..f221d3d072 100644
--- a/test/plugins/test_lyrics.py
+++ b/test/plugins/test_lyrics.py
@@ -29,8 +29,6 @@
"Beets song": "via plugins, beets becomes a panacea",
}
-_p = pytest.param
-
def xfail_on_ci(msg: str) -> pytest.MarkDecorator:
return pytest.mark.xfail(
@@ -245,7 +243,7 @@ def file_name(self):
"https://www.lacoccinelle.net/259956-the-beatles-lady-madonna.html",
)
),
- _p(
+ pytest.param(
"Lady Madonna",
"https://www.azlyrics.com/lyrics/beatles/ladymadonna.html",
marks=xfail_on_ci("AZLyrics is blocked by Cloudflare"),
@@ -392,12 +390,12 @@ def test_synced_config_option(self, fetch_lyrics, expected_lyrics):
@pytest.mark.parametrize(
"response_data, expected_lyrics",
[
- _p(
+ pytest.param(
{"syncedLyrics": "", "plainLyrics": "la la la"},
"la la la",
id="pick plain lyrics",
),
- _p(
+ pytest.param(
{
"statusCode": 404,
"error": "Not Found",
From 057221380b8169104dc4fbbff9fa5819e624c21e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0ar=C5=ABnas=20Nejus?=
Date: Fri, 20 Sep 2024 13:56:37 +0100
Subject: [PATCH 13/15] 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.
---
beetsplug/lyrics.py | 72 ++++++++++++++++++++-----------------
test/plugins/test_lyrics.py | 4 +--
2 files changed, 41 insertions(+), 35 deletions(-)
diff --git a/beetsplug/lyrics.py b/beetsplug/lyrics.py
index 82532d1a91..2aba286124 100644
--- a/beetsplug/lyrics.py
+++ b/beetsplug/lyrics.py
@@ -26,12 +26,19 @@
import unicodedata
import warnings
from functools import partial
-from typing import ClassVar
+from typing import TYPE_CHECKING, ClassVar
from urllib.parse import quote, urlencode
import requests
from unidecode import unidecode
+import beets
+from beets import plugins, ui
+
+if TYPE_CHECKING:
+ from beets.importer import ImportTask
+ from beets.library import Item
+
try:
import bs4
from bs4 import SoupStrainer
@@ -47,10 +54,6 @@
except ImportError:
HAS_LANGDETECT = False
-
-import beets
-from beets import plugins, ui
-
DIV_RE = re.compile(r"<(/?)div>?", re.I)
COMMENT_RE = re.compile(r"", re.S)
TAG_RE = re.compile(r"<[^>]*>")
@@ -256,20 +259,27 @@ def fetch_url(self, url):
self._log.debug("failed to fetch: {0} ({1})", url, r.status_code)
return None
- def fetch(self, artist, title, album=None, length=None):
- raise NotImplementedError()
+ def fetch(
+ self, artist: str, title: str, album: str, length: int
+ ) -> str | None:
+ raise NotImplementedError
class LRCLib(Backend):
base_url = "https://lrclib.net/api/get"
- def fetch(self, artist, title, album=None, length=None):
- params = {
+ def fetch(
+ self, artist: str, title: str, album: str, length: int
+ ) -> str | None:
+ params: dict[str, str | int] = {
"artist_name": artist,
"track_name": title,
- "album_name": album,
- "duration": length,
}
+ if album:
+ params["album_name"] = album
+
+ if length:
+ params["duration"] = length
try:
response = requests.get(
@@ -322,7 +332,7 @@ def encode(cls, text: str) -> str:
return quote(unidecode(text))
- def fetch(self, artist, title, album=None, length=None):
+ def fetch(self, artist: str, title: str, *_) -> str | None:
url = self.build_url(artist, title)
html = self.fetch_url(url)
@@ -370,7 +380,7 @@ def __init__(self, config, log):
"User-Agent": USER_AGENT,
}
- def fetch(self, artist, title, album=None, length=None):
+ def fetch(self, artist: str, title: str, *_) -> str | None:
"""Fetch lyrics from genius.com
Because genius doesn't allow accessing lyrics via the api,
@@ -501,7 +511,7 @@ class Tekstowo(DirectBackend):
def encode(cls, text: str) -> str:
return cls.non_alpha_to_underscore(unidecode(text.lower()))
- def fetch(self, artist, title, album=None, length=None):
+ def fetch(self, artist: str, title: str, *_) -> str | None:
if html := self.fetch_url(self.build_url(artist, title)):
return self.extract_lyrics(html)
@@ -675,7 +685,7 @@ def is_page_candidate(self, url_link, url_title, title, artist):
ratio = difflib.SequenceMatcher(None, song_title, title).ratio()
return ratio >= typo_ratio
- def fetch(self, artist, title, album=None, length=None):
+ def fetch(self, artist: str, title: str, *_) -> str | None:
query = f"{artist} {title}"
url = "https://www.googleapis.com/customsearch/v1?key=%s&cx=%s&q=%s" % (
self.api_key,
@@ -885,10 +895,7 @@ def func(lib, opts, args):
for item in items:
if not opts.local_only and not self.config["local"]:
self.fetch_item_lyrics(
- lib,
- item,
- write,
- opts.force_refetch or self.config["force"],
+ item, write, opts.force_refetch or self.config["force"]
)
if item.lyrics:
if opts.printlyr:
@@ -974,15 +981,13 @@ def writerest_indexes(self, directory):
with open(conffile, "w") as output:
output.write(REST_CONF_TEMPLATE)
- def imported(self, session, task):
+ def imported(self, _, task: ImportTask) -> None:
"""Import hook for fetching lyrics automatically."""
if self.config["auto"]:
for item in task.imported_items():
- self.fetch_item_lyrics(
- session.lib, item, False, self.config["force"]
- )
+ self.fetch_item_lyrics(item, False, self.config["force"])
- def fetch_item_lyrics(self, lib, item, write, force):
+ def fetch_item_lyrics(self, item: Item, write: bool, force: bool) -> None:
"""Fetch and store lyrics for a single item. If ``write``, then the
lyrics will also be written to the file itself.
"""
@@ -991,18 +996,17 @@ def fetch_item_lyrics(self, lib, item, write, force):
self._log.info("lyrics already present: {0}", item)
return
- lyrics = None
- album = item.album
- length = round(item.length)
+ lyrics_matches = []
+ album, length = item.album, round(item.length)
for artist, titles in search_pairs(item):
- lyrics = [
- self.get_lyrics(artist, title, album=album, length=length)
+ lyrics_matches = [
+ self.get_lyrics(artist, title, album, length)
for title in titles
]
- if any(lyrics):
+ if any(lyrics_matches):
break
- lyrics = "\n\n---\n\n".join(filter(None, lyrics))
+ lyrics = "\n\n---\n\n".join(filter(None, lyrics_matches))
if lyrics:
self._log.info("fetched lyrics: {0}", item)
@@ -1027,18 +1031,20 @@ def fetch_item_lyrics(self, lib, item, write, force):
item.try_write()
item.store()
- def get_lyrics(self, artist, title, album=None, length=None):
+ def get_lyrics(self, artist: str, title: str, *args) -> str | 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, album=album, length=length)
+ lyrics = backend.fetch(artist, title, *args)
if lyrics:
self._log.debug(
"got lyrics from backend: {0}", backend.__class__.__name__
)
return _scrape_strip_cruft(lyrics, True)
+ return None
+
def append_translation(self, text, to_lang):
from xml.etree import ElementTree
diff --git a/test/plugins/test_lyrics.py b/test/plugins/test_lyrics.py
index f221d3d072..876b8346c2 100644
--- a/test/plugins/test_lyrics.py
+++ b/test/plugins/test_lyrics.py
@@ -201,7 +201,7 @@ def test_backend_source(self, backend):
"""
title = "Lady Madonna"
- lyrics = backend.fetch("The Beatles", title)
+ lyrics = backend.fetch("The Beatles", title, "", 0)
assert lyrics
assert PHRASE_BY_TITLE[title] in lyrics.lower()
@@ -366,7 +366,7 @@ def backend_name(self):
def fetch_lyrics(self, backend, requests_mock, response_data):
requests_mock.get(lyrics.LRCLib.base_url, json=response_data)
- return partial(backend.fetch, "la", "la", "la")
+ return partial(backend.fetch, "la", "la", "la", 0)
@pytest.mark.parametrize(
"response_data",
From 94e16e673bb094f204bb5597a36332cc1ad16361 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0ar=C5=ABnas=20Nejus?=
Date: Fri, 18 Oct 2024 01:03:01 +0100
Subject: [PATCH 14/15] Google: test the entire fetch method
---
beetsplug/lyrics.py | 22 ++++-----
test/plugins/test_lyrics.py | 93 +++++++++++++++++++++++++++++--------
2 files changed, 83 insertions(+), 32 deletions(-)
diff --git a/beetsplug/lyrics.py b/beetsplug/lyrics.py
index 2aba286124..3c99bfb8a4 100644
--- a/beetsplug/lyrics.py
+++ b/beetsplug/lyrics.py
@@ -231,7 +231,7 @@ def __init__(self, config, log):
self._log = log
self.config = config
- def fetch_url(self, url):
+ def fetch_url(self, url, **kwargs):
"""Retrieve the content at a given URL, or return None if the source
is unreachable.
"""
@@ -249,6 +249,7 @@ def fetch_url(self, url):
"User-Agent": USER_AGENT,
},
timeout=10,
+ **kwargs,
)
except requests.RequestException as exc:
self._log.debug("lyrics request failed: {0}", exc)
@@ -604,11 +605,7 @@ class Google(Backend):
"""Fetch lyrics from Google search results."""
REQUIRES_BS = True
-
- def __init__(self, config, log):
- super().__init__(config, log)
- self.api_key = config["google_API_key"].as_str()
- self.engine_id = config["google_engine_ID"].as_str()
+ SEARCH_URL = "https://www.googleapis.com/customsearch/v1"
def is_lyrics(self, text, artist=None):
"""Determine whether the text seems to be valid lyrics."""
@@ -686,14 +683,13 @@ def is_page_candidate(self, url_link, url_title, title, artist):
return ratio >= typo_ratio
def fetch(self, artist: str, title: str, *_) -> str | None:
- query = f"{artist} {title}"
- url = "https://www.googleapis.com/customsearch/v1?key=%s&cx=%s&q=%s" % (
- self.api_key,
- self.engine_id,
- quote(query.encode("utf-8")),
- )
+ params = {
+ "key": self.config["google_API_key"].as_str(),
+ "cx": self.config["google_engine_ID"].as_str(),
+ "q": f"{artist} {title}",
+ }
- data = self.fetch_url(url)
+ data = self.fetch_url(self.SEARCH_URL, params=params)
if not data:
self._log.debug("google backend returned no data")
return None
diff --git a/test/plugins/test_lyrics.py b/test/plugins/test_lyrics.py
index 876b8346c2..a222500b92 100644
--- a/test/plugins/test_lyrics.py
+++ b/test/plugins/test_lyrics.py
@@ -16,6 +16,7 @@
import os
from functools import partial
+from urllib.parse import urlparse
import pytest
@@ -224,45 +225,99 @@ def plugin_config(self):
def file_name(self):
return "examplecom/beetssong"
+ @pytest.fixture
+ def response_data(self, url_title, url):
+ return {
+ "items": [
+ {
+ "title": url_title,
+ "link": url,
+ "displayLink": urlparse(url).netloc,
+ }
+ ]
+ }
+
+ @pytest.fixture
+ def fetch_lyrics(
+ self, backend, requests_mock, response_data, artist, title
+ ):
+ requests_mock.get(backend.SEARCH_URL, json=response_data)
+ requests_mock.real_http = True
+
+ return partial(backend.fetch, artist, title)
+
@pytest.mark.on_lyrics_update
@pytest.mark.parametrize(
- "title, url",
+ "artist, title, url_title, url",
[
*(
- ("Lady Madonna", url)
- for url in (
- "http://www.chartlyrics.com/_LsLsZ7P4EK-F-LD4dJgDQ/Lady+Madonna.aspx", # noqa: E501
- "http://www.absolutelyrics.com/lyrics/view/the_beatles/lady_madonna", # noqa: E501
- "https://www.letras.mus.br/the-beatles/275/",
- "https://www.lyricsmania.com/lady_madonna_lyrics_the_beatles.html",
- "https://www.lyricsmode.com/lyrics/b/beatles/lady_madonna.html",
- "https://www.paroles.net/the-beatles/paroles-lady-madonna",
- "https://www.songlyrics.com/the-beatles/lady-madonna-lyrics/",
- "https://sweetslyrics.com/the-beatles/lady-madonna-lyrics",
- "https://www.musica.com/letras.asp?letra=59862",
- "https://www.lacoccinelle.net/259956-the-beatles-lady-madonna.html",
+ ("The Beatles", "Lady Madonna", url_title, url)
+ for url_title, url in (
+ (
+ "The Beatles Lady Madonna lyrics",
+ "http://www.chartlyrics.com/_LsLsZ7P4EK-F-LD4dJgDQ/Lady+Madonna.aspx",
+ ),
+ (
+ "Lady Madonna Lyrics :: The Beatles - Absolute Lyrics",
+ "http://www.absolutelyrics.com/lyrics/view/the_beatles/lady_madonna",
+ ),
+ (
+ "Lady Madonna - The Beatles - LETRAS.MUS.BR",
+ "https://www.letras.mus.br/the-beatles/275/",
+ ),
+ (
+ "The Beatles - Lady Madonna Lyrics",
+ "https://www.lyricsmania.com/lady_madonna_lyrics_the_beatles.html",
+ ),
+ (
+ "Lady Madonna lyrics by The Beatles - original song full text. Official Lady Madonna lyrics, 2024 version | LyricsMode.com", # noqa: E501
+ "https://www.lyricsmode.com/lyrics/b/beatles/lady_madonna.html",
+ ),
+ (
+ "Paroles Lady Madonna par The Beatles - Lyrics - Paroles.net",
+ "https://www.paroles.net/the-beatles/paroles-lady-madonna",
+ ),
+ (
+ "THE BEATLES - LADY MADONNA LYRICS",
+ "https://www.songlyrics.com/the-beatles/lady-madonna-lyrics/",
+ ),
+ (
+ "The Beatles - Lady Madonna",
+ "https://sweetslyrics.com/the-beatles/lady-madonna-lyrics",
+ ),
+ (
+ "Lady Madonna - Letra - The Beatles - Musica.com",
+ "https://www.musica.com/letras.asp?letra=59862",
+ ),
+ (
+ "Paroles et traduction The Beatles : Lady Madonna - paroles de chanson", # noqa: E501
+ "https://www.lacoccinelle.net/259956-the-beatles-lady-madonna.html",
+ ),
)
),
pytest.param(
+ "The Beatles",
"Lady Madonna",
+ "The Beatles - Lady Madonna Lyrics | AZLyrics.com",
"https://www.azlyrics.com/lyrics/beatles/ladymadonna.html",
marks=xfail_on_ci("AZLyrics is blocked by Cloudflare"),
),
(
+ "Amy Winehouse",
"Jazz'n'blues",
- "https://www.lyricsontop.com/amy-winehouse-songs/jazz-n-blues-lyrics.html", # noqa: E501
+ "Amy Winehouse - Jazz N' Blues lyrics complete",
+ "https://www.lyricsontop.com/amy-winehouse-songs/jazz-n-blues-lyrics.html",
),
],
)
- def test_backend_source(self, backend, title, url):
+ def test_backend_source(self, fetch_lyrics, title):
"""Test if lyrics present on websites registered in beets google custom
search engine are correctly scraped.
"""
- response = backend.fetch_url(url)
- result = lyrics.scrape_lyrics_from_html(response).lower()
+ lyrics = fetch_lyrics()
- assert backend.is_lyrics(result)
- assert PHRASE_BY_TITLE[title] in result
+ assert lyrics
+ assert PHRASE_BY_TITLE[title].lower() in lyrics.lower()
def test_mocked_source_ok(self, backend, lyrics_html):
"""Test that lyrics of the mocked page are correctly scraped"""
From 4970dda03acb7490049637813e025125cd254d31 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0ar=C5=ABnas=20Nejus?=
Date: Sat, 19 Oct 2024 01:48:01 +0100
Subject: [PATCH 15/15] Test lyrics texts explicitly
Add explicit checks for lyrics texts fetched from the tested sources.
- Introduced `LyricsPage` class to represent lyrics pages for integrated
tests.
- Configured expected lyrics for each of the URLs that are being
fetched.
- Consolidated integrated tests in a new `TestLyricsSources` class.
- Mocked Google Search API to return the lyrics page under test.
---
test/plugins/lyrics_pages.py | 564 +++++++++++++++++++++++++++++++++++
test/plugins/test_lyrics.py | 168 +++--------
2 files changed, 612 insertions(+), 120 deletions(-)
create mode 100644 test/plugins/lyrics_pages.py
diff --git a/test/plugins/lyrics_pages.py b/test/plugins/lyrics_pages.py
new file mode 100644
index 0000000000..84c2457ba4
--- /dev/null
+++ b/test/plugins/lyrics_pages.py
@@ -0,0 +1,564 @@
+from __future__ import annotations
+
+import os
+import textwrap
+from typing import NamedTuple
+from urllib.parse import urlparse
+
+import pytest
+
+
+def xfail_on_ci(msg: str) -> pytest.MarkDecorator:
+ return pytest.mark.xfail(
+ bool(os.environ.get("GITHUB_ACTIONS")),
+ reason=msg,
+ raises=AssertionError,
+ )
+
+
+class LyricsPage(NamedTuple):
+ """Lyrics page representation for integrated tests."""
+
+ url: str
+ lyrics: str
+ artist: str = "The Beatles"
+ track_title: str = "Lady Madonna"
+ url_title: str | None = None # only relevant to the Google backend
+ marks: list[str] = [] # markers for pytest.param
+
+ def __str__(self) -> str:
+ """Return name of this test case."""
+ return f"{self.backend}-{self.source}"
+
+ @classmethod
+ def make(cls, url, lyrics, *args, **kwargs):
+ return cls(url, textwrap.dedent(lyrics).strip(), *args, **kwargs)
+
+ @property
+ def root_url(self) -> str:
+ return urlparse(self.url).netloc
+
+ @property
+ def source(self) -> str:
+ return self.root_url.replace("www.", "").split(".")[0]
+
+ @property
+ def backend(self) -> str:
+ if (source := self.source) in {"genius", "tekstowo", "lrclib"}:
+ return source
+ return "google"
+
+
+lyrics_pages = [
+ LyricsPage.make(
+ "http://www.absolutelyrics.com/lyrics/view/the_beatles/lady_madonna",
+ """
+ The Beatles - Lady Madonna
+
+ Lady Madonna, children at your feet.
+ Wonder how you manage to make ends meet.
+ Who finds the money? When you pay the rent?
+ Did you think that money was heaven sent?
+ Friday night arrives without a suitcase.
+ Sunday morning creep in like a nun.
+ Monday's child has learned to tie his bootlace.
+ See how they run.
+ Lady Madonna, baby at your breast.
+ Wonder how you manage to feed the rest.
+ See how they run.
+ Lady Madonna, lying on the bed,
+ Listen to the music playing in your head.
+ Tuesday afternoon is never ending.
+ Wednesday morning papers didn't come.
+ Thursday night you stockings needed mending.
+ See how they run.
+ Lady Madonna, children at your feet.
+ Wonder how you manage to make ends meet.
+ """,
+ url_title="Lady Madonna Lyrics :: The Beatles - Absolute Lyrics",
+ ),
+ LyricsPage.make(
+ "https://www.azlyrics.com/lyrics/beatles/ladymadonna.html",
+ """
+ Lady Madonna, children at your feet
+ Wonder how you manage to make ends meet
+ Who finds the money when you pay the rent
+ Did you think that money was Heaven sent?
+ Friday night arrives without a suitcase
+ Sunday morning creeping like a nun
+ Monday's child has learned to tie his bootlace
+ See how they run
+
+ Lady Madonna, baby at your breast
+ Wonders how you manage to feed the rest?
+
+ See how they run
+
+ Lady Madonna lying on the bed
+ Listen to the music playing in your head
+
+ Tuesday afternoon is never ending
+ Wednesday morning papers didn't come
+ Thursday night your stockings needed mending
+ See how they run
+
+ Lady Madonna, children at your feet
+ Wonder how you manage to make ends meet
+ """,
+ url_title="The Beatles - Lady Madonna Lyrics | AZLyrics.com",
+ marks=[xfail_on_ci("AZLyrics is blocked by Cloudflare")],
+ ),
+ LyricsPage.make(
+ "http://www.chartlyrics.com/_LsLsZ7P4EK-F-LD4dJgDQ/Lady+Madonna.aspx",
+ """
+ Lady Madonna,
+ Children at your feet
+ Wonder how you manage to make ends meet.
+
+ Who finds the money
+ When you pay the rent?
+ Did you think that money was heaven-sent?
+
+ Friday night arrives without a suitcase.
+ Sunday morning creeping like a nun.
+ Monday's child has learned to tie his bootlace.
+
+ See how they run.
+
+ Lady Madonna,
+ Baby at your breast
+ Wonders how you manage to feed the rest.
+
+ See how they run.
+
+ Lady Madonna,
+ Lying on the bed.
+ Listen to the music playing in your head.
+
+ Tuesday afternoon is never ending.
+ Wednesday morning papers didn't come.
+ Thursday night your stockings needed mending.
+
+ See how they run.
+
+ Lady Madonna,
+ Children at your feet
+ Wonder how you manage to make ends meet.
+ """,
+ url_title="The Beatles Lady Madonna lyrics",
+ ),
+ LyricsPage.make(
+ "https://genius.com/The-beatles-lady-madonna-lyrics",
+ """
+ [Intro: Instrumental]
+
+ [Verse 1: Paul McCartney]
+ Lady Madonna, children at your feet
+ Wonder how you manage to make ends meet
+ Who finds the money when you pay the rent?
+ Did you think that money was heaven sent?
+
+ [Bridge: Paul McCartney]
+ Friday night arrives without a suitcase
+ Sunday morning creeping like a nun
+ Monday's child has learned to tie his bootlace
+ See how they run
+
+ [Verse 2: Paul McCartney]
+ Lady Madonna, baby at your breast
+ Wonders how you manage to feed the rest
+
+ [Bridge: Paul McCartney, John Lennon & George Harrison]
+ [Tenor Saxophone Solo: Ronnie Scott]
+ See how they run
+
+ [Verse 3: Paul McCartney]
+ Lady Madonna, lying on the bed
+ Listen to the music playing in your head
+
+ [Bridge: Paul McCartney]
+ Tuesday afternoon is never ending
+ Wednesday morning papers didn't come
+ Thursday night your stockings needed mending
+ See how they run
+
+ [Verse 4: Paul McCartney]
+ Lady Madonna, children at your feet
+ Wonder how you manage to make ends meet
+
+ [Outro: Instrumental]
+ """,
+ marks=[xfail_on_ci("Genius returns 403 FORBIDDEN in CI")],
+ ),
+ LyricsPage.make(
+ "https://www.lacoccinelle.net/259956-the-beatles-lady-madonna.html",
+ """
+ Lady Madonna
+ Mademoiselle Madonna
+
+ Lady Madonna, children at your feet.
+ Mademoiselle Madonna, les enfants à vos pieds
+ Wonder how you manage to make ends meet.
+ Je me demande comment vous vous débrouillez pour joindre les deux bouts
+ Who finds the money, when you pay the rent ?
+ Qui trouve l'argent pour payer le loyer ?
+ Did you think that money was heaven sent ?
+ Pensiez-vous que ça allait être envoyé du ciel ?
+
+ Friday night arrives without a suitcase.
+ Le vendredi soir arrive sans bagages
+ Sunday morning creeping like a nun.
+ Le dimanche matin elle se traine comme une nonne
+ Monday's child has learned to tie his bootlace.
+ Lundi l'enfant a appris à lacer ses chaussures
+ See how they run.
+ Regardez comme ils courent
+
+ Lady Madonna, baby at your breast.
+ Mademoiselle Madonna, le bébé a votre sein
+ Wonder how you manage to feed the rest.
+ Je me demande comment vous faites pour nourrir le reste
+
+ Lady Madonna, lying on the bed,
+ Mademoiselle Madonna, couchée sur votre lit
+ Listen to the music playing in your head.
+ Vous écoutez la musique qui joue dans votre tête
+ """,
+ url_title="Paroles et traduction The Beatles : Lady Madonna - paroles de chanson", # noqa: E501
+ ),
+ LyricsPage.make(
+ # note that this URL needs to be followed with a slash, otherwise it
+ # redirects to the same URL with a slash
+ "https://www.letras.mus.br/the-beatles/275/",
+ """
+ Lady Madonna
+ Children at your feet
+ Wonder how you manage
+ To make ends meet
+ Who finds the money
+ When you pay the rent?
+ Did you think that money
+ Was Heaven sent?
+ Friday night arrives without a suitcase
+ Sunday morning creeping like a nun
+ Monday's child has learned
+ To tie his bootlace
+ See how they run
+ Lady Madonna
+ Baby at your breast
+ Wonders how you manage
+ To feed the rest
+ See how they run
+ Lady Madonna
+ Lying on the bed
+ Listen to the music
+ Playing in your head
+ Tuesday afternoon is neverending
+ Wednesday morning papers didn't come
+ Thursday night your stockings
+ Needed mending
+ See how they run
+ Lady Madonna
+ Children at your feet
+ Wonder how you manage
+ To make ends meet
+ """,
+ url_title="Lady Madonna - The Beatles - LETRAS.MUS.BR",
+ ),
+ LyricsPage.make(
+ "https://lrclib.net/api/get/14038",
+ """
+ [00:08.35] Lady Madonna, children at your feet
+ [00:12.85] Wonder how you manage to make ends meet
+ [00:17.56] Who finds the money when you pay the rent
+ [00:21.78] Did you think that money was heaven sent
+ [00:26.22] Friday night arrives without a suitcase
+ [00:30.02] Sunday morning creeping like a nun
+ [00:34.53] Monday's child has learned to tie his bootlace
+ [00:39.18] See how they run
+ [00:43.33] Lady Madonna, baby at your breast
+ [00:48.50] Wonders how you manage to feed the rest
+ [00:52.54]
+ [01:01.32] Ba-ba, ba-ba, ba-ba, ba-ba-ba
+ [01:05.03] Ba-ba, ba-ba, ba-ba, ba, ba-ba, ba-ba
+ [01:09.58] Ba-ba, ba-ba, ba-ba, ba-ba-ba
+ [01:14.27] See how they run
+ [01:19.05] Lady Madonna, lying on the bed
+ [01:22.99] Listen to the music playing in your head
+ [01:27.92]
+ [01:36.33] Tuesday afternoon is never ending
+ [01:40.47] Wednesday morning papers didn't come
+ [01:44.76] Thursday night your stockings needed mending
+ [01:49.35] See how they run
+ [01:53.73] Lady Madonna, children at your feet
+ [01:58.65] Wonder how you manage to make ends meet
+ [02:06.04]
+ """,
+ ),
+ LyricsPage.make(
+ "https://www.lyricsmania.com/lady_madonna_lyrics_the_beatles.html",
+ """
+ Lady Madonna, children at your feet.
+ Wonder how you manage to make ends meet.
+ Who finds the money? When you pay the rent?
+ Did you think that money was heaven sent?
+
+ Friday night arrives without a suitcase.
+ Sunday morning creep in like a nun.
+ Monday's child has learned to tie his bootlace.
+ See how they run.
+
+ Lady Madonna, baby at your breast.
+ Wonder how you manage to feed the rest.
+
+ See how they run.
+ Lady Madonna, lying on the bed,
+ Listen to the music playing in your head.
+
+ Tuesday afternoon is never ending.
+ Wednesday morning papers didn't come.
+ Thursday night you stockings needed mending.
+ See how they run.
+
+ Lady Madonna, children at your feet.
+ Wonder how you manage to make ends meet.
+ """,
+ url_title="The Beatles - Lady Madonna Lyrics",
+ ),
+ LyricsPage.make(
+ "https://www.lyricsmode.com/lyrics/b/beatles/lady_madonna.html",
+ """
+ Lady Madonna, children at your feet.
+ Wonder how you manage to make ends meet.
+ Who finds the money? When you pay the rent?
+ Did you think that money was heaven sent?
+
+ Friday night arrives without a suitcase.
+ Sunday morning creep in like a nun.
+ Mondays child has learned to tie his bootlace.
+ See how they run.
+
+ Lady Madonna, baby at your breast.
+ Wonder how you manage to feed the rest.
+
+ See how they run.
+ Lady Madonna, lying on the bed,
+ Listen to the music playing in your head.
+
+ Tuesday afternoon is never ending.
+ Wednesday morning papers didn't come.
+ Thursday night you stockings needed mending.
+ See how they run.
+
+ Lady Madonna, children at your feet.
+ Wonder how you manage to make ends meet.
+ """,
+ url_title="Lady Madonna lyrics by The Beatles - original song full text. Official Lady Madonna lyrics, 2024 version | LyricsMode.com", # noqa: E501
+ ),
+ LyricsPage.make(
+ "https://www.lyricsontop.com/amy-winehouse-songs/jazz-n-blues-lyrics.html",
+ """
+ It's all gone within two days,
+ Follow my father
+ His extravagant ways
+ So, if I got it out I'll spend it all.
+ Heading In parkway, til I hit the wall.
+ I cross my fingers at the cash machine,
+ As I check my balance I kiss the screen,
+ I love it when it says I got the main's
+ To got o Miss Sixty and pick up my jeans.
+ Money ever last long
+ Had to fight what's wrong,
+ Blow it all on bags and shoes,
+ Jazz n' blues.
+ Money ever last long,
+ Had to fight what's wrong,
+ Blow it all on bags and shoes,
+ Jazz n' blues.
+
+ Standing to the … bar today,
+ Waiting impatient to throw my cash away,
+ For that Russian JD and coke
+ Had the drinks all night, and now I am bold
+ But that's cool, cause I can buy more from you.
+ And I didn't forgot about that 50 Compton,
+ Tell you what? My fancy's coming through
+ I'll take you at shopping, can you wait til next June?
+ Yeah, Money ever last long
+ Had to fight what's wrong,
+ Blow it all on bags and shoes,
+ Jazz n' blues.
+ Money ever last long,
+ Had to fight what's wrong,
+ Blow it all on bags and shoes,
+ Jazz n' blues.
+
+ (Instrumental Break)
+
+ Money ever last long
+ Had to fight what's wrong,
+ Blow it all on bags and shoes,
+ Jazz n' blues.
+ Money ever last long,
+ Had to fight what's wrong,
+ Blow it all on bags and shoes,
+ Jazz n' blues.
+ Money ever last long,
+ Had to fight what's wrong,
+ Blow it all on bags and shoes,
+ Jazz n' blues.
+ """,
+ artist="Amy Winehouse",
+ track_title="Jazz N' Blues",
+ url_title="Amy Winehouse - Jazz N' Blues lyrics complete",
+ ),
+ LyricsPage.make(
+ "https://www.musica.com/letras.asp?letra=59862",
+ """
+ Lady Madonna
+ Lady Madonna, children at your feet
+ Wonder how you manage to make ends meet
+ Who finds the money when you pay the rent?
+ Did you think that money was heaven sent?
+ Friday night arrives without a suitcase
+ Sunday morning creeping like a nun
+ Monday's child has learned to tie his bootlace
+ See how they run
+ Lady Madonna, baby at your breast
+ Wonders how you manage to feed the rest
+ See how they run
+ Lady Madonna lying on the bed
+ Listen to the music playing in your head
+ Tuesday afternoon is never ending
+ Wednesday morning papers didn't come
+ Thursday night your stockings needed mending
+ See how they run
+ Lady Madonna, children at your feet
+ Wonder how you manage to make ends meet
+ """,
+ url_title="Lady Madonna - Letra - The Beatles - Musica.com",
+ ),
+ LyricsPage.make(
+ "https://www.paroles.net/the-beatles/paroles-lady-madonna",
+ """
+ Lady Madonna, children at your feet.
+ Wonder how you manage to make ends meet.
+ Who finds the money? When you pay the rent?
+ Did you think that money was heaven sent?
+
+ Friday night arrives without a suitcase.
+ Sunday morning creep in like a nun.
+ Monday's child has learned to tie his bootlace.
+ See how they run.
+
+ Lady Madonna, baby at your breast.
+ Wonders how you manage to feed the rest.
+
+ See how they run.
+ Lady Madonna, lying on the bed,
+ Listen to the music playing in your head.
+ """,
+ url_title="Paroles Lady Madonna par The Beatles - Lyrics - Paroles.net",
+ ),
+ LyricsPage.make(
+ "https://www.songlyrics.com/the-beatles/lady-madonna-lyrics",
+ """
+ Lady Madonna, children at your feet
+ Wonder how you manage to make ends meet
+ Who finds the money? When you pay the rent?
+ Did you think that money was Heaven sent?
+ Friday night arrives without a suitcase
+ Sunday morning creep in like a nun
+ Monday's child has learned to tie his bootlace
+ See how they run
+
+ Lady Madonna, baby at your breast
+ Wonder how you manage to feed the rest
+
+ See how they run
+
+ Lady Madonna, lying on the bed
+ Listen to the music playing in your head
+
+ Tuesday afternoon is never ending
+ Wednesday morning papers didn't come
+ Thursday night you stockings needed mending
+ See how they run
+
+ Lady Madonna, children at your feet
+ Wonder how you manage to make ends meet
+ """,
+ url_title="THE BEATLES - LADY MADONNA LYRICS",
+ ),
+ LyricsPage.make(
+ "https://sweetslyrics.com/the-beatles/lady-madonna-lyrics",
+ """
+ Lady Madonna, children at your feet.
+ Wonder how you manage to make ends meet.
+ Who finds the money when you pay the rent?
+ Did you think that money was heaven sent?
+
+ Friday night arrives without a suitcase.
+ Sunday morning creeping like a nun.
+ Monday's child has learned to tie his bootlace.
+ See how they run...
+
+ Lady Madonna, baby at your breast.
+ Wonders how you manage to feed the rest.
+
+ (Sax solo)
+
+ See how they run...
+
+ Lady Madonna, lying on the bed.
+ Listen to the music playing in your head.
+
+ Tuesday afternoon is never ending.
+ Wednesday morning papers didn't come.
+ Thursday night your stockings needed mending.
+ See how they run...
+
+ Lady Madonna, children at your feet.
+ Wonder how you manage to make ends meet.
+ """,
+ url_title="The Beatles - Lady Madonna",
+ ),
+ LyricsPage.make(
+ "https://www.tekstowo.pl/piosenka,the_beatles,lady_madonna.html",
+ """
+ Lady Madonna,
+ Children at your feet
+ Wonder how you manage to make ends meet.
+
+ Who find the money
+ When you pay the rent?
+ Did you think that money was Heaven sent?
+
+ Friday night arrives without a suitcase
+ Sunday morning creeping like a nun
+ Monday's child has learned to tie his bootlace
+
+ See how they run
+
+ Lady Madonna
+ Baby at your breast
+ Wonders how you manage to feed the rest
+
+ See how they run
+
+ Lady Madonna
+ Lying on the bed
+ Listen to the music playing in your head
+
+ Tuesday afternoon is neverending
+ Wednesday morning papers didn't come
+ Thursday night your stockings needed mending
+
+ See how they run
+
+ Lady Madonna,
+ Children at your feet
+ Wonder how you manage to make ends meet
+ """,
+ ),
+]
diff --git a/test/plugins/test_lyrics.py b/test/plugins/test_lyrics.py
index a222500b92..99e6f8a4e8 100644
--- a/test/plugins/test_lyrics.py
+++ b/test/plugins/test_lyrics.py
@@ -14,9 +14,7 @@
"""Tests for the 'lyrics' plugin."""
-import os
from functools import partial
-from urllib.parse import urlparse
import pytest
@@ -24,6 +22,8 @@
from beets.test.helper import PluginMixin
from beetsplug import lyrics
+from .lyrics_pages import LyricsPage, lyrics_pages
+
PHRASE_BY_TITLE = {
"Lady Madonna": "friday night arrives without a suitcase",
"Jazz'n'blues": "as i check my balance i kiss the screen",
@@ -31,14 +31,6 @@
}
-def xfail_on_ci(msg: str) -> pytest.MarkDecorator:
- return pytest.mark.xfail(
- bool(os.environ.get("GITHUB_ACTIONS")),
- reason=msg,
- raises=AssertionError,
- )
-
-
class TestLyricsUtils:
@pytest.mark.parametrize(
"artist, title",
@@ -181,12 +173,16 @@ def plugin_config(self):
return {}
@pytest.fixture
- def backend(self, backend_name, plugin_config):
- """Set configuration and returns the backend instance."""
+ def lyrics_plugin(self, backend_name, plugin_config):
+ """Set configuration and returns the plugin's instance."""
plugin_config["sources"] = [backend_name]
self.config[self.plugin].set(plugin_config)
- lyrics_plugin = lyrics.LyricsPlugin()
+ return lyrics.LyricsPlugin()
+
+ @pytest.fixture
+ def backend(self, lyrics_plugin):
+ """Return a lyrics backend instance."""
return lyrics_plugin.backends[0]
@pytest.fixture
@@ -195,17 +191,48 @@ def lyrics_html(self, lyrics_root_dir, file_name):
encoding="utf-8"
)
- @pytest.mark.on_lyrics_update
- def test_backend_source(self, backend):
- """Test default backends with a song known to exist in respective
- databases.
- """
- title = "Lady Madonna"
- lyrics = backend.fetch("The Beatles", title, "", 0)
+@pytest.mark.on_lyrics_update
+class TestLyricsSources(LyricsBackendTest):
+ @pytest.fixture(scope="class")
+ def plugin_config(self):
+ return {"google_API_key": "test", "synced": True}
+
+ @pytest.fixture(
+ params=[pytest.param(lp, marks=lp.marks) for lp in lyrics_pages],
+ ids=str,
+ )
+ def lyrics_page(self, request):
+ return request.param
+
+ @pytest.fixture
+ def backend_name(self, lyrics_page):
+ return lyrics_page.backend
+
+ @pytest.fixture(autouse=True)
+ def _patch_google_search(self, requests_mock, lyrics_page):
+ """Mock the Google Search API to return the lyrics page under test."""
+ requests_mock.real_http = True
+
+ data = {
+ "items": [
+ {
+ "title": lyrics_page.url_title,
+ "link": lyrics_page.url,
+ "displayLink": lyrics_page.root_url,
+ }
+ ]
+ }
+ requests_mock.get(lyrics.Google.SEARCH_URL, json=data)
+
+ 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_page.artist, lyrics_page.track_title, "", 0
+ )
assert lyrics
- assert PHRASE_BY_TITLE[title] in lyrics.lower()
+ assert lyrics == lyrics_page.lyrics
class TestGoogleLyrics(LyricsBackendTest):
@@ -225,100 +252,6 @@ def plugin_config(self):
def file_name(self):
return "examplecom/beetssong"
- @pytest.fixture
- def response_data(self, url_title, url):
- return {
- "items": [
- {
- "title": url_title,
- "link": url,
- "displayLink": urlparse(url).netloc,
- }
- ]
- }
-
- @pytest.fixture
- def fetch_lyrics(
- self, backend, requests_mock, response_data, artist, title
- ):
- requests_mock.get(backend.SEARCH_URL, json=response_data)
- requests_mock.real_http = True
-
- return partial(backend.fetch, artist, title)
-
- @pytest.mark.on_lyrics_update
- @pytest.mark.parametrize(
- "artist, title, url_title, url",
- [
- *(
- ("The Beatles", "Lady Madonna", url_title, url)
- for url_title, url in (
- (
- "The Beatles Lady Madonna lyrics",
- "http://www.chartlyrics.com/_LsLsZ7P4EK-F-LD4dJgDQ/Lady+Madonna.aspx",
- ),
- (
- "Lady Madonna Lyrics :: The Beatles - Absolute Lyrics",
- "http://www.absolutelyrics.com/lyrics/view/the_beatles/lady_madonna",
- ),
- (
- "Lady Madonna - The Beatles - LETRAS.MUS.BR",
- "https://www.letras.mus.br/the-beatles/275/",
- ),
- (
- "The Beatles - Lady Madonna Lyrics",
- "https://www.lyricsmania.com/lady_madonna_lyrics_the_beatles.html",
- ),
- (
- "Lady Madonna lyrics by The Beatles - original song full text. Official Lady Madonna lyrics, 2024 version | LyricsMode.com", # noqa: E501
- "https://www.lyricsmode.com/lyrics/b/beatles/lady_madonna.html",
- ),
- (
- "Paroles Lady Madonna par The Beatles - Lyrics - Paroles.net",
- "https://www.paroles.net/the-beatles/paroles-lady-madonna",
- ),
- (
- "THE BEATLES - LADY MADONNA LYRICS",
- "https://www.songlyrics.com/the-beatles/lady-madonna-lyrics/",
- ),
- (
- "The Beatles - Lady Madonna",
- "https://sweetslyrics.com/the-beatles/lady-madonna-lyrics",
- ),
- (
- "Lady Madonna - Letra - The Beatles - Musica.com",
- "https://www.musica.com/letras.asp?letra=59862",
- ),
- (
- "Paroles et traduction The Beatles : Lady Madonna - paroles de chanson", # noqa: E501
- "https://www.lacoccinelle.net/259956-the-beatles-lady-madonna.html",
- ),
- )
- ),
- pytest.param(
- "The Beatles",
- "Lady Madonna",
- "The Beatles - Lady Madonna Lyrics | AZLyrics.com",
- "https://www.azlyrics.com/lyrics/beatles/ladymadonna.html",
- marks=xfail_on_ci("AZLyrics is blocked by Cloudflare"),
- ),
- (
- "Amy Winehouse",
- "Jazz'n'blues",
- "Amy Winehouse - Jazz N' Blues lyrics complete",
- "https://www.lyricsontop.com/amy-winehouse-songs/jazz-n-blues-lyrics.html",
- ),
- ],
- )
- def test_backend_source(self, fetch_lyrics, title):
- """Test if lyrics present on websites registered in beets google custom
- search engine are correctly scraped.
- """
- lyrics = fetch_lyrics()
-
- assert lyrics
- assert PHRASE_BY_TITLE[title].lower() in lyrics.lower()
-
def test_mocked_source_ok(self, backend, lyrics_html):
"""Test that lyrics of the mocked page are correctly scraped"""
result = lyrics.scrape_lyrics_from_html(lyrics_html).lower()
@@ -374,11 +307,6 @@ class TestGeniusLyrics(LyricsBackendTest):
def backend_name(self):
return "genius"
- @pytest.mark.on_lyrics_update
- @xfail_on_ci("Genius returns 403 FORBIDDEN in CI")
- def test_backend_source(self, backend):
- super().test_backend_source(backend)
-
@pytest.mark.parametrize(
"file_name, expected_line_count",
[