Part of #6924.
### What changed
- Types added to `beetsplug/duplicates.py`.
- Refactors `beetsplug/duplicates.py` so album and item commands now
share one execution path through `_run_command()`.
- Replaces the old generic `_merge()` dispatch with an explicit
`merge_func` passed into `_duplicates()`, so album and item merge
behavior stays separate but simpler.
- Tightens typing across the plugin by using clearer `Album`, `Item`,
`AlbumOrItem`, and `LibModel` annotations in the main helper methods.
### Why it matters
- The plugin's architecture is now more direct: command setup decides
`'albums'` vs `'items'`, and the shared duplicate-processing flow
handles the rest.
- This removes branching and type ambiguity from the core duplicate
logic, which makes the code easier to follow and safer to change.
- Functional behavior stays the same at a high level, but the plugin is
now easier to maintain, reason about, and extend.
Part of #6924.
- This PR is a typing and interface cleanup for `beetsplug/mpdstats.py`
and `beetsplug/thumbnails.py`. It does not add new features; it makes
the code's internal contracts clearer and more explicit.
- In `mpdstats`, MPD responses and `now_playing` state now have concrete
types, path lookup uses the correct beets path shape, and player event
handling is narrowed to known states. This makes the control flow easier
to follow and reduces hidden assumptions in the plugin.
- In `thumbnails`, the code stops passing full `Album` objects where
only `album.path` or `artpath` is needed. That separates album metadata
from file-path operations, removes ambiguity around optional
`album.artpath`, and makes thumbnail generation steps more direct.
- Tests were updated to match the narrower method signatures and the
corrected path handling.
- High-level impact: safer internal APIs, clearer data flow, and lower
risk of type-related bugs. The only visible behavior change is that
missing album art is now logged as a warning instead of info.
Part of #6924.
- This change tightens typing across metadata source plugins and shared
request/MusicBrainz utilities, so plugin code lines up better with the
data it actually consumes.
- Biggest architecture change is in `beetsplug/_utils/musicbrainz.py`:
common release fields move into `BaseRelease`, and new
`RecordingWithReleases` plus `get_recording_with_releases()` make
`'recording with releases'` an explicit shape instead of loose dict
guessing.
- Plugin code in `listenbrainz`, `chroma`, `acousticbrainz`, `beatport`,
`deezer`, `spotify`, `discogs`, and `mbsubmit` is then updated to use
clearer return types, typed collections, and safer access patterns for
optional API data.
- High-level impact: less ambiguity at plugin boundaries, better
editor/type-checker help, and fewer runtime mistakes from assuming
fields exist when remote APIs may omit them.
- There is also a small cleanup in `mbsubmit`: output formatting now
goes through `format()` with a cached config value, which removes a
cross-plugin helper dependency and keeps the command simpler.
Part of #6924.
- This change tightens typing across `beetsplug/aura.py`,
`beetsplug/export.py`, and `beetsplug/web/__init__.py` so plugin
interfaces better match real runtime behavior.
- High-level impact: no big feature shift, but architecture become more
explicit. Plugin boundaries, data shapes, and format handling are
clearer, which should make future changes safer and type errors easier
to catch.
#### `aura`
- base document methods are more explicit
- request args are lined up with `MultiDict`
- add safer handling for missing included resources.
I could not resolve typing without using `type: ignore` in two places
and I found both pieces of logic should be removed:
1. Field value conversion
- It caused `sqlite3.InterfaceError` when using `filter[genres]`,
because it was split into, e.g. `"genres" -> ["g", "e", "n", "r", "e",
"s"]`.
- The rest of the field types are either `str` and `int`. `MatchQuery`
happily accepts a `str` pattern when matching `int` fields.
2. When sorting by a `field`, excluding empty/null values: I'm pretty
sure this is not the behaviour we want 😅.
Extended tests to prove both of the above.
#### `export`
- simplify format selection and moves export flow to a single
iterable-based pipeline
- `json`, `jsonlines`, `csv`, and `xml` now share one higher-level data
path, with path-like byte fields normalized before writing
#### `web`
- add clearer endpoint and decorator annotations
- a small safety fix for missing item files returning `404` instead of
failing later
Part of #6924.
- This PR makes the `beetsplug.bpd` server stack easier to understand by
adding explicit type annotations across the main layers: `BaseServer`,
`Server`, `Connection`, `MPDConnection`, `ControlConnection`, `Command`,
and `GstPlayer`.
- At an architectural level, it clarifies how data moves through the
plugin:
- command handlers now have clearer input and return types,
- shared server state like `playlist`, `connections`, and notification
sets is declared directly,
- internal playback paths that do not come from a live client are
modeled explicitly with `MPDConnection | None`.
- It also includes a few small refactors that support that clearer
structure without changing the design:
- `_parse_range` is moved into `BaseServer` so the shared logic is
actually available where it is used,
- command parsing is simplified,
- socket reads now separate raw bytes from decoded text,
- VFS and library lookups are guarded more carefully.
- High-level impact: this is mainly a maintainability and correctness
change, not a feature change. It makes internal interfaces in `bpd` more
explicit, improves static type checking, and reduces the chance of
type-related bugs while keeping the plugin's overall behavior and
architecture the same.
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.