Refactor item tagging and fix several underlying issues.
### Fixes
- Synchronise all artist list fields. Notably, `artist_sort` /
`artists_sort` and `artist_credit` / `artists_credit` fields have not
been synchronised.
- Fix `overwrite_null` configuration which was previously ignored for
fields defined in `autotag/__init__.py::SPECIAL_FIELDS`.
### Updates
- Move metadata application logic into `Match` objects: add
`Match.apply_metadata`, `AlbumMatch.apply_metadata`,
`AlbumMatch.apply_album_metadata`, and `TrackMatch.apply_metadata`;
callers now use those methods instead of legacy free functions.
- Remove legacy functions from `beets.autotag.__init__`
(`apply_item_metadata`, `apply_album_metadata`, `apply_metadata`) and
related globals (`SPECIAL_FIELDS`, `log`), and export only core types
(`AlbumInfo`, `AlbumMatch`, `TrackInfo`, `TrackMatch`, `Proposal`,
`Recommendation`, `tag_album`, `tag_item`).
- Add structured metadata facilities to `Info` and subclasses:
- `Info.type` class property and `nullable_fields` for per-type
'overwrite_null' config.
- `Info.raw_data` and `Info.item_data` computed properties to apply
`artist_credit` rules, filter nulls, and map media-specific field names.
- `AlbumInfo` and `TrackInfo` extend `raw_data`/`item_data` behavior to
handle album/track specifics (date zeroing, `tracktotal`,
`mb_releasetrackid`, per-disc numbering).
- Introduce `TrackInfo.merge_with_album` to merge track-level data with
album-level fallback for a final item payload.
- Move `correct_list_fields` to `hooks.py` and update it to keep
**unmapped** / **non-media** single/list fields in sync (`artist` <->
`artists`, `albumtype` <-> `albumtypes`, etc.).
- Wire changes through the codebase:
- Pass `Item` objects into `TrackMatch` in `match.tag_item` to enable
item-level metadata application.
- Replace calls to removed `autotag` apply functions with
`Match.apply_metadata` invocations in `beets/importer/tasks.py`,
`beetsplug/bpsync.py`, and `beetsplug/mbsync.py`.
- Update importer logic to set album artist fallbacks for `albumartists`
/ `mb_albumartistids` when missing.
- Add and update tests:
- New `test/autotag/test_hooks.py` and `test/autotag/test_match.py` to
validate new data mapping, list field synchronization, overwrite
behavior, and assignment logic.
## Refactor: Move display logic into `Distance` and `Match` as
properties
Display-related logic previously scattered across `display.py` and
`session.py` is consolidated into the data classes themselves.
### What changed
**`Distance` gains three properties:**
- `penalties` — list of cleaned-up penalty key strings
- `color` — threshold-based `ColorName` derived from the distance value
- `string` — colorized similarity percentage
**`Match` gains two properties:**
- `disambig_string` — formatted comma-separated disambiguation string
- `base_disambig_data` — override point for subclass-specific field
pre-processing (e.g. `media` for `AlbumMatch`,
`index`/`track_alt`/`album` for `TrackMatch`)
**`display.py` / `session.py`:** Standalone functions `dist_string`,
`dist_colorize`, `penalty_string`, `disambig_string`,
`get_album_disambig_fields`, `get_singleton_disambig_fields` are
removed. Call sites now use the properties directly.
A minor fix in `show_match_header` collects output into a list and uses
`textwrap.indent` for a single `ui.print_` call, replacing the previous
per-line prints.
### Impact
- Display logic lives next to the data it describes — easier to find,
easier to test
- `display.py` and `session.py` become thinner; no shared utility
functions to keep in sync
Move disambig string and penalty formatting logic from display.py
into Distance and Match classes as properties.
Add Distance.color, Distance.string, Distance.penalties,
Match.disambig_string, and Match.base_disambig_data to consolidate
display logic closer to the data.
Remove now-redundant standalone functions from display.py and
session.py.
This PR refactors import-match layout rendering by centralizing layout
selection and line generation in `beets.util.layout`, simplifying the
`ShowChange` display path, and tightening the layout
data model.
## What Changed
- Added `get_layout_lines()` and `get_layout_method()` in
`beets.util.layout` so layout selection (`column` vs `newline`) is
handled in one place.
- Replaced `Side` from a mutable `TypedDict` with an immutable
`NamedTuple` that exposes derived helpers (`rendered`, prefix/suffix
widths, and rendered width).
- Simplified `split_into_lines()` from a 3-width tuple API to
`(first_width, width)` and removed legacy last-line empty-string
handling.
- Refactored `beets.ui.commands.import_/display.py` `ShowChange` to call
`get_layout_lines()` directly and removed duplicate per-class
layout-selection logic.
- Updated tracklist width calculation to use `Side` helpers and explicit
width assignment via `_replace(width=...)`.
- Reworked `ShowChange` tests into snapshot-style assertions for both
`newline` and `column` layouts, and updated util layout tests to the new
`split_into_lines()` signature.
## Why
- Reduces duplicated wrapping/layout logic across UI code paths.
- Makes layout behavior easier to reason about and test at the utility
boundary.
- Narrows the display layer to orchestration while keeping
transformation/rendering logic in reusable utilities.
- Improves maintainability by moving from loosely typed dict mutation to
a typed, self-describing data structure.
Replace multiple small tests with two comprehensive snapshot tests
covering the same edge cases and newline and column layouts.
Use BeetsTestCase to ensure that the local dev config is ignored.