diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index 18c77b2d27..7efb1220e3 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -37,7 +37,15 @@ jobs:
poetry install --extras=replaygain --extras=reflink
- name: Install Python dependencies
- run: poetry install --only=main,test --extras=autobpm
+ run: poetry install --only=main,test --extras=autobpm --extras=lyrics
+
+ - name: Get changed lyrics files
+ id: lyrics-update
+ uses: tj-actions/changed-files@v45
+ with:
+ files: |
+ beetsplug/lyrics.py
+ test/plugins/test_lyrics.py
- if: ${{ env.IS_MAIN_PYTHON != 'true' }}
name: Test without coverage
@@ -45,6 +53,8 @@ jobs:
- if: ${{ env.IS_MAIN_PYTHON == 'true' }}
name: Test with coverage
+ env:
+ LYRICS_UPDATED: ${{ steps.lyrics-update.outputs.any_changed }}
uses: liskin/gh-problem-matcher-wrap@v3
with:
linters: pytest
diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst
index ee4edbdcb7..e52d57d47e 100644
--- a/CONTRIBUTING.rst
+++ b/CONTRIBUTING.rst
@@ -382,7 +382,7 @@ to get a basic view on how tests are written. Since we are currently migrating
the tests from `unittest`_ to `pytest`_, new tests should be written using
`pytest`_. Contributions migrating existing tests are welcome!
-External API requests under test should be mocked with `requests_mock`_,
+External API requests under test should be mocked with `requests-mock`_,
However, we still want to know whether external APIs are up and that they
return expected responses, therefore we test them weekly with our `integration
test`_ suite.
diff --git a/beets/test/helper.py b/beets/test/helper.py
index 124063d766..4effa47f82 100644
--- a/beets/test/helper.py
+++ b/beets/test/helper.py
@@ -48,7 +48,7 @@
import beets
import beets.plugins
-from beets import autotag, config, importer, logging, util
+from beets import autotag, importer, logging, util
from beets.autotag.hooks import AlbumInfo, TrackInfo
from beets.importer import ImportSession
from beets.library import Album, Item, Library
@@ -153,12 +153,27 @@ def check_reflink_support(path: str) -> bool:
return reflink.supported_at(path)
+class ConfigMixin:
+ @cached_property
+ def config(self) -> beets.IncludeLazyConfig:
+ """Base beets configuration for tests."""
+ config = beets.config
+ config.sources = []
+ config.read(user=False, defaults=True)
+
+ config["plugins"] = []
+ config["verbose"] = 1
+ config["ui"]["color"] = False
+ config["threaded"] = False
+ return config
+
+
NEEDS_REFLINK = unittest.skipUnless(
check_reflink_support(gettempdir()), "no reflink support for libdir"
)
-class TestHelper(_common.Assertions):
+class TestHelper(_common.Assertions, ConfigMixin):
"""Helper mixin for high-level cli and plugin tests.
This mixin provides methods to isolate beets' global state provide
@@ -184,8 +199,6 @@ def setup_beets(self):
- ``libdir`` Path to a subfolder of ``temp_dir``, containing the
library's media files. Same as ``config['directory']``.
- - ``config`` The global configuration used by beets.
-
- ``lib`` Library instance created with the settings from
``config``.
@@ -202,15 +215,6 @@ def setup_beets(self):
)
self.env_patcher.start()
- self.config = beets.config
- self.config.sources = []
- self.config.read(user=False, defaults=True)
-
- self.config["plugins"] = []
- self.config["verbose"] = 1
- self.config["ui"]["color"] = False
- self.config["threaded"] = False
-
self.libdir = os.path.join(self.temp_dir, b"libdir")
os.mkdir(syspath(self.libdir))
self.config["directory"] = os.fsdecode(self.libdir)
@@ -229,8 +233,6 @@ def teardown_beets(self):
self.io.restore()
self.lib._close()
self.remove_temp_dir()
- beets.config.clear()
- beets.config._materialized = False
# Library fixtures methods
@@ -452,7 +454,7 @@ def setUp(self):
self.i = _common.item(self.lib)
-class PluginMixin:
+class PluginMixin(ConfigMixin):
plugin: ClassVar[str]
preload_plugin: ClassVar[bool] = True
@@ -473,7 +475,7 @@ def load_plugins(self, *plugins: str) -> None:
"""
# FIXME this should eventually be handled by a plugin manager
plugins = (self.plugin,) if hasattr(self, "plugin") else plugins
- beets.config["plugins"] = plugins
+ self.config["plugins"] = plugins
beets.plugins.load_plugins(plugins)
beets.plugins.find_plugins()
@@ -494,7 +496,7 @@ def unload_plugins(self) -> None:
# FIXME this should eventually be handled by a plugin manager
for plugin_class in beets.plugins._instances:
plugin_class.listeners = None
- beets.config["plugins"] = []
+ self.config["plugins"] = []
beets.plugins._classes = set()
beets.plugins._instances = {}
Item._types = getattr(Item, "_original_types", {})
@@ -504,7 +506,7 @@ def unload_plugins(self) -> None:
@contextmanager
def configure_plugin(self, config: Any):
- beets.config[self.plugin].set(config)
+ self.config[self.plugin].set(config)
self.load_plugins(self.plugin)
yield
@@ -624,7 +626,7 @@ def _get_import_session(self, import_dir: bytes) -> ImportSession:
def setup_importer(
self, import_dir: bytes | None = None, **kwargs
) -> ImportSession:
- config["import"].set_args({**self.default_import_config, **kwargs})
+ self.config["import"].set_args({**self.default_import_config, **kwargs})
self.importer = self._get_import_session(import_dir or self.import_dir)
return self.importer
diff --git a/beetsplug/lyrics.py b/beetsplug/lyrics.py
index 899bf39921..3c99bfb8a4 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"<[^>]*>")
@@ -152,7 +155,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 +170,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 = [
@@ -222,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.
"""
@@ -240,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)
@@ -250,20 +260,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(
@@ -316,7 +333,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)
@@ -364,7 +381,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,
@@ -495,7 +512,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)
@@ -536,6 +553,8 @@ def _scrape_strip_cruft(html, plain_text_out=False):
html = BREAK_RE.sub("\n", html) # eats up surrounding '\n'.
html = re.sub(r"(?s)<(script).*?\1>", "", html) # Strip script tags.
html = re.sub("\u2005", " ", html) # replace unicode with regular space
+ html = re.sub("", "", html) # remove Google Ads tags
+ html = re.sub(r"?(em|strong)[^>]*>", "", html) # remove italics / bold
if plain_text_out: # Strip remaining HTML tags
html = COMMENT_RE.sub("", html)
@@ -586,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."""
@@ -667,15 +682,14 @@ 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):
- 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")),
- )
+ def fetch(self, artist: str, title: str, *_) -> str | None:
+ 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
@@ -877,10 +891,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:
@@ -966,15 +977,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.
"""
@@ -983,18 +992,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)
@@ -1019,18 +1027,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/docs/changelog.rst b/docs/changelog.rst
index d3a12ceef0..5e4e3c5bcc 100644
--- a/docs/changelog.rst
+++ b/docs/changelog.rst
@@ -13,6 +13,15 @@ New features:
many different artists at once.
Bug fixes:
+
+* :doc:`plugins/lyrics`: Rewrite lyrics tests using pytest to provide isolated
+ 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:
Other changes:
@@ -60,7 +69,7 @@ Bug fixes:
issues in the future.
:bug:`5289`
* :doc:`plugins/discogs`: Fix the ``TypeError`` when there is no description.
-* Remove single quotes from all SQL queries
+* Use single quotes in all SQL queries.
:bug:`4709`
* :doc:`plugins/lyrics`: Update ``tekstowo`` backend to fetch lyrics directly
since recent updates to their website made it unsearchable.
diff --git a/poetry.lock b/poetry.lock
index 8058e4dbf1..6aac1d2087 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -1,4 +1,4 @@
-# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand.
+# This file is automatically @generated by Poetry 1.8.4 and should not be changed by hand.
[[package]]
name = "accessible-pygments"
@@ -138,6 +138,10 @@ files = [
{file = "Brotli-1.1.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a37b8f0391212d29b3a91a799c8e4a2855e0576911cdfb2515487e30e322253d"},
{file = "Brotli-1.1.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:e84799f09591700a4154154cab9787452925578841a94321d5ee8fb9a9a328f0"},
{file = "Brotli-1.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f66b5337fa213f1da0d9000bc8dc0cb5b896b726eefd9c6046f699b169c41b9e"},
+ {file = "Brotli-1.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5dab0844f2cf82be357a0eb11a9087f70c5430b2c241493fc122bb6f2bb0917c"},
+ {file = "Brotli-1.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e4fe605b917c70283db7dfe5ada75e04561479075761a0b3866c081d035b01c1"},
+ {file = "Brotli-1.1.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:1e9a65b5736232e7a7f91ff3d02277f11d339bf34099a56cdab6a8b3410a02b2"},
+ {file = "Brotli-1.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:58d4b711689366d4a03ac7957ab8c28890415e267f9b6589969e74b6e42225ec"},
{file = "Brotli-1.1.0-cp310-cp310-win32.whl", hash = "sha256:be36e3d172dc816333f33520154d708a2657ea63762ec16b62ece02ab5e4daf2"},
{file = "Brotli-1.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:0c6244521dda65ea562d5a69b9a26120769b7a9fb3db2fe9545935ed6735b128"},
{file = "Brotli-1.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a3daabb76a78f829cafc365531c972016e4aa8d5b4bf60660ad8ecee19df7ccc"},
@@ -150,8 +154,14 @@ files = [
{file = "Brotli-1.1.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:19c116e796420b0cee3da1ccec3b764ed2952ccfcc298b55a10e5610ad7885f9"},
{file = "Brotli-1.1.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:510b5b1bfbe20e1a7b3baf5fed9e9451873559a976c1a78eebaa3b86c57b4265"},
{file = "Brotli-1.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a1fd8a29719ccce974d523580987b7f8229aeace506952fa9ce1d53a033873c8"},
+ {file = "Brotli-1.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c247dd99d39e0338a604f8c2b3bc7061d5c2e9e2ac7ba9cc1be5a69cb6cd832f"},
+ {file = "Brotli-1.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1b2c248cd517c222d89e74669a4adfa5577e06ab68771a529060cf5a156e9757"},
+ {file = "Brotli-1.1.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:2a24c50840d89ded6c9a8fdc7b6ed3692ed4e86f1c4a4a938e1e92def92933e0"},
+ {file = "Brotli-1.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f31859074d57b4639318523d6ffdca586ace54271a73ad23ad021acd807eb14b"},
{file = "Brotli-1.1.0-cp311-cp311-win32.whl", hash = "sha256:39da8adedf6942d76dc3e46653e52df937a3c4d6d18fdc94a7c29d263b1f5b50"},
{file = "Brotli-1.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:aac0411d20e345dc0920bdec5548e438e999ff68d77564d5e9463a7ca9d3e7b1"},
+ {file = "Brotli-1.1.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:32d95b80260d79926f5fab3c41701dbb818fde1c9da590e77e571eefd14abe28"},
+ {file = "Brotli-1.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b760c65308ff1e462f65d69c12e4ae085cff3b332d894637f6273a12a482d09f"},
{file = "Brotli-1.1.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:316cc9b17edf613ac76b1f1f305d2a748f1b976b033b049a6ecdfd5612c70409"},
{file = "Brotli-1.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:caf9ee9a5775f3111642d33b86237b05808dafcd6268faa492250e9b78046eb2"},
{file = "Brotli-1.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:70051525001750221daa10907c77830bc889cb6d865cc0b813d9db7fefc21451"},
@@ -162,8 +172,24 @@ files = [
{file = "Brotli-1.1.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:4093c631e96fdd49e0377a9c167bfd75b6d0bad2ace734c6eb20b348bc3ea180"},
{file = "Brotli-1.1.0-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:7e4c4629ddad63006efa0ef968c8e4751c5868ff0b1c5c40f76524e894c50248"},
{file = "Brotli-1.1.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:861bf317735688269936f755fa136a99d1ed526883859f86e41a5d43c61d8966"},
+ {file = "Brotli-1.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:87a3044c3a35055527ac75e419dfa9f4f3667a1e887ee80360589eb8c90aabb9"},
+ {file = "Brotli-1.1.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c5529b34c1c9d937168297f2c1fde7ebe9ebdd5e121297ff9c043bdb2ae3d6fb"},
+ {file = "Brotli-1.1.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:ca63e1890ede90b2e4454f9a65135a4d387a4585ff8282bb72964fab893f2111"},
+ {file = "Brotli-1.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e79e6520141d792237c70bcd7a3b122d00f2613769ae0cb61c52e89fd3443839"},
{file = "Brotli-1.1.0-cp312-cp312-win32.whl", hash = "sha256:5f4d5ea15c9382135076d2fb28dde923352fe02951e66935a9efaac8f10e81b0"},
{file = "Brotli-1.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:906bc3a79de8c4ae5b86d3d75a8b77e44404b0f4261714306e3ad248d8ab0951"},
+ {file = "Brotli-1.1.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8bf32b98b75c13ec7cf774164172683d6e7891088f6316e54425fde1efc276d5"},
+ {file = "Brotli-1.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7bc37c4d6b87fb1017ea28c9508b36bbcb0c3d18b4260fcdf08b200c74a6aee8"},
+ {file = "Brotli-1.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c0ef38c7a7014ffac184db9e04debe495d317cc9c6fb10071f7fefd93100a4f"},
+ {file = "Brotli-1.1.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:91d7cc2a76b5567591d12c01f019dd7afce6ba8cba6571187e21e2fc418ae648"},
+ {file = "Brotli-1.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a93dde851926f4f2678e704fadeb39e16c35d8baebd5252c9fd94ce8ce68c4a0"},
+ {file = "Brotli-1.1.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f0db75f47be8b8abc8d9e31bc7aad0547ca26f24a54e6fd10231d623f183d089"},
+ {file = "Brotli-1.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6967ced6730aed543b8673008b5a391c3b1076d834ca438bbd70635c73775368"},
+ {file = "Brotli-1.1.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7eedaa5d036d9336c95915035fb57422054014ebdeb6f3b42eac809928e40d0c"},
+ {file = "Brotli-1.1.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d487f5432bf35b60ed625d7e1b448e2dc855422e87469e3f450aa5552b0eb284"},
+ {file = "Brotli-1.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:832436e59afb93e1836081a20f324cb185836c617659b07b129141a8426973c7"},
+ {file = "Brotli-1.1.0-cp313-cp313-win32.whl", hash = "sha256:43395e90523f9c23a3d5bdf004733246fba087f2948f87ab28015f12359ca6a0"},
+ {file = "Brotli-1.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:9011560a466d2eb3f5a6e4929cf4a09be405c64154e12df0dd72713f6500e32b"},
{file = "Brotli-1.1.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:a090ca607cbb6a34b0391776f0cb48062081f5f60ddcce5d11838e67a01928d1"},
{file = "Brotli-1.1.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2de9d02f5bda03d27ede52e8cfe7b865b066fa49258cbab568720aa5be80a47d"},
{file = "Brotli-1.1.0-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2333e30a5e00fe0fe55903c8832e08ee9c3b1382aacf4db26664a16528d51b4b"},
@@ -173,6 +199,10 @@ files = [
{file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:fd5f17ff8f14003595ab414e45fce13d073e0762394f957182e69035c9f3d7c2"},
{file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:069a121ac97412d1fe506da790b3e69f52254b9df4eb665cd42460c837193354"},
{file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:e93dfc1a1165e385cc8239fab7c036fb2cd8093728cbd85097b284d7b99249a2"},
+ {file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_2_aarch64.whl", hash = "sha256:aea440a510e14e818e67bfc4027880e2fb500c2ccb20ab21c7a7c8b5b4703d75"},
+ {file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_2_i686.whl", hash = "sha256:6974f52a02321b36847cd19d1b8e381bf39939c21efd6ee2fc13a28b0d99348c"},
+ {file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_2_ppc64le.whl", hash = "sha256:a7e53012d2853a07a4a79c00643832161a910674a893d296c9f1259859a289d2"},
+ {file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_2_x86_64.whl", hash = "sha256:d7702622a8b40c49bffb46e1e3ba2e81268d5c04a34f460978c6b5517a34dd52"},
{file = "Brotli-1.1.0-cp36-cp36m-win32.whl", hash = "sha256:a599669fd7c47233438a56936988a2478685e74854088ef5293802123b5b2460"},
{file = "Brotli-1.1.0-cp36-cp36m-win_amd64.whl", hash = "sha256:d143fd47fad1db3d7c27a1b1d66162e855b5d50a89666af46e1679c496e8e579"},
{file = "Brotli-1.1.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:11d00ed0a83fa22d29bc6b64ef636c4552ebafcef57154b4ddd132f5638fbd1c"},
@@ -184,6 +214,10 @@ files = [
{file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:919e32f147ae93a09fe064d77d5ebf4e35502a8df75c29fb05788528e330fe74"},
{file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:23032ae55523cc7bccb4f6a0bf368cd25ad9bcdcc1990b64a647e7bbcce9cb5b"},
{file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:224e57f6eac61cc449f498cc5f0e1725ba2071a3d4f48d5d9dffba42db196438"},
+ {file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:cb1dac1770878ade83f2ccdf7d25e494f05c9165f5246b46a621cc849341dc01"},
+ {file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:3ee8a80d67a4334482d9712b8e83ca6b1d9bc7e351931252ebef5d8f7335a547"},
+ {file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:5e55da2c8724191e5b557f8e18943b1b4839b8efc3ef60d65985bcf6f587dd38"},
+ {file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:d342778ef319e1026af243ed0a07c97acf3bad33b9f29e7ae6a1f68fd083e90c"},
{file = "Brotli-1.1.0-cp37-cp37m-win32.whl", hash = "sha256:587ca6d3cef6e4e868102672d3bd9dc9698c309ba56d41c2b9c85bbb903cdb95"},
{file = "Brotli-1.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:2954c1c23f81c2eaf0b0717d9380bd348578a94161a65b3a2afc62c86467dd68"},
{file = "Brotli-1.1.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:efa8b278894b14d6da122a72fefcebc28445f2d3f880ac59d46c90f4c13be9a3"},
@@ -196,6 +230,10 @@ files = [
{file = "Brotli-1.1.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:1ab4fbee0b2d9098c74f3057b2bc055a8bd92ccf02f65944a241b4349229185a"},
{file = "Brotli-1.1.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:141bd4d93984070e097521ed07e2575b46f817d08f9fa42b16b9b5f27b5ac088"},
{file = "Brotli-1.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fce1473f3ccc4187f75b4690cfc922628aed4d3dd013d047f95a9b3919a86596"},
+ {file = "Brotli-1.1.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:d2b35ca2c7f81d173d2fadc2f4f31e88cc5f7a39ae5b6db5513cf3383b0e0ec7"},
+ {file = "Brotli-1.1.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:af6fa6817889314555aede9a919612b23739395ce767fe7fcbea9a80bf140fe5"},
+ {file = "Brotli-1.1.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:2feb1d960f760a575dbc5ab3b1c00504b24caaf6986e2dc2b01c09c87866a943"},
+ {file = "Brotli-1.1.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:4410f84b33374409552ac9b6903507cdb31cd30d2501fc5ca13d18f73548444a"},
{file = "Brotli-1.1.0-cp38-cp38-win32.whl", hash = "sha256:db85ecf4e609a48f4b29055f1e144231b90edc90af7481aa731ba2d059226b1b"},
{file = "Brotli-1.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:3d7954194c36e304e1523f55d7042c59dc53ec20dd4e9ea9d151f1b62b4415c0"},
{file = "Brotli-1.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5fb2ce4b8045c78ebbc7b8f3c15062e435d47e7393cc57c25115cfd49883747a"},
@@ -208,6 +246,10 @@ files = [
{file = "Brotli-1.1.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:949f3b7c29912693cee0afcf09acd6ebc04c57af949d9bf77d6101ebb61e388c"},
{file = "Brotli-1.1.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:89f4988c7203739d48c6f806f1e87a1d96e0806d44f0fba61dba81392c9e474d"},
{file = "Brotli-1.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:de6551e370ef19f8de1807d0a9aa2cdfdce2e85ce88b122fe9f6b2b076837e59"},
+ {file = "Brotli-1.1.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0737ddb3068957cf1b054899b0883830bb1fec522ec76b1098f9b6e0f02d9419"},
+ {file = "Brotli-1.1.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:4f3607b129417e111e30637af1b56f24f7a49e64763253bbc275c75fa887d4b2"},
+ {file = "Brotli-1.1.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:6c6e0c425f22c1c719c42670d561ad682f7bfeeef918edea971a79ac5252437f"},
+ {file = "Brotli-1.1.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:494994f807ba0b92092a163a0a283961369a65f6cbe01e8891132b7a320e61eb"},
{file = "Brotli-1.1.0-cp39-cp39-win32.whl", hash = "sha256:f0d8a7a6b5983c2496e364b969f0e526647a06b075d034f3297dc66f3b360c64"},
{file = "Brotli-1.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:cdad5b9014d83ca68c25d2e9444e28e967ef16e80f6b436918c700c117a85467"},
{file = "Brotli-1.1.0.tar.gz", hash = "sha256:81de08ac11bcb85841e440c13611c00b67d3bf82698314928d0b676362546724"},
@@ -937,6 +979,14 @@ files = [
{file = "jellyfish-1.1.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2fcaefebe9d67f282d89d3a66646b77184a42b3eca2771636789b2dc1288c003"},
{file = "jellyfish-1.1.0-cp312-none-win32.whl", hash = "sha256:e512c99941a257541ffd9f75c7a5c4689de0206841b72f1eb015599d17fed2c3"},
{file = "jellyfish-1.1.0-cp312-none-win_amd64.whl", hash = "sha256:2b928bad2887c662783a4d9b5828ed1fa0e943f680589f7fc002c456fc02e184"},
+ {file = "jellyfish-1.1.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5d510b04e2a39f27aef391ca18bf527ec5d9a2438a63731b87faada83996cb92"},
+ {file = "jellyfish-1.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:57d005cc5daa4d0a8d88341d86b1dce24e3f1d7721da75326c0b7af598a4f58c"},
+ {file = "jellyfish-1.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:889edab0fb2a29d29c148c9327752df525c9bdaef03eef01d1bd9c1f90b47ebf"},
+ {file = "jellyfish-1.1.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:937b657aacba8fe8482ebc5fea5ba1aee987ecb9da0f037bfb8a1a9045d05746"},
+ {file = "jellyfish-1.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3cb5088436ce1fdabcb46aed3a3cc215f0432313596f4e5abe5300ed833b697c"},
+ {file = "jellyfish-1.1.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:af74156301a0ff05a22e8cf46250678e23fa447279ba6dffbf9feff01128f51d"},
+ {file = "jellyfish-1.1.0-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:3f978bc430bbed4df3c10b2a66be7b5bddd09e6c2856c7a17fa2298fb193d4d4"},
+ {file = "jellyfish-1.1.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:b460f0bbde533f6f8624c1d7439e7f511b227ca18a58781e7f38f21961bd3f09"},
{file = "jellyfish-1.1.0-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:7cd4b706cb6c4739846d78a398c67996cb451b09a732a625793cfe8d4f37af1b"},
{file = "jellyfish-1.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:61cded25b47fe6b4c2ea9478c0a5a7531845218525a1b2627c67907ee9fe9b15"},
{file = "jellyfish-1.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:04bf33577059afba33227977e4a2c08ccb954eb77c849fde564af3e31ee509d9"},
@@ -2480,6 +2530,23 @@ urllib3 = ">=1.21.1,<3"
socks = ["PySocks (>=1.5.6,!=1.5.7)"]
use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
+[[package]]
+name = "requests-mock"
+version = "1.12.1"
+description = "Mock out responses from the requests package"
+optional = false
+python-versions = ">=3.5"
+files = [
+ {file = "requests-mock-1.12.1.tar.gz", hash = "sha256:e9e12e333b525156e82a3c852f22016b9158220d2f47454de9cae8a77d371401"},
+ {file = "requests_mock-1.12.1-py2.py3-none-any.whl", hash = "sha256:b1e37054004cdd5e56c84454cc7df12b25f90f382159087f4b6915aaeef39563"},
+]
+
+[package.dependencies]
+requests = ">=2.22,<3"
+
+[package.extras]
+fixture = ["fixtures"]
+
[[package]]
name = "requests-oauthlib"
version = "2.0.0"
@@ -3144,4 +3211,4 @@ web = ["flask", "flask-cors"]
[metadata]
lock-version = "2.0"
python-versions = ">=3.8,<4"
-content-hash = "07f39a89dbb7ea5102327e5b2cccfde258ad190ba21b0793c044c2f45aa89f00"
+content-hash = "0670123d3c99c46eb1fa24b64d8c5a6bece2463877c0c19fd4f622827b65b9b4"
diff --git a/pyproject.toml b/pyproject.toml
index 736e8c14dd..b7463bf597 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -84,6 +84,7 @@ python3-discogs-client = ">=2.3.15"
py7zr = "*"
pyxdg = "*"
rarfile = "*"
+requests-mock = ">=1.12.1"
requests_oauthlib = "*"
responses = ">=0.3.0"
diff --git a/setup.cfg b/setup.cfg
index 8cf0dc3d05..15ca23f658 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -8,6 +8,7 @@ addopts =
-ra
--strict-config
markers =
+ on_lyrics_update: mark a test to run only after lyrics source code is updated
integration_test: mark a test as an integration test
[coverage:run]
diff --git a/test/conftest.py b/test/conftest.py
index 8b29946ae2..8248a6bbf6 100644
--- a/test/conftest.py
+++ b/test/conftest.py
@@ -1,12 +1,25 @@
+from __future__ import annotations
+
import os
import pytest
-def pytest_runtest_setup(item: pytest.Item):
- """Skip integration tests if INTEGRATION_TEST environment variable is not set."""
- if os.environ.get("INTEGRATION_TEST"):
- return
+def skip_marked_items(items: list[pytest.Item], marker_name: str, reason: str):
+ for item in (i for i in items if i.get_closest_marker(marker_name)):
+ test_name = item.nodeid.split("::", 1)[-1]
+ item.add_marker(pytest.mark.skip(f"{reason}: {test_name}"))
+
+
+def pytest_collection_modifyitems(
+ config: pytest.Config, items: list[pytest.Item]
+):
+ if not os.environ.get("INTEGRATION_TEST") == "true":
+ skip_marked_items(
+ items, "integration_test", "INTEGRATION_TEST=1 required"
+ )
- if next(item.iter_markers(name="integration_test"), None):
- pytest.skip(f"INTEGRATION_TEST=1 required: {item.nodeid}")
+ if not os.environ.get("LYRICS_UPDATED") == "true":
+ skip_marked_items(
+ items, "on_lyrics_update", "No change in lyrics source code"
+ )
diff --git a/test/plugins/lyrics_download_samples.py b/test/plugins/lyrics_download_samples.py
deleted file mode 100644
index 4d68e7d50a..0000000000
--- a/test/plugins/lyrics_download_samples.py
+++ /dev/null
@@ -1,60 +0,0 @@
-# This file is part of beets.
-# Copyright 2016, Fabrice Laporte
-#
-# Permission is hereby granted, free of charge, to any person obtaining
-# a copy of this software and associated documentation files (the
-# "Software"), to deal in the Software without restriction, including
-# without limitation the rights to use, copy, modify, merge, publish,
-# distribute, sublicense, and/or sell copies of the Software, and to
-# permit persons to whom the Software is furnished to do so, subject to
-# the following conditions:
-#
-# The above copyright notice and this permission notice shall be
-# included in all copies or substantial portions of the Software.
-
-
-import os
-import sys
-
-import requests
-
-from test.plugins import test_lyrics
-
-
-def mkdir_p(path):
- try:
- os.makedirs(path)
- except OSError:
- if os.path.isdir(path):
- pass
- else:
- raise
-
-
-def safe_open_w(path):
- """Open "path" for writing, creating any parent directories as needed."""
- mkdir_p(os.path.dirname(path))
- return open(path, "w")
-
-
-def main(argv=None):
- """Download one lyrics sample page per referenced source."""
- if argv is None:
- argv = sys.argv
- print("Fetching samples from:")
- for s in test_lyrics.GOOGLE_SOURCES + test_lyrics.DEFAULT_SOURCES:
- print(s["url"])
- url = s["url"] + s["path"]
- fn = test_lyrics.url_to_filename(url)
- if not os.path.isfile(fn):
- html = requests.get(
- url,
- verify=False,
- timeout=10,
- ).text
- with safe_open_w(fn) as f:
- f.write(html.encode("utf-8"))
-
-
-if __name__ == "__main__":
- sys.exit(main())
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 a65ae84827..99e6f8a4e8 100644
--- a/test/plugins/test_lyrics.py
+++ b/test/plugins/test_lyrics.py
@@ -14,704 +14,380 @@
"""Tests for the 'lyrics' plugin."""
-import itertools
-import os
-import re
-import unittest
-from unittest.mock import MagicMock, patch
+from functools import partial
-import confuse
import pytest
-import requests
-from beets import logging
from beets.library import Item
-from beets.test import _common
-from beets.util import bytestring_path
+from beets.test.helper import PluginMixin
from beetsplug import lyrics
-log = logging.getLogger("beets.test_lyrics")
-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):
- def setUp(self):
- """Set up configuration."""
- lyrics.LyricsPlugin()
-
- 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)
-
- # Make sure that the original artist name is still the first entry
- assert ("CHVRCHΞS", ["song"]) == list(lyrics.search_pairs(item))[0]
+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",
+ "Beets song": "via plugins, beets becomes a panacea",
+}
+
+
+class TestLyricsUtils:
+ @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",
+ [
+ ("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"]),
+ ],
+ )
+ 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)
-
- 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)
-
- 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)
-
- item = Item(title="Song: B", artist="A")
- assert ("A", ["Song"]) in lyrics.search_pairs(item)
- assert ("A", ["Song: B"]) in lyrics.search_pairs(item)
-
- 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
-
- def test_is_lyrics(self):
- texts = ["LyricsMania.com - Copyright (c) 2013 - All Rights Reserved"]
- texts += [
- """All material found on this site is property\n
- of mywickedsongtext brand"""
- ]
- for t in texts:
- assert not google.is_lyrics(t)
+ assert actual_artists == [artist, *expected_extra_artists]
+
+ @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")
- def test_slugify(self):
- text = "http://site.com/\xe7afe-au_lait(boisson)"
- assert google.slugify(text) == "http://site.com/cafe_au_lait"
+ actual_titles = {
+ t: None for _, tit in lyrics.search_pairs(item) for t in tit
+ }
- def test_scrape_strip_cruft(self):
- text = """
+ assert list(actual_titles) == [title, *expected_extra_titles]
+
+ @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
+
+ @pytest.mark.parametrize(
+ "initial_text, expected",
+ [
+ (
+ """
one
two !
- four """
- 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"
+ four """,
+ "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"
assert lyrics._scrape_merge_paragraphs(text) == "one\ntwo\nthree"
- def test_missing_lyrics(self):
- assert not google.is_lyrics(LYRICS_TEXTS["missing_texts"])
+ @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):
+ return pytestconfig.rootpath / "test" / "rsrc" / "lyrics"
+
+
+class LyricsBackendTest(PluginMixin):
+ plugin = "lyrics"
+
+ @pytest.fixture
+ def plugin_config(self):
+ """Return lyrics configuration to test."""
+ return {}
+
+ @pytest.fixture
+ 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)
+
+ return lyrics.LyricsPlugin()
+
+ @pytest.fixture
+ def backend(self, lyrics_plugin):
+ """Return a lyrics backend instance."""
+ return lyrics_plugin.backends[0]
+
+ @pytest.fixture
+ def lyrics_html(self, lyrics_root_dir, file_name):
+ return (lyrics_root_dir / f"{file_name}.txt").read_text(
+ encoding="utf-8"
+ )
-def url_to_filename(url):
- url = re.sub(r"https?://|www.", "", url)
- url = re.sub(r".html", "", url)
- fn = "".join(x for x in url if (x.isalnum() or x == "/"))
- fn = fn.split("/")
- fn = os.path.join(
- LYRICS_ROOT_DIR,
- bytestring_path(fn[0]),
- bytestring_path(fn[-1] + ".txt"),
- )
- return fn
-
-
-class MockFetchUrl:
- def __init__(self, pathval="fetched_path"):
- self.pathval = pathval
- self.fetched = None
-
- def __call__(self, url, filename=None):
- self.fetched = url
- fn = url_to_filename(url)
- with open(fn, encoding="utf8") as f:
- content = f.read()
- return content
-
-
-class LyricsAssertions:
- """A mixin with lyrics-specific assertions."""
-
- def assertLyricsContentOk(self, title, text, msg=""): # noqa: N802
- """Compare lyrics text to expected lyrics for given title."""
- if not text:
- return
-
- keywords = set(LYRICS_TEXTS[google.slugify(title)].split())
- words = {x.strip(".?, ()") for x in text.lower().split()}
-
- if not keywords <= words:
- details = (
- f"{keywords!r} is not a subset of {words!r}."
- f" Words only in expected set {keywords - words!r},"
- f" Words only in result set {words - keywords!r}."
- )
- self.fail(f"{details} : {msg}")
-
-
-LYRICS_ROOT_DIR = os.path.join(_common.RSRC, b"lyrics")
-yaml_path = os.path.join(_common.RSRC, b"lyricstext.yaml")
-LYRICS_TEXTS = confuse.load_yaml(yaml_path)
-
-
-class LyricsGoogleBaseTest(unittest.TestCase):
- def setUp(self):
- """Set up configuration."""
- try:
- __import__("bs4")
- except ImportError:
- self.skipTest("Beautiful Soup 4 not available")
-
-
-class LyricsPluginSourcesTest(LyricsGoogleBaseTest, LyricsAssertions):
- """Check that beets google custom search engine sources are correctly
- scraped.
- """
-
- DEFAULT_SONG = dict(artist="The Beatles", title="Lady Madonna")
-
- DEFAULT_SOURCES = [
- # dict(artist=u'Santana', title=u'Black magic woman',
- # backend=lyrics.MusiXmatch),
- dict(
- DEFAULT_SONG,
- backend=lyrics.Genius,
- # GitHub actions is on some form of Cloudflare blacklist.
- skip=os.environ.get("GITHUB_ACTIONS") == "true",
- ),
- dict(artist="Boy In Space", title="u n eye", backend=lyrics.Tekstowo),
- ]
-
- GOOGLE_SOURCES = [
- dict(
- DEFAULT_SONG,
- url="http://www.absolutelyrics.com",
- path="/lyrics/view/the_beatles/lady_madonna",
- ),
- dict(
- DEFAULT_SONG,
- url="http://www.azlyrics.com",
- path="/lyrics/beatles/ladymadonna.html",
- # AZLyrics returns a 403 on GitHub actions.
- skip=os.environ.get("GITHUB_ACTIONS") == "true",
- ),
- dict(
- DEFAULT_SONG,
- url="http://www.chartlyrics.com",
- path="/_LsLsZ7P4EK-F-LD4dJgDQ/Lady+Madonna.aspx",
- ),
- # dict(DEFAULT_SONG,
- # url=u'http://www.elyricsworld.com',
- # path=u'/lady_madonna_lyrics_beatles.html'),
- dict(
- url="http://www.lacoccinelle.net",
- artist="Jacques Brel",
- title="Amsterdam",
- path="/paroles-officielles/275679.html",
- ),
- dict(
- DEFAULT_SONG, url="http://letras.mus.br/", path="the-beatles/275/"
- ),
- dict(
- DEFAULT_SONG,
- url="http://www.lyricsmania.com/",
- path="lady_madonna_lyrics_the_beatles.html",
- ),
- dict(
- DEFAULT_SONG,
- url="http://www.lyricsmode.com",
- path="/lyrics/b/beatles/lady_madonna.html",
- ),
- dict(
- url="http://www.lyricsontop.com",
- artist="Amy Winehouse",
- title="Jazz'n'blues",
- path="/amy-winehouse-songs/jazz-n-blues-lyrics.html",
- ),
- # dict(DEFAULT_SONG,
- # url='http://www.metrolyrics.com/',
- # path='lady-madonna-lyrics-beatles.html'),
- # dict(url='http://www.musica.com/', path='letras.asp?letra=2738',
- # artist=u'Santana', title=u'Black magic woman'),
- dict(
- url="http://www.paroles.net/",
- artist="Lilly Wood & the prick",
- title="Hey it's ok",
- path="lilly-wood-the-prick/paroles-hey-it-s-ok",
- ),
- dict(
- DEFAULT_SONG,
- url="http://www.songlyrics.com",
- path="/the-beatles/lady-madonna-lyrics",
- ),
- dict(
- DEFAULT_SONG,
- url="http://www.sweetslyrics.com",
- path="/761696.The%20Beatles%20-%20Lady%20Madonna.html",
- ),
- ]
-
- def setUp(self):
- LyricsGoogleBaseTest.setUp(self)
- self.plugin = lyrics.LyricsPlugin()
-
- @pytest.mark.integration_test
- def test_backend_sources_ok(self):
- """Test default backends with songs known to exist in respective
- databases.
- """
- # Don't test any sources marked as skipped.
- sources = [s for s in self.DEFAULT_SOURCES if not s.get("skip", False)]
- for s in sources:
- with self.subTest(s["backend"].__name__):
- backend = s["backend"](self.plugin.config, self.plugin._log)
- res = backend.fetch(s["artist"], s["title"])
- self.assertLyricsContentOk(s["title"], res)
-
- @pytest.mark.integration_test
- def test_google_sources_ok(self):
- """Test if lyrics present on websites registered in beets google custom
- search engine are correctly scraped.
- """
- # Don't test any sources marked as skipped.
- sources = [s for s in self.GOOGLE_SOURCES if not s.get("skip", False)]
- for s in sources:
- url = s["url"] + s["path"]
- res = lyrics.scrape_lyrics_from_html(raw_backend.fetch_url(url))
- assert google.is_lyrics(res), url
- self.assertLyricsContentOk(s["title"], res, url)
-
-
-class LyricsGooglePluginMachineryTest(LyricsGoogleBaseTest, LyricsAssertions):
- """Test scraping heuristics on a fake html page."""
+@pytest.mark.on_lyrics_update
+class TestLyricsSources(LyricsBackendTest):
+ @pytest.fixture(scope="class")
+ def plugin_config(self):
+ return {"google_API_key": "test", "synced": True}
- source = dict(
- url="http://www.example.com",
- artist="John Doe",
- title="Beets song",
- path="/lyrics/beetssong",
+ @pytest.fixture(
+ params=[pytest.param(lp, marks=lp.marks) for lp in lyrics_pages],
+ ids=str,
)
-
- def setUp(self):
- """Set up configuration"""
- LyricsGoogleBaseTest.setUp(self)
- self.plugin = lyrics.LyricsPlugin()
-
- @patch.object(lyrics.Backend, "fetch_url", MockFetchUrl())
- def test_mocked_source_ok(self):
- """Test that lyrics of the mocked page are correctly scraped"""
- url = self.source["url"] + self.source["path"]
- res = lyrics.scrape_lyrics_from_html(raw_backend.fetch_url(url))
- assert google.is_lyrics(res), url
- self.assertLyricsContentOk(self.source["title"], res, url)
-
- @patch.object(lyrics.Backend, "fetch_url", MockFetchUrl())
- def test_is_page_candidate_exact_match(self):
- """Test matching html page title with song infos -- when song infos are
- present in the title.
- """
- from bs4 import BeautifulSoup, SoupStrainer
-
- s = self.source
- url = str(s["url"] + s["path"])
- html = raw_backend.fetch_url(url)
- soup = BeautifulSoup(
- html, "html.parser", parse_only=SoupStrainer("title")
- )
- assert google.is_page_candidate(
- url, soup.title.string, s["title"], s["artist"]
- ), url
-
- def test_is_page_candidate_fuzzy_match(self):
- """Test matching html page title with song infos -- when song infos are
- not present in the title.
- """
- s = self.source
- url = s["url"] + s["path"]
- url_title = "example.com | Beats song by John doe"
-
- # very small diffs (typo) are ok eg 'beats' vs 'beets' with same artist
- assert google.is_page_candidate(
- url, url_title, s["title"], s["artist"]
- ), url
- # reject different title
- url_title = "example.com | seets bong lyrics by John doe"
- assert not google.is_page_candidate(
- url, url_title, s["title"], s["artist"]
- ), url
-
- def test_is_page_candidate_special_chars(self):
- """Ensure that `is_page_candidate` doesn't crash when the artist
- and such contain special regular expression characters.
- """
- # https://github.com/beetbox/beets/issues/1673
- s = self.source
- url = s["url"] + s["path"]
- url_title = "foo"
-
- google.is_page_candidate(url, url_title, s["title"], "Sunn O)))")
-
-
-# test Genius backend
-
-
-class GeniusBaseTest(unittest.TestCase):
- def setUp(self):
- """Set up configuration."""
- try:
- __import__("bs4")
- except ImportError:
- self.skipTest("Beautiful Soup 4 not available")
-
-
-class GeniusScrapeLyricsFromHtmlTest(GeniusBaseTest):
- """tests Genius._scrape_lyrics_from_html()"""
-
- def setUp(self):
- """Set up configuration"""
- GeniusBaseTest.setUp(self)
- self.plugin = lyrics.LyricsPlugin()
-
- def test_no_lyrics_div(self):
- """Ensure we don't crash when the scraping the html for a genius page
- doesn't contain
- """
- # https://github.com/beetbox/beets/issues/3535
- # expected return value None
- url = "https://genius.com/sample"
- mock = MockFetchUrl()
- assert genius._scrape_lyrics_from_html(mock(url)) is None
-
- def test_good_lyrics(self):
- """Ensure we are able to scrape a page with lyrics"""
- url = "https://genius.com/Ttng-chinchilla-lyrics"
- mock = MockFetchUrl()
- lyrics = genius._scrape_lyrics_from_html(mock(url))
- assert lyrics is not None
- assert lyrics.count("\n") == 28
-
- def test_good_lyrics_multiple_divs(self):
- """Ensure we are able to scrape a page with lyrics"""
- url = "https://genius.com/2pac-all-eyez-on-me-lyrics"
- mock = MockFetchUrl()
- lyrics = genius._scrape_lyrics_from_html(mock(url))
- assert lyrics is not None
- assert lyrics.count("\n") == 133
-
- # TODO: find an example of a lyrics page with multiple divs and test it
-
-
-class GeniusFetchTest(GeniusBaseTest):
- """tests Genius.fetch()"""
-
- def setUp(self):
- """Set up configuration"""
- GeniusBaseTest.setUp(self)
- self.plugin = lyrics.LyricsPlugin()
-
- @patch.object(lyrics.Genius, "_scrape_lyrics_from_html")
- @patch.object(lyrics.Backend, "fetch_url", return_value=True)
- def test_json(self, mock_fetch_url, mock_scrape):
- """Ensure we're finding artist matches"""
- with patch.object(
- lyrics.Genius,
- "_search",
- return_value={
- "response": {
- "hits": [
- {
- "result": {
- "primary_artist": {
- "name": "\u200bblackbear",
- },
- "url": "blackbear_url",
- }
- },
- {
- "result": {
- "primary_artist": {"name": "El\u002dp"},
- "url": "El-p_url",
- }
- },
- ]
+ 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,
}
- },
- ) as mock_json:
- # genius uses zero-width-spaces (\u200B) for lowercase
- # artists so we make sure we can match those
- assert genius.fetch("blackbear", "Idfc") is not None
- mock_fetch_url.assert_called_once_with("blackbear_url")
- mock_scrape.assert_called_once_with(True)
-
- # genius uses the hyphen minus (\u002D) as their dash
- assert genius.fetch("El-p", "Idfc") is not None
- mock_fetch_url.assert_called_with("El-p_url")
- mock_scrape.assert_called_with(True)
-
- # test no matching artist
- assert genius.fetch("doesntexist", "none") is None
-
- # test invalid json
- mock_json.return_value = None
- assert genius.fetch("blackbear", "Idfc") is None
-
- # TODO: add integration test hitting real api
-
-
-# test Tekstowo
-
-
-class TekstowoBaseTest(unittest.TestCase):
- def setUp(self):
- """Set up configuration."""
- try:
- __import__("bs4")
- except ImportError:
- self.skipTest("Beautiful Soup 4 not available")
-
-
-class TekstowoExtractLyricsTest(TekstowoBaseTest):
- """tests Tekstowo.extract_lyrics()"""
-
- def setUp(self):
- """Set up configuration"""
- TekstowoBaseTest.setUp(self)
- self.plugin = lyrics.LyricsPlugin()
- tekstowo.config = self.plugin.config
-
- def test_good_lyrics(self):
- """Ensure we are able to scrape a page with lyrics"""
- url = "https://www.tekstowo.pl/piosenka,24kgoldn,city_of_angels_1.html"
- mock = MockFetchUrl()
- assert tekstowo.extract_lyrics(mock(url))
-
- def test_no_lyrics(self):
- """Ensure we don't crash when the scraping the html for a Tekstowo page
- doesn't contain lyrics
- """
- url = (
- "https://www.tekstowo.pl/piosenka,beethoven,"
- "beethoven_piano_sonata_17_tempest_the_3rd_movement.html"
- )
- mock = MockFetchUrl()
- assert not tekstowo.extract_lyrics(mock(url))
-
-
-class TekstowoIntegrationTest(TekstowoBaseTest, LyricsAssertions):
- """Tests Tekstowo lyric source with real requests"""
-
- def setUp(self):
- """Set up configuration"""
- TekstowoBaseTest.setUp(self)
- self.plugin = lyrics.LyricsPlugin()
- tekstowo.config = self.plugin.config
-
- @pytest.mark.integration_test
- def test_normal(self):
- """Ensure we can fetch a song's lyrics in the ordinary case"""
- lyrics = tekstowo.fetch("Boy in Space", "u n eye")
- self.assertLyricsContentOk("u n eye", lyrics)
-
- @pytest.mark.integration_test
- def test_no_matching_results(self):
- """Ensure we fetch nothing if there are search results
- returned but no matches"""
- # https://github.com/beetbox/beets/issues/4406
- # expected return value None
- lyrics = tekstowo.fetch("Kelly Bailey", "Black Mesa Inbound")
- assert lyrics is 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)
- assert lyrics == mock_response["plainLyrics"]
-
- self.plugin.config["synced"] = True
- lyrics = lrclib.fetch("la", "la", "la", 999)
- assert 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)
+ requests_mock.get(lyrics.Google.SEARCH_URL, json=data)
- assert 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)
+ 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 is None
+ assert lyrics
+ assert lyrics == lyrics_page.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)
+class TestGoogleLyrics(LyricsBackendTest):
+ """Test scraping heuristics on a fake html page."""
- assert lyrics is None
+ TITLE = "Beets song"
+ @pytest.fixture(scope="class")
+ def backend_name(self):
+ return "google"
-class LRCLibIntegrationTest(LyricsAssertions):
- def setUp(self):
- self.plugin = lyrics.LyricsPlugin()
- lrclib.config = self.plugin.config
+ @pytest.fixture(scope="class")
+ def plugin_config(self):
+ return {"google_API_key": "test"}
- @pytest.mark.integration_test
- def test_track_with_lyrics(self):
- lyrics = lrclib.fetch("Boy in Space", "u n eye", "Live EP", 160)
- self.assertLyricsContentOk("u n eye", lyrics)
+ @pytest.fixture(scope="class")
+ def file_name(self):
+ return "examplecom/beetssong"
- @pytest.mark.integration_test
- def test_instrumental_track(self):
- lyrics = lrclib.fetch(
- "Kelly Bailey", "Black Mesa Inbound", "Half Life 2 Soundtrack", 134
+ 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()
+
+ assert result
+ assert backend.is_lyrics(result)
+ assert PHRASE_BY_TITLE[self.TITLE] in result
+
+ @pytest.mark.parametrize(
+ "url_title, artist, should_be_candidate",
+ [
+ ("John Doe - beets song Lyrics", "John Doe", True),
+ ("example.com | Beats song by John doe", "John Doe", True),
+ ("example.com | seets bong lyrics by John doe", "John Doe", False),
+ ("foo", "Sun O)))", False),
+ ],
+ )
+ def test_is_page_candidate(
+ self, backend, lyrics_html, url_title, artist, should_be_candidate
+ ):
+ result = backend.is_page_candidate(
+ "http://www.example.com/lyrics/beetssong",
+ url_title,
+ self.TITLE,
+ artist,
)
- assert lyrics is None
-
- @pytest.mark.integration_test
- def test_nonexistent_track(self):
- lyrics = lrclib.fetch("blah", "blah", "blah", 999)
- assert lyrics is None
+ assert bool(result) == should_be_candidate
+ @pytest.mark.parametrize(
+ "lyrics",
+ [
+ "LyricsMania.com - Copyright (c) 2013 - All Rights Reserved",
+ """All material found on this site is property\n
+ of mywickedsongtext brand""",
+ """
+Lyricsmania staff is working hard for you to add $TITLE lyrics as soon
+as they'll be released by $ARTIST, check back soon!
+In case you have the lyrics to $TITLE and want to send them to us, fill out
+the following form.
+""",
+ ],
+ )
+ def test_bad_lyrics(self, backend, lyrics):
+ assert not backend.is_lyrics(lyrics)
-# test utilities
-
+ def test_slugify(self, backend):
+ text = "http://site.com/\xe7afe-au_lait(boisson)"
+ assert backend.slugify(text) == "http://site.com/cafe_au_lait"
+
+
+class TestGeniusLyrics(LyricsBackendTest):
+ @pytest.fixture(scope="class")
+ def backend_name(self):
+ return "genius"
+
+ @pytest.mark.parametrize(
+ "file_name, expected_line_count",
+ [
+ ("geniuscom/2pacalleyezonmelyrics", 134),
+ ("geniuscom/Ttngchinchillalyrics", 29),
+ ("geniuscom/sample", 0), # see https://github.com/beetbox/beets/issues/3535
+ ],
+ ) # fmt: skip
+ def test_scrape(self, backend, lyrics_html, expected_line_count):
+ result = backend._scrape_lyrics_from_html(lyrics_html) or ""
+
+ assert len(result.splitlines()) == expected_line_count
+
+
+class TestTekstowoLyrics(LyricsBackendTest):
+ @pytest.fixture(scope="class")
+ def backend_name(self):
+ return "tekstowo"
+
+ @pytest.mark.parametrize(
+ "file_name, expecting_lyrics",
+ [
+ ("tekstowopl/piosenka24kgoldncityofangels1", True),
+ (
+ "tekstowopl/piosenkabeethovenbeethovenpianosonata17tempestthe3rdmovement", # noqa: E501
+ False,
+ ),
+ ],
+ )
+ def test_scrape(self, backend, lyrics_html, expecting_lyrics):
+ assert bool(backend.extract_lyrics(lyrics_html)) == expecting_lyrics
-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"
+class TestLRCLibLyrics(LyricsBackendTest):
+ @pytest.fixture(scope="class")
+ def backend_name(self):
+ return "lrclib"
- # more accents and quotes
- text = "l'été c'est fait pour jouer"
- assert lyrics.slug(text) == "l-ete-c-est-fait-pour-jouer"
+ @pytest.fixture
+ def fetch_lyrics(self, backend, requests_mock, response_data):
+ requests_mock.get(lyrics.LRCLib.base_url, json=response_data)
- # 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"
+ return partial(backend.fetch, "la", "la", "la", 0)
- # variations of dashes should get standardized
- dashes = ["\u200d", "\u2010"]
- for dash1, dash2 in itertools.combinations(dashes, 2):
- assert lyrics.slug(dash1) == lyrics.slug(dash2)
+ @pytest.mark.parametrize(
+ "response_data",
+ [
+ {
+ "syncedLyrics": "[00:00.00] la la la",
+ "plainLyrics": "la la la",
+ }
+ ],
+ )
+ @pytest.mark.parametrize(
+ "plugin_config, expected_lyrics",
+ [
+ ({"synced": True}, "[00:00.00] la la la"),
+ ({"synced": False}, "la la la"),
+ ],
+ )
+ def test_synced_config_option(self, fetch_lyrics, expected_lyrics):
+ assert fetch_lyrics() == expected_lyrics
+
+ @pytest.mark.parametrize(
+ "response_data, expected_lyrics",
+ [
+ pytest.param(
+ {"syncedLyrics": "", "plainLyrics": "la la la"},
+ "la la la",
+ id="pick plain lyrics",
+ ),
+ pytest.param(
+ {
+ "statusCode": 404,
+ "error": "Not Found",
+ "message": "Failed to find specified track",
+ },
+ None,
+ id="not found",
+ ),
+ ],
+ )
+ def test_fetch_lyrics(self, fetch_lyrics, expected_lyrics):
+ assert fetch_lyrics() == expected_lyrics
diff --git a/test/rsrc/lyrics/geniuscom/Wutangclancreamlyrics.txt b/test/rsrc/lyrics/geniuscom/Wutangclancreamlyrics.txt
deleted file mode 100644
index 08518f8ee2..0000000000
--- a/test/rsrc/lyrics/geniuscom/Wutangclancreamlyrics.txt
+++ /dev/null
@@ -1,2227 +0,0 @@
-
-
-
-
-
-
-
-Wu-Tang Clan – C.R.E.A.M. Lyrics | Genius Lyrics
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Beets is awesome!
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- About “C.R.E.A.M.”
-
-
-
-
-
-
Arguably one of the most iconic songs in hip-hop , the underlying idea of “C.R.E.A.M.” is found in its title—cash rules everything . The timeless piano riffs and background vocals come from a chopped up sample of The Charmels ‘ 1967 record, “As Long As I’ve Got You,” that make up the entire track.
-
-
Although it was released as an official single in 1994, “C.R.E.A.M.” was first recorded in 1991, around the same time as RZA’s assault case , and featured himself and Ghostface Killah. The track went through several revisions and was later re-recorded by Raekwon and Inspectah Deck in 1993—an early title of the song was “Lifestyles of the Mega-Rich.”
-
-
In 2017, RZA explained to Power 106 how the final version of the track came together:
-
-
Once we got to the studio, I decided that this track had to be on the Wu-Tang album. I reminded Rae and Deck of their verses—their verses were long. […] Method Man, the master of hooks at the time, came in with this hook right here: ‘cash rules everything around me, cream, get the money.’ Once he added that element, I knew it was going to be a smash.
-
-
Since its release, the song and chorus have been referenced countless times by several artists. It has also been featured in movies such as Eminem ’s 8 Mile and the N.W.A biopic, Straight Outta Compton .
-
-
-
-
-
-
-
-
-
-
-
What has RZA said about the song?
-
-
-
-
-
-
-
-
-
-
-
What has Raekwon said about the song?
-
-
-
-
-‘C.R.E.A.M.’ did a lot for my career personally. It gave me an opportunity to revisit the times where that cream meant that much to us. So, yeah, when I think of this record it just automatically puts me back into ‘87/’88 where we were standing in front of the building. It’s cold outside. We didn’t care. We’re out there, all black on trying to make dollars. Just trying to make some money and trying to eat. Survive.
-
-This song, I remember writing to the beat a long time ago before we actually came out. That beat is old. That was probably like a ‘89 beat. RZA had it that long because he had a bunch of breaks. He had all kind of things and he was making beats back then, but we was just picking and that beat happened to always sit around and I would be like, ‘I want that beat, so don’t give that beat to nobody.’ And he kept his word and let me have it.
-
-Meth came up with the hook but our dude named Raider Ruckus, this was like Meth’s homeboy back then, like they was real close, he came up with the phrase ‘cash rules everything around me.’ So when he showed Meth what it was and was like, ‘Cash rules everything around me,’ Meth was like, ‘Word, you right!’ And turned it into a movie, and I came in later that day and heard it and co-signed it.
-
-
-
via Complex
-
-
-
-
-
-
-
-
-
What has U-God said about the song?
-
-
-
-
“C.R.E.A.M.” is a true song. Everything Inspectah Deck and Raekwon said is 100 percent true. Not one line in that entire song is a lie, or even a slight exaggeration. Deck did sell base, and he did go to jail at the age of fifteen. Rae was sticking up white boys on ball courts, rocking the same damn ’Lo sweater. And of “course, Meth on the hook was like butter on the popcorn. Meth knew the hard times, too, being out there smoking woolies and pumping crack, etc. That raspy shit he was kicking just echoed in everyone’s head long after the song was done playing. The realism on “C.R.E.A.M.” is what resonates with so many people all over the world. People everywhere know that sentiment of being slaves to the dollar. Cash is king, and we are its lowly subjects. That’s pretty much the case in every nation around the world, the desperation to put your life and your freedom on the line to make a couple dollars. Whether you’re working, stripping, hustling, or slinging, whether you’re a business owner or homeless, cash rules everything around us.
-
-
Source: Raw:My Journey into Wu-Tang
-
-
-
-
-
-
-
-
-
What songs were sampled on the beat for “C.R.E.A.M.?”
-
-
-
-
The vocals and background sample that can be heard on the song’s intro were taken taken from The Charmels ’ 1967 song “As Long as I’ve Got You” :
-
-
-VIDEO
-
-
-
The classic keys sample that can be heard throughout the beat was also taken from the previously mentioned song:
-
-
-VIDEO
-
-
-
-
-
-
-
-
-
-
What has Method Man said about the song?
-
-
-
-
Meth told Complex ,
-
-
-‘C.R.E.A.M.’ was the one that really put us on the map if you wanna be technical. I wasn’t there when they recorded ‘C.R.E.A.M.’ I came in after the fact. RZA was like, ‘Put a hook on this song’ and I put a hook on it. That’s how it always went. I liked doing hooks.
-
-The hook for that was done by my man Raider Ruckus. We used to work at the Statue of Liberty and when we were coming home we used to come up with all these made-up words that were acronyms.
-
-We had words like ‘BIBWAM’ which meant, ‘Bitches Is Busted Without A Man’ and all this other crazy shit. Raider Ruckus was so ill with the way he put the words together. We would call money ‘cream’ so he took each letter and made a word out of it and killed it the way he did it.
-
-Something like that had never been done before as far as a hook or even a way of speaking. This is just showing and proving that we paid attention in class when we was kids. You can’t do shit like that unless you got a brain in your fucking head! You got to have some level of intelligence to do something like that.
-
-The best acronym for a word that I heard was ‘P.R.O.J.E.C.T.S.’ by Killah Priest. He said ‘People Relying On Just Enough Cash To Survive.’ And he’s the one that came up with ‘Basic Instructions Before Leaving Earth,’ the acronym for B.I.B.L.E. This ain’t no fluke shit man.
-
-There’s a reason you got millions upon millions of fucking kids running around with Wu-Tang tattoos. You don’t just put something on your body permanently unless it’s official. At that time, when you’re coming out brand new and representing where you come from, everybody from that area wants you to win because they win. That’s what it was like for us.
-
-We were the only dudes from Staten Island doing it so everybody from Staten Island wanted us to win. Not just dudes from Staten Island, but dudes from Brooklyn too because they had peoples in the group too. Then it was just grimy niggas who loved to see real shit, saying, ‘We riding with them Wu-Tang niggas. Fuck all that shiny suit shit!’ That ain’t no take on Puff, a lot of niggas was wearing suits and shit man, but that ain’t us.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 8.
-
-
-
- C.R.E.A.M.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/test/rsrc/lyricstext.yaml b/test/rsrc/lyricstext.yaml
deleted file mode 100644
index 4cec7802a0..0000000000
--- a/test/rsrc/lyricstext.yaml
+++ /dev/null
@@ -1,62 +0,0 @@
-# Song used by LyricsGooglePluginMachineryTest
-
-Beets_song: |
- beets is the media library management system for obsessive music geeks the purpose of
- beets is to get your music collection right once and for all it catalogs your collection
- automatically improving its metadata as it goes it then provides a bouquet of tools for
- manipulating and accessing your music here's an example of beets' brainy tag corrector doing its
- because beets is designed as a library it can do almost anything you can imagine for your
- music collection via plugins beets becomes a panacea
-
-missing_texts: |
- Lyricsmania staff is working hard for you to add $TITLE lyrics as soon
- as they'll be released by $ARTIST, check back soon!
- In case you have the lyrics to $TITLE and want to send them to us, fill out
- the following form.
-
-# Songs lyrics used to test the different sources present in the google custom search engine.
-# Text is randomized for copyright infringement reason.
-
-Amsterdam: |
- coup corps coeur invitent mains comme trop morue le hantent mais la dames joli revenir aux
- mangent croquer pleine plantent rire de sortent pleins fortune d'amsterdam bruit ruisselants
- large poissons braguette leur putains blanches jusque pissent dans soleils dansent et port
- bien vertu nez sur chaleur femmes rotant dorment marins boivent bu les que d'un qui je
- une cou hambourg plus ils dents ou tournent or berges d'ailleurs tout ciel haubans ce son lueurs
- en lune ont mouchent leurs long frottant jusqu'en vous regard montrent langueurs chantent
- tordent pleure donnent drames mornes des panse pour un sent encore referment nappes au meurent
- geste quand puis alors frites grosses batave expire naissent reboivent oriflammes grave riant a
- enfin rance fier y bouffer s'entendre se mieux
-
-Lady_Madonna: |
- feed his money tuesday manage didn't head feet see arrives at in madonna rest morning children
- wonder how make thursday your to sunday music papers come tie you has was is listen suitcase
- ends friday run that needed breast they child baby mending on lady learned a nun like did wednesday
- bed think without afternoon night meet the playing lying
-
-Jazz_n_blues: |
- all shoes money through follow blow til father to his hit jazz kiss now cool bar cause 50 night
- heading i'll says yeah cash forgot blues out what for ways away fingers waiting got ever bold
- screen sixty throw wait on about last compton days o pick love wall had within jeans jd next
- miss standing from it's two long fight extravagant tell today more buy shopping that didn't
- what's but russian up can parkway balance my and gone am it as at in check if bags when cross
- machine take you drinks coke june wrong coming fancy's i n' impatient so the main's spend
- that's
-
-Hey_it_s_ok: |
- and forget be when please it against fighting mama cause ! again what said
- things papa hey to much lovers way wet was too do drink and i who forgive
- hey fourteen please know not wanted had myself ok friends bed times looked
- swear act found the my mean
-
-Black_magic_woman: |
- blind heart sticks just don't into back alone see need yes your out devil make that to black got
- you might me woman turning spell stop baby with 'round a on stone messin' magic i of
- tricks up leave turn bad so pick she's my can't
-
-u_n_eye: |
- let see cool bed for sometimes are place told in yeah or ride open hide blame knee your my borders
- perfect i of laying lies they love the night all out saying fast things said that on face hit hell
- no low not bullets bullet fly time maybe over is roof a it know now airplane where and tonight
- brakes just waste we go an to you was going eye start need insane cross gotta mood life with
- hurts too whoa me fight little every oh would thousand but high lay space do down private