From 29580d2f00eb02f1ce77ebe3e1435804f7252ba1 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0ar=C5=ABnas=20Nejus?=
Date: Fri, 29 May 2026 18:47:00 +0100
Subject: [PATCH 1/5] Import everything from parent beets.autotag
---
beets/autotag/__init__.py | 27 ++++++++++++++++++++++-----
beets/importer/tasks.py | 6 ++----
beets/metadata_plugins.py | 3 ++-
beets/test/helper.py | 2 +-
beets/ui/commands/import_/display.py | 4 ++--
beets/ui/commands/import_/session.py | 10 ++++++++--
beetsplug/beatport.py | 2 +-
beetsplug/bench.py | 2 +-
beetsplug/bpsync.py | 3 +--
beetsplug/chroma.py | 4 ++--
beetsplug/deezer.py | 2 +-
beetsplug/discogs/__init__.py | 3 +--
beetsplug/discogs/states.py | 2 +-
beetsplug/discogs/types.py | 2 +-
beetsplug/lyrics.py | 2 +-
beetsplug/mbpseudo.py | 7 ++-----
beetsplug/mbsubmit.py | 2 +-
beetsplug/mbsync.py | 3 +--
beetsplug/musicbrainz.py | 2 +-
beetsplug/spotify.py | 2 +-
beetsplug/tidal/__init__.py | 2 +-
beetsplug/titlecase.py | 4 ++--
docs/changelog.rst | 9 ++++++---
docs/dev/plugins/autotagger.rst | 2 +-
test/autotag/test_distance.py | 5 +++--
test/autotag/test_hooks.py | 4 ++--
test/autotag/test_match.py | 9 +++++++--
test/conftest.py | 2 +-
test/plugins/test_art.py | 3 +--
test/plugins/test_chroma.py | 2 +-
test/plugins/test_mbpseudo.py | 3 +--
test/plugins/test_mbsync.py | 2 +-
test/plugins/test_missing.py | 2 +-
test/plugins/test_titlecase.py | 2 +-
test/test_importer.py | 3 +--
test/ui/commands/test_import.py | 3 +--
36 files changed, 84 insertions(+), 63 deletions(-)
diff --git a/beets/autotag/__init__.py b/beets/autotag/__init__.py
index 094ed9e9b..12aa65076 100644
--- a/beets/autotag/__init__.py
+++ b/beets/autotag/__init__.py
@@ -21,8 +21,18 @@ from importlib import import_module
# Parts of external interface.
from beets.util.deprecation import deprecate_for_maintainers, deprecate_imports
-from .hooks import AlbumInfo, AlbumMatch, TrackInfo, TrackMatch
-from .match import Proposal, Recommendation, tag_album, tag_item
+from .distance import Distance, distance, string_dist, track_distance
+from .hooks import (
+ AlbumInfo,
+ AlbumMatch,
+ AttrDict,
+ Info,
+ Match,
+ TrackInfo,
+ TrackMatch,
+ correct_list_fields,
+)
+from .match import Proposal, Recommendation, assign_items, tag_album, tag_item
def __getattr__(name: str):
@@ -32,18 +42,25 @@ def __getattr__(name: str):
)
return import_module("beets.util").get_most_common_tags
- return deprecate_imports(
- __name__, {"Distance": "beets.autotag.distance"}, name
- )
+ return deprecate_imports(__name__, {}, name)
__all__ = [
"AlbumInfo",
"AlbumMatch",
+ "AttrDict",
+ "Distance",
+ "Info",
+ "Match",
"Proposal",
"Recommendation",
"TrackInfo",
"TrackMatch",
+ "assign_items",
+ "correct_list_fields",
+ "distance",
+ "string_dist",
"tag_album",
"tag_item",
+ "track_distance",
]
diff --git a/beets/importer/tasks.py b/beets/importer/tasks.py
index cbc12b62b..e31d581bb 100644
--- a/beets/importer/tasks.py
+++ b/beets/importer/tasks.py
@@ -28,8 +28,7 @@ from typing import TYPE_CHECKING, Any
import mediafile
from beets import config, library, plugins, util
-from beets.autotag.hooks import AlbumMatch
-from beets.autotag.match import tag_album, tag_item
+from beets.autotag import AlbumMatch, tag_album, tag_item
from beets.dbcore.query import PathQuery
from beets.util import extension
from beets.util.extension import remux_mpeglayer3_wav
@@ -39,8 +38,7 @@ from .state import ImportState
if TYPE_CHECKING:
from collections.abc import Iterable, Sequence
- from beets.autotag.hooks import TrackMatch
- from beets.autotag.match import Recommendation
+ from beets.autotag import Recommendation, TrackMatch
from .session import ImportSession
diff --git a/beets/metadata_plugins.py b/beets/metadata_plugins.py
index 0bac956a0..7bcd30399 100644
--- a/beets/metadata_plugins.py
+++ b/beets/metadata_plugins.py
@@ -36,7 +36,8 @@ QueryType = Literal["album", "track"]
if TYPE_CHECKING:
from collections.abc import Callable, Iterable, Iterator, Sequence
- from .autotag.hooks import AlbumInfo, Item, TrackInfo
+ from .autotag import AlbumInfo, TrackInfo
+ from .library.models import Item
# Global logger.
log = logging.getLogger("beets")
diff --git a/beets/test/helper.py b/beets/test/helper.py
index cb66917ad..e2f6fda00 100644
--- a/beets/test/helper.py
+++ b/beets/test/helper.py
@@ -46,7 +46,7 @@ from mediafile import Image, MediaFile
import beets
import beets.plugins
from beets import importer, logging, util
-from beets.autotag.hooks import AlbumInfo, TrackInfo
+from beets.autotag import AlbumInfo, TrackInfo
from beets.importer import ImportSession
from beets.library import Item, Library
from beets.test import _common
diff --git a/beets/ui/commands/import_/display.py b/beets/ui/commands/import_/display.py
index b4903adae..187da0bda 100644
--- a/beets/ui/commands/import_/display.py
+++ b/beets/ui/commands/import_/display.py
@@ -7,7 +7,7 @@ from functools import cached_property
from typing import TYPE_CHECKING
from beets import config, ui
-from beets.autotag.hooks import TrackInfo
+from beets.autotag import TrackInfo
from beets.util import displayable_path
from beets.util.color import colorize
from beets.util.diff import colordiff
@@ -17,7 +17,7 @@ from beets.util.units import human_seconds_short
if TYPE_CHECKING:
import confuse
- from beets.autotag.hooks import AlbumMatch, Match, TrackMatch
+ from beets.autotag import AlbumMatch, Match, TrackMatch
from beets.library.models import Item
from beets.util.color import ColorName
diff --git a/beets/ui/commands/import_/session.py b/beets/ui/commands/import_/session.py
index de8d09a18..5806b1b0f 100644
--- a/beets/ui/commands/import_/session.py
+++ b/beets/ui/commands/import_/session.py
@@ -4,8 +4,14 @@ from collections import Counter
from itertools import chain
from beets import config, importer, logging, plugins, ui
-from beets.autotag.hooks import AlbumMatch, TrackMatch
-from beets.autotag.match import Proposal, Recommendation, tag_album, tag_item
+from beets.autotag import (
+ AlbumMatch,
+ Proposal,
+ Recommendation,
+ TrackMatch,
+ tag_album,
+ tag_item,
+)
from beets.util import PromptChoice, displayable_path
from beets.util.color import colorize
from beets.util.units import human_bytes, human_seconds_short
diff --git a/beetsplug/beatport.py b/beetsplug/beatport.py
index 92e8839fd..c3506f7bf 100644
--- a/beetsplug/beatport.py
+++ b/beetsplug/beatport.py
@@ -32,7 +32,7 @@ from requests_oauthlib.oauth1_session import (
import beets
import beets.ui
from beets import config
-from beets.autotag.hooks import AlbumInfo, TrackInfo
+from beets.autotag import AlbumInfo, TrackInfo
from beets.exceptions import UserError
from beets.metadata_plugins import MetadataSourcePlugin
from beets.util import unique_list
diff --git a/beetsplug/bench.py b/beetsplug/bench.py
index cc29b14cb..9df32af43 100644
--- a/beetsplug/bench.py
+++ b/beetsplug/bench.py
@@ -18,7 +18,7 @@ import cProfile
import timeit
from beets import importer, plugins, ui
-from beets.autotag.match import tag_album
+from beets.autotag import tag_album
from beets.plugins import BeetsPlugin
from beets.util.functemplate import Template
from beets.util.pathformats import PF_KEY_DEFAULT
diff --git a/beetsplug/bpsync.py b/beetsplug/bpsync.py
index 6583e3fe8..c13ade036 100644
--- a/beetsplug/bpsync.py
+++ b/beetsplug/bpsync.py
@@ -15,8 +15,7 @@
"""Update library's tags using Beatport."""
from beets import library, ui, util
-from beets.autotag.distance import Distance
-from beets.autotag.hooks import AlbumMatch, TrackMatch
+from beets.autotag import AlbumMatch, Distance, TrackMatch
from beets.plugins import BeetsPlugin, apply_item_changes
from beets.util.deprecation import deprecate_for_user
diff --git a/beetsplug/chroma.py b/beetsplug/chroma.py
index eb001d90e..0cd280b0a 100644
--- a/beetsplug/chroma.py
+++ b/beetsplug/chroma.py
@@ -28,7 +28,7 @@ import acoustid
import confuse
from beets import config, ui, util
-from beets.autotag.distance import Distance
+from beets.autotag import Distance
from beets.exceptions import UserError
from beets.metadata_plugins import MetadataSourcePlugin, get_metadata_source
from beets.util.color import colorize
@@ -36,7 +36,7 @@ from beets.util.color import colorize
if TYPE_CHECKING:
from collections.abc import Iterable, Iterator
- from beets.autotag.hooks import TrackInfo
+ from beets.autotag import TrackInfo
from beets.library.models import Item
from beetsplug.musicbrainz import MusicBrainzPlugin
diff --git a/beetsplug/deezer.py b/beetsplug/deezer.py
index 209ae035c..49b570aaa 100644
--- a/beetsplug/deezer.py
+++ b/beetsplug/deezer.py
@@ -23,7 +23,7 @@ from typing import TYPE_CHECKING, ClassVar
import requests
from beets import config, ui
-from beets.autotag.hooks import AlbumInfo, TrackInfo
+from beets.autotag import AlbumInfo, TrackInfo
from beets.dbcore import types
from beets.exceptions import UserError
from beets.metadata_plugins import IDResponse, SearchApiMetadataSourcePlugin
diff --git a/beetsplug/discogs/__init__.py b/beetsplug/discogs/__init__.py
index a1c95269b..fcf4c090e 100644
--- a/beetsplug/discogs/__init__.py
+++ b/beetsplug/discogs/__init__.py
@@ -37,8 +37,7 @@ from requests.exceptions import ConnectionError
import beets
import beets.ui
from beets import config, util
-from beets.autotag.distance import string_dist
-from beets.autotag.hooks import AlbumInfo, TrackInfo
+from beets.autotag import AlbumInfo, TrackInfo, string_dist
from beets.exceptions import UserError
from beets.metadata_plugins import IDResponse, SearchApiMetadataSourcePlugin
diff --git a/beetsplug/discogs/states.py b/beetsplug/discogs/states.py
index 9e1405a3f..9324b5024 100644
--- a/beetsplug/discogs/states.py
+++ b/beetsplug/discogs/states.py
@@ -29,7 +29,7 @@ from .types import ArtistInfo
if TYPE_CHECKING:
from confuse import ConfigView
- from beets.autotag.hooks import TrackInfo
+ from beets.autotag import TrackInfo
from . import DiscogsPlugin
from .types import Artist, Track, TracklistInfo
diff --git a/beetsplug/discogs/types.py b/beetsplug/discogs/types.py
index cbd95e901..14ef138b9 100644
--- a/beetsplug/discogs/types.py
+++ b/beetsplug/discogs/types.py
@@ -19,7 +19,7 @@ from typing import TYPE_CHECKING
from typing_extensions import NotRequired, TypedDict
if TYPE_CHECKING:
- from beets.autotag.hooks import TrackInfo
+ from beets.autotag import TrackInfo
class ReleaseFormat(TypedDict):
diff --git a/beetsplug/lyrics.py b/beetsplug/lyrics.py
index 8dbfed256..263f6baa1 100644
--- a/beetsplug/lyrics.py
+++ b/beetsplug/lyrics.py
@@ -34,7 +34,7 @@ from bs4 import BeautifulSoup
from unidecode import unidecode
from beets import plugins, ui
-from beets.autotag.distance import string_dist
+from beets.autotag import string_dist
from beets.dbcore import types
from beets.dbcore.query import FalseQuery
from beets.library import Item, parse_query_string
diff --git a/beetsplug/mbpseudo.py b/beetsplug/mbpseudo.py
index 30bb9dd56..5b0ade385 100644
--- a/beetsplug/mbpseudo.py
+++ b/beetsplug/mbpseudo.py
@@ -25,9 +25,7 @@ import mediafile
from typing_extensions import override
from beets import config
-from beets.autotag.distance import distance
-from beets.autotag.hooks import AlbumInfo
-from beets.autotag.match import assign_items
+from beets.autotag import AlbumInfo, assign_items, distance
from beets.plugins import find_plugins
from beets.util.id_extractors import extract_release_id
from beetsplug.musicbrainz import (
@@ -39,8 +37,7 @@ from beetsplug.musicbrainz import (
if TYPE_CHECKING:
from collections.abc import Iterable, Sequence
- from beets.autotag.distance import Distance
- from beets.autotag.hooks import AlbumMatch
+ from beets.autotag import AlbumMatch, Distance
from beets.library import Item
from ._utils.musicbrainz import (
diff --git a/beetsplug/mbsubmit.py b/beetsplug/mbsubmit.py
index 2d97c2bc9..7136f4c29 100644
--- a/beetsplug/mbsubmit.py
+++ b/beetsplug/mbsubmit.py
@@ -24,7 +24,7 @@ implemented by MusicBrainz yet.
import subprocess
from beets import ui
-from beets.autotag.match import Recommendation
+from beets.autotag import Recommendation
from beets.plugins import BeetsPlugin
from beets.util import PromptChoice, displayable_path
from beetsplug.info import print_data
diff --git a/beetsplug/mbsync.py b/beetsplug/mbsync.py
index d61679a39..609b43829 100644
--- a/beetsplug/mbsync.py
+++ b/beetsplug/mbsync.py
@@ -17,8 +17,7 @@
from collections import defaultdict
from beets import library, metadata_plugins, ui, util
-from beets.autotag.distance import Distance
-from beets.autotag.hooks import AlbumMatch, TrackMatch
+from beets.autotag import AlbumMatch, Distance, TrackMatch
from beets.plugins import BeetsPlugin, apply_item_changes
diff --git a/beetsplug/musicbrainz.py b/beetsplug/musicbrainz.py
index 65af1c27d..ea236ffde 100644
--- a/beetsplug/musicbrainz.py
+++ b/beetsplug/musicbrainz.py
@@ -27,7 +27,7 @@ from confuse.exceptions import NotFoundError
from typing_extensions import NotRequired
from beets import config, plugins, util
-from beets.autotag.hooks import AlbumInfo, TrackInfo
+from beets.autotag import AlbumInfo, TrackInfo
from beets.metadata_plugins import IDResponse, SearchApiMetadataSourcePlugin
from beets.util.deprecation import deprecate_for_user
from beets.util.id_extractors import extract_release_id
diff --git a/beetsplug/spotify.py b/beetsplug/spotify.py
index b3a933336..6a5bbdc37 100644
--- a/beetsplug/spotify.py
+++ b/beetsplug/spotify.py
@@ -34,7 +34,7 @@ import confuse
import requests
from beets import ui
-from beets.autotag.hooks import AlbumInfo, TrackInfo
+from beets.autotag import AlbumInfo, TrackInfo
from beets.dbcore import types
from beets.exceptions import UserError
from beets.library import Library
diff --git a/beetsplug/tidal/__init__.py b/beetsplug/tidal/__init__.py
index a617050b2..214722603 100644
--- a/beetsplug/tidal/__init__.py
+++ b/beetsplug/tidal/__init__.py
@@ -9,7 +9,7 @@ from typing import TYPE_CHECKING, overload
import confuse
from beets import ui
-from beets.autotag.hooks import AlbumInfo, TrackInfo
+from beets.autotag import AlbumInfo, TrackInfo
from beets.exceptions import UserError
from beets.logging import getLogger
from beets.metadata_plugins import MetadataSourcePlugin
diff --git a/beetsplug/titlecase.py b/beetsplug/titlecase.py
index 634f5fe4d..c7062d0d9 100644
--- a/beetsplug/titlecase.py
+++ b/beetsplug/titlecase.py
@@ -25,11 +25,11 @@ from typing import TYPE_CHECKING, TypedDict
from titlecase import titlecase
from beets import ui
-from beets.autotag.hooks import AlbumInfo
+from beets.autotag import AlbumInfo
from beets.plugins import BeetsPlugin
if TYPE_CHECKING:
- from beets.autotag.hooks import Info
+ from beets.autotag import Info
from beets.importer import ImportSession, ImportTask
from beets.library import Item
diff --git a/docs/changelog.rst b/docs/changelog.rst
index 851a2f5e6..c4d642dbe 100644
--- a/docs/changelog.rst
+++ b/docs/changelog.rst
@@ -68,9 +68,12 @@ Bug fixes
errors (e.g. a file locked by another process) are logged as warnings instead
of crashing beets. :bug:`6193`
-..
- For plugin developers
- ~~~~~~~~~~~~~~~~~~~~~
+For plugin developers
+~~~~~~~~~~~~~~~~~~~~~
+
+- Plugin authors can import all autotagger helpers directly from
+ ``beets.autotag``, including match classes, distance helpers, and
+ ``assign_items``, without relying on lower-level autotag modules.
Other changes
~~~~~~~~~~~~~
diff --git a/docs/dev/plugins/autotagger.rst b/docs/dev/plugins/autotagger.rst
index dc0e41be3..5ab2891c9 100644
--- a/docs/dev/plugins/autotagger.rst
+++ b/docs/dev/plugins/autotagger.rst
@@ -29,7 +29,7 @@ Here`s a minimal example:
# beetsplug/myawesomeplugin.py
from typing import Sequence
- from beets.autotag.hooks import Item
+ from beets.library import Item
from beets.metadata_plugins import MetadataSourcePlugin
diff --git a/test/autotag/test_distance.py b/test/autotag/test_distance.py
index b5226d41d..11b6d29d5 100644
--- a/test/autotag/test_distance.py
+++ b/test/autotag/test_distance.py
@@ -2,13 +2,14 @@ import re
import pytest
-from beets.autotag.distance import (
+from beets.autotag import (
+ AlbumInfo,
Distance,
+ TrackInfo,
distance,
string_dist,
track_distance,
)
-from beets.autotag.hooks import AlbumInfo, TrackInfo
from beets.library import Item
from beets.metadata_plugins import MetadataSourcePlugin, get_penalty
from beets.plugins import BeetsPlugin
diff --git a/test/autotag/test_hooks.py b/test/autotag/test_hooks.py
index 9d2d10db9..51b2ab470 100644
--- a/test/autotag/test_hooks.py
+++ b/test/autotag/test_hooks.py
@@ -20,11 +20,11 @@ from functools import cached_property
import pytest
-from beets.autotag.distance import Distance
-from beets.autotag.hooks import (
+from beets.autotag import (
AlbumInfo,
AlbumMatch,
AttrDict,
+ Distance,
TrackInfo,
TrackMatch,
correct_list_fields,
diff --git a/test/autotag/test_match.py b/test/autotag/test_match.py
index daa6fb9db..302df212f 100644
--- a/test/autotag/test_match.py
+++ b/test/autotag/test_match.py
@@ -3,8 +3,13 @@ from typing import ClassVar
import pytest
from beets import metadata_plugins
-from beets.autotag.hooks import AlbumInfo, TrackInfo
-from beets.autotag.match import assign_items, tag_album, tag_item
+from beets.autotag import (
+ AlbumInfo,
+ TrackInfo,
+ assign_items,
+ tag_album,
+ tag_item,
+)
from beets.library import Item
diff --git a/test/conftest.py b/test/conftest.py
index ec202a071..986882f9f 100644
--- a/test/conftest.py
+++ b/test/conftest.py
@@ -5,7 +5,7 @@ from functools import cache
import pytest
-from beets.autotag.distance import Distance
+from beets.autotag import Distance
from beets.dbcore.query import Query
from beets.test._common import DummyIO
from beets.test.helper import ConfigMixin
diff --git a/test/plugins/test_art.py b/test/plugins/test_art.py
index f71c2aabf..3616790f6 100644
--- a/test/plugins/test_art.py
+++ b/test/plugins/test_art.py
@@ -28,8 +28,7 @@ from requests_mock import ANY as ANYREEQUEST
from requests_mock.exceptions import NoMockAddress
from beets import config, importer, logging, util
-from beets.autotag.distance import Distance
-from beets.autotag.hooks import AlbumInfo, AlbumMatch
+from beets.autotag import AlbumInfo, AlbumMatch, Distance
from beets.library import Album
from beets.test import _common
from beets.test.helper import FetchImageHelper, PytestTestHelper
diff --git a/test/plugins/test_chroma.py b/test/plugins/test_chroma.py
index d4269523d..8041e8c91 100644
--- a/test/plugins/test_chroma.py
+++ b/test/plugins/test_chroma.py
@@ -17,7 +17,7 @@ from unittest.mock import MagicMock, patch
import pytest
from beets import metadata_plugins
-from beets.autotag.hooks import AlbumInfo, TrackInfo
+from beets.autotag import AlbumInfo, TrackInfo
from beets.library import Item
from beets.test.helper import ImportTestCase, IOMixin, PluginMixin
from beetsplug import chroma
diff --git a/test/plugins/test_mbpseudo.py b/test/plugins/test_mbpseudo.py
index 74095af55..581fd970a 100644
--- a/test/plugins/test_mbpseudo.py
+++ b/test/plugins/test_mbpseudo.py
@@ -6,8 +6,7 @@ from typing import TYPE_CHECKING
import pytest
-from beets.autotag.distance import Distance
-from beets.autotag.hooks import AlbumInfo, AlbumMatch, TrackInfo
+from beets.autotag import AlbumInfo, AlbumMatch, Distance, TrackInfo
from beets.library import Item
from beets.test.helper import PluginMixin
from beetsplug.mbpseudo import (
diff --git a/test/plugins/test_mbsync.py b/test/plugins/test_mbsync.py
index c9b67a06e..1719c1fe7 100644
--- a/test/plugins/test_mbsync.py
+++ b/test/plugins/test_mbsync.py
@@ -14,7 +14,7 @@
from unittest.mock import Mock, patch
-from beets.autotag.hooks import AlbumInfo, TrackInfo
+from beets.autotag import AlbumInfo, TrackInfo
from beets.library import Item
from beets.test.helper import PytestPluginTestHelper
diff --git a/test/plugins/test_missing.py b/test/plugins/test_missing.py
index a9529e907..de74ac6c4 100644
--- a/test/plugins/test_missing.py
+++ b/test/plugins/test_missing.py
@@ -6,7 +6,7 @@ from unittest.mock import patch
import pytest
-from beets.autotag.hooks import AlbumInfo, TrackInfo
+from beets.autotag import AlbumInfo, TrackInfo
from beets.library import Album, Item
from beets.test.helper import IOMixin, PluginMixin, TestHelper
diff --git a/test/plugins/test_titlecase.py b/test/plugins/test_titlecase.py
index e079e5cbd..f392cd69f 100644
--- a/test/plugins/test_titlecase.py
+++ b/test/plugins/test_titlecase.py
@@ -16,7 +16,7 @@
from unittest.mock import patch
-from beets.autotag.hooks import AlbumInfo, TrackInfo
+from beets.autotag import AlbumInfo, TrackInfo
from beets.importer import ImportSession, ImportTask
from beets.library import Item
from beets.test.helper import PluginTestCase
diff --git a/test/test_importer.py b/test/test_importer.py
index 816c3371e..bbc14a69d 100644
--- a/test/test_importer.py
+++ b/test/test_importer.py
@@ -36,8 +36,7 @@ import pytest
from mediafile import MediaFile
from beets import config, importer, logging, util
-from beets.autotag.distance import Distance
-from beets.autotag.hooks import AlbumInfo, AlbumMatch, TrackInfo
+from beets.autotag import AlbumInfo, AlbumMatch, Distance, TrackInfo
from beets.importer.tasks import albums_in_dir
from beets.test import _common
from beets.test.helper import (
diff --git a/test/ui/commands/test_import.py b/test/ui/commands/test_import.py
index d9759b71c..dbc3be41f 100644
--- a/test/ui/commands/test_import.py
+++ b/test/ui/commands/test_import.py
@@ -5,8 +5,7 @@ from unittest.mock import Mock, patch
import pytest
from beets import config, library
-from beets.autotag.hooks import AlbumInfo, AlbumMatch, TrackInfo
-from beets.autotag.match import distance
+from beets.autotag import AlbumInfo, AlbumMatch, TrackInfo, distance
from beets.exceptions import UserError
from beets.test import _common
from beets.test.helper import BeetsTestCase, IOMixin
From fd46e5b7738a3e11ef01e6fa8dc6fb8e0c7c82ec Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0ar=C5=ABnas=20Nejus?=
Date: Fri, 29 May 2026 18:49:18 +0100
Subject: [PATCH 2/5] Move Match classes to beets.autotag.match
---
beets/autotag/__init__.py | 14 +--
beets/autotag/hooks.py | 170 +--------------------------------
beets/autotag/match.py | 196 ++++++++++++++++++++++++++++++++++----
3 files changed, 187 insertions(+), 193 deletions(-)
diff --git a/beets/autotag/__init__.py b/beets/autotag/__init__.py
index 12aa65076..84d3f4fe4 100644
--- a/beets/autotag/__init__.py
+++ b/beets/autotag/__init__.py
@@ -22,17 +22,17 @@ from importlib import import_module
from beets.util.deprecation import deprecate_for_maintainers, deprecate_imports
from .distance import Distance, distance, string_dist, track_distance
-from .hooks import (
- AlbumInfo,
+from .hooks import AlbumInfo, AttrDict, Info, TrackInfo, correct_list_fields
+from .match import (
AlbumMatch,
- AttrDict,
- Info,
Match,
- TrackInfo,
+ Proposal,
+ Recommendation,
TrackMatch,
- correct_list_fields,
+ assign_items,
+ tag_album,
+ tag_item,
)
-from .match import Proposal, Recommendation, assign_items, tag_album, tag_item
def __getattr__(name: str):
diff --git a/beets/autotag/hooks.py b/beets/autotag/hooks.py
index 5a1806c8b..dc65fc6b0 100644
--- a/beets/autotag/hooks.py
+++ b/beets/autotag/hooks.py
@@ -17,13 +17,12 @@
from __future__ import annotations
from copy import deepcopy
-from dataclasses import dataclass, field
from functools import cached_property
-from typing import TYPE_CHECKING, Any, ClassVar, TypeVar
+from typing import Any, ClassVar, TypeVar
from typing_extensions import Self
-from beets import config, logging, plugins
+from beets import config, logging
from beets.util import cached_classproperty, unique_list
from beets.util.deprecation import (
ALBUM_LEGACY_TO_LIST_FIELD,
@@ -31,13 +30,6 @@ from beets.util.deprecation import (
deprecate_for_maintainers,
)
-if TYPE_CHECKING:
- from collections.abc import Sequence
-
- from beets.library import Album, Item
-
- from .distance import Distance
-
V = TypeVar("V")
JSONDict = dict[str, Any]
@@ -542,161 +534,3 @@ class TrackInfo(Info):
| track.item_data
)
return merged
-
-
-# Structures that compose all the information for a candidate match.
-@dataclass
-class Match:
- """Represent a chosen metadata candidate and its application behavior."""
-
- disambig_fields_key: ClassVar[str]
-
- distance: Distance
- info: Info
-
- def apply_metadata(self) -> None:
- """Apply this match's metadata to its target library objects."""
- raise NotImplementedError
-
- @cached_property
- def type(self) -> str:
- return self.info.type
-
- @cached_property
- def config_from_scratch(self) -> bool:
- return bool(config["import"]["from_scratch"])
-
- def from_scratch(self, override: bool | None) -> bool:
- if override is not None:
- return override
-
- return self.config_from_scratch
-
- @property
- def disambig_fields(self) -> Sequence[str]:
- """Return configured disambiguation fields that exist on this match."""
- chosen_fields = config["match"][self.disambig_fields_key].as_str_seq()
- valid_fields = [f for f in chosen_fields if f in self.info]
- if missing_fields := set(chosen_fields) - set(valid_fields):
- log.warning(
- "Disambiguation string keys {} do not exist.", missing_fields
- )
-
- return valid_fields
-
- @property
- def base_disambig_data(self) -> JSONDict:
- """Return supplemental values used when formatting disambiguation."""
- return {}
-
- @property
- def disambig_string(self) -> str:
- """Build a display string from the candidate's disambiguation fields.
-
- Merges base disambiguation data with instance-specific field values,
- then formats them as a comma-separated string in field definition order.
- """
- data = {
- k: self.info[k] for k in self.disambig_fields
- } | self.base_disambig_data
- return ", ".join(str(data[k]) for k in self.disambig_fields)
-
-
-@dataclass
-class AlbumMatch(Match):
- """Represent an album candidate together with its item-to-track mapping."""
-
- disambig_fields_key = "album_disambig_fields"
-
- info: AlbumInfo
- mapping: dict[Item, TrackInfo]
- extra_items: list[Item] = field(default_factory=list)
- extra_tracks: list[TrackInfo] = field(default_factory=list)
-
- def __post_init__(self) -> None:
- """Notify listeners when an album candidate has been matched."""
- plugins.send("album_matched", match=self)
-
- @property
- def item_info_pairs(self) -> list[tuple[Item, TrackInfo]]:
- """Return matched items together with their selected track metadata."""
- return list(self.mapping.items())
-
- @property
- def items(self) -> list[Item]:
- """Return the items that participate in this album match."""
- return [i for i, _ in self.item_info_pairs]
-
- @property
- def base_disambig_data(self) -> JSONDict:
- """Return album-specific values used in disambiguation displays."""
- return {
- "media": (
- f"{mediums}x{self.info.media}"
- if (mediums := self.info.mediums) and mediums > 1
- else self.info.media
- )
- }
-
- @property
- def merged_pairs(self) -> list[tuple[Item, JSONDict]]:
- """Generate item-data pairs with album-level fallback values."""
- return [
- (i, ti.merge_with_album(self.info))
- for i, ti in self.item_info_pairs
- ]
-
- def apply_metadata(self, from_scratch: bool | None = None) -> None:
- """Apply metadata to each of the items.
-
- If ``from_scratch`` is provided, its value determines whether the
- items existing metadata are cleared before applying new metadata.
- Otherwise, the configured ``from_scratch`` setting is used.
- """
- for item, data in self.merged_pairs:
- if self.from_scratch(from_scratch):
- item.clear()
-
- item.update(data)
-
- def apply_album_metadata(self, album: Album) -> None:
- """Apply album-level metadata to the Album object."""
- album.update(self.info.item_data)
-
-
-@dataclass
-class TrackMatch(Match):
- """Represent a singleton candidate and the item it updates."""
-
- disambig_fields_key = "singleton_disambig_fields"
-
- info: TrackInfo
- item: Item
-
- @property
- def base_disambig_data(self) -> JSONDict:
- """Return singleton-specific values used in disambiguation displays."""
- return {
- "index": f"Index {self.info.index}",
- "track_alt": f"Track {self.info.track_alt}",
- "album": (
- f"[{self.info.album}]"
- if (
- config["import"]["singleton_album_disambig"].get()
- and self.info.album
- )
- else ""
- ),
- }
-
- def apply_metadata(self, from_scratch: bool | None = None) -> None:
- """Apply metadata to the item.
-
- If ``from_scratch`` is provided, its value determines whether the
- item's existing metadata is cleared before applying new metadata.
- Otherwise, the configured ``from_scratch`` setting is used.
- """
- if self.from_scratch(from_scratch):
- self.item.clear()
-
- self.item.update(self.info.item_data)
diff --git a/beets/autotag/match.py b/beets/autotag/match.py
index 9cda3884d..f8ccbb22a 100644
--- a/beets/autotag/match.py
+++ b/beets/autotag/match.py
@@ -18,28 +18,30 @@ releases and tracks.
from __future__ import annotations
+from dataclasses import dataclass, field
from enum import IntEnum
-from typing import TYPE_CHECKING, NamedTuple, TypeVar
+from functools import cached_property
+from typing import TYPE_CHECKING, Any, ClassVar, NamedTuple, TypeVar
import lap
import numpy as np
-from beets import config, logging, metadata_plugins
+from beets import config, logging, metadata_plugins, plugins
from beets.util import get_most_common_tags
from .distance import VA_ARTISTS, distance, track_distance
-from .hooks import AlbumMatch, Info, TrackMatch
if TYPE_CHECKING:
from collections.abc import Iterable, Sequence
- from beets.library import Item
+ from beets.library import Album, Item
- from .hooks import AlbumInfo, TrackInfo
+ from .distance import Distance
+ from .hooks import AlbumInfo, Info, TrackInfo
-
-AnyMatch = TypeVar("AnyMatch", TrackMatch, AlbumMatch)
-Candidates = dict[Info.Identifier, AnyMatch]
+ JSONDict = dict[str, Any]
+ AnyMatch = TypeVar("AnyMatch", "TrackMatch", "AlbumMatch")
+ Candidates = dict[Info.Identifier, AnyMatch]
# Global logger.
log = logging.getLogger("beets")
@@ -59,16 +61,6 @@ class Recommendation(IntEnum):
strong = 3
-# A structure for holding a set of possible matches to choose between. This
-# consists of a list of possible candidates (i.e., AlbumInfo or TrackInfo
-# objects) and a recommendation value.
-
-
-class Proposal(NamedTuple):
- candidates: Sequence[AlbumMatch | TrackMatch]
- recommendation: Recommendation
-
-
# Primary matching functionality.
@@ -103,6 +95,174 @@ def assign_items(
return list(mapping.items()), extra_items, extra_tracks
+# Structures that compose all the information for a candidate match.
+@dataclass
+class Match:
+ """Represent a chosen metadata candidate and its application behavior."""
+
+ disambig_fields_key: ClassVar[str]
+
+ distance: Distance
+ info: Info
+
+ def apply_metadata(self) -> None:
+ """Apply this match's metadata to its target library objects."""
+ raise NotImplementedError
+
+ @cached_property
+ def type(self) -> str:
+ return self.info.type
+
+ @cached_property
+ def config_from_scratch(self) -> bool:
+ return bool(config["import"]["from_scratch"])
+
+ def from_scratch(self, override: bool | None) -> bool:
+ if override is not None:
+ return override
+
+ return self.config_from_scratch
+
+ @property
+ def disambig_fields(self) -> Sequence[str]:
+ """Return configured disambiguation fields that exist on this match."""
+ chosen_fields = config["match"][self.disambig_fields_key].as_str_seq()
+ valid_fields = [f for f in chosen_fields if f in self.info]
+ if missing_fields := set(chosen_fields) - set(valid_fields):
+ log.warning(
+ "Disambiguation string keys {} do not exist.", missing_fields
+ )
+
+ return valid_fields
+
+ @property
+ def base_disambig_data(self) -> JSONDict:
+ """Return supplemental values used when formatting disambiguation."""
+ return {}
+
+ @property
+ def disambig_string(self) -> str:
+ """Build a display string from the candidate's disambiguation fields.
+
+ Merges base disambiguation data with instance-specific field values,
+ then formats them as a comma-separated string in field definition order.
+ """
+ data = {
+ k: self.info[k] for k in self.disambig_fields
+ } | self.base_disambig_data
+ return ", ".join(str(data[k]) for k in self.disambig_fields)
+
+
+@dataclass
+class AlbumMatch(Match):
+ """Represent an album candidate together with its item-to-track mapping."""
+
+ disambig_fields_key = "album_disambig_fields"
+
+ info: AlbumInfo
+ mapping: dict[Item, TrackInfo]
+ extra_items: list[Item] = field(default_factory=list)
+ extra_tracks: list[TrackInfo] = field(default_factory=list)
+
+ def __post_init__(self) -> None:
+ """Notify listeners when an album candidate has been matched."""
+ plugins.send("album_matched", match=self)
+
+ @property
+ def item_info_pairs(self) -> list[tuple[Item, TrackInfo]]:
+ """Return matched items together with their selected track metadata."""
+ return list(self.mapping.items())
+
+ @property
+ def items(self) -> list[Item]:
+ """Return the items that participate in this album match."""
+ return [i for i, _ in self.item_info_pairs]
+
+ @property
+ def base_disambig_data(self) -> JSONDict:
+ """Return album-specific values used in disambiguation displays."""
+ return {
+ "media": (
+ f"{mediums}x{self.info.media}"
+ if (mediums := self.info.mediums) and mediums > 1
+ else self.info.media
+ )
+ }
+
+ @property
+ def merged_pairs(self) -> list[tuple[Item, JSONDict]]:
+ """Generate item-data pairs with album-level fallback values."""
+ return [
+ (i, ti.merge_with_album(self.info))
+ for i, ti in self.item_info_pairs
+ ]
+
+ def apply_metadata(self, from_scratch: bool | None = None) -> None:
+ """Apply metadata to each of the items.
+
+ If ``from_scratch`` is provided, its value determines whether the
+ items existing metadata are cleared before applying new metadata.
+ Otherwise, the configured ``from_scratch`` setting is used.
+ """
+ for item, data in self.merged_pairs:
+ if self.from_scratch(from_scratch):
+ item.clear()
+
+ item.update(data)
+
+ def apply_album_metadata(self, album: Album) -> None:
+ """Apply album-level metadata to the Album object."""
+ album.update(self.info.item_data)
+
+
+@dataclass
+class TrackMatch(Match):
+ """Represent a singleton candidate and the item it updates."""
+
+ disambig_fields_key = "singleton_disambig_fields"
+
+ info: TrackInfo
+ item: Item
+
+ @property
+ def base_disambig_data(self) -> JSONDict:
+ """Return singleton-specific values used in disambiguation displays."""
+ return {
+ "index": f"Index {self.info.index}",
+ "track_alt": f"Track {self.info.track_alt}",
+ "album": (
+ f"[{self.info.album}]"
+ if (
+ config["import"]["singleton_album_disambig"].get()
+ and self.info.album
+ )
+ else ""
+ ),
+ }
+
+ def apply_metadata(self, from_scratch: bool | None = None) -> None:
+ """Apply metadata to the item.
+
+ If ``from_scratch`` is provided, its value determines whether the
+ item's existing metadata is cleared before applying new metadata.
+ Otherwise, the configured ``from_scratch`` setting is used.
+ """
+ if self.from_scratch(from_scratch):
+ self.item.clear()
+
+ self.item.update(self.info.item_data)
+
+
+# A structure for holding a set of possible matches to choose between. This
+# consists of a list of possible candidates (i.e., AlbumInfo or TrackInfo
+# objects) and a recommendation value.
+
+
+class Proposal(NamedTuple):
+ candidates: Sequence[AlbumMatch | TrackMatch]
+ recommendation: Recommendation
+
+
def match_by_id(album_id: str | None, consensus: bool) -> Iterable[AlbumInfo]:
"""Return album candidates for the given album id.
From 33653ceffc0eed7c298b2477df82d414e8bf3986 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0ar=C5=ABnas=20Nejus?=
Date: Fri, 29 May 2026 19:34:15 +0100
Subject: [PATCH 3/5] Update git-blame-ignore-revs
---
.git-blame-ignore-revs | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs
index d4c47e4ec..aa001ffac 100644
--- a/.git-blame-ignore-revs
+++ b/.git-blame-ignore-revs
@@ -95,6 +95,8 @@ fbfdfd54446fab6782ef0629da303f14f0a2ecdf
c490ac5810b70f3cf5fd8649669838e8fdb19f4d
# Importer restructure
9147577b2b19f43ca827e9650261a86fb0450cef
+# Renamed all `action` occurrences with `Action`.
+68acaa6470a04474a2aefe9b1de71ae7fa10b861
# Move functionality under MusicBrainz plugin
529aaac7dced71266c6d69866748a7d044ec20ff
# musicbrainz: reorder methods
@@ -184,4 +186,6 @@ d4fd36a03384b0f71144932833d9c633c1f9545f
# Reformat using skip-magic-trailing-comma
adfb73b66dba0fc799880af176ef6f9ac0a7ee68
# Refactor test_library
-6493d4dd018e08eb5d16d5381ebff47deea98bb1
\ No newline at end of file
+6493d4dd018e08eb5d16d5381ebff47deea98bb1
+# Import everything from parent beets.autotag
+29580d2f00eb02f1ce77ebe3e1435804f7252ba1
From 5a22b407359fcfe0c44273701a8519fd695b59d8 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0ar=C5=ABnas=20Nejus?=
Date: Tue, 2 Jun 2026 08:43:23 +0100
Subject: [PATCH 4/5] lyrics: introduce apply_methods
---
beetsplug/lyrics.py | 30 ++++++++++++++++++++++++------
1 file changed, 24 insertions(+), 6 deletions(-)
diff --git a/beetsplug/lyrics.py b/beetsplug/lyrics.py
index 263f6baa1..b88536c88 100644
--- a/beetsplug/lyrics.py
+++ b/beetsplug/lyrics.py
@@ -44,7 +44,7 @@ from beets.util.lyrics import INSTRUMENTAL_LYRICS, Lyrics
from ._utils.requests import HTTPNotFoundError, RequestHandler
if TYPE_CHECKING:
- from collections.abc import Iterable, Iterator
+ from collections.abc import Callable, Iterable, Iterator
import confuse
@@ -60,6 +60,8 @@ if TYPE_CHECKING:
TranslatorAPI,
)
+ HtmlTransformer = Callable[[str], str]
+
class CaptchaError(requests.exceptions.HTTPError):
def __init__(self, *args, **kwargs) -> None:
@@ -444,6 +446,13 @@ class MusiXmatch(Backend):
return Lyrics(lyrics, self.__class__.name, url)
+def apply_transforms(html: str, methods: Iterable[HtmlTransformer]) -> str:
+ for method in methods:
+ html = method(html)
+
+ return html
+
+
class Html:
collapse_space = partial(re.compile(r"(^| ) +", re.M).sub, r"\1")
expand_br = partial(re.compile(r"\s* ]*>\s*", re.I).sub, "\n")
@@ -470,15 +479,17 @@ class Html:
@classmethod
def normalize_space(cls, text: str) -> str:
text = unescape(text).replace("\r", "").replace("\xa0", " ")
- return cls.collapse_space(cls.expand_br(text))
+ return apply_transforms(text, [cls.collapse_space, cls.expand_br])
@classmethod
def remove_ads(cls, text: str) -> str:
- return cls.remove_adslot(cls.remove_aside(text))
+ return apply_transforms(text, [cls.remove_aside, cls.remove_adslot])
@classmethod
def merge_paragraphs(cls, text: str) -> str:
- return cls.merge_blocks(cls.merge_lines(cls.remove_empty_tags(text)))
+ return apply_transforms(
+ text, [cls.remove_empty_tags, cls.merge_lines, cls.merge_blocks]
+ )
class SoupMixin:
@@ -679,8 +690,15 @@ class Google(SearchBackend):
@classmethod
def pre_process_html(cls, html: str) -> str:
"""Pre-process the HTML content before scraping."""
- html = Html.remove_ads(super().pre_process_html(html))
- return Html.remove_formatting(Html.merge_paragraphs(html))
+ return apply_transforms(
+ html,
+ [
+ super().pre_process_html,
+ Html.remove_ads,
+ Html.merge_paragraphs,
+ Html.remove_formatting,
+ ],
+ )
def get_text(self, *args, **kwargs) -> str:
"""Handle an error so that we can continue with the next URL."""
From fb9de7a619f521b8d4d7ba8f457576a3ef4949d6 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0ar=C5=ABnas=20Nejus?=
Date: Wed, 3 Jun 2026 14:00:09 +0100
Subject: [PATCH 5/5] lyrics: fix musica parsing
---
beetsplug/lyrics.py | 35 +++++++++++++++++++++++++++++++++--
docs/changelog.rst | 2 ++
test/plugins/lyrics_pages.py | 5 +++++
test/plugins/test_lyrics.py | 20 ++++++++++++--------
4 files changed, 52 insertions(+), 10 deletions(-)
diff --git a/beetsplug/lyrics.py b/beetsplug/lyrics.py
index b88536c88..9aec77e86 100644
--- a/beetsplug/lyrics.py
+++ b/beetsplug/lyrics.py
@@ -457,7 +457,9 @@ class Html:
collapse_space = partial(re.compile(r"(^| ) +", re.M).sub, r"\1")
expand_br = partial(re.compile(r"\s* ]*>\s*", re.I).sub, "\n")
#: two newlines between paragraphs on the same line (musica, letras.mus.br)
- merge_blocks = partial(re.compile(r"(?)
]*>").sub, "\n\n")
+ merge_blocks = partial(
+ re.compile(r"(?)(
]*>)+", re.S).sub, "\n\n"
+ )
#: a single new line between paragraphs on separate lines
#: (paroles.net, sweetslyrics.com, lacoccinelle.net)
merge_lines = partial(re.compile(r"
\s+]*>(?!___)").sub, "\n")
@@ -467,6 +469,26 @@ class Html:
)
#: remove Google Ads tags (musica.com)
remove_aside = partial(re.compile("").sub, "")
+ #: remove inline script blocks that split lyrics paragraphs
+ remove_scripts = partial(
+ re.compile(r"]*>", re.I | re.S).sub, ""
+ )
+ #: remove comments that split lyrics paragraphs
+ remove_comments = partial(re.compile(r"", re.S).sub, "")
+ #: remove title-only paragraph from the musica.com lyrics block
+ remove_musica_title = partial(
+ re.compile(
+ r"(]*>.*?)"
+ r"
\s*[^<]+ \s*
",
+ re.I | re.S,
+ ).sub,
+ r"\1",
+ )
+ #: remove non-lyrics explanation blocks (musica.com)
+ remove_significado = partial(
+ re.compile(r"
]*>.*", re.I | re.S).sub,
+ "",
+ )
#: remove adslot-Content_1 div from the lyrics text (paroles.net)
remove_adslot = partial(
re.compile(r"\n
[^\n]+-- Content_\d+ --.*?\n
", re.S).sub, "\n"
@@ -496,7 +518,15 @@ class SoupMixin:
@classmethod
def pre_process_html(cls, html: str) -> str:
"""Pre-process the HTML content before scraping."""
- return Html.normalize_space(html)
+ return apply_transforms(
+ html,
+ [
+ Html.normalize_space,
+ Html.remove_significado,
+ Html.remove_scripts,
+ Html.remove_musica_title,
+ ],
+ )
@classmethod
def get_soup(cls, html: str) -> BeautifulSoup:
@@ -695,6 +725,7 @@ class Google(SearchBackend):
[
super().pre_process_html,
Html.remove_ads,
+ Html.remove_comments,
Html.merge_paragraphs,
Html.remove_formatting,
],
diff --git a/docs/changelog.rst b/docs/changelog.rst
index c4d642dbe..c5fc77246 100644
--- a/docs/changelog.rst
+++ b/docs/changelog.rst
@@ -67,6 +67,8 @@ Bug fixes
- :doc:`plugins/fetchart`: Catch ``OSError`` in ``_set_art`` so that permission
errors (e.g. a file locked by another process) are logged as warnings instead
of crashing beets. :bug:`6193`
+- :doc:`plugins/lyrics`: Improve Musica.com lyric scraping so fetched lyrics no
+ longer omit the opening verse or include non-lyric page content.
For plugin developers
~~~~~~~~~~~~~~~~~~~~~
diff --git a/test/plugins/lyrics_pages.py b/test/plugins/lyrics_pages.py
index df57d05b2..61cd13ef8 100644
--- a/test/plugins/lyrics_pages.py
+++ b/test/plugins/lyrics_pages.py
@@ -426,6 +426,11 @@ lyrics_pages = [
LyricsPage.make(
"https://www.musica.com/letras.asp?letra=59862",
"""
+ 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
diff --git a/test/plugins/test_lyrics.py b/test/plugins/test_lyrics.py
index 0a6c99438..285a8ad7f 100644
--- a/test/plugins/test_lyrics.py
+++ b/test/plugins/test_lyrics.py
@@ -165,8 +165,8 @@ class TestHtml:
assert lyrics.Html.normalize_space(initial) == expected
def test_scrape_merge_paragraphs(self):
- text = "one
two
three"
- expected = "one\ntwo\n\nthree"
+ text = 'one
two
three'
+ expected = "one\n\ntwo\n\nthree"
assert lyrics.Html.merge_paragraphs(text) == expected
@@ -451,20 +451,24 @@ class TestLyricsSources(LyricsBackendTest):
"beetsplug.lyrics.LyricsRequestHandler.create_session",
lambda _: requests.Session(),
)
+ expected_lyrics = Lyrics(
+ lyrics_page.lyrics,
+ lyrics_page.backend,
+ url=lyrics_page.url,
+ language=lyrics_page.language,
+ )
- assert lyrics_plugin.find_lyrics(
+ actual_lyrics = lyrics_plugin.find_lyrics(
Item(
artist=lyrics_page.artist,
title=lyrics_page.track_title,
album="",
length=186.0,
)
- ) == Lyrics(
- lyrics_page.lyrics,
- lyrics_page.backend,
- url=lyrics_page.url,
- language=lyrics_page.language,
)
+ assert actual_lyrics
+ assert actual_lyrics.text == expected_lyrics.text
+ assert actual_lyrics == expected_lyrics
class TestGoogleLyrics(LyricsBackendTest):