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.
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).
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>
### 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.
### 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>
## 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.
# 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`
- 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>
- 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"
- Genre alias normalization feature implementation
- Extend and simplify _filter_valid()
- Remove drop_ignored_genres()
- More smaller ignorelist related refactoring
## 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
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.
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.
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.
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`.
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.
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