GitHub

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 container
  • unmatched_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:

  • Source prepares current metadata
  • distance() 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 as id_consensus or va_likely
  • tests were updated to reflect the new Source-based flow
  • .git-blame-ignore-revs was updated for mechanical refactor commits

Reviewer guide

If reviewing this PR, the easiest way to read it is:

  1. Start with beets/autotag/source.py
  2. Then look at tag_album() and tag_item() in
    beets/autotag/match.py
  3. Then review the distance() signature change
  4. 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.

Read the original on github.com ↗