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)
## Description
Fixes#6862.
Items with no artist/title tags produce a search with an empty query and
no filters. The request was still sent to the metadata source API, and
MusicBrainz answers it with `400 Bad Request`, logging a traceback once
per affected file. `_search_api` now returns no candidates instead of
issuing a request that cannot match anything.
## To Do
- [x] ~Documentation~ (bugfix, no user-facing option changed)
- [x] Changelog.
- [x] Tests. (`test_search_api_skips_request_without_query_and_filters`
fails without the fix, passes with it.)
So I was looking to add types to `beets.util.bluelet` and realised that
it is only used by `BPD` plugin. Instead of investing any time into it,
I scrapped it and used `asyncio` directly in `BPD`. I used GitHub search
to check that it is not used outside of our codebase.
---
- Replaces the custom coroutine scheduler in `beets.util.bluelet` with
Python's built-in `asyncio` for the `bpd` plugin.
- In `beetsplug/bpd/__init__.py`, the server architecture shifts from
Bluelet generators and event objects to native async I/O:
- connection handling now uses `asyncio.start_server`
- connection flows are rewritten as `async def` methods with `await`
- notification delivery is handled with background `asyncio` tasks
- socket lifecycle and disconnect handling move to `asyncio` stream
readers/writers
- This removes an internal async framework from the codebase,
consolidates `bpd` on a standard runtime model, and makes the networking
layer simpler to reason about and maintain.
- Tests in `test/plugins/test_bpd.py` are updated to mock
`asyncio.start_server` instead of Bluelet internals, matching the new
server entrypoint and preserving coverage around dynamic port
assignment.
- Track active notification tasks by connection so repeated dispatches do not
send duplicate idle responses while a previous send is still draining.
- Add regression coverage for serialized notification delivery and idle command
disconnect handling.
Fixes#4339.
The album half of this issue is already fixed, `album_for_id` guards the
`contributors` key. This covers the remaining call site, `_get_track`,
which still assumes the key exists:
```python
artist, artist_id = self.get_artist(
track_data.get("contributors", [track_data["artist"]])
)
```
Two problems. The default argument to `.get` is evaluated eagerly, so
`track_data["artist"]` runs on every call, even when `contributors` is
present. A track payload carrying `contributors` but no `artist` raises
KeyError, despite the code reading as though the fallback only applies
when `contributors` is missing. And the fallback itself depends on
`artist` being there, which is the same assumption sampsyo asked the
plugin to stop making.
I mirrored the shape already merged for the album path: use
`contributors` when present, fall back to `artist` when that's the one
we have, and leave the artist fields unset when neither key exists.
`str(artist_id)` now also only runs when there's an id, so a track with
no artist info gets `None` instead of the string "None". The album path
still calls `str(artist_id)` unconditionally and can store "None" the
same way, but I left it alone to keep this to the one call site. Happy
to follow up on it.
`_get_track` had no test coverage, so I added three tests: contributors
without artist, artist without contributors (the old fallback still
behaves the same), and neither key. The first and third fail on master
with KeyError at the `.get` line, and all three pass with the change.
ruff check and format are clean on both files.
_get_track resolved the artist with track_data.get('contributors', [track_data['artist']]),
whose default is evaluated eagerly, so track_data['artist'] raises KeyError whenever the
artist key is absent, even when contributors is present. Guard the fallback the same way
album_for_id already does. Fixes#4339.
Follow-up to #6842, based on @Serene-Arc follow-up
[comment](https://github.com/beetbox/beets/pull/6842#issuecomment-5187227636)
.
This makes the upgrade available when `duplicate_action: ask`, allowing
users to explicitly select it in the interactive duplicate prompt rather
than relying solely on configuration.
- [x] Documentation. (If you've added a new command-line flag, for
example, find the appropriate page under `docs/` to describe it.)
- [x] Changelog. (Add an entry to `docs/changelog.rst` to the bottom of
one of the lists near the top of the document.)
- [x] Tests. (Very much encouraged but not strictly required.)