Part of #6924.
Closes#2802.
Closes#4880.
## Summary
- Fix `modify` interactive selection by displaying the changes already
prepared for each object and syncing only the objects returned by
`ui.input_select_objects`. Previously, the selection prompt could apply
the pending mutations again, producing incorrect output or raising a
`TypeError`.
- Fix `move --album --timid` when choosing individual albums. The
selection preview now uses the command's model-specific path expansion,
so it displays the album's item paths instead of trying to read
item-only fields from an `Album`.
- Remove `beets.ui.commands.utils.do_query` and let `modify`, `move`,
`remove`, `update`, `write`, and `edit` own their item and album queries
and no-match behavior.
- Keep thin model-specific query wrappers where mypy needs help
preserving item and album types. Shared `modify` parameters use
`Unpack[TypedDict]` so callers remain checked without duplicating the
parameter list.
- Narrow the return type of `ui.input_select_objects` from `Any` to
`Sequence[T]` and keep `remove` confirmation counts accurate.
- Document both user-facing fixes in the changelog.
## Tests
- Add CLI regression coverage for `modify` selection output and timid
album selection in `move`.
- Update command tests for `fields`, `import`, `list`, `move`, `remove`,
and `update` to exercise their CLI entrypoints, including selective
removal and confirmation counts.
- Remove the obsolete `do_query` unit tests with the helper.
- GitHub checks pass for linting, formatting, typing, docs, CodeQL, base
installation, all Ubuntu Python 3.11-3.14 jobs, and Windows Python 3.13.
The remaining matrix jobs were still running when checked.
Part of #6924.
- This PR tightens typing across core surfaces in `beets`, especially
around `ui` command handling, `library` models, `autotag`, `util`,
logging, and test helpers.
- Architecturally, the change makes several implicit contracts explicit:
- `ui` command parsers and subcommands now have clearer typed
interfaces.
- `library` removal flow is split into internal `_remove()` and public
`remove(...)`, which better separates shared database-change behavior
from model-specific delete logic.
- Utilities like `fix_extension()` and playlist helpers now expose more
precise return/value types.
- High-level impact is mostly safety and maintainability rather than new
functionality. The goal is to make core APIs easier to reason about,
easier to type-check, and less likely to drift between declared and
actual behavior.
- There are a few small behavioral hardening changes:
- `Distance` arithmetic/comparison now rejects unsupported operand types
instead of silently accepting invalid values.
- Some CLI and command code paths were adjusted to make album/item
branching explicit and type-safe.
- A few test fixtures and helpers were updated to match the stricter
contracts.
- Reviewer takeaway: this is primarily a core typing cleanup with light
refactoring, aimed at improving internal API clarity and catching
mistakes earlier, with only limited runtime behavior changes in edge
cases.
## Description
LRCLib stores track metadata independently of the lyrics themselves, so
an entry can come back with both `plainLyrics` and `syncedLyrics` null
while `instrumental` is still `False`.
`LRCLyrics.is_valid` accepted such an entry as a match on duration
alone, and `get_text` then returned `self.plain`, i.e. `None`, despite
being annotated `-> str`. The `None` propagated into `Lyrics`, and the
first access of its text raised:
```
AttributeError: 'NoneType' object has no attribute 'splitlines'
File "beets/util/lyrics.py", line 108, in _split_lines
for line in self.text.splitlines()
```
`beet lyrics` surfaces this per track, but during an **import** the
exception escapes the pipeline stage and aborts the entire run, so one
such track strands every file queued behind it. Instrumental-heavy
material (lo-fi, game soundtracks, ambient) hits it often.
### Reproducing
Real response from the public API,
`https://lrclib.net/api/search?track_name=Anther&artist_name=Blue%20Wednesday`:
```json
{
"id": 37048543,
"trackName": "Anther",
"artistName": "Blue Wednesday",
"duration": 189.0,
"instrumental": false,
"plainLyrics": null,
"syncedLyrics": null
}
```
Against `master`, with no configuration involved:
```
candidate.is_valid = True <- accepted as a match
get_text() returned = None <- annotated `-> str`
AttributeError: 'NoneType' object has no attribute 'splitlines'
```
## Changes
- An entry with no lyrics text at all is no longer a valid match, so the
search continues to other candidates and backends and ultimately reports
that no lyrics were found. This is deliberately kept distinct from an
instrumental track, where "no lyrics" is itself the answer and the
existing `instrumental` handling is unchanged. Marking these as
instrumental would assert something the API response does not tell us:
the lyrics may simply not have been contributed yet.
- A null `plainLyrics` now falls back to synced lyrics rather than
discarding lyrics that are present.
- `LRCLibAPI.Item.plainLyrics` and `LRCLyrics.plain` are annotated as
nullable, matching what the API actually returns.
First part of #6924.
## Summary
- Give command handlers across core commands and many plugins a
consistent `Library`, options, and `list[str]` interface.
- Describe command-specific options with small `Protocol` types where
handlers depend on particular flags, while retaining `optparse.Values`
for handlers that do not need a narrower shape. This makes the CLI
boundary easier for type checkers to follow without coupling handlers to
a concrete options container.
- Broaden `beets.dbcore.sort` from `list` to `Sequence` and tighten
several item/album collection annotations to match the data actually
passed between APIs.
- Fix small runtime mismatches exposed while making these types
concrete:
- `beetsplug.ipfs` invokes `PlayPlugin._play_command` with the expected
options shape and closes remote libraries after use.
- `beetsplug.bpd` reads `control_port` through the config API that
matches its actual value type.
- `beets.ui.commands.update` handles excluded fields, missing prior
items, optional flags, and byte paths safely.
- Command option defaults and config handoffs are normalized where their
runtime values can be optional.
## Tests
- Add regression coverage for IPFS playback through the Play plugin and
context-managed remote libraries.
- Refresh lyrics integration fixtures for the current LRCLIB and LRCGET
responses.
Most changes are annotations and interface clarification. The behavioral
changes are limited to the mismatches listed above.
Fixes: https://github.com/beetbox/beets/issues/6923
### What changed
- This PR is mostly a typing and module-boundary cleanup.
- Code now prefers public package exports like `beets.library` and
`beets.dbcore` instead of reaching into deeper internal modules.
- Shared model typing was renamed from `AnyLibModel` to `AlbumOrItem`,
which makes intent clearer where code handles either an `Album` or an
`Item`.
- Several typing fixes were added around `import`, `embedart`,
`_utils.art`, and `beets.util.functemplate`.
### Architecture impact
- The main architectural shift is toward using stable, package-level
APIs such as `beets.library` and `beets.dbcore` as the import boundary.
- `dbcore.Results` now behaves like a `Sequence`, which lets callers
depend on a simpler, more general interface instead of a concrete
internal result type.
- `beets.util.functemplate` got a deeper type pass and some small
internal cleanup, but its role in the system stays the same.
### High-level impact
- Improves type safety and IDE support across importer, library, plugin,
and template code.
- Reduces coupling to internal module layout, which should make future
refactors safer.
- Makes a few core interfaces easier to understand and reuse, especially
around library model collections and import-session callbacks.
- Overall, this looks like low-risk maintenance work with small
correctness improvements and no intended feature change.
Now we are able to type Results simply as a Sequence. This way, we can
also now use a more generic representation `Sequence[LibModel]` to refer
to *any* of the two models.
Fixes: https://github.com/beetbox/beets/issues/6921
- Adds explicit typing to the plugin event system in `beets.plugins`,
including per-event argument shapes and return types for
`register_listener()` and `send()`. This makes the listener API much
clearer and turns implicit plugin contracts into checked interfaces.
- Updates all plugins to match those typed event signatures, by adding
concrete parameter/return types and aligning handlers with the events
they subscribe to. The architectural effect is better consistency across
plugin boundaries, especially around importer hooks and metadata
callbacks.
- Includes a small set of follow-up fixes uncovered by the typing work
in places like `advancedrewrite`, `badfiles`, `playlist`, `permissions`,
`importsource`, and query/path handling. These are mostly correctness
and config/path-type cleanups rather than new features.
- High-level impact: this change improves maintainability and static
analysis across the plugin layer, reduces ambiguity in hook behavior,
and makes future plugin changes safer without changing the overall
architecture or user-facing workflows in a major way.
## Description
The monolithic `_get_genre` method was broken down into several private
instance methods and refactored for readability. The contract is kept
and is already well tested (`test_get_genre`)
- **Core Helpers** - were moved from within `_get_genre` to a reusable
instance method and a `cached_property`:
- `_try_resolve_stage`: Handles the canonicalization and logging of
genres for a specific stage.
- `fallback`: Provides the configured fallback genre. Is used as a last
resort in `_try_resolve_existing_genres` and when `_get_genre` couldn't
find any genre in any stage at all.
- **Lookup Stages** - some were complex enough to deserve their own
instance method for readability, some stay inline in `_get_genre`:
- `_try_resolve_existing_genres`: Manages the initial check for
pre-existing genres and the `cleanup_existing` logic when `force` is
disabled.
- track stage: stays inline
- album stage: indentical to track stage, but not worth moving /
deduplication doesn't buy much (see subsequent PR though)
- `_fetch_artist_stage`: Fetches and resolves artist-level genres,
including multi-valued album artists and "Various Artists" logic.
- `_fetch_va_genres`: specifically handles the plurality logic for
"Various Artists" albums.
- **Fallbacks**:
- `_try_resolve_original_fallback`: Handles the "keep_existing" logic
that attempts to use/canonicalize originally present genres if no new
ones are found.
Make sure to also look at subsequent PR's:
- https://github.com/beetbox/beets/pull/6890
- https://github.com/beetbox/beets/pull/6893
## To Do
- [x] ~Documentation~
- [x] Changelog. (Not required, refactor only)
- [x] ~Tests~ (_get_genre was already well covered and the signature of
the method was kept)