## `lastimport`: Rename `play_count` field to `lastfm_play_count`
Renames the flexible field written by the `lastimport` plugin from
`play_count` to `lastfm_play_count` to avoid a silent collision with the
same-named field written by the `mpdstats` plugin.
### Impact
- **Breaking change**: existing databases with `play_count` populated by
`lastimport` must be migrated manually — automatic migration is not
possible due to the field name clash. Users are instructed to run `beet
modify lastfm_play_count='$play_count'`.
- Separation of concerns
- API client
- Rest of plugin logic
- Make `tunelog`helper available as `extra_debug` in `logger.py` - now
usable everywhere in beets.
- Named types for parsed whitelist and canonicalization tree
- Group all canonicalization tree processing tools together at the top
of the file as plain functions.
### New modules
- `client.py` - Last.fm API client (`LastFmClient`) extracted from main
plugin
**No functional changes** - pure refactoring to improve structure.
Fixes#6436.
On Windows, `beet config -e` falls back to `open_anything()` when
`VISUAL`/`EDITOR` are unset. That fallback returned `start`, but `start`
is a `cmd.exe` builtin (not an executable), so `os.execlp()` raised
`FileNotFoundError`.
This changes the Windows fallback command to `cmd /c start ""` so the
builtin is invoked via the shell and file opening works as intended.
## Changes
- Update `open_anything()` Windows fallback to `cmd /c start ""`.
- Add regression test for `interactive_open()` with the Windows fallback
command.
- Add regression test for `beet config -e` on simulated Windows with no
editor env vars.
- Add changelog entry under Unreleased bug fixes.
I really wanted to combine `random` with `play` to shuffle tracks in a
temporary playlist, but couldn't find easy way to do it. I'd like to
introduce a new flag `-R / --randomize` to the `play` plugin to shuffle
tracks before passing them to the player.
I couldn't really decide between `-r` and `-R`, not sure if there's any
rule there. If you believe lower case would be better, just let me know.
### Key changes
- **`get_model_changes` signature tightened**: The `old` parameter is no
longer optional — callers must pass an explicit model. The
`None`-fallback logic (`old or new.get_fresh_from_db()`) is pushed up to
`show_model_changes`, which is the appropriate place for that default
behaviour.
- **Changelog updated**: Plugin developers are notified to update their
imports.
Addresses #2661.
Currently, the missing plugin when ran in album mode only allows for
getting all release groups attached to a single artist. Users may want
to restrict this search to only show releases of a specific type (such
as albums or compilations). This CR adds a new `--release-type` flag to
the missing plugin. If users want to filter to a specific type (or set
of types), they simply need to provide this flag for every release type
that they want included.
As part of this change, the default behavior has been shifted to only
select `album` type releases as is suggested in the issue- to avoid
breaking default behavior I could easily switch this back. I am also
wondering if it might make sense to address the following idea
(https://github.com/beetbox/beets/discussions/5101) in a follow-up.
Bug fix: This change also fixes a bug where `--album` mode incorrectly
reported albums already in the library as missing. The original code
compared release group IDs from MusicBrainz against `Album` objects
(which never matched). This now correctly compares against
`mb_releasegroupid`.
## 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.