mirror of
https://github.com/beetbox/beets.git
synced 2026-07-22 03:17:26 -04:00
Merge branch 'master' into fix-mpdstats-cli-host
This commit is contained in:
@@ -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
|
||||
@@ -183,3 +185,15 @@ d4fd36a03384b0f71144932833d9c633c1f9545f
|
||||
0cd20798aa585f162433423f767c047b7474a70e
|
||||
# Reformat using skip-magic-trailing-comma
|
||||
adfb73b66dba0fc799880af176ef6f9ac0a7ee68
|
||||
# Refactor test_library
|
||||
6493d4dd018e08eb5d16d5381ebff47deea98bb1
|
||||
# Import everything from parent beets.autotag
|
||||
29580d2f00eb02f1ce77ebe3e1435804f7252ba1
|
||||
# Add setup fixture to TestHelper
|
||||
7bad226c33c2d48b5102cff213c67f16eeb09a7b
|
||||
# Fix mbcollection tests
|
||||
544ea3af5d85df187e080f407d3aaf84eac4ee63
|
||||
# Fix ftintitle tests
|
||||
ca8544d4bcab503e1e4c2cd6688655c8211aa7a1
|
||||
# Fix autobpm tests
|
||||
5804a21620098e8da78b1f4a5f85063033586632
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
@@ -162,6 +162,14 @@ class TestHelper(RunMixin, ConfigMixin):
|
||||
fixtures.
|
||||
"""
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def setup(self):
|
||||
self.setup_beets()
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
self.teardown_beets()
|
||||
|
||||
lib: Library
|
||||
|
||||
resource_path = Path(os.fsdecode(_common.RSRC)) / "full.mp3"
|
||||
@@ -407,27 +415,9 @@ class BeetsTestCase(unittest.TestCase, TestHelper):
|
||||
completes. Also provides some additional assertion methods, a
|
||||
temporary directory, and a DummyIO.
|
||||
|
||||
DEPRECATED: Use pytest + PytestTestHelper instead.
|
||||
DEPRECATED: Use TestHelper instead.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
self.setup_beets()
|
||||
|
||||
def tearDown(self):
|
||||
self.teardown_beets()
|
||||
|
||||
|
||||
class PytestTestHelper(TestHelper):
|
||||
"""Same as the BeetsTestCase unittest setup but for pytest."""
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def setup(self):
|
||||
self.setup_beets()
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
self.teardown_beets()
|
||||
|
||||
|
||||
class ItemInDBTestCase(BeetsTestCase):
|
||||
"""A test case that includes an in-memory library object (`lib`) and
|
||||
@@ -488,13 +478,13 @@ class PluginMixin(ConfigMixin):
|
||||
|
||||
class PluginTestCase(PluginMixin, BeetsTestCase):
|
||||
"""
|
||||
DEPRECATED: Use pytest + PytestPluginTestHelper instead.
|
||||
DEPRECATED: Use PluginTestHelper instead.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class PytestPluginTestHelper(PluginMixin, PytestTestHelper):
|
||||
class PluginTestHelper(PluginMixin, TestHelper):
|
||||
"""Helper mixin for pytest-based plugin tests.
|
||||
|
||||
This mixin provides the standard beets test setup and automatically
|
||||
@@ -502,7 +492,7 @@ class PytestPluginTestHelper(PluginMixin, PytestTestHelper):
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class TestMyPlugin(PytestPluginTestHelper):
|
||||
class TestMyPlugin(PluginTestHelper):
|
||||
plugin: ClassVar[str] = "myplugin"
|
||||
"""
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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*<br[^>]*>\s*", re.I).sub, "\n")
|
||||
#: two newlines between paragraphs on the same line (musica, letras.mus.br)
|
||||
merge_blocks = partial(re.compile(r"(?<!>)</p><p[^>]*>").sub, "\n\n")
|
||||
merge_blocks = partial(
|
||||
re.compile(r"(?<!>)(</p><p[^>]*>)+", 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"</p>\s+<p[^>]*>(?!___)").sub, "\n")
|
||||
@@ -458,6 +469,26 @@ class Html:
|
||||
)
|
||||
#: remove Google Ads tags (musica.com)
|
||||
remove_aside = partial(re.compile("<aside .+?</aside>").sub, "")
|
||||
#: remove inline script blocks that split lyrics paragraphs
|
||||
remove_scripts = partial(
|
||||
re.compile(r"<script\b[^>]*>.*?</script\b[^>]*>", 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"(<div\s+id=['\"]letra['\"][^>]*>.*?)"
|
||||
r"<p>\s*<strong>[^<]+</strong>\s*</p>",
|
||||
re.I | re.S,
|
||||
).sub,
|
||||
r"\1",
|
||||
)
|
||||
#: remove non-lyrics explanation blocks (musica.com)
|
||||
remove_significado = partial(
|
||||
re.compile(r"<div\s+id=['\"]significado['\"][^>]*>.*", re.I | re.S).sub,
|
||||
"",
|
||||
)
|
||||
#: remove adslot-Content_1 div from the lyrics text (paroles.net)
|
||||
remove_adslot = partial(
|
||||
re.compile(r"\n</div>[^\n]+-- Content_\d+ --.*?\n<div>", re.S).sub, "\n"
|
||||
@@ -470,22 +501,32 @@ 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:
|
||||
@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:
|
||||
@@ -679,8 +720,16 @@ 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.remove_comments,
|
||||
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."""
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -69,10 +69,15 @@ 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
|
||||
~~~~~~~~~~~~~~~~~~~~~
|
||||
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
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -28,11 +28,10 @@ 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
|
||||
from beets.test.helper import FetchImageHelper, TestHelper
|
||||
from beets.util import clean_module_tempdir, syspath
|
||||
from beets.util.artresizer import ArtResizer
|
||||
from beetsplug import fetchart
|
||||
@@ -68,7 +67,7 @@ class DummyRemoteArtSource(fetchart.RemoteArtSource):
|
||||
return iter(())
|
||||
|
||||
|
||||
class UseThePlugin(PytestTestHelper):
|
||||
class UseThePlugin(TestHelper):
|
||||
modules = (fetchart.__name__, ArtResizer.__module__)
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
|
||||
@@ -1,27 +1,19 @@
|
||||
import pytest
|
||||
|
||||
from beets.test.helper import ImportHelper, PluginMixin
|
||||
from beets.test.helper import ImportHelper, PluginTestHelper
|
||||
|
||||
pytestmark = pytest.mark.requires_import("librosa")
|
||||
|
||||
|
||||
class TestAutoBPMPlugin(PluginMixin, ImportHelper):
|
||||
class TestAutoBPMPlugin(ImportHelper, PluginTestHelper):
|
||||
plugin = "autobpm"
|
||||
|
||||
@pytest.fixture(scope="class", name="lib")
|
||||
def fixture_lib(self):
|
||||
self.setup_beets()
|
||||
|
||||
yield self.lib
|
||||
|
||||
self.teardown_beets()
|
||||
|
||||
@pytest.fixture(scope="class")
|
||||
@pytest.fixture
|
||||
def item(self):
|
||||
return self.add_item_fixture()
|
||||
|
||||
@pytest.fixture(scope="class")
|
||||
def importer(self, lib):
|
||||
@pytest.fixture
|
||||
def importer(self):
|
||||
self.import_media = []
|
||||
self.prepare_album_for_import(1)
|
||||
track = self.import_media[0]
|
||||
@@ -29,27 +21,27 @@ class TestAutoBPMPlugin(PluginMixin, ImportHelper):
|
||||
track.save()
|
||||
return self.setup_importer(autotag=False)
|
||||
|
||||
def test_command(self, lib, item):
|
||||
def test_command(self, item):
|
||||
item.bpm = None
|
||||
item.store()
|
||||
self.run_command("autobpm", lib=lib)
|
||||
self.run_command("autobpm")
|
||||
|
||||
item.load()
|
||||
assert item.bpm == 117
|
||||
|
||||
def test_command_force(self, lib, item):
|
||||
def test_command_force(self, item):
|
||||
item.bpm = 10
|
||||
item.store()
|
||||
|
||||
self.run_command("autobpm", lib=lib)
|
||||
self.run_command("autobpm")
|
||||
item.load()
|
||||
assert item.bpm == 10
|
||||
|
||||
self.run_command("autobpm", "--force", lib=lib)
|
||||
self.run_command("autobpm", "--force")
|
||||
item.load()
|
||||
assert item.bpm == 117
|
||||
|
||||
def test_command_overwrite(self, lib, item):
|
||||
def test_command_overwrite(self, item):
|
||||
item.bpm = 10
|
||||
item.store()
|
||||
|
||||
@@ -57,23 +49,23 @@ class TestAutoBPMPlugin(PluginMixin, ImportHelper):
|
||||
self.config[self.plugin]["overwrite"] = True
|
||||
self.load_plugins()
|
||||
|
||||
self.run_command("autobpm", lib=lib)
|
||||
self.run_command("autobpm")
|
||||
item.load()
|
||||
assert item.bpm == 117
|
||||
|
||||
def test_command_quiet(self, lib, item, caplog):
|
||||
def test_command_quiet(self, item, caplog):
|
||||
item.bpm = 10
|
||||
item.store()
|
||||
|
||||
with caplog.at_level("DEBUG"):
|
||||
self.run_command("autobpm", "--quiet", lib=lib)
|
||||
self.run_command("autobpm", "--quiet")
|
||||
assert not any("already exists" in msg for msg in caplog.messages)
|
||||
|
||||
with caplog.at_level("DEBUG"):
|
||||
self.run_command("autobpm", lib=lib)
|
||||
self.run_command("autobpm")
|
||||
assert any("already exists" in msg for msg in caplog.messages)
|
||||
|
||||
def test_import(self, lib, importer):
|
||||
def test_import(self, importer):
|
||||
importer.run()
|
||||
|
||||
assert lib.items().get().bpm == 117
|
||||
assert self.lib.items().get().bpm == 117
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -37,20 +37,8 @@ def _artist(name: str, **kwargs):
|
||||
} | kwargs
|
||||
|
||||
|
||||
class PytestTestHelper(TestHelper):
|
||||
"""Same as the BeetsTestCase unittest setup but for pytest."""
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def setup(self):
|
||||
self.setup_beets()
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
self.teardown_beets()
|
||||
|
||||
|
||||
@patch("beetsplug.discogs.DiscogsPlugin.setup", Mock())
|
||||
class TestDGAlbumInfo(PytestTestHelper):
|
||||
class TestDGAlbumInfo(TestHelper):
|
||||
def _make_release(self, tracks=None):
|
||||
"""Returns a Bag that mimics a discogs_client.Release. The list
|
||||
of elements on the returned Bag is incomplete, including just
|
||||
@@ -468,7 +456,7 @@ class TestDGAlbumInfo(PytestTestHelper):
|
||||
|
||||
|
||||
@patch("beetsplug.discogs.DiscogsPlugin.setup", Mock())
|
||||
class TestDGSearchQuery(PytestTestHelper):
|
||||
class TestDGSearchQuery(TestHelper):
|
||||
def test_default_search_filters_without_extra_tags(self):
|
||||
"""Discogs search uses only the type filter when no extra_tags are set."""
|
||||
plugin = DiscogsPlugin()
|
||||
|
||||
@@ -32,7 +32,7 @@ from beets.test.helper import (
|
||||
FetchImageHelper,
|
||||
ImportHelper,
|
||||
IOMixin,
|
||||
PytestPluginTestHelper,
|
||||
PluginTestHelper,
|
||||
)
|
||||
from beets.util import bytestring_path, displayable_path, syspath
|
||||
from beets.util.artresizer import ArtResizer
|
||||
@@ -77,7 +77,7 @@ def require_artresizer_compare(test):
|
||||
return wrapper
|
||||
|
||||
|
||||
class PytestImportHelper(PytestPluginTestHelper, ImportHelper):
|
||||
class PytestImportHelper(PluginTestHelper, ImportHelper):
|
||||
@pytest.fixture(autouse=True)
|
||||
def setup_import_helper(self, setup):
|
||||
self.import_media = []
|
||||
|
||||
@@ -16,265 +16,228 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, TypeAlias
|
||||
|
||||
import pytest
|
||||
|
||||
from beets.library.models import Album
|
||||
from beets.test.helper import PluginTestCase
|
||||
from beets.test.helper import PluginTestHelper
|
||||
from beetsplug import ftintitle
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Generator
|
||||
|
||||
from beets.library.models import Item
|
||||
|
||||
ConfigValue: TypeAlias = str | bool | list[str]
|
||||
|
||||
|
||||
class FtInTitlePluginFunctional(PluginTestCase):
|
||||
class TestFtInTitlePluginFunctional(PluginTestHelper):
|
||||
plugin = "ftintitle"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def env() -> Generator[FtInTitlePluginFunctional, None, None]:
|
||||
case = FtInTitlePluginFunctional(methodName="runTest")
|
||||
case.setUp()
|
||||
try:
|
||||
yield case
|
||||
finally:
|
||||
case.tearDown()
|
||||
|
||||
|
||||
def set_config(
|
||||
env: FtInTitlePluginFunctional, cfg: dict[str, ConfigValue] | None
|
||||
) -> None:
|
||||
cfg = {} if cfg is None else cfg
|
||||
defaults = {
|
||||
"drop": False,
|
||||
"auto": True,
|
||||
"keep_in_artist": False,
|
||||
"custom_words": [],
|
||||
}
|
||||
env.config["ftintitle"].set(defaults)
|
||||
env.config["ftintitle"].set(cfg)
|
||||
|
||||
|
||||
def add_item(
|
||||
env: FtInTitlePluginFunctional,
|
||||
path: str,
|
||||
artist: str,
|
||||
title: str,
|
||||
albumartist: str | None,
|
||||
) -> Item:
|
||||
return env.add_item(
|
||||
path=path,
|
||||
artist=artist,
|
||||
artist_sort=artist,
|
||||
title=title,
|
||||
albumartist=albumartist,
|
||||
@pytest.mark.parametrize(
|
||||
"cfg, cmd_args, given, expected",
|
||||
[
|
||||
pytest.param(
|
||||
{},
|
||||
("ftintitle",),
|
||||
("Alice", "Song 1", "Alice"),
|
||||
("Alice", "Song 1"),
|
||||
id="no-featured-artist",
|
||||
),
|
||||
pytest.param(
|
||||
{"format": "feat {0}"},
|
||||
("ftintitle",),
|
||||
("Alice ft. Bob", "Song 1", None),
|
||||
("Alice", "Song 1 feat Bob"),
|
||||
id="no-albumartist-custom-format",
|
||||
),
|
||||
pytest.param(
|
||||
{},
|
||||
("ftintitle",),
|
||||
("Alice", "Song 1", None),
|
||||
("Alice", "Song 1"),
|
||||
id="no-albumartist-no-feature",
|
||||
),
|
||||
pytest.param(
|
||||
{"format": "featuring {0}"},
|
||||
("ftintitle",),
|
||||
("Alice ft Bob", "Song 1", "George"),
|
||||
("Alice", "Song 1 featuring Bob"),
|
||||
id="guest-artist-custom-format",
|
||||
),
|
||||
pytest.param(
|
||||
{},
|
||||
("ftintitle",),
|
||||
("Alice", "Song 1", "George"),
|
||||
("Alice", "Song 1"),
|
||||
id="guest-artist-no-feature",
|
||||
),
|
||||
# ---- drop (-d) variants ----
|
||||
pytest.param(
|
||||
{},
|
||||
("ftintitle", "-d"),
|
||||
("Alice ft Bob", "Song 1", "Alice"),
|
||||
("Alice", "Song 1"),
|
||||
id="drop-self-ft",
|
||||
),
|
||||
pytest.param(
|
||||
{},
|
||||
("ftintitle", "-d"),
|
||||
("Alice", "Song 1", "Alice"),
|
||||
("Alice", "Song 1"),
|
||||
id="drop-self-no-ft",
|
||||
),
|
||||
pytest.param(
|
||||
{},
|
||||
("ftintitle", "-d"),
|
||||
("Alice ft Bob", "Song 1", "George"),
|
||||
("Alice", "Song 1"),
|
||||
id="drop-guest-ft",
|
||||
),
|
||||
pytest.param(
|
||||
{},
|
||||
("ftintitle", "-d"),
|
||||
("Alice", "Song 1", "George"),
|
||||
("Alice", "Song 1"),
|
||||
id="drop-guest-no-ft",
|
||||
),
|
||||
# ---- custom format variants ----
|
||||
pytest.param(
|
||||
{"format": "feat. {}"},
|
||||
("ftintitle",),
|
||||
("Alice ft Bob", "Song 1", "Alice"),
|
||||
("Alice", "Song 1 feat. Bob"),
|
||||
id="custom-format-feat-dot",
|
||||
),
|
||||
pytest.param(
|
||||
{"format": "featuring {}"},
|
||||
("ftintitle",),
|
||||
("Alice feat. Bob", "Song 1", "Alice"),
|
||||
("Alice", "Song 1 featuring Bob"),
|
||||
id="custom-format-featuring",
|
||||
),
|
||||
pytest.param(
|
||||
{"format": "with {}"},
|
||||
("ftintitle",),
|
||||
("Alice feat Bob", "Song 1", "Alice"),
|
||||
("Alice", "Song 1 with Bob"),
|
||||
id="custom-format-with",
|
||||
),
|
||||
# ---- keep_in_artist variants ----
|
||||
pytest.param(
|
||||
{"format": "feat. {}", "keep_in_artist": True},
|
||||
("ftintitle",),
|
||||
("Alice ft Bob", "Song 1", "Alice"),
|
||||
("Alice ft Bob", "Song 1 feat. Bob"),
|
||||
id="keep-in-artist-add-to-title",
|
||||
),
|
||||
pytest.param(
|
||||
{"format": "feat. {}", "keep_in_artist": True},
|
||||
("ftintitle", "-d"),
|
||||
("Alice ft Bob", "Song 1", "Alice"),
|
||||
("Alice ft Bob", "Song 1"),
|
||||
id="keep-in-artist-drop-from-title",
|
||||
),
|
||||
# ---- custom_words variants ----
|
||||
pytest.param(
|
||||
{"format": "featuring {}", "custom_words": ["med"]},
|
||||
("ftintitle",),
|
||||
("Alice med Bob", "Song 1", "Alice"),
|
||||
("Alice", "Song 1 featuring Bob"),
|
||||
id="custom-feat-words",
|
||||
),
|
||||
pytest.param(
|
||||
{
|
||||
"format": "featuring {}",
|
||||
"keep_in_artist": True,
|
||||
"custom_words": ["med"],
|
||||
},
|
||||
("ftintitle",),
|
||||
("Alice med Bob", "Song 1", "Alice"),
|
||||
("Alice med Bob", "Song 1 featuring Bob"),
|
||||
id="custom-feat-words-keep-in-artists",
|
||||
),
|
||||
pytest.param(
|
||||
{
|
||||
"format": "featuring {}",
|
||||
"keep_in_artist": True,
|
||||
"custom_words": ["med"],
|
||||
},
|
||||
("ftintitle", "-d"),
|
||||
("Alice med Bob", "Song 1", "Alice"),
|
||||
("Alice med Bob", "Song 1"),
|
||||
id="custom-feat-words-keep-in-artists-drop-from-title",
|
||||
),
|
||||
# ---- preserve_album_artist variants ----
|
||||
pytest.param(
|
||||
{"format": "feat. {}", "preserve_album_artist": True},
|
||||
("ftintitle",),
|
||||
("Alice feat. Bob", "Song 1", "Alice"),
|
||||
("Alice", "Song 1 feat. Bob"),
|
||||
id="skip-if-artist-and-album-artists-is-the-same-different-match",
|
||||
),
|
||||
pytest.param(
|
||||
{"format": "feat. {}", "preserve_album_artist": False},
|
||||
("ftintitle",),
|
||||
("Alice feat. Bob", "Song 1", "Alice"),
|
||||
("Alice", "Song 1 feat. Bob"),
|
||||
id="skip-if-artist-and-album-artists-is-the-same-different-match-b",
|
||||
),
|
||||
pytest.param(
|
||||
{"format": "feat. {}", "preserve_album_artist": True},
|
||||
("ftintitle",),
|
||||
("Alice feat. Bob", "Song 1", "Alice feat. Bob"),
|
||||
("Alice feat. Bob", "Song 1"),
|
||||
id="skip-if-artist-and-album-artists-is-the-same-matching-match",
|
||||
),
|
||||
pytest.param(
|
||||
{"format": "feat. {}", "preserve_album_artist": False},
|
||||
("ftintitle",),
|
||||
("Alice feat. Bob", "Song 1", "Alice feat. Bob"),
|
||||
("Alice", "Song 1 feat. Bob"),
|
||||
id="skip-if-artist-and-album-artists-is-the-same-matching-match-b",
|
||||
),
|
||||
# ---- titles with brackets/parentheses ----
|
||||
pytest.param(
|
||||
{"format": "ft. {}", "bracket_keywords": ["mix"]},
|
||||
("ftintitle",),
|
||||
("Alice ft. Bob", "Song 1 (Club Mix)", "Alice"),
|
||||
("Alice", "Song 1 ft. Bob (Club Mix)"),
|
||||
id="ft-inserted-before-matching-bracket-keyword",
|
||||
),
|
||||
pytest.param(
|
||||
{"format": "ft. {}", "bracket_keywords": ["nomatch"]},
|
||||
("ftintitle",),
|
||||
("Alice ft. Bob", "Song 1 (Club Remix)", "Alice"),
|
||||
("Alice", "Song 1 (Club Remix) ft. Bob"),
|
||||
id="ft-inserted-at-end-no-bracket-keyword-match",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_ftintitle_functional(
|
||||
self,
|
||||
cfg: dict[str, str | bool | list[str]],
|
||||
cmd_args: tuple[str, ...],
|
||||
given: tuple[str, str, str | None],
|
||||
expected: tuple[str, str],
|
||||
) -> None:
|
||||
config = {
|
||||
"drop": False,
|
||||
"auto": True,
|
||||
"keep_in_artist": False,
|
||||
"custom_words": [],
|
||||
**cfg,
|
||||
}
|
||||
|
||||
artist, title, albumartist = given
|
||||
item = self.add_item(
|
||||
path="/",
|
||||
artist=artist,
|
||||
artist_sort=artist,
|
||||
title=title,
|
||||
albumartist=albumartist,
|
||||
)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"cfg, cmd_args, given, expected",
|
||||
[
|
||||
pytest.param(
|
||||
None,
|
||||
("ftintitle",),
|
||||
("Alice", "Song 1", "Alice"),
|
||||
("Alice", "Song 1"),
|
||||
id="no-featured-artist",
|
||||
),
|
||||
pytest.param(
|
||||
{"format": "feat {0}"},
|
||||
("ftintitle",),
|
||||
("Alice ft. Bob", "Song 1", None),
|
||||
("Alice", "Song 1 feat Bob"),
|
||||
id="no-albumartist-custom-format",
|
||||
),
|
||||
pytest.param(
|
||||
None,
|
||||
("ftintitle",),
|
||||
("Alice", "Song 1", None),
|
||||
("Alice", "Song 1"),
|
||||
id="no-albumartist-no-feature",
|
||||
),
|
||||
pytest.param(
|
||||
{"format": "featuring {0}"},
|
||||
("ftintitle",),
|
||||
("Alice ft Bob", "Song 1", "George"),
|
||||
("Alice", "Song 1 featuring Bob"),
|
||||
id="guest-artist-custom-format",
|
||||
),
|
||||
pytest.param(
|
||||
None,
|
||||
("ftintitle",),
|
||||
("Alice", "Song 1", "George"),
|
||||
("Alice", "Song 1"),
|
||||
id="guest-artist-no-feature",
|
||||
),
|
||||
# ---- drop (-d) variants ----
|
||||
pytest.param(
|
||||
None,
|
||||
("ftintitle", "-d"),
|
||||
("Alice ft Bob", "Song 1", "Alice"),
|
||||
("Alice", "Song 1"),
|
||||
id="drop-self-ft",
|
||||
),
|
||||
pytest.param(
|
||||
None,
|
||||
("ftintitle", "-d"),
|
||||
("Alice", "Song 1", "Alice"),
|
||||
("Alice", "Song 1"),
|
||||
id="drop-self-no-ft",
|
||||
),
|
||||
pytest.param(
|
||||
None,
|
||||
("ftintitle", "-d"),
|
||||
("Alice ft Bob", "Song 1", "George"),
|
||||
("Alice", "Song 1"),
|
||||
id="drop-guest-ft",
|
||||
),
|
||||
pytest.param(
|
||||
None,
|
||||
("ftintitle", "-d"),
|
||||
("Alice", "Song 1", "George"),
|
||||
("Alice", "Song 1"),
|
||||
id="drop-guest-no-ft",
|
||||
),
|
||||
# ---- custom format variants ----
|
||||
pytest.param(
|
||||
{"format": "feat. {}"},
|
||||
("ftintitle",),
|
||||
("Alice ft Bob", "Song 1", "Alice"),
|
||||
("Alice", "Song 1 feat. Bob"),
|
||||
id="custom-format-feat-dot",
|
||||
),
|
||||
pytest.param(
|
||||
{"format": "featuring {}"},
|
||||
("ftintitle",),
|
||||
("Alice feat. Bob", "Song 1", "Alice"),
|
||||
("Alice", "Song 1 featuring Bob"),
|
||||
id="custom-format-featuring",
|
||||
),
|
||||
pytest.param(
|
||||
{"format": "with {}"},
|
||||
("ftintitle",),
|
||||
("Alice feat Bob", "Song 1", "Alice"),
|
||||
("Alice", "Song 1 with Bob"),
|
||||
id="custom-format-with",
|
||||
),
|
||||
# ---- keep_in_artist variants ----
|
||||
pytest.param(
|
||||
{"format": "feat. {}", "keep_in_artist": True},
|
||||
("ftintitle",),
|
||||
("Alice ft Bob", "Song 1", "Alice"),
|
||||
("Alice ft Bob", "Song 1 feat. Bob"),
|
||||
id="keep-in-artist-add-to-title",
|
||||
),
|
||||
pytest.param(
|
||||
{"format": "feat. {}", "keep_in_artist": True},
|
||||
("ftintitle", "-d"),
|
||||
("Alice ft Bob", "Song 1", "Alice"),
|
||||
("Alice ft Bob", "Song 1"),
|
||||
id="keep-in-artist-drop-from-title",
|
||||
),
|
||||
# ---- custom_words variants ----
|
||||
pytest.param(
|
||||
{"format": "featuring {}", "custom_words": ["med"]},
|
||||
("ftintitle",),
|
||||
("Alice med Bob", "Song 1", "Alice"),
|
||||
("Alice", "Song 1 featuring Bob"),
|
||||
id="custom-feat-words",
|
||||
),
|
||||
pytest.param(
|
||||
{
|
||||
"format": "featuring {}",
|
||||
"keep_in_artist": True,
|
||||
"custom_words": ["med"],
|
||||
},
|
||||
("ftintitle",),
|
||||
("Alice med Bob", "Song 1", "Alice"),
|
||||
("Alice med Bob", "Song 1 featuring Bob"),
|
||||
id="custom-feat-words-keep-in-artists",
|
||||
),
|
||||
pytest.param(
|
||||
{
|
||||
"format": "featuring {}",
|
||||
"keep_in_artist": True,
|
||||
"custom_words": ["med"],
|
||||
},
|
||||
("ftintitle", "-d"),
|
||||
("Alice med Bob", "Song 1", "Alice"),
|
||||
("Alice med Bob", "Song 1"),
|
||||
id="custom-feat-words-keep-in-artists-drop-from-title",
|
||||
),
|
||||
# ---- preserve_album_artist variants ----
|
||||
pytest.param(
|
||||
{"format": "feat. {}", "preserve_album_artist": True},
|
||||
("ftintitle",),
|
||||
("Alice feat. Bob", "Song 1", "Alice"),
|
||||
("Alice", "Song 1 feat. Bob"),
|
||||
id="skip-if-artist-and-album-artists-is-the-same-different-match",
|
||||
),
|
||||
pytest.param(
|
||||
{"format": "feat. {}", "preserve_album_artist": False},
|
||||
("ftintitle",),
|
||||
("Alice feat. Bob", "Song 1", "Alice"),
|
||||
("Alice", "Song 1 feat. Bob"),
|
||||
id="skip-if-artist-and-album-artists-is-the-same-different-match-b",
|
||||
),
|
||||
pytest.param(
|
||||
{"format": "feat. {}", "preserve_album_artist": True},
|
||||
("ftintitle",),
|
||||
("Alice feat. Bob", "Song 1", "Alice feat. Bob"),
|
||||
("Alice feat. Bob", "Song 1"),
|
||||
id="skip-if-artist-and-album-artists-is-the-same-matching-match",
|
||||
),
|
||||
pytest.param(
|
||||
{"format": "feat. {}", "preserve_album_artist": False},
|
||||
("ftintitle",),
|
||||
("Alice feat. Bob", "Song 1", "Alice feat. Bob"),
|
||||
("Alice", "Song 1 feat. Bob"),
|
||||
id="skip-if-artist-and-album-artists-is-the-same-matching-match-b",
|
||||
),
|
||||
# ---- titles with brackets/parentheses ----
|
||||
pytest.param(
|
||||
{"format": "ft. {}", "bracket_keywords": ["mix"]},
|
||||
("ftintitle",),
|
||||
("Alice ft. Bob", "Song 1 (Club Mix)", "Alice"),
|
||||
("Alice", "Song 1 ft. Bob (Club Mix)"),
|
||||
id="ft-inserted-before-matching-bracket-keyword",
|
||||
),
|
||||
pytest.param(
|
||||
{"format": "ft. {}", "bracket_keywords": ["nomatch"]},
|
||||
("ftintitle",),
|
||||
("Alice ft. Bob", "Song 1 (Club Remix)", "Alice"),
|
||||
("Alice", "Song 1 (Club Remix) ft. Bob"),
|
||||
id="ft-inserted-at-end-no-bracket-keyword-match",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_ftintitle_functional(
|
||||
env: FtInTitlePluginFunctional,
|
||||
cfg: dict[str, str | bool | list[str]] | None,
|
||||
cmd_args: tuple[str, ...],
|
||||
given: tuple[str, str, str | None],
|
||||
expected: tuple[str, str],
|
||||
) -> None:
|
||||
set_config(env, cfg)
|
||||
ftintitle.FtInTitlePlugin()
|
||||
with self.configure_plugin(config):
|
||||
self.run_command(*cmd_args)
|
||||
|
||||
artist, title, albumartist = given
|
||||
item = add_item(env, "/", artist, title, albumartist)
|
||||
item.load()
|
||||
|
||||
env.run_command(*cmd_args)
|
||||
item.load()
|
||||
|
||||
expected_artist, expected_title = expected
|
||||
assert item["artist"] == expected_artist
|
||||
assert item["title"] == expected_title
|
||||
expected_artist, expected_title = expected
|
||||
assert item["artist"] == expected_artist
|
||||
assert item["title"] == expected_title
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
||||
@@ -22,13 +22,13 @@ from typing import TYPE_CHECKING, ClassVar
|
||||
import pytest
|
||||
|
||||
from beets import plugins
|
||||
from beets.test.helper import PytestPluginTestHelper
|
||||
from beets.test.helper import PluginTestHelper
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Callable
|
||||
|
||||
|
||||
class HookTestCase(PytestPluginTestHelper):
|
||||
class HookTestCase(PluginTestHelper):
|
||||
plugin = "hook"
|
||||
preload_plugin = False
|
||||
|
||||
|
||||
@@ -165,8 +165,8 @@ class TestHtml:
|
||||
assert lyrics.Html.normalize_space(initial) == expected
|
||||
|
||||
def test_scrape_merge_paragraphs(self):
|
||||
text = "one</p> <p class='myclass'>two</p><p>three"
|
||||
expected = "one\ntwo\n\nthree"
|
||||
text = 'one</p><p class="myclass"></p><p>two</p><p>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):
|
||||
|
||||
@@ -24,14 +24,6 @@ class TestMbCollectionPlugin(PluginMixin, TestHelper):
|
||||
self.config["musicbrainz"]["pass"] = "testpass"
|
||||
self.config["mbcollection"]["collection"] = self.COLLECTION_ID
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def helper(self):
|
||||
self.setup_beets()
|
||||
|
||||
yield self
|
||||
|
||||
self.teardown_beets()
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"user_collections,expectation",
|
||||
[
|
||||
@@ -66,7 +58,7 @@ class TestMbCollectionPlugin(PluginMixin, TestHelper):
|
||||
with expectation:
|
||||
mbcollection.MusicBrainzCollectionPlugin().collection
|
||||
|
||||
def test_mbupdate(self, helper, requests_mock, monkeypatch):
|
||||
def test_mbupdate(self, requests_mock, monkeypatch):
|
||||
"""Verify mbupdate sync of a MusicBrainz collection with the library.
|
||||
|
||||
This test ensures that the command:
|
||||
@@ -85,7 +77,7 @@ class TestMbCollectionPlugin(PluginMixin, TestHelper):
|
||||
"00000000-0000-0000-0000-000000000001",
|
||||
"00000000-0000-0000-0000-000000000002",
|
||||
]:
|
||||
helper.lib.add(Album(mb_albumid=mb_albumid))
|
||||
self.lib.add(Album(mb_albumid=mb_albumid))
|
||||
|
||||
# The relevant collection
|
||||
requests_mock.get(
|
||||
@@ -138,13 +130,11 @@ class TestMbCollectionPlugin(PluginMixin, TestHelper):
|
||||
re.compile(rf".*{collection_releases}/not_in_library")
|
||||
)
|
||||
|
||||
helper.run_command("mbupdate", "--remove")
|
||||
self.run_command("mbupdate", "--remove")
|
||||
|
||||
assert requests_mock.call_count == 6
|
||||
|
||||
def test_mbupdate_logs_unauthorized_errors(
|
||||
self, helper, requests_mock, caplog
|
||||
):
|
||||
def test_mbupdate_logs_unauthorized_errors(self, requests_mock, caplog):
|
||||
response = requests.Response()
|
||||
response.status_code = 401
|
||||
requests_mock.get(
|
||||
@@ -153,7 +143,7 @@ class TestMbCollectionPlugin(PluginMixin, TestHelper):
|
||||
)
|
||||
|
||||
with caplog.at_level("ERROR", logger="beets.mbcollection"):
|
||||
helper.run_command("mbupdate")
|
||||
self.run_command("mbupdate")
|
||||
|
||||
expected_message = (
|
||||
"Failed to update MusicBrainz collection: HTTP Error: 401"
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -14,12 +14,12 @@
|
||||
|
||||
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
|
||||
from beets.test.helper import PluginTestHelper
|
||||
|
||||
|
||||
class TestMbsyncCli(PytestPluginTestHelper):
|
||||
class TestMbsyncCli(PluginTestHelper):
|
||||
plugin = "mbsync"
|
||||
|
||||
@patch(
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 (
|
||||
|
||||
1210
test/test_library.py
1210
test/test_library.py
File diff suppressed because it is too large
Load Diff
@@ -39,13 +39,13 @@ from beets.test.helper import (
|
||||
ImportHelper,
|
||||
IOMixin,
|
||||
PluginMixin,
|
||||
PytestPluginTestHelper,
|
||||
PluginTestHelper,
|
||||
TerminalImportMixin,
|
||||
)
|
||||
from beets.util import PromptChoice, displayable_path, syspath
|
||||
|
||||
|
||||
class TestPluginRegistration(IOMixin, PytestPluginTestHelper):
|
||||
class TestPluginRegistration(IOMixin, PluginTestHelper):
|
||||
class RatingPlugin(plugins.BeetsPlugin):
|
||||
item_types: ClassVar[dict[str, types.Type]] = {
|
||||
"rating": types.Float(),
|
||||
@@ -97,7 +97,7 @@ class TestPluginRegistration(IOMixin, PytestPluginTestHelper):
|
||||
assert out == "one; two; three\n"
|
||||
|
||||
|
||||
class PytestImportHelper(ImportHelper, PytestPluginTestHelper):
|
||||
class PytestImportHelper(ImportHelper, PluginTestHelper):
|
||||
@pytest.fixture(autouse=True)
|
||||
def setup_import_helper(self, setup):
|
||||
self.import_media = []
|
||||
@@ -179,7 +179,7 @@ class TestEvents(PytestImportHelper):
|
||||
]
|
||||
|
||||
|
||||
class TestListeners(PytestPluginTestHelper):
|
||||
class TestListeners(PluginTestHelper):
|
||||
def test_register(self):
|
||||
class DummyPlugin(plugins.BeetsPlugin):
|
||||
def __init__(self):
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user