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
diff --git a/beets/autotag/__init__.py b/beets/autotag/__init__.py
index 094ed9e9b..84d3f4fe4 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, AttrDict, Info, TrackInfo, correct_list_fields
+from .match import (
+ AlbumMatch,
+ Match,
+ Proposal,
+ Recommendation,
+ TrackMatch,
+ 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/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.
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..9aec77e86 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
@@ -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,11 +446,20 @@ 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")
#: 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") @@ -458,6 +469,26 @@ class Html: ) #: remove Google Ads tags (musica.com) remove_aside = partial(re.compile("