Skip to content

Refactor of metadata plugin and opt in all metadata plugins to new baseclass. #5764

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
a4a1d93
Move musicbrainz to beetsplug directory
snejus Feb 16, 2025
bdd65c4
musicbrainz: reorder methods
snejus Feb 16, 2025
dd46122
Define MusicBrainzPlugin
snejus Apr 21, 2025
bad4496
musicbrainz: use self.config and self._log
snejus Feb 17, 2025
d3f365e
Remove ...for_mbid methods and simplify the rest
snejus Feb 17, 2025
cd4dde2
Use candidate function from plugins instead of hooks
snejus Apr 21, 2025
a43d610
musicbrainz: move handling of extra tags to musicbrainz plugin
snejus Feb 17, 2025
eaeca88
plugins: add types and documentation to metadata backends methods and…
snejus Feb 17, 2025
88092b2
Centralize AutotagStub test setup into AutotagImportTestCase
snejus Feb 17, 2025
667d022
musicbrainz: synchronise plugin import path
snejus Feb 17, 2025
7425f60
musicbrainz: update patches
snejus Feb 17, 2025
1bee32a
musicbrainz: fix types
snejus Feb 17, 2025
8fcf340
Move musicbrainz documentation to a separate file
snejus Feb 17, 2025
8b97ef7
musicbrainz: set default config in the code
snejus Mar 2, 2025
a3d6d88
Move scrub test to a separate file
snejus Apr 21, 2025
68c22e3
Add changelog note about musicbrainz
snejus Apr 22, 2025
28c246f
Deprecate musicbrainz.enabled configuration
snejus Apr 22, 2025
febf910
Add musicbrainz to plugins docs
snejus Apr 22, 2025
0c2fc7b
Use wraps for notify_info_yielded decorator
snejus May 4, 2025
fb0d14e
plugins: restructure id extraction
snejus May 5, 2025
55c65cb
Added proposal for metadata plugin split.
semohr May 6, 2025
ae76972
Release id extraction as tuple to allow parsing of multiple sources for
semohr May 6, 2025
161073a
Renamed to metadatapluginnext and regex as class methods.
semohr May 6, 2025
6b0a5ae
Moved mb extract id into the mb plugin. Mb plugin now extends
semohr May 6, 2025
e70eb4e
Beatport now also extends from MetadataSourcePluginNext.
semohr May 6, 2025
87be814
Discogs now extends MetadataSourcePluginNext
semohr May 7, 2025
6e7bfaf
Spotify now extends MetadataSourcePluginNext. Also fixed
semohr May 7, 2025
d508c20
Deezer now extends MetadataSourcePluginNext
semohr May 7, 2025
0de6ad0
Chroma now extends MetadataSourcePluginNext
semohr May 7, 2025
3a1bc91
Enhanced searchapi plugin metaclass and reimplemented logic for deeze…
semohr May 7, 2025
f5a9b98
Removed references to old Metadataplugin.
semohr May 7, 2025
2e0aee8
Linting issue
semohr May 7, 2025
8128e66
Move musicbrainz to beetsplug directory
snejus Feb 16, 2025
b47cd96
musicbrainz: reorder methods
snejus Feb 16, 2025
6de882b
Define MusicBrainzPlugin
snejus Apr 21, 2025
bacb359
musicbrainz: use self.config and self._log
snejus Feb 17, 2025
2fdcc86
Remove ...for_mbid methods and simplify the rest
snejus Feb 17, 2025
b60f7c5
Use candidate function from plugins instead of hooks
snejus Apr 21, 2025
72fd81b
musicbrainz: move handling of extra tags to musicbrainz plugin
snejus Feb 17, 2025
87eaa4d
plugins: add types and documentation to metadata backends methods and…
snejus Feb 17, 2025
f34f3d0
Centralize AutotagStub test setup into AutotagImportTestCase
snejus Feb 17, 2025
2045541
musicbrainz: synchronise plugin import path
snejus Feb 17, 2025
79d5ccf
musicbrainz: update patches
snejus Feb 17, 2025
2bb4919
Move musicbrainz documentation to a separate file
snejus Feb 17, 2025
318f256
musicbrainz: set default config in the code
snejus Mar 2, 2025
1dfcb2a
Move scrub test to a separate file
snejus Apr 21, 2025
13908f7
Add changelog note about musicbrainz
snejus Apr 22, 2025
de8ae89
Deprecate musicbrainz.enabled configuration
snejus Apr 22, 2025
8df2022
Add musicbrainz to plugins docs
snejus Apr 22, 2025
53717ee
Use wraps for notify_info_yielded decorator
snejus May 4, 2025
cab0246
Fix types in all edited files
snejus May 7, 2025
ad0a784
plugins: restructure id extraction
snejus May 8, 2025
594eea1
Merge remote-tracking branch 'upstream/restructure-id-extractors' int…
semohr May 8, 2025
14b02b6
lint fix
semohr May 8, 2025
7836fac
Formatting issue
semohr May 8, 2025
59c1101
Enhanced docs and added reference to metadatasourceplugin
semohr May 13, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 7 additions & 104 deletions beets/autotag/hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,16 @@

import re
from functools import total_ordering
from typing import TYPE_CHECKING, Any, Callable, NamedTuple, TypeVar
from typing import TYPE_CHECKING, Any, NamedTuple, TypeVar

from jellyfish import levenshtein_distance
from unidecode import unidecode

from beets import config, logging, plugins
from beets.autotag import mb
from beets import config, logging
from beets.util import as_string, cached_classproperty

if TYPE_CHECKING:
from collections.abc import Iterable, Iterator
from collections.abc import Iterator

from beets.library import Item

Expand Down Expand Up @@ -56,7 +55,7 @@ def __hash__(self):
return id(self)


class AlbumInfo(AttrDict):
class AlbumInfo(AttrDict[Any]):
"""Describes a canonical release that may be used to match a release
in the library. Consists of these data members:

Expand Down Expand Up @@ -166,7 +165,7 @@ def copy(self) -> AlbumInfo:
return dupe


class TrackInfo(AttrDict):
class TrackInfo(AttrDict[Any]):
"""Describes a canonical track present on a release. Appears as part
of an AlbumInfo's ``tracks`` list. Consists of these data members:

Expand Down Expand Up @@ -357,8 +356,8 @@ class Distance:
for each individual penalty.
"""

def __init__(self):
self._penalties = {}
def __init__(self) -> None:
self._penalties: dict[str, list[float]] = {}
self.tracks: dict[TrackInfo, Distance] = {}

@cached_classproperty
Expand Down Expand Up @@ -591,99 +590,3 @@ class AlbumMatch(NamedTuple):
class TrackMatch(NamedTuple):
distance: Distance
info: TrackInfo


# Aggregation of sources.


def album_for_mbid(release_id: str) -> AlbumInfo | None:
"""Get an AlbumInfo object for a MusicBrainz release ID. Return None
if the ID is not found.
"""
try:
if album := mb.album_for_id(release_id):
plugins.send("albuminfo_received", info=album)
return album
except mb.MusicBrainzAPIError as exc:
exc.log(log)
return None


def track_for_mbid(recording_id: str) -> TrackInfo | None:
"""Get a TrackInfo object for a MusicBrainz recording ID. Return None
if the ID is not found.
"""
try:
if track := mb.track_for_id(recording_id):
plugins.send("trackinfo_received", info=track)
return track
except mb.MusicBrainzAPIError as exc:
exc.log(log)
return None


def album_for_id(_id: str) -> AlbumInfo | None:
"""Get AlbumInfo object for the given ID string."""
return album_for_mbid(_id) or plugins.album_for_id(_id)


def track_for_id(_id: str) -> TrackInfo | None:
"""Get TrackInfo object for the given ID string."""
return track_for_mbid(_id) or plugins.track_for_id(_id)


def invoke_mb(call_func: Callable, *args):
try:
return call_func(*args)
except mb.MusicBrainzAPIError as exc:
exc.log(log)
return ()


@plugins.notify_info_yielded("albuminfo_received")
def album_candidates(
items: list[Item],
artist: str,
album: str,
va_likely: bool,
extra_tags: dict,
) -> Iterable[tuple]:
"""Search for album matches. ``items`` is a list of Item objects
that make up the album. ``artist`` and ``album`` are the respective
names (strings), which may be derived from the item list or may be
entered by the user. ``va_likely`` is a boolean indicating whether
the album is likely to be a "various artists" release. ``extra_tags``
is an optional dictionary of additional tags used to further
constrain the search.
"""

if config["musicbrainz"]["enabled"]:
# Base candidates if we have album and artist to match.
if artist and album:
yield from invoke_mb(
mb.match_album, artist, album, len(items), extra_tags
)

# Also add VA matches from MusicBrainz where appropriate.
if va_likely and album:
yield from invoke_mb(
mb.match_album, None, album, len(items), extra_tags
)

# Candidates from plugins.
yield from plugins.candidates(items, artist, album, va_likely, extra_tags)


@plugins.notify_info_yielded("trackinfo_received")
def item_candidates(item: Item, artist: str, title: str) -> Iterable[tuple]:
"""Search for item matches. ``item`` is the Item to be matched.
``artist`` and ``title`` are strings and either reflect the item or
are specified by the user.
"""

# MusicBrainz candidates.
if config["musicbrainz"]["enabled"] and artist and title:
yield from invoke_mb(mb.match_track, artist, title)

# Plugin candidates.
yield from plugins.item_candidates(item, artist, title)
44 changes: 19 additions & 25 deletions beets/autotag/match.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
import lap
import numpy as np

from beets import config, logging, plugins
from beets import config, logging, metadata_plugins, plugins
from beets.autotag import (
AlbumInfo,
AlbumMatch,
Expand Down Expand Up @@ -335,8 +335,8 @@ def distance(
return dist


def match_by_id(items: Iterable[Item]):
"""If the items are tagged with a MusicBrainz album ID, returns an
def match_by_id(items: Iterable[Item]) -> AlbumInfo | None:
"""If the items are tagged with an external source ID, return an
AlbumInfo object for the corresponding album. Otherwise, returns
None.
"""
Expand All @@ -356,7 +356,7 @@ def match_by_id(items: Iterable[Item]):
return None
# If all album IDs are equal, look up the album.
log.debug("Searching for discovered album ID: {0}", first)
return hooks.album_for_mbid(first)
return metadata_plugins.album_for_id(first)


def _recommendation(
Expand Down Expand Up @@ -511,15 +511,14 @@ def tag_album(
if search_ids:
for search_id in search_ids:
log.debug("Searching for album ID: {0}", search_id)
if info := hooks.album_for_id(search_id):
if info := metadata_plugins.album_for_id(search_id):
_add_candidate(items, candidates, info)

# Use existing metadata or text search.
else:
# Try search based on current ID.
id_info = match_by_id(items)
if id_info:
_add_candidate(items, candidates, id_info)
if info := match_by_id(items):
_add_candidate(items, candidates, info)
rec = _recommendation(list(candidates.values()))
log.debug("Album ID match recommendation is {0}", rec)
if candidates and not config["import"]["timid"]:
Expand All @@ -540,12 +539,6 @@ def tag_album(
search_artist, search_album = cur_artist, cur_album
log.debug("Search terms: {0} - {1}", search_artist, search_album)

extra_tags = None
if config["musicbrainz"]["extra_tags"]:
tag_list = config["musicbrainz"]["extra_tags"].get()
extra_tags = {k: v for (k, v) in likelies.items() if k in tag_list}
log.debug("Additional search terms: {0}", extra_tags)

# Is this album likely to be a "various artist" release?
va_likely = (
(not consensus["artist"])
Expand All @@ -555,8 +548,8 @@ def tag_album(
log.debug("Album might be VA: {0}", va_likely)

# Get the results from the data sources.
for matched_candidate in hooks.album_candidates(
items, search_artist, search_album, va_likely, extra_tags
for matched_candidate in metadata_plugins.candidates(
items, search_artist, search_album, va_likely
):
_add_candidate(items, candidates, matched_candidate)

Expand All @@ -576,22 +569,21 @@ def tag_item(
"""Find metadata for a single track. Return a `Proposal` consisting
of `TrackMatch` objects.

`search_artist` and `search_title` may be used
to override the current metadata for the purposes of the MusicBrainz
title. `search_ids` may be used for restricting the search to a list
of metadata backend IDs.
`search_artist` and `search_title` may be used to override the item
metadata in the search query. `search_ids` may be used for restricting the
search to a list of metadata backend IDs.
"""
# Holds candidates found so far: keys are MBIDs; values are
# (distance, TrackInfo) pairs.
candidates = {}
rec: Recommendation | None = None

# First, try matching by MusicBrainz ID.
# First, try matching by the external source ID.
trackids = search_ids or [t for t in [item.mb_trackid] if t]
if trackids:
for trackid in trackids:
log.debug("Searching for track ID: {0}", trackid)
if info := hooks.track_for_id(trackid):
if info := metadata_plugins.track_for_id(trackid):
dist = track_distance(item, info, incl_artist=True)
candidates[info.track_id] = hooks.TrackMatch(dist, info)
# If this is a good match, then don't keep searching.
Expand All @@ -612,12 +604,14 @@ def tag_item(
return Proposal([], Recommendation.none)

# Search terms.
if not (search_artist and search_title):
search_artist, search_title = item.artist, item.title
search_artist = search_artist or item.artist
search_title = search_title or item.title
log.debug("Item search terms: {0} - {1}", search_artist, search_title)

# Get and evaluate candidate metadata.
for track_info in hooks.item_candidates(item, search_artist, search_title):
for track_info in metadata_plugins.item_candidates(
item, search_artist, search_title
):
dist = track_distance(item, track_info, incl_artist=True)
candidates[track_info.track_id] = hooks.TrackMatch(dist, track_info)

Expand Down
Loading