Commit Graph

15009 Commits

Author SHA1 Message Date
Samad
4ccdcc90c9 replaygain: add metaflac backend
Add a MetaflacBackend that computes ReplayGain for FLAC files with metaflac --add-replay-gain and reads the values back with --show-tag. Only FLAC is supported. Includes docs, a changelog entry, and tests.
2026-07-15 22:22:07 +01:00
Šarūnas Nejus
804dd34947 Fix uncaught KeyError on date queries containing a stray pipe (#6835)
Date queries detect *relative* dates (`-1w`, `+3m`) with a small regex
whose character classes were written as if `|` meant alternation:

```python
relative_re = "(?P<sign>[+|-]?)(?P<quantity>[0-9]+)(?P<timespan>[y|m|w|d])"
```

Inside `[...]`, `|` is a literal member, so `[y|m|w|d]` also matches
`|`. A date query with a stray pipe — e.g. `beet list added:2000|2001`
(a natural mistake for someone expecting `|` to mean "or") — captures
`|` as the timespan, and `Period.parse` then evaluates
`relative_units["|"]`, raising an uncaught `KeyError` with a traceback.
Malformed date strings are meant to raise
`InvalidQueryArgumentValueError` (see `test_invalid_date_query`), which
upper layers render as a friendly "a valid date/time string" message.

### Fix

Drop the stray `|` from both character classes so the value falls
through to the normal invalid-date path:

```python
relative_re = "(?P<sign>[+-]?)(?P<quantity>[0-9]+)(?P<timespan>[ymwd])"
```

Before: `DateQuery("added", "2000|2001")` → `KeyError: '|'`
After: → `InvalidQueryArgumentValueError: '2000|2001' is not a valid
date/time string`

### Tests / changelog

- Added `test_pipe_in_relative_date_query` (fails before, passes after).
- Added a Bug-fixes changelog entry.

Disclosure: prepared with assistance from Claude (an AI tool); I
reviewed and verified the change locally (fail-before/pass-after).
2026-07-15 21:06:28 +01:00
TowyTowy
4a1e9164a1 Fix uncaught KeyError on date queries containing a stray pipe
The relative-date regex used character classes [+|-] and [y|m|w|d],
which mistakenly treat | as a literal member rather than alternation.
As a result a value like added:2000|2001 (as a user might type expecting
| to mean "or") captured | as the relative-date unit and crashed with
an uncaught KeyError in Period.parse instead of raising the documented
InvalidQueryArgumentValueError.

Remove the stray | from both character classes so malformed date strings
fall through to the normal invalid-date error path.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-15 21:55:46 +02:00
Šarūnas Nejus
db9e044155 typing: add types to beets.dbcore (#6820)
### What changed

- This PR is mainly a typing pass over `beets.dbcore`, especially in
`db.py`, `query.py`, `sort.py`, and `types.py`.
- The main architecture change is in `db.py`: model materialization is
now centralized in `Database._make_model()`, while `Results` only
handles lazy row iteration and uses a materializer callback.
- Reloading is also cleaner now: `Model.get_fresh_from_db()` goes
through `Database._reload()`, which keeps database lookup logic in the
database layer and preserves the model's concrete type.
- Along the way, a few runtime mismatches found by typing were corrected
instead of just adjusting annotations.

### Why

- The goal is to make the typed API match the real behavior of `dbcore`.
- This improves separation of responsibilities: querying and result
iteration stay in `Results`, while row-to-model construction and reload
behavior live in `Database`.

### High-level impact

- `dbcore` has a clearer internal structure and a more explicit API
surface.
- Type information is more accurate across queries, sorting, models, and
field types, which should make future refactors safer.
- A few small correctness fixes are included:
  - `DurationType.format()` now always returns `str`.
- `PathType.from_sql()` only expands paths when the normalized value is
actually `bytes`.
- Several `__eq__` implementations now use explicit `isinstance(...)`
checks.
- Reload behavior now has regression coverage in
`test/dbcore/test_db.py`.
- Overall, this is a low-risk cleanup: mostly typing and API-shape
improvements, with a few small behavior fixes discovered during the
work.
2026-07-15 20:28:19 +01:00
Šarūnas Nejus
0f00bc9c1f Add commits to ignore 2026-07-15 20:23:57 +01:00
Šarūnas Nejus
1266256f5e typing: fix beets.dbcore.types 2026-07-15 20:23:55 +01:00
Šarūnas Nejus
ea3d6af616 typing: type beets.dbcore.types 2026-07-15 20:23:55 +01:00
Šarūnas Nejus
b31c3bf9d4 typing: fix beets.dbcore.query 2026-07-15 20:23:55 +01:00
Šarūnas Nejus
b1701c2e12 typing: type beets.dbcore.query 2026-07-15 20:23:55 +01:00
Šarūnas Nejus
a000232509 typing: fix beets.dbcore.sort 2026-07-15 20:23:55 +01:00
Šarūnas Nejus
ca8e9f69a8 typing: type beets.dbcore.sort 2026-07-15 20:23:55 +01:00
Šarūnas Nejus
7a553aad7e typing: fix beets.dbcore.db 2026-07-15 20:23:54 +01:00
Šarūnas Nejus
8d03803fcf typing: type beets.dbcore.db 2026-07-15 20:23:54 +01:00
Vinícius Martins Cotrim
6a051f9699 Recognize native Spotify URIs when extracting IDs (#6840)
### Description

`extract_release_id` now accepts the native Spotify URI format
(`spotify:album:<id>`, `spotify:track:<id>`) in addition to the already
supported `open.spotify.com` URLs and bare IDs.

Co-authored-by: Marina Pena Malschitzky Crespo <marinacrespo@estudante.ufscar.br>
2026-07-15 21:21:25 +02:00
Šarūnas Nejus
3736513c1c lyrics: add lrcmux backend (#6816)
## Description

This PR adds https://lrcmux.dev
([repo](https://github.com/f1nniboy/lrcmux)) as a new lyrics fetcher
backend. lrcmux aggregates various different lyrics sources and exposes
them via a single API, so it's perfect as a replacement for the
currently defunct Musixmatch fetcher. Users can host their own instance
if they prefer not to share their queries with a central API.
2026-07-15 15:27:57 +01:00
f1nn
0d65a46bbc lyrics: add lrcmux backend 2026-07-15 16:12:21 +02:00
J0J0 Todos
361405a42d lastgenre: Genre spelling normalization (aliases) (#6466)
# Description

This PR introduces a regex-based **normalization (alias) system** to
unify variant genre tags and improves the plugin's documentation.

The normalization feature uses an ordered list of regular expression
aliases to map variant spellings or synonyms to a single canonical name.
The mapping keys act as `re.Match.expand()` templates, supporting
`\g<N>` (or shorter, eg. `\1`) back-references to regex capture groups:

```yaml
lastgenre:
    aliases:
        # Maps 'hip-hop' -> 'hip hop' using back-references 
        # (regex full-match)
        \1hop:
            - (hip|trip|jazz)y?[ /-]*hop
        
        # Static normalization examples
        drum and bass:
            - dnb
            - d&b
```

- **Where?**: Integrated `normalize_genre` before `is_ignored` in both
the `LastFmClient` (for clean lookup/filtering) and the core
`_resolve_genres` loop (for uniform processing of existing file tags).
- Updated default whitelist and genre tree files for canonical-name
consistency, and curated the bundled `aliases.yaml` from the top 1,000
Last.fm tags to cover common inconsistencies without over-normalizing.
- Any normalization that occurs is logged (extra debug log level `beet
-vvv ...`)
- **Additional documentation changes**: 
- Added a **[Choosing the Right
Tool](file:///Users/jtiefenbacher/git/beets/docs/plugins/lastgenre.rst)**
guide. This is for endusers but note that it might even be _a primary
starting point for maintainer review._
- Added **non-whitelist mode** details to the **Canonicalization**
chapter.
- **Additional unrelated changes**: 
- Fixes a few variable name inconsistencies in the recently merged
`ignorelist` features' tests.
- Improved naming of ignorelist related type `IgnorePatternsByArtist`
2026-07-15 16:09:37 +02:00
J0J0 Todos
c51f5755f3 lastgenre: Whitelist/Tree fixes; Default aliases
- Fixes some inconsistencies in spelling in the data files we ship with
  the plugin as well as adds some new genres to both files that seem to
  be returned often by last.fm.
  - For some changes the default aliases feature helps to streamline
    inconsistencies that come from last.fm already.
- The default aliases file that ships with beets
  - Tests fixes that prove that most of the alias normalization patterns
    work as they are supposed to.

Co-authored-by: Šarūnas Nejus <snejus@protonmail.com>
2026-07-15 16:04:51 +02:00
J0J0 Todos
fda3fef148 lastgenre: Genre aliases docs; Tools chapter, More
- Document the genre alias normalization feature and the default aliases
  we ship with the plugin (by using rst "literalinclude" referring to
  aliases.yaml directly)
- Clarify what canonicalization acutually does when used without a
  whitelist enabled.
- New docs chapter "Using the right tool"
2026-07-15 16:04:51 +02:00
J0J0 Todos
3ce8c9bbf5 lastgenre: Tests for _filter_valid helper,
even though it's indirectly tested via resolve_genres tests and
elsewhere, it makes sense to keep them to help with future refactoring.
2026-07-15 16:04:51 +02:00
J0J0 Todos
1674b5e7f9 lastgenre: Tests for genre alias normalization
Co-authored-by: Šarūnas Nejus <snejus@protonmail.com>
2026-07-15 16:04:51 +02:00
J0J0 Todos
e3568273ee lastgenre: Sync instance var naming
client and main plugin classes use the same names for instance
variables now
2026-07-15 16:04:51 +02:00
J0J0 Todos
4b9fa31a5b lastgenre: Genre aliases; Refactoring
- Genre alias normalization feature implementation
- Extend and simplify _filter_valid()
- Remove drop_ignored_genres()
- More smaller ignorelist related refactoring
2026-07-15 16:04:51 +02:00
J0J0 Todos
a78c94c443 Changelog for #6466 genre aliases 2026-07-15 16:04:51 +02:00
Šarūnas Nejus
dab3bd337d ci: test base installation dependencies (#6824)
## Summary

- add a CI job that installs Beets without dependency groups or plugin
extras
- run `beet version` with an isolated configuration directory to verify
the base installation

Fixes #5713
2026-07-15 14:41:20 +01:00
Šarūnas Nejus
5616799a8e Merge branch 'master' into issues/5713-base-install-smoke-test 2026-07-15 14:36:03 +01:00
Šarūnas Nejus
b37fd0e75e Preserve null artpath in edit (#6839)
Fixes #2438.

When editing album metadata, `artpath: null` was converted through the
string parsing path and could become a path ending in `None`.

This keeps YAML null values as `None` when dumping and applying edit
data, and adds a regression test for editing album metadata while
`artpath` is missing.

- Added a regression test.
- Added a changelog entry.
2026-07-15 14:35:03 +01:00
AleksZyro
4fb451f450 Treat null edit values as safe 2026-07-15 14:30:42 +01:00
AleksZyro
bb0c0992e5 Format changelog entry 2026-07-15 14:30:42 +01:00
AleksZyro
a6d576d039 Preserve null artpath in edit 2026-07-15 14:30:42 +01:00
Šarūnas Nejus
4775599c35 Make InQuery hashable when its pattern is a list (#6838)
Fixes #5354.

`beet splupdate` raises `TypeError: unhashable type: 'list'` whenever a
smartplaylist configuration contains a `playlist:` query. smartplaylist
collects playlists in a `set`, which hashes the query. `playlist:`
parses to `PlaylistQuery`, a subclass of `InQuery` whose `pattern` is a
list, and `InQuery` inherited `FieldQuery.__hash__`, which does
`hash(self.pattern)` and fails on a list.

As @wisp3rwind suggested on the issue, this overrides `__hash__` on
`InQuery` to hash a tuple copy of the pattern. It stays order-sensitive
to match the inherited `FieldQuery.__eq__`, so the hash/eq contract
holds.

Changes:
- `beets/dbcore/query.py`: add `InQuery.__hash__`.
- `test/dbcore/test_query.py`: regression test that an `InQuery` with a
list pattern can be put in a set.
- changelog entry under Bug fixes.

Left the order-insensitive `__eq__`/`__hash__` and the smartplaylist
`set`-vs-`dict` cleanup you also floated out of scope here; happy to
follow up on either if you want them.
2026-07-15 08:07:30 +01:00
Dhruv Maniya
bdfacbfcc7 ci: use Poe for base installation 2026-07-15 08:05:05 +01:00
Dhruv Maniya
1f182de4a2 ci: test base installation dependencies 2026-07-15 08:05:05 +01:00
Valentyn Kit
eb7c832fc9 Make InQuery hashable when its pattern is a list
InQuery inherited FieldQuery.__hash__, which hashes the pattern directly.
A list pattern raises `unhashable type: 'list'`, which broke the
smartplaylist `splupdate` command whenever a `playlist:` query was configured.
2026-07-15 08:02:01 +01:00
Šarūnas Nejus
35e481148f missing: honor -f/--format in album mode (#6831)
Fixes #3804.

The `missing` plugin ignored `-f`/`--format` (and the `format_album`
config) in album mode. `beet missing -a` always printed a hardcoded
`<artist> - <title>` and never consulted the format string. Now each
missing release group is turned into an `Album` and emitted with
`format_album`, so the format option is respected. With the default
`$albumartist - $album` the output is unchanged, matching the behavior
already documented in `docs/plugins/missing.rst`.
2026-07-15 07:59:43 +01:00
Sebastian Cao
2ada8a947d move the changelog entry under unreleased 2026-07-15 14:32:13 +08:00
Sebastian Cao
7d4f160c36 missing: honor -f/--format in album mode 2026-07-15 14:31:58 +08:00
Šarūnas Nejus
95abdfb5a3 Improve subsonicupdate error for non-JSON responses (#6837)
Fixes #5635.

When the Subsonic server returns a non-JSON response, `subsonicupdate`
currently logs the JSON decode error directly. This can produce a
cryptic message like `Expecting value`.

This logs a clearer error with the server URL and HTTP status, and adds
a regression test for an HTML error response.
2026-07-14 19:33:46 +01:00
AleksZyro
04f2c3e794 Improve subsonicupdate server error 2026-07-14 20:09:56 +02:00
Sebastian Mohr
a85068bef8 Removed a few more unittests (#6822)
Found a few more places were it is possible to easily get rid of the
`unittest` setup in favor for `pytest`.


Touches and removes `unittest` from the following files:
- `test_ihate`
- `test_the`
- `test_substitute`
- `test_m3u`
- `test_acousticbrainz`
- `test_advancedwrite`
- `test_tidal`
- `test_inline`
- `test_mpdstats`
- `test_replace`
- `test_rewrite`

related to #5361
2026-07-14 17:28:26 +02:00
Sebastian Mohr
19f944698d rewrite: PluginTestCase -> PluginTestHelper 2026-07-14 17:24:17 +02:00
Sebastian Mohr
5364644238 replace: PluginTestCase -> PluginTestHelper 2026-07-14 17:24:17 +02:00
Sebastian Mohr
9f75017d72 mpdstats: PluginTestCase -> PluginTestHelper 2026-07-14 17:24:17 +02:00
Sebastian Mohr
a06c4b4736 inline: PluginTestCase -> PluginTestHelper 2026-07-14 17:24:17 +02:00
Sebastian Mohr
ca2acfc28a tidal: PluginTestCase -> PluginTestHelper 2026-07-14 17:24:17 +02:00
Sebastian Mohr
2b90679100 advancedrewrite: PluginTestCase -> PluginTestHelper 2026-07-14 17:24:17 +02:00
Sebastian Mohr
9f75e2b211 acousticbrainz: Removed unittest.TestCase 2026-07-14 17:24:17 +02:00
Sebastian Mohr
757af4aebd util/m3u: Removed unittest.TestCase 2026-07-14 17:24:17 +02:00
Sebastian Mohr
d19e36a0a1 substitute: PluginTestCase -> PluginTestHelper 2026-07-14 17:24:17 +02:00
Sebastian Mohr
9616065591 the: Removed unittest.TestCase 2026-07-14 17:24:17 +02:00