Commit Graph
15206 Commits
Author SHA1 Message Date
Šarūnas Nejus 14eabc6060 typing: add types to command handlers 2026-08-19 23:57:01 +01:00
Šarūnas Nejus 293ae110c7 typing: type utils and modules that are mostly missing annotations (#6925)
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.
2026-08-18 23:28:52 +01:00
Šarūnas Nejus c4953067cb Add commits to blame ignore 2026-08-18 23:22:16 +01:00
Šarūnas Nejus 33a01fe0b3 typing: rename AnyLibModel -> AlbumOrItem 2026-08-18 23:20:39 +01:00
Šarūnas Nejus 27900f58fb Prefer imports from modules that provide import aliases 2026-08-18 23:20:39 +01:00
Šarūnas Nejus f37306b0cc typing: remove redundant types 2026-08-18 23:20:39 +01:00
Šarūnas Nejus 0cae01cad2 typing: fix types in beets.util.functemplate 2026-08-18 23:20:39 +01:00
Šarūnas Nejus d8fc064e9c typing: add types to beets.util.functemplate 2026-08-18 23:20:39 +01:00
Šarūnas Nejus d143e2cb92 typing: fix types under beets.ui.commands.import_ 2026-08-18 23:20:39 +01:00
Šarūnas Nejus 2a9786aad7 typing: add types to beets.ui.import_ 2026-08-18 23:20:39 +01:00
Šarūnas Nejus 4aa5aa62ac typing: fix types in beetsplug._utils.art 2026-08-18 23:20:38 +01:00
Šarūnas Nejus 6acef1d586 typing: add types to beetsplug._utils.art 2026-08-18 23:20:38 +01:00
Šarūnas Nejus 26713de0d6 typing: add types to embedart 2026-08-18 23:20:38 +01:00
Šarūnas Nejus 9adcb601f8 typing: make dbcore.db.Results a Sequence
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.
2026-08-18 23:20:38 +01:00
Šarūnas Nejus 09e06f3efb typing: type plugins.send and BeetsPlugin.register_listener explicitly (#6886)
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.
2026-08-18 23:20:16 +01:00
Šarūnas Nejus 1c9d4bb8a8 Add commits to git blame ignore 2026-08-18 10:46:53 +01:00
Šarūnas Nejus 0c05551251 Update events docs 2026-08-18 10:46:33 +01:00
Šarūnas Nejus 12553051af typing: fix typing in plugins 2026-08-18 10:46:33 +01:00
Šarūnas Nejus 1969622874 typing: fix types in event handlers 2026-08-18 10:46:33 +01:00
Šarūnas Nejus 5039c8f6cf typing: add types to event handlers 2026-08-18 10:46:33 +01:00
Šarūnas Nejus dcc1fd0e4a typing: Import events module instead 2026-08-18 10:46:33 +01:00
Šarūnas Nejus e9021e17e9 typing: Define events in a separate module, import explicitly 2026-08-18 10:46:33 +01:00
Šarūnas Nejus 88627e062b typing: type handlers and listeners explicitly 2026-08-18 10:46:33 +01:00
J0J0 Todos e1ffd17302 lastgenre: Refactor _get_genre (#6474)
## 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)
2026-08-18 07:15:51 +02:00
J0J0 Todos 87633a892b lastgenre: Fix CanonTree type alias docs comment 2026-08-18 07:01:09 +02:00
J0J0 Todos c1a98e225d lastgenre: Type alias GenresWithLabel
Note that the artist stage is slightly different and GenresWithLabel does
not fit there.
2026-08-18 07:01:09 +02:00
J0J0 Todos 5b0e9e94cd lastgenre: Test new fetch_va_genres helper 2026-08-18 07:01:09 +02:00
J0J0 Todos 1059c2663d lastgenre: Rename genres to existing_genres 2026-08-18 07:01:09 +02:00
J0J0 Todos 5d651170af lastgenre: Extract original fallback stage to method 2026-08-18 07:01:09 +02:00
J0J0 Todos f202d54ea4 lastgenre: Extract complex artist stage to methods 2026-08-18 07:01:09 +02:00
J0J0 Todos d03537c0d5 lastgenre: Name vars resolved instead of result 2026-08-18 07:01:09 +02:00
J0J0 Todos e8d3143a1b lastgenre: Improve _try_resolve_stage docstring 2026-08-18 07:01:09 +02:00
J0J0 Todos 4617b08aa7 lastgenre: Extract existing genre stage
- fallback as cached property
- Leave force check in main get_genre()
- Leave fall back handling in get_genre()
2026-08-18 07:01:09 +02:00
J0J0 Todos c599cf0adf lastgenre: Conditional assign keep_genres (force mode) 2026-08-18 07:01:09 +02:00
J0J0 Todos e694f09c3f lastgenre: Move get_genre helpers to methods 2026-08-18 07:01:09 +02:00
Šarūnas Nejus 5406ed7409 fix: skip metadata source search when query and filters are empty (#6873)
## 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.)
2026-08-17 09:40:05 +01:00
Šarūnas Nejus cf385e7614 Merge branch 'master' into fix/skip-empty-metadata-search 2026-08-17 09:35:05 +01:00
Šarūnas Nejus 575f2d92b0 bpd plugin: use asyncio directly and remove bluelet (#6903)
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.
2026-08-17 05:17:16 +01:00
Šarūnas Nejus d53af83386 Serialize BPD notifications per connection
- 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.
2026-08-17 05:05:21 +01:00
Šarūnas Nejus 9b16f67af1 Remove bluelet in favor of using asyncio in bpd plugin 2026-08-17 05:05:21 +01:00
Sanjay Santhanam 127a2d8914 docs: keep changelog entry under unreleased 2026-08-15 16:59:28 -07:00
Henry b7993d19ac fix(discogs): retry malformed search responses #6912 2026-08-15 09:53:26 -07:00
henry 746cecf204 Fix KeyError in Deezer track conversion when artist is missing (#6929)
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.
2026-08-14 17:28:46 -07:00
Cohen Karnell d97d78f830 Fix KeyError in Deezer track conversion when artist is missing
_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.
2026-08-14 12:02:07 -04:00
Serene 1cb3af87e2 Add interactive upgrade option for duplicate_actions (#6910)
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.)
2026-08-13 19:29:17 +10:00
Sebastian Mohr 54b5d41acd Merge branch 'master' into fix/skip-empty-metadata-search 2026-08-12 12:28:20 +02:00
Trey Turner afd07ce5d3 fix(discogs): retry malformed search responses 2026-08-11 16:04:08 -05:00
Alok Saboo abacc3aad0 Merge branch 'master' into upgrade2 2026-08-11 08:19:15 -04:00
Šarūnas Nejus dc1709e148 tidal: Normalize copyright text into a concise label name (#6907)
Fixes #6796

`_parse_label()` used to store Tidal's raw copyright info as the label
name.

Adds `_normalize_label()`, a single-pass helper that:

- removes leading `©` / `℗` / `(C)` / `(P)` marker and year
- chops corporate-relationship clauses (`, a Division of X`, `, a BMG
  Company`)
- keeps the second half of `under exclusive license to X` statements
- splits territorial rights statements (`X for the United States and Y
  for the world outside...`), keeping the first label
- drops bare trailing legal-entity suffixes (`Inc.`, `LLC`, `Ltd.`,
`Co.`)

Tests show all functionality in action, but for the curious, here's the
results of running the helper function on a random selection of albums:
https://gist.github.com/NoDancing/90fae264e46023de176a11cb5c065a58
2026-08-11 00:44:26 +01:00
NoDancing 5187fb31d9 tidal: restore licen[sc]e option lost in formatting 2026-08-11 00:39:31 +01:00