This commit is contained in:
Šarūnas Nejus 2026-03-23 01:56:24 +00:00 committed by GitHub
commit bde42a7305
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 80 additions and 96 deletions

View file

@ -175,3 +175,5 @@ a6fcb7ba0f237530ff394a423a7cbe2ac4853c91
ffb43290066c78cb72603b7e2a0a1c90056361dd
# lastgenre: Move fetching to client module
b4beee8ff3754b001e7504c05a2b838bfa689022
# autotag: use explicit imports
0c2f3ed073d44a21efd5b7e6a28c99f52d3f8c03

View file

@ -25,18 +25,18 @@ import lap
import numpy as np
from beets import config, logging, metadata_plugins
from beets.autotag import AlbumMatch, TrackMatch, hooks
from beets.util import get_most_common_tags
from .distance import VA_ARTISTS, distance, track_distance
from .hooks import Info
from .hooks import AlbumMatch, Info, TrackMatch
if TYPE_CHECKING:
from collections.abc import Iterable, Sequence
from beets.autotag import AlbumInfo, TrackInfo
from beets.library import Item
from .hooks import AlbumInfo, TrackInfo
AnyMatch = TypeVar("AnyMatch", TrackMatch, AlbumMatch)
Candidates = dict[Info.Identifier, AnyMatch]
@ -159,7 +159,7 @@ def _recommendation(
# Downgrade to the max rec if it is lower than the current rec for an
# applied penalty.
keys = set(min_dist.keys())
if isinstance(results[0], hooks.AlbumMatch):
if isinstance(results[0], AlbumMatch):
for track_dist in min_dist.tracks.values():
keys.update(list(track_dist.keys()))
max_rec_view = config["match"]["max_rec"]
@ -232,7 +232,7 @@ def _add_candidate(
return
log.debug("Success. Distance: {}", dist)
results[info.identifier] = hooks.AlbumMatch(
results[info.identifier] = AlbumMatch(
dist, info, dict(item_info_pairs), extra_items, extra_tracks
)
@ -349,7 +349,7 @@ def tag_item(
log.debug("Searching for track IDs: {}", ", ".join(trackids))
for info in metadata_plugins.tracks_for_ids(trackids):
dist = track_distance(item, info, incl_artist=True)
candidates[info.identifier] = hooks.TrackMatch(dist, info, item)
candidates[info.identifier] = TrackMatch(dist, info, item)
# If this is a good match, then don't keep searching.
rec = _recommendation(_sort_candidates(candidates.values()))
@ -375,9 +375,7 @@ def tag_item(
item, search_artist, search_name
):
dist = track_distance(item, track_info, incl_artist=True)
candidates[track_info.identifier] = hooks.TrackMatch(
dist, track_info, item
)
candidates[track_info.identifier] = TrackMatch(dist, track_info, item)
# Sort by distance and return with recommendation.
log.debug("Found {} candidates.", len(candidates))

View file

@ -27,7 +27,9 @@ from typing import TYPE_CHECKING, Any
import mediafile
from beets import autotag, config, library, plugins, util
from beets import config, library, plugins, util
from beets.autotag.hooks import AlbumMatch
from beets.autotag.match import tag_album, tag_item
from beets.dbcore.query import PathQuery
from .state import ImportState
@ -35,6 +37,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 .session import ImportSession
@ -159,12 +162,12 @@ class ImportTask(BaseImportTask):
"""
choice_flag: Action | None = None
match: autotag.AlbumMatch | autotag.TrackMatch | None = None
match: AlbumMatch | TrackMatch | None = None
# Keep track of the current task item
cur_album: str | None = None
cur_artist: str | None = None
candidates: Sequence[autotag.AlbumMatch | autotag.TrackMatch] = []
candidates: Sequence[AlbumMatch | TrackMatch] | None = None
rec: Recommendation | None = None
def __init__(
@ -178,9 +181,7 @@ class ImportTask(BaseImportTask):
self.should_merge_duplicates = False
self.is_album = True
def set_choice(
self, choice: Action | autotag.AlbumMatch | autotag.TrackMatch
):
def set_choice(self, choice: Action | AlbumMatch | TrackMatch):
"""Given an AlbumMatch or TrackMatch object or an action constant,
indicates that an action has been selected for this task.
@ -249,7 +250,7 @@ class ImportTask(BaseImportTask):
if self.choice_flag in (Action.ASIS, Action.RETAG):
return self.items
elif self.choice_flag == Action.APPLY and isinstance(
self.match, autotag.AlbumMatch
self.match, AlbumMatch
):
return self.match.items
else:
@ -363,7 +364,7 @@ class ImportTask(BaseImportTask):
restricted to only those IDs.
"""
self.cur_artist, self.cur_album, (self.candidates, self.rec) = (
autotag.tag_album(self.items, search_ids=search_ids)
tag_album(self.items, search_ids=search_ids)
)
def find_duplicates(self, lib: library.Library) -> list[library.Album]:
@ -500,7 +501,7 @@ class ImportTask(BaseImportTask):
self.album = lib.add_album(self.imported_items())
if self.choice_flag == Action.APPLY and isinstance(
self.match, autotag.AlbumMatch
self.match, AlbumMatch
):
# Copy album flexible fields to the DB
# TODO: change the flow so we create the `Album` object earlier,
@ -684,9 +685,7 @@ class SingletonImportTask(ImportTask):
plugins.send("item_imported", lib=lib, item=item)
def lookup_candidates(self, search_ids: list[str]) -> None:
self.candidates, self.rec = autotag.tag_item(
self.item, search_ids=search_ids
)
self.candidates, self.rec = tag_item(self.item, search_ids=search_ids)
def find_duplicates(self, lib: library.Library) -> list[library.Item]: # type: ignore[override] # Need splitting Singleton and Album tasks into separate classes
"""Return a list of items from `lib` that have the same artist

View file

@ -7,7 +7,7 @@ from functools import cached_property
from typing import TYPE_CHECKING
from beets import config, ui
from beets.autotag import hooks
from beets.autotag.hooks 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 import autotag
from beets.autotag.hooks import AlbumMatch, Match, TrackMatch
from beets.library.models import Item
from beets.util.color import ColorName
@ -34,7 +34,7 @@ class ChangeRepresentation:
cur_artist: str
cur_name: str
match: autotag.hooks.Match
match: Match
@cached_property
def changed_prefix(self) -> str:
@ -123,7 +123,7 @@ class ChangeRepresentation:
else:
ui.print_(f"{self.indent_detail}*", f"{type_}:", name_r)
def make_medium_info_line(self, track_info: hooks.TrackInfo) -> str:
def make_medium_info_line(self, track_info: TrackInfo) -> str:
"""Construct a line with the current medium's info."""
track_media = track_info.get("media", "Media")
# Build output string.
@ -138,11 +138,11 @@ class ChangeRepresentation:
else:
return ""
def format_index(self, track_info: hooks.TrackInfo | Item) -> str:
def format_index(self, track_info: TrackInfo | Item) -> str:
"""Return a string representing the track index of the given
TrackInfo or Item object.
"""
if isinstance(track_info, hooks.TrackInfo):
if isinstance(track_info, TrackInfo):
index = track_info.index
medium_index = track_info.medium_index
medium = track_info.medium
@ -160,7 +160,7 @@ class ChangeRepresentation:
return str(index)
def make_track_numbers(
self, item: Item, track_info: hooks.TrackInfo
self, item: Item, track_info: TrackInfo
) -> tuple[str, str, bool]:
"""Format colored track indices."""
cur_track = self.format_index(item)
@ -183,7 +183,7 @@ class ChangeRepresentation:
@staticmethod
def make_track_titles(
item: Item, track_info: hooks.TrackInfo
item: Item, track_info: TrackInfo
) -> tuple[str, str, bool]:
"""Format colored track titles."""
new_title = track_info.name
@ -199,7 +199,7 @@ class ChangeRepresentation:
@staticmethod
def make_track_lengths(
item: Item, track_info: hooks.TrackInfo
item: Item, track_info: TrackInfo
) -> tuple[str, str, bool]:
"""Format colored track lengths."""
changed = False
@ -227,9 +227,7 @@ class ChangeRepresentation:
return lhs_length, rhs_length, changed
def make_line(
self, item: Item, track_info: hooks.TrackInfo
) -> tuple[Side, Side]:
def make_line(self, item: Item, track_info: TrackInfo) -> tuple[Side, Side]:
"""Extract changes from item -> new TrackInfo object, and colorize
appropriately. Returns (lhs, rhs) for column printing.
"""
@ -304,7 +302,7 @@ class ChangeRepresentation:
class AlbumChange(ChangeRepresentation):
match: autotag.hooks.AlbumMatch
match: AlbumMatch
def show_match_tracks(self) -> None:
"""Print out the tracks of the match, summarizing changes the match
@ -364,12 +362,10 @@ class AlbumChange(ChangeRepresentation):
class TrackChange(ChangeRepresentation):
"""Track change representation, comparing item with match."""
match: autotag.hooks.TrackMatch
match: TrackMatch
def show_change(
cur_artist: str, cur_album: str, match: hooks.AlbumMatch
) -> None:
def show_change(cur_artist: str, cur_album: str, match: AlbumMatch) -> None:
"""Print out a representation of the changes that will be made if an
album's tags are changed according to `match`, which must be an AlbumMatch
object.
@ -386,7 +382,7 @@ def show_change(
change.show_match_tracks()
def show_item_change(item: Item, match: hooks.TrackMatch) -> None:
def show_item_change(item: Item, match: TrackMatch) -> None:
"""Print out the change that would occur by tagging `item` with the
metadata from `match`, a TrackMatch object.
"""

View file

@ -3,8 +3,9 @@ from __future__ import annotations
from collections import Counter
from itertools import chain
from beets import autotag, config, importer, logging, plugins, ui
from beets.autotag import Recommendation
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.util import PromptChoice, displayable_path
from beets.util.color import colorize
from beets.util.units import human_bytes, human_seconds_short
@ -84,7 +85,7 @@ class TerminalImportSession(importer.ImportSession):
post_choice = choice.callback(self, task)
if isinstance(post_choice, importer.Action):
return post_choice
elif isinstance(post_choice, autotag.Proposal):
elif isinstance(post_choice, Proposal):
# Use the new candidates and continue around the loop.
task.candidates = post_choice.candidates
task.rec = post_choice.recommendation
@ -93,7 +94,7 @@ class TerminalImportSession(importer.ImportSession):
else:
# We have a candidate! Finish tagging. Here, choice is an
# AlbumMatch object.
assert isinstance(choice, autotag.AlbumMatch)
assert isinstance(choice, AlbumMatch)
return choice
def choose_item(self, task):
@ -127,13 +128,13 @@ class TerminalImportSession(importer.ImportSession):
post_choice = choice.callback(self, task)
if isinstance(post_choice, importer.Action):
return post_choice
elif isinstance(post_choice, autotag.Proposal):
elif isinstance(post_choice, Proposal):
candidates = post_choice.candidates
rec = post_choice.recommendation
else:
# Chose a candidate.
assert isinstance(choice, autotag.TrackMatch)
assert isinstance(choice, TrackMatch)
return choice
def resolve_duplicate(self, task, found_duplicates):
@ -519,10 +520,10 @@ def manual_search(session, task):
name = ui.input_("Album:" if task.is_album else "Track:").strip()
if task.is_album:
_, _, prop = autotag.tag_album(task.items, artist, name)
_, _, prop = tag_album(task.items, artist, name)
return prop
else:
return autotag.tag_item(task.item, artist, name)
return tag_item(task.item, artist, name)
def manual_id(session, task):
@ -534,10 +535,10 @@ def manual_id(session, task):
search_id = ui.input_(prompt).strip()
if task.is_album:
_, _, prop = autotag.tag_album(task.items, search_ids=search_id.split())
_, _, prop = tag_album(task.items, search_ids=search_id.split())
return prop
else:
return autotag.tag_item(task.item, search_ids=search_id.split())
return tag_item(task.item, search_ids=search_id.split())
def abort_action(session, task):

View file

@ -18,7 +18,7 @@ import cProfile
import timeit
from beets import importer, library, plugins, ui
from beets.autotag import match
from beets.autotag.match import tag_album
from beets.plugins import BeetsPlugin
from beets.util.functemplate import Template
from beetsplug._utils import vfs
@ -83,7 +83,7 @@ def match_benchmark(lib, prof, query=None, album_id=None):
# Run the match.
def _run_match():
match.tag_album(items, search_ids=[album_id])
tag_album(items, search_ids=[album_id])
if prof:
cProfile.runctx(

View file

@ -23,7 +23,7 @@ from typing import TYPE_CHECKING, ClassVar
import requests
from beets import ui
from beets.autotag import AlbumInfo, TrackInfo
from beets.autotag.hooks import AlbumInfo, TrackInfo
from beets.dbcore import types
from beets.metadata_plugins import IDResponse, SearchApiMetadataSourcePlugin

View file

@ -38,8 +38,8 @@ from beetsplug.musicbrainz import (
if TYPE_CHECKING:
from collections.abc import Iterable, Sequence
from beets.autotag import AlbumMatch
from beets.autotag.distance import Distance
from beets.autotag.hooks import AlbumMatch
from beets.library import Item
from beetsplug._typing import JSONDict

View file

@ -24,7 +24,7 @@ implemented by MusicBrainz yet.
import subprocess
from beets import ui
from beets.autotag import Recommendation
from beets.autotag.match import Recommendation
from beets.plugins import BeetsPlugin
from beets.util import PromptChoice, displayable_path
from beetsplug.info import print_data

View file

@ -25,9 +25,8 @@ from urllib.parse import urljoin
from confuse.exceptions import NotFoundError
import beets
import beets.autotag.hooks
from beets import config, plugins, util
from beets.autotag.hooks 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
@ -225,11 +224,7 @@ def _preferred_release_event(
return release.get("country"), release.get("date")
def _set_date_str(
info: beets.autotag.hooks.AlbumInfo,
date_str: str,
original: bool = False,
):
def _set_date_str(info: AlbumInfo, date_str: str, original: bool = False):
"""Given a (possibly partial) YYYY-MM-DD string and an AlbumInfo
object, set the object's release date fields appropriately. If
`original`, then set the original_year, etc., fields.
@ -250,8 +245,8 @@ def _set_date_str(
def _merge_pseudo_and_actual_album(
pseudo: beets.autotag.hooks.AlbumInfo, actual: beets.autotag.hooks.AlbumInfo
) -> beets.autotag.hooks.AlbumInfo:
pseudo: AlbumInfo, actual: AlbumInfo
) -> AlbumInfo:
"""
Merges a pseudo release with its actual release.
@ -333,7 +328,7 @@ class MusicBrainzPlugin(
medium: int | None = None,
medium_index: int | None = None,
medium_total: int | None = None,
) -> beets.autotag.hooks.TrackInfo:
) -> TrackInfo:
"""Translates a MusicBrainz recording result dictionary into a beets
``TrackInfo`` object. Three parameters are optional and are used
only for tracks that appear on releases (non-singletons): ``index``,
@ -343,7 +338,7 @@ class MusicBrainzPlugin(
"""
title = _key_with_preferred_alias(recording, key="title")
info = beets.autotag.hooks.TrackInfo(
info = TrackInfo(
title=title,
track_id=recording["id"],
index=index,
@ -431,7 +426,7 @@ class MusicBrainzPlugin(
return info
def album_info(self, release: JSONDict) -> beets.autotag.hooks.AlbumInfo:
def album_info(self, release: JSONDict) -> AlbumInfo:
"""Takes a MusicBrainz release result dictionary and returns a beets
AlbumInfo object containing the interesting data about that release.
"""
@ -553,7 +548,7 @@ class MusicBrainzPlugin(
album_artist_ids = _artist_ids(release["artist-credit"])
release_title = _key_with_preferred_alias(release, key="title")
info = beets.autotag.hooks.AlbumInfo(
info = AlbumInfo(
album=release_title,
album_id=release["id"],
artist=artist_name,
@ -758,9 +753,7 @@ class MusicBrainzPlugin(
mb_entity, dict(params.filters), limit=params.limit
)
def album_for_id(
self, album_id: str
) -> beets.autotag.hooks.AlbumInfo | None:
def album_for_id(self, album_id: str) -> AlbumInfo | None:
"""Fetches an album by its MusicBrainz ID and returns an AlbumInfo
object or None if the album is not found. May raise a
MusicBrainzAPIError.
@ -801,9 +794,7 @@ class MusicBrainzPlugin(
else:
return release
def track_for_id(
self, track_id: str
) -> beets.autotag.hooks.TrackInfo | None:
def track_for_id(self, track_id: str) -> TrackInfo | None:
"""Fetches a track by its MusicBrainz ID. Returns a TrackInfo object
or None if no track is found. May raise a MusicBrainzAPIError.
"""

View file

@ -2,13 +2,13 @@ import re
import pytest
from beets.autotag import AlbumInfo, TrackInfo
from beets.autotag.distance import (
Distance,
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

View file

@ -3,7 +3,8 @@ from typing import ClassVar
import pytest
from beets import metadata_plugins
from beets.autotag import AlbumInfo, TrackInfo, match
from beets.autotag.hooks import AlbumInfo, TrackInfo
from beets.autotag.match import assign_items, tag_album, tag_item
from beets.library import Item
@ -40,9 +41,7 @@ class TestAssignment:
items = [Item(title=title) for title in item_titles]
tracks = [TrackInfo(title=title) for title in track_titles]
item_info_pairs, extra_items, extra_tracks = match.assign_items(
items, tracks
)
item_info_pairs, extra_items, extra_tracks = assign_items(items, tracks)
assert (
{i.title: t.title for i, t in item_info_pairs},
@ -94,7 +93,7 @@ class TestAssignment:
expected = list(zip(items, trackinfo)), [], []
assert match.assign_items(items, trackinfo) == expected
assert assign_items(items, trackinfo) == expected
class TestTagMultipleDataSources:
@ -163,21 +162,21 @@ class TestTagMultipleDataSources:
assert set(sources) == {"Discogs", "Deezer"}
def test_search_album_ids(self, shared_album_id):
_, _, proposal = match.tag_album([Item()], search_ids=[shared_album_id])
_, _, proposal = tag_album([Item()], search_ids=[shared_album_id])
self.check_proposal(proposal)
def test_search_album_current_id(self, shared_album_id):
_, _, proposal = match.tag_album([Item(mb_albumid=shared_album_id)])
_, _, proposal = tag_album([Item(mb_albumid=shared_album_id)])
self.check_proposal(proposal)
def test_search_track_ids(self, shared_track_id):
proposal = match.tag_item(Item(), search_ids=[shared_track_id])
proposal = tag_item(Item(), search_ids=[shared_track_id])
self.check_proposal(proposal)
def test_search_track_current_id(self, shared_track_id):
proposal = match.tag_item(Item(mb_trackid=shared_track_id))
proposal = tag_item(Item(mb_trackid=shared_track_id))
self.check_proposal(proposal)

View file

@ -28,8 +28,8 @@ import pytest
import responses
from beets import config, importer, logging, util
from beets.autotag import AlbumInfo, AlbumMatch
from beets.autotag.distance import Distance
from beets.autotag.hooks import AlbumInfo, AlbumMatch
from beets.test import _common
from beets.test.helper import (
BeetsTestCase,

View file

@ -6,9 +6,8 @@ from typing import TYPE_CHECKING
import pytest
from beets.autotag import AlbumMatch
from beets.autotag.distance import Distance
from beets.autotag.hooks import AlbumInfo, TrackInfo
from beets.autotag.hooks import AlbumInfo, AlbumMatch, TrackInfo
from beets.library import Item
from beets.test.helper import PluginMixin
from beetsplug.mbpseudo import (

View file

@ -36,8 +36,8 @@ import pytest
from mediafile import MediaFile
from beets import config, importer, logging, util
from beets.autotag import AlbumInfo, AlbumMatch, TrackInfo
from beets.autotag.distance import Distance
from beets.autotag.hooks import AlbumInfo, AlbumMatch, TrackInfo
from beets.importer.tasks import albums_in_dir
from beets.test import _common
from beets.test.helper import (

View file

@ -4,7 +4,8 @@ from unittest.mock import Mock, patch
import pytest
from beets import autotag, config, library, ui
from beets import config, library, ui
from beets.autotag.hooks import AlbumInfo, AlbumMatch, TrackInfo
from beets.autotag.match import distance
from beets.test import _common
from beets.test.helper import BeetsTestCase, IOMixin
@ -66,16 +67,16 @@ class ShowChangeTestCase(IOMixin, BeetsTestCase):
_common.item(track=3, title="caf\xe9"),
_common.item(track=4, title=f"title with {long_name}"),
]
info = autotag.AlbumInfo(
info = AlbumInfo(
album="caf\xe9",
album_id="album id",
artist="the artist",
artist_id="artist id",
tracks=[
autotag.TrackInfo(title="first title", index=1),
autotag.TrackInfo(title="second title", index=2),
autotag.TrackInfo(title="third title", index=3),
autotag.TrackInfo(title="fourth title", index=4),
TrackInfo(title="first title", index=1),
TrackInfo(title="second title", index=2),
TrackInfo(title="third title", index=3),
TrackInfo(title="fourth title", index=4),
],
)
item_info_pairs = list(zip(items, info.tracks))
@ -86,9 +87,7 @@ class ShowChangeTestCase(IOMixin, BeetsTestCase):
show_change(
f"another artist with {long_name}",
"another album",
autotag.AlbumMatch(
change_dist, info, dict(item_info_pairs), set(), set()
),
AlbumMatch(change_dist, info, dict(item_info_pairs)),
)
return self.io.getoutput()