Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions beets/autotag/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
Match,
Proposal,
Recommendation,
SearchQuery,
TrackMatch,
assign_items,
tag_album,
Expand All @@ -40,6 +41,7 @@ def __getattr__(name: str):
"Match",
"Proposal",
"Recommendation",
"SearchQuery",
"Source",
"TrackInfo",
"TrackMatch",
Expand Down
121 changes: 57 additions & 64 deletions beets/autotag/match.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import numpy as np

from beets import config, logging, metadata_plugins, plugins
from beets.util import Likelies

Check failure on line 16 in beets/autotag/match.py

View workflow job for this annotation

GitHub Actions / Check linting

ruff (F401)

beets/autotag/match.py:16:24: F401 `beets.util.Likelies` imported but unused help: Remove unused import: `beets.util.Likelies`

from .distance import VA_ARTISTS, distance, track_distance

Expand Down Expand Up @@ -380,96 +381,91 @@
)


@dataclass(frozen=True)
class SearchQuery:
"""User supplied search query"""

title: str | None = None
artist: str | None = None

@property
def va_likely(self) -> bool:
"""Return True if the search query is likely a compilation."""
return self.artist is not None and self.artist.lower() in VA_ARTISTS


def tag_album(
source: Source,
search_artist: str | None = None,
search_name: str | None = None,
search_ids: list[str] = [],
search_query: SearchQuery | None = None,
search_ids: list[str] | None = None,
) -> Proposal:
"""Return `Proposal` containing `AlbumMatch` candidates.

The `AlbumMatch` objects are generated by searching the metadata
backends. By default, the metadata of the items is used for the
search. This can be customized by setting the parameters.
`search_ids` is a list of metadata backend IDs: if specified,
it will restrict the candidates to those IDs, ignoring
`search_artist` and `search album`. The `mapping` field of the
album has the matched `items` as keys.
"""Find metadata for a album. Return a `Proposal` containing `AlbumMatch`
candidates.

The recommendation is calculated from the match quality of the
candidates.
"""
# Get current metadata.
log.debug("Tagging {}", source.desc)

# The output result, keys are (data_source, album_id) pairs, values are
# AlbumMatch objects.
candidates: Candidates[AlbumMatch] = {}
rec: Recommendation | None = None

# Search by explicit ID.
if search_ids:
log.debug("Searching for album IDs: {}", ", ".join(search_ids))
for _info in metadata_plugins.albums_for_ids(search_ids):
_add_candidate(source, candidates, _info)

# Use existing metadata or text search.
else:
# Try search based on current ID.
for info in match_by_id(source.id, source.id_consensus):
# Search by ids either user provided or discovered from the source.
albumids = search_ids or [t for t in [source.id] if t]
if albumids:
log.debug("Searching for album IDs: {}", ", ".join(albumids))
for info in metadata_plugins.albums_for_ids(albumids):
_add_candidate(source, candidates, info)

rec = _recommendation(list(candidates.values()))
log.debug("Album ID match recommendation is {}", rec)
if candidates and not config["import"]["timid"]:
# If we have a very good MBID match, return immediately.
# Otherwise, this match will compete against metadata-based
# matches.
if rec == Recommendation.strong:
log.debug("ID match.")
return Proposal(list(candidates.values()), rec)

# Search terms.
if not (search_artist and search_name):
# No explicit search terms -- use current metadata.
search_artist, search_name = source.artist, source.name
log.debug("Search terms: {} - {}", search_artist, search_name)

# Is this album likely to be a "various artist" release?
va_likely = source.va_likely or (search_artist.lower() in VA_ARTISTS)
log.debug("Album might be VA: {}", va_likely)

# Get the results from the data sources.
for matched_candidate in metadata_plugins.candidates(
source.items, search_artist, search_name, va_likely
):
_add_candidate(source, candidates, matched_candidate)
# If this is a good match, then don't keep searching.
rec = _recommendation(_sort_candidates(candidates.values()))
if rec == Recommendation.strong and not config["import"]["timid"]:
log.debug("Album ID match.")
return Proposal(_sort_candidates(candidates.values()), rec)

# If ID provided by user, don't proceed with search.
if search_ids:
if candidates:
assert rec is not None
return Proposal(_sort_candidates(candidates.values()), rec)
log.debug(
"No candidates found for user-provided Album IDs. Skipping further search."
)
return Proposal([], Recommendation.none)

# Search by metadata, either user provided or discovered from the source. We
# let the plugins decide on their own how to handle user-provided search queries.
# This allows plugins to skip unnecessary and cases. It is hard to account for all
# these here so using the escape hatch in the plugins is a better approach.
for matched_candidate in metadata_plugins.candidates(source, search_query):
_add_candidate(source, candidates, matched_candidate)

# Sort by distance and return with recommendation.
log.debug("Evaluating {} candidates.", len(candidates))
# Sort and get the recommendation.
candidates_sorted = _sort_candidates(candidates.values())
rec = _recommendation(candidates_sorted)
return Proposal(candidates_sorted, rec)


def tag_item(
source: Source,
search_artist: str | None = None,
search_name: str | None = None,
search_query: SearchQuery | None = None,
search_ids: list[str] | None = None,
) -> Proposal:
"""Find metadata for a single track. Return a `Proposal` consisting
of `TrackMatch` objects.

`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.
"""
log.debug("Tagging {}", source.desc)

# Holds candidates found so far: keys are (data_source, track_id) pairs,
# values TrackMatch objects
candidates: Candidates[TrackMatch] = {}
rec: Recommendation | None = None

item = source.items[0]

# First, try matching by the external source ID.
trackids = search_ids or [t for t in [source.id] if t]
if trackids:
Expand All @@ -489,17 +485,14 @@
if candidates:
assert rec is not None
return Proposal(_sort_candidates(candidates.values()), rec)
log.debug(
"No candidates found for user-provided Track IDs. Skipping further search."
)
return Proposal([], Recommendation.none)

# Search terms.
search_artist = search_artist or source.artist
search_name = search_name or source.name
log.debug("Item search terms: {} - {}", search_artist, search_name)

# Get and evaluate candidate metadata.
for track_info in metadata_plugins.item_candidates(
item, search_artist, search_name
):
# Search by metadata, either user provided or items from the source. We
# let the plugins decide on their own how to handle user-provided search queries.
for track_info in metadata_plugins.item_candidates(source, search_query):
dist = track_distance(item, track_info, incl_artist=True)
candidates[track_info.identifier] = TrackMatch(dist, track_info, item)

Expand Down
Loading
Loading