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("