Skip to content
Open
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
8 changes: 5 additions & 3 deletions beets/autotag/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
from .match import Proposal, Recommendation, tag_album, tag_item

if TYPE_CHECKING:
from collections.abc import Mapping, Sequence
from collections.abc import Sequence

from beets.library import Album, Item, LibModel

Expand Down Expand Up @@ -210,11 +210,13 @@ def apply_album_metadata(album_info: AlbumInfo, album: Album):
correct_list_fields(album)


def apply_metadata(album_info: AlbumInfo, mapping: Mapping[Item, TrackInfo]):
def apply_metadata(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (code-quality): Low code quality found in apply_metadata - 19% (low-code-quality)


ExplanationThe quality score for this function is below the quality threshold of 25%.
This score is a combination of the method length, cognitive complexity and working memory.

How can you solve this?

It might be worth refactoring this function to make it shorter and more readable.

  • Reduce the function length by extracting pieces of functionality out into
    their own functions. This is the most important thing you can do - ideally a
    function should be less than 10 lines.
  • Reduce nesting, perhaps by introducing guard clauses to return early.
  • Ensure that variables are tightly scoped, so that code using related concepts
    sits together within the function rather than being scattered.

album_info: AlbumInfo, mapping: list[tuple[Item, TrackInfo]]
):
"""Set the items' metadata to match an AlbumInfo object using a
mapping from Items to TrackInfo objects.
"""
for item, track_info in mapping.items():
for item, track_info in mapping:
# Artist or artist credit.
if config["artist_credit"]:
item.artist = (
Expand Down
4 changes: 2 additions & 2 deletions beets/autotag/distance.py
Original file line number Diff line number Diff line change
Expand Up @@ -422,7 +422,7 @@ def track_distance(
def distance(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (code-quality): Low code quality found in distance - 16% (low-code-quality)


ExplanationThe quality score for this function is below the quality threshold of 25%.
This score is a combination of the method length, cognitive complexity and working memory.

How can you solve this?

It might be worth refactoring this function to make it shorter and more readable.

  • Reduce the function length by extracting pieces of functionality out into
    their own functions. This is the most important thing you can do - ideally a
    function should be less than 10 lines.
  • Reduce nesting, perhaps by introducing guard clauses to return early.
  • Ensure that variables are tightly scoped, so that code using related concepts
    sits together within the function rather than being scattered.

items: Sequence[Item],
album_info: AlbumInfo,
mapping: dict[Item, TrackInfo],
mapping: list[tuple[Item, TrackInfo]],
) -> Distance:
"""Determines how "significant" an album metadata change would be.
Returns a Distance object. `album_info` is an AlbumInfo object
Expand Down Expand Up @@ -518,7 +518,7 @@ def distance(

# Tracks.
dist.tracks = {}
for item, track in mapping.items():
for item, track in mapping:
dist.tracks[track] = track_distance(item, track, album_info.va)
dist.add("tracks", dist.tracks[track].distance)

Expand Down
40 changes: 34 additions & 6 deletions beets/autotag/hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,14 @@
from __future__ import annotations

from copy import deepcopy
from typing import TYPE_CHECKING, Any, NamedTuple, TypeVar
from dataclasses import dataclass
from functools import cached_property
from typing import TYPE_CHECKING, Any, TypeVar

from typing_extensions import Self

from beets.util import cached_classproperty

if TYPE_CHECKING:
from beets.library import Item

Expand Down Expand Up @@ -54,6 +58,10 @@ def __hash__(self) -> int: # type: ignore[override]
class Info(AttrDict[Any]):
"""Container for metadata about a musical entity."""

@cached_property
def name(self) -> str:
raise NotImplementedError

def __init__(
self,
album: str | None = None,
Expand Down Expand Up @@ -95,6 +103,10 @@ class AlbumInfo(Info):
user items, and later to drive tagging decisions once selected.
"""

@cached_property
def name(self) -> str:
return self.album or ""

def __init__(
self,
tracks: list[TrackInfo],
Expand Down Expand Up @@ -167,6 +179,10 @@ class TrackInfo(Info):
stand alone for singleton matching.
"""

@cached_property
def name(self) -> str:
return self.title or ""

def __init__(
self,
*,
Expand Down Expand Up @@ -214,16 +230,28 @@ def __init__(


# Structures that compose all the information for a candidate match.
@dataclass
class Match:
distance: Distance
info: Info

@cached_classproperty
def type(cls) -> str:
Copy link

Copilot AI Nov 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Normal methods should have 'self', rather than 'cls', as their first parameter.

Copilot uses AI. Check for mistakes.
return cls.__name__.removesuffix("Match") # type: ignore[attr-defined]

class AlbumMatch(NamedTuple):
distance: Distance

@dataclass
class AlbumMatch(Match):
info: AlbumInfo
mapping: dict[Item, TrackInfo]
mapping: list[tuple[Item, TrackInfo]]
extra_items: list[Item]
extra_tracks: list[TrackInfo]

@cached_property
def items(self) -> list[Item]:
return [i for i, _ in self.mapping]

class TrackMatch(NamedTuple):
distance: Distance

@dataclass
class TrackMatch(Match):
info: TrackInfo
31 changes: 16 additions & 15 deletions beets/autotag/match.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ class Proposal(NamedTuple):
def assign_items(
items: Sequence[Item],
tracks: Sequence[TrackInfo],
) -> tuple[dict[Item, TrackInfo], list[Item], list[TrackInfo]]:
) -> tuple[list[tuple[Item, TrackInfo]], list[Item], list[TrackInfo]]:
"""Given a list of Items and a list of TrackInfo objects, find the
best mapping between them. Returns a mapping from Items to TrackInfo
objects, a set of extra Items, and a set of extra TrackInfo
Expand All @@ -86,14 +86,15 @@ def assign_items(
# Each item in `assigned_item_idxs` list corresponds to a track in the
# `tracks` list. Each value is either an index into the assigned item in
# `items` list, or -1 if that track has no match.
mapping = {
items[iidx]: t
mapping = [
(items[iidx], t)
for iidx, t in zip(assigned_item_idxs, tracks)
if iidx != -1
}
extra_items = list(set(items) - mapping.keys())
]
mapping.sort(key=lambda it: (it[0].disc, it[0].track, it[0].title))
extra_items = list(set(items) - {i for i, _ in mapping})
extra_items.sort(key=lambda i: (i.disc, i.track, i.title))
extra_tracks = list(set(tracks) - set(mapping.values()))
extra_tracks = list(set(tracks) - {t for _, t in mapping})
extra_tracks.sort(key=lambda t: (t.index, t.title))
return mapping, extra_items, extra_tracks

Expand Down Expand Up @@ -239,7 +240,7 @@ def _add_candidate(
def tag_album(
items,
search_artist: str | None = None,
search_album: str | None = None,
search_name: str | None = None,
search_ids: list[str] = [],
) -> tuple[str, str, Proposal]:
"""Return a tuple of the current artist name, the current album
Expand Down Expand Up @@ -295,10 +296,10 @@ def tag_album(
)

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

# Is this album likely to be a "various artist" release?
va_likely = (
Expand All @@ -310,7 +311,7 @@ def tag_album(

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

Expand All @@ -324,7 +325,7 @@ def tag_album(
def tag_item(
item,
search_artist: str | None = None,
search_title: str | None = None,
search_name: str | None = None,
search_ids: list[str] | None = None,
) -> Proposal:
"""Find metadata for a single track. Return a `Proposal` consisting
Expand Down Expand Up @@ -366,12 +367,12 @@ def tag_item(

# Search terms.
search_artist = search_artist or item.artist
search_title = search_title or item.title
log.debug("Item search terms: {} - {}", search_artist, search_title)
search_name = search_name or item.title
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_title
item, search_artist, search_name
):
dist = track_distance(item, track_info, incl_artist=True)
candidates[track_info.track_id] = hooks.TrackMatch(dist, track_info)
Expand Down
6 changes: 3 additions & 3 deletions beets/importer/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -244,18 +244,18 @@ def imported_items(self):
matched items.
"""
if self.choice_flag in (Action.ASIS, Action.RETAG):
return list(self.items)
return self.items
elif self.choice_flag == Action.APPLY and isinstance(
self.match, autotag.AlbumMatch
):
return list(self.match.mapping.keys())
return self.match.items
else:
assert False

def apply_metadata(self):
"""Copy metadata from match info to the items."""
if config["import"]["from_scratch"]:
for item in self.match.mapping:
for item in self.match.items:
item.clear()

autotag.apply_metadata(self.match.info, self.match.mapping)
Expand Down
Loading