CI Report:
https://ci-tests.linuxserver.io/linuxserver/beets/nightly-87128df3-ls326/index.html
LinuxServer Changes:
Full Changelog: nightly-74c2d98e-ls325...nightly-87128df3-ls326
Remote Changes:
autotagging: introduce Source class for matching (#6681)
beets.autotag Source-Centric Refactor
Fixes: #6684
Core change
This PR restructures autotagging around a new
beets.autotag.source.Source object.
Before this change, album and singleton matching pulled "current
metadata" from several places:
- importer tasks
- matching code
- distance calculation
- UI display code
After this change, those flows all receive the same input shape: a
single Source value that carries:
- the original
items - the current display/search fields (
artist,name) - extracted metadata in
data - current external ID information (
id,id_consensus)
That makes Source the new "current thing being matched" abstraction
for both albums and tracks.
Architecture
1. New single input object: Source
Source centralizes metadata extraction that used to be scattered
across the autotag pipeline.
Before
items
├─ importer computes current artist/album
├─ match code recomputes common tags
├─ distance recomputes common tags
└─ UI receives separate current values
After
Source
├─ source.artist
├─ source.name
├─ source.data
├─ source.items
├─ source.id / source.id_consensus
└─ source.va_likely
This is the biggest architectural shift in the PR.
2. tag_album() and tag_item() now operate on Source
The matching entry points now take a Source instead of a loose mix of:
- raw
items item- current artist/title values
- repeated ID lookups
That gives album and track matching the same calling convention and
reduces special-case plumbing.
# Before tag_album(items, ...) tag_item(item, ...) # After tag_album(source, ...) tag_item(source, ...)
3. distance() is now narrower and more explicit
distance() no longer derives "current metadata" internally from a list
of items.
Instead, it receives:
original: the already-extracted metadata containerunmatched_count: the number of unmatched local tracks
Before
distance(items, album_info, item_info_pairs)
-> recompute common tags from items
-> infer unmatched track count
After
distance(source.data, album_info, item_info_pairs, unmatched_count)
-> compare against already-prepared metadata
This is a good separation of responsibilities:
Sourceprepares current metadatadistance()only compares metadata
4. Importer tasks now cache the current source
ImportTask and SingletonImportTask now expose a cached source
property:
- album tasks use
Source.from_items(...) - singleton tasks use
Source.from_item(...)
That means importer state no longer needs separate cur_artist and
cur_album fields, and downstream code can read from task.source
instead.
5. UI/display code now depends on the same source model
Import session and display functions now use task.source.artist and
task.source.name when showing candidate changes or prompting for
manual search/ID entry.
This removes another copy of "current metadata" logic from the UI layer.
6. AttrDict moves to beets.util
AttrDict was previously defined inside beets.autotag.hooks. It now
lives in beets.util.
High-level effect:
- metadata-container behavior is treated as shared infrastructure
- autotag-specific modules depend on a common utility instead of owning
the abstraction
7. beets.autotag public API is flatter
Related cleanup continues by exporting more autotag types directly from
beets.autotag, including Source.
This reduces import-path churn for callers and makes the package
boundary easier to understand.
High-level impact
What this improves
1. One place defines "the current metadata we are matching from"
The main benefit is conceptual clarity.
Reviewers can now think about autotagging as:
local files -> Source -> candidate lookup -> distance -> proposal/UI
instead of following several separate paths that each rediscover the
same metadata.
2. Album and singleton flows now look alike
Albums and single tracks now use the same structure and similar call
patterns.
That should make future autotag changes easier because new behavior can
usually be added once at the Source boundary rather than twice in
parallel code paths.
3. Less coupling between importer, matcher, distance logic, and UI
Previously, each layer needed to know how to reconstruct "current
artist/album/common tags".
Now:
- importer builds
Source - matching uses
Source - UI displays from
Source distance()receives prepared metadata
Each layer does less guessing about the others.
4. Simpler data flow for debugging and future refactors
When a match looks wrong, there is now a clearer place to inspect the
original input: Source.
That should help with debugging, logging, and future changes to metadata
extraction rules.
Data flow at a glance
ImportTask / SingletonImportTask
↓
`task.source`
↓
`tag_album(source)` / `tag_item(source)`
↓
candidate search + item/track assignment
↓
`distance(source.data, info, pairs, unmatched_count)`
↓
`Proposal`
↓
UI display and user choice
Notable follow-on cleanup included here
get_most_common_tags()now returns only the extracted metadata
dictionary, not a(likelies, consensus)pair- call sites that depended on consensus now compute only the specific
signals they need, such asid_consensusorva_likely - tests were updated to reflect the new
Source-based flow .git-blame-ignore-revswas updated for mechanical refactor commits
Reviewer guide
If reviewing this PR, the easiest way to read it is:
- Start with
beets/autotag/source.py - Then look at
tag_album()andtag_item()in
beets/autotag/match.py - Then review the
distance()signature change - Finally scan importer/UI call sites to see how they now pass
task.source
That path shows the architectural intent without getting lost in the
mechanical updates.