## Refactor: Extract UI utilities into `beets/util`
This PR decouples terminal/display utilities from `beets/ui` by moving
them into focused modules under `beets/util`. No user-facing behaviour
changes.
### What moved where
| Utility | From | To |
|---|---|---|
| `colorize`, `uncolorize`, `color_len`, `color_split`, ANSI constants |
`beets/ui/__init__.py` | `beets/util/color.py` |
| `colordiff`, `_field_diff`, `get_model_changes` |
`beets/ui/__init__.py` | `beets/util/diff.py` |
| `indent`, `split_into_lines`, `print_column_layout`,
`print_newline_layout` | `beets/ui/__init__.py` | `beets/util/layout.py`
|
### Notable design change in `layout.py`
`print_column_layout` / `print_newline_layout` previously called
`ui.print_()` internally, creating a hard dependency on `beets.ui`. They
are now renamed to `get_column_layout` / `get_newline_layout` and
converted to **generators**, yielding lines instead of printing them.
The caller (`display.py`) is responsible for printing via `ui.print_()`.
### New public API
`get_model_changes` is introduced in `beets/util/diff.py` as the pure,
testable function for computing field-level diffs. `show_model_changes`
in `beets/ui` now delegates to it.
### Tests
- Moved alongside the code: `test/util/test_color.py`,
`test/util/test_diff.py`, `test/util/test_layout.py`.
- Removed duplicate `ShowModelChangeTest` from `test/ui/test_ui.py` —
coverage is preserved in `test/util/test_diff.py`.
## Problem
1. When items are imported into the library in a don't copy-move mode
(`-C -M` options), they will be registered inside the Beets database
using their **original paths**. However, during subsequent processing
(*e.g.*, a `convert` operation), a path following the Beets path format
can be generated.
2. When generating playlists using `smartplaylist` plugin, only the path
registered inside the Beets database (the **original path**) can be use
inside the output playlist. This block the compatibility with other
plugins.
## Solution
I added a a new optional configuration option known as ``dest_regen``
(as well as its equivalent ``dest-regen`` on the CLI) to regenerate
items' path in the generated playlist instead of using the ones of the
library, just like a `convert` or a `move` operation would have done.
This operation will happen before the ``relative_to`` and ``prefix``
options, which makes sense to do so and not in another order, otherwise
this new option (``dest_regen``)) would overwrite the desired behavior
of the other mentioned options (``relative_to`` and ``prefix``). It is
then helpful to generate playlists compatible with the ``convert``
plugin.
The aforementioned commits introduced a nmuber of changes since I
implemented this test:
- The syntax `self.assertExists(m3u_filepath)` was an old and now invalid
way of checking existence of a path using assertion, change to `assert
m3u_filepath.exists()` which now use string instead of bytes
- Use of `Path()` and strings instead of `path.join` and bytes for
handling directory path
Test functions inspired from `test_playlist_update_output_extm3u()` in
`test_smartplaylist.py`.
Test successfully passed using:
`poetry run pytest test/plugins/test_smartplaylist.py`
Fixes#6316
When importing compilations/various artists albums, several fields used the
hardcoded string "Various Artists" instead of the user-configured `va_name`
setting:
- In the **musicbrainz plugin**, only `info.artist` was overridden with `va_name`
when a release was identified as VA. The `artist_sort`, `artists_sort`,
`artist_credit`, `artists_credit`, and `artists` fields were left with the raw
MusicBrainz value ("Various Artists"), which then propagated to
`albumartist_sort`, `albumartists_sort`, `albumartist_credit`,
`albumartists_credit`, and `albumartists` on items.
- In the **beatport plugin**, the VA artist name was hardcoded to
"Various Artists" instead of reading from config.
## Changes
- `beetsplug/musicbrainz.py`: When `info.va` is true, override all artist-related
fields (`artist_sort`, `artists`, `artists_sort`, `artist_credit`,
`artists_credit`) with `va_name`, not just `artist`.
- `beetsplug/beatport.py`: Replace hardcoded "Various Artists" with
`config["va_name"].as_str()`.
- `docs/changelog.rst`: Add changelog entries for both fixes.
When importing compilations, albumartist_sort, albumartists_sort,
albumartist_credit, albumartists_credit, and albumartists were
hardcoded to "Various Artists" instead of using the user-configured
va_name setting. This also fixes the same issue in the beatport plugin.
Fixes#6316
Fixes#6412.
This is my first time submitting a PR for an open source project so
please point out any mistakes!
## Summary
- Add `discogs.extra_tags` configuration option to narrow Discogs search
queries using existing tag values.
- Map supported tags (`barcode`, `catalognum`, `country`, `label`,
`media`, `year`) to corresponding Discogs search parameters.
- Update Discogs plugin documentation and tests to cover the new
behavior.
## Details
The Discogs plugin now mirrors `musicbrainz.extra_tags` by allowing
users to specify additional tags that should be used when building
Discogs search filters.
- New config option: `discogs.extra_tags` (default: `[]`).
- Supported tags and their Discogs search parameters:
- `barcode` → `barcode`
- `catalognum` → `catno` (whitespace removed)
- `country` → `country`
- `label` → `label`
- `media` → `format`
- `year` → `year`
- Tags `alias` and `tracks` are recognized but intentionally ignored for
Discogs, since the Discogs API does not provide direct equivalents for
these MusicBrainz-specific fields.
When `extra_tags` are configured, the plugin uses `beets.util.plurality`
over the items in the import session to select the most common value for
each configured tag and adds the corresponding Discogs filter.
## Testing
- Added unit tests in `test/plugins/test_discogs.py` to verify:
- Default search filters remain unchanged when `extra_tags` is not set.
- `discogs.extra_tags: [label, catalognum]` results in `label` and
`catno` filters populated from library items (with catalog number
whitespace stripped).
- Ran:
- `pytest test/plugins/test_discogs.py`
- `pytest test/plugins/test_musicbrainz.py`
## Enforce Changelog Entries Under 'Unreleased' Section
I've had enough checking this manually 😆. Adds a CI lint step
that prevents contributors from accidentally adding changelog entries
under an already-released version header in `docs/changelog.rst`.
### How it works
The check runs `git diff --word-diff=plain -U1000` against the base
branch, then pipes through `awk` to scan the diff for new list entries
(`{+- ...`) that appear after any versioned release header (e.g. `1.2.3
(`). If such an entry is found, the step fails with a human-readable
error pointing to the offending line which GitHub should show in the
diff view.
* `--word-diff=plain` is required to match _truly new_ changelog entries
instead of some formatting adjustments in the middle of the line.
* `-U1000` should ensure that we grab the first 1000 lines in the
changelog to reliably match the headers.