autotagging: consistent handling of albums and singletons#6682
Open
snejus wants to merge 4 commits into
Open
Conversation
|
Thank you for the PR! The changelog has not been updated, so here is a friendly reminder to check if you need to add an entry. |
Contributor
There was a problem hiding this comment.
Pull request overview
grug see big refactor in beets.autotag: album + singleton matching now use stateful candidate collection objects (AlbumCandidates / TrackCandidates) instead of one-shot tag_album() / tag_item() returning Proposal. new beets.autotag.candidates module hold search + dedupe + sort + recommendation, while match.py focus on match objects and validation.
Changes:
- add
Candidatescollections with.resolve(),.search(),.search_ids(),.matches, and.recommendation - move recommendation + candidate-search orchestration into new
beets/autotag/candidates.py, and move match validation intoAlbumMatch.try_create()/TrackMatch.try_create() - update importer tasks, terminal UI, and plugins to read recommendation from
task.candidates.recommendationand to mutate same candidate collection for manual search/ID lookup; update tests accordingly
Reviewed changes
Copilot reviewed 14 out of 14 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
beets/autotag/candidates.py |
New module for candidate lifecycle: search, dedupe, sorting, recommendation, and resolve flow. |
beets/autotag/match.py |
Removes proposal/tag helpers; makes Match generic and moves validation into try_create. |
beets/autotag/hooks.py |
Adds InfoT typing helper and improves Info.__repr__ for logging/debug. |
beets/autotag/__init__.py |
Re-exports new candidate types; stops exporting old proposal/tag helpers. |
beets/importer/tasks.py |
Tasks now own cached candidates collections; lookup uses .resolve(); singleton task uses TrackCandidates. |
beets/ui/commands/import_/session.py |
UI now uses task.candidates.recommendation; manual search/ID mutate candidate collection (no more Proposal). |
beets/ui/commands/import_/display.py |
Adjusts match typing and normalizes displayed match type casing. |
beetsplug/mbsubmit.py |
Uses task.candidates.recommendation instead of removed task.rec. |
beetsplug/bench.py |
Bench uses Source + AlbumCandidates.resolve() instead of tag_album. |
test/autotag/test_candidates.py |
New tests covering multi-data-source candidate aggregation via new collections. |
test/autotag/test_match.py |
Removes tests for old proposal/tag API; keeps assign_items coverage. |
test/test_importer.py |
Updates importer tests to use set_choice(Action.APPLY) instead of constructing dummy AlbumMatch. |
test/plugins/test_art.py |
Updates fetchart importer test setup to use set_choice(Action.APPLY). |
test/plugins/test_mbpseudo.py |
Marks one scenario as xfail (documents known design mismatch with dynamic match adjustment). |
|
95e0a81 to
34ae2e4
Compare
ee1aea8 to
7d412a1
Compare
3ca6d85 to
da82f8f
Compare
7d412a1 to
ecbb5b1
Compare
adf1b2b to
8f25874
Compare
d26e8f2 to
1952f55
Compare
8f25874 to
f2ed137
Compare
1952f55 to
97e8a4f
Compare
f2ed137 to
ba32b7b
Compare
97e8a4f to
90e087b
Compare
9f1f381 to
d8a6b49
Compare
90e087b to
3827a35
Compare
d8a6b49 to
349a55b
Compare
d466ca8 to
8668ec6
Compare
e6faa37 to
1f90f35
Compare
5e3860a to
609a54c
Compare
1f90f35 to
83a1c44
Compare
- Replace `tag_album`/`tag_item` proposal flows with `AlbumCandidates` and `TrackCandidates` that own candidate aggregation, ID/text search, sorting, and recommendation calculation. - Update importer/session/plugin call sites to use `task.candidates.recommendation` and in-place candidate resolution, reducing duplicated control flow and centralizing match behavior. - Adjust related tests and exports for the new API, normalize displayed match type casing, and mark the current `mbpseudo` dynamic adjustment behavior as expected-failing pending redesign.
83a1c44 to
bd4b43f
Compare
609a54c to
e30f69a
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This PR refactors
beets.autotagso matching is organized around persistent candidate collections instead of one-shot helper functions.Fixes: #6685
Supersedes: #6117
In one line
The autotag pipeline moves from "
tag_album()/tag_item()return aProposal" to "AlbumCandidates/TrackCandidatesown search, deduplication, sorting, and recommendation state", and that logic is then split into a dedicatedbeets.autotag.candidatesmodule.Why this change exists
Before this PR, matching behavior was spread across:
tag_album()andtag_item()match.pycandidatesandrecProposalwrapper used to pass results aroundThat made album and singleton flows similar in purpose, but different in structure.
This PR gives both flows the same model:
SourceArchitectural change
Before
After
This is the core refactor: matching is now modeled as a stateful collection object instead of a function that assembles and returns a result bundle.
Main architecture changes
1. Candidate lifecycle moved into
CandidatesobjectsNew collection types:
CandidatesAlbumCandidatesTrackCandidatesThese objects now own:
matchesrecommendationresolve()That centralizes the matching pipeline in one place instead of duplicating it across album and singleton helper functions.
2. Matching collections were split into a new module
The latest commit extracts:
CandidatesAlbumCandidatesTrackCandidatesRecommendationfrom
beets/autotag/match.pyintobeets/autotag/candidates.py.That improves module boundaries:
High-level effect: clearer separation between "one possible match" and "the collection of possible matches".
3. Match validation moved into match classes
AlbumMatchandTrackMatchnow own their own construction rules throughtry_create():AlbumMatch.try_create()TrackMatch.try_create()TrackMatchThis removes helper-style validation code and keeps creation rules next to the match types they belong to.
API and control-flow changes
Removed old proposal-based flow
The old API centered around:
tag_album()tag_item()ProposalThat flow is replaced by candidate collections exposed through tasks.
New task-owned candidate state
Importer tasks now keep candidate state directly:
ImportTask.candidates->AlbumCandidatesSingletonImportTask.candidates->TrackCandidatesCall sites now use:
task.candidates.resolve(...)task.candidates.search(...)task.candidates.search_ids(...)task.candidates.recommendationtask.candidates[0]This simplifies session/UI logic because it no longer has to swap whole
Proposalobjects in and out.High-level impact by area
beets.autotagbeets.autotag.candidatesmodulebeets.autotag.__init__InfoobjectsMatchobjectsImporter and UI
task.candidates.recommendationProposalPlugins
Plugins consuming importer state now read recommendation from the task's candidates:
beetsplug.mbsubmitnow checkstask.candidates.recommendationbeetsplug.benchnow benchmarks throughAlbumCandidates(...).resolve(...)This aligns plugin integrations with the new matching model.
Tests
test/autotag/test_candidates.pytest_match.pybecomes narrower and more match-focusedtest_mbpseudo.pynow explicitly marks one case asxfail, documenting an existing design mismatch where the plugin adjustsAlbumMatchdynamicallyEnd-to-end flow after this PR
This is easier to reason about than the old "return proposal, maybe replace proposal, maybe replace recommendation" control flow.
Practical reviewer mental model
Think of the refactor as three separate layers:
Infoobjects describe metadata from external sourcesMatchobjects represent one validated tagging outcomeCandidatesobjects manage the search and ranking of many possible matchesSo the importer/UI mostly talks to
Candidates, whileCandidatescreatesMatchobjects, andMatchobjects wrapInfo.User-visible behavior
This is mostly a structural refactor, not a feature change.
Notable external effects are small:
.capitalize()mbpseudoscenario is now explicitly marked as expected-failing rather than implicitly toleratedReviewer takeaway
The purpose of this PR is to make album and singleton matching follow the same architecture, with a single object responsible for candidate search state and recommendation logic.
That should make future changes easier in one shared place, especially around: