Releases: linuxserver/docker-beets
2.9.0-ls324
CI Report:
https://ci-tests.linuxserver.io/linuxserver/beets/2.9.0-ls324/index.html
LinuxServer Changes:
Full Changelog: 2.8.0-ls323...2.9.0-ls324
Remote Changes:
Updating PIP version of beets to 2.9.0
nightly-c6409d2e-ls265
CI Report:
https://ci-tests.linuxserver.io/linuxserver/beets/nightly-c6409d2e-ls265/index.html
LinuxServer Changes:
Full Changelog: nightly-26485613-ls264...nightly-c6409d2e-ls265
Remote Changes:
Retry listenbrainz requests for temporary failures
nightly-26485613-ls264
CI Report:
https://ci-tests.linuxserver.io/linuxserver/beets/nightly-26485613-ls264/index.html
LinuxServer Changes:
Full Changelog: nightly-0f2bb215-ls263...nightly-26485613-ls264
Remote Changes:
Store relative paths in the DB to make beets library portable (#6460)
Migrate Item & Album Paths to Library-Relative Storage
Fixes: #133
Core Problem Solved
Before: Beets stored absolute file system paths in SQLite (e.g.
/home/user/Music/Artist/album/track.mp3). This made library databases
non-portable — moving the music directory or sharing a database across
machines broke all path references.
After: Paths are stored relative to the music directory (e.g.
Artist/album/track.mp3), and expanded back to absolute paths
transparently on read. The database is now portable.
Architecture Changes
1. Context Variable for Music Directory (beets/context.py)
A new contextvars.ContextVar (_music_dir_var) holds the active music
directory, set once during Library.__init__ via
context.set_music_dir(). This avoids passing library.directory
through every call stack.
Library.__init__()
└─ context.set_music_dir(self.directory) ← single write point
↓
PathType.to_sql() / PathType.from_sql() ← read at DB layer
pathutils.normalize_path_for_db()
pathutils.expand_path_from_db()
2. Path Relativization Moved to the DB Layer (beets/dbcore/)
Previously, path conversion lived in Item._setitem /
Item.__getitem__ — model-specific overrides. It is now pushed down
into PathType.to_sql / PathType.from_sql in dbcore/types.py,
through two helpers in the new beets/dbcore/pathutils.py:
| Helper | Direction | Behaviour |
|---|---|---|
normalize_path_for_db(path) |
write | Strips music dir prefix → |
| relative path | ||
expand_path_from_db(path) |
read | Prepends music dir → absolute |
| path |
All models using PathType (currently Item and Album) benefit
automatically — no per-model overrides required.
3. PathQuery Updated for Relative Storage
(beets/dbcore/query.py)
Queries like path:/home/user/Music/Artist now normalize the search
term to its relative form before hitting the database, so SQL
comparisons match stored values correctly. Both col_clause (SQL path)
and match (in-memory path) use normalize_path_for_db.
4. One-Time Database Migration (RelativePathMigration)
Existing absolute paths in path and artpath columns are migrated on
startup:
Library startup
└─ _migrate()
└─ RelativePathMigration._migrate_data(Item, Album)
└─ _migrate_field("path")
└─ _migrate_field("artpath")
↓
Reads rows where field starts with b"/"
Writes os.path.relpath(path, music_dir) in batches
self.directory assignment was moved before super().__init__() in
Library.__init__ so the migration can access the music dir when it
runs.
5. Context Propagation to Background Threads
The music dir context variable must be available in worker threads
(pipeline stages, replaygain pool). Two propagation points were added:
beets/util/pipeline.py:Pipeline.run_parallel()snapshots the
calling context withcontextvars.copy_context()and passes a
per-thread copy to eachPipelineThread. Each stage coroutine is
invoked viactx.run(...).beetsplug/replaygain.py: Pool workers and their callbacks are
wrapped inctx.run(...)soexpand_path_from_dbworks correctly
inside the process pool.beets/util/__init__.py:par_mapsimilarly propagates context
into its thread pool workers.
Data Flow: Read & Write Path
item.path = "/home/user/Music/Artist/track.mp3" ← absolute on write
↓
PathType.to_sql()
↓
normalize_path_for_db() → b"Artist/track.mp3" ← stored in SQLite
item.path ← absolute on read
↑
PathType.from_sql()
↑
expand_path_from_db() ← b"Artist/track.mp3" ← fetched from SQLite
Key Invariants
- Public API unchanged:
item.pathalways returns an absolute
bytespath. - Raw DB value is relative: direct SQL reads return the relative
form (tests assert both). - Paths outside the music dir are stored as-is (e.g. IPFS paths —
seebeetsplug/ipfs.pyfix). - Migration is idempotent: rows already relative (no leading
/)
are skipped.
nightly-1eff9865-ls263
CI Report:
https://ci-tests.linuxserver.io/linuxserver/beets/nightly-1eff9865-ls263/index.html
LinuxServer Changes:
No changes
Remote Changes:
Support for python 3.14 (#6267)
This PR adds testing targets for version 3.14, enabling us to verify
compatibility with Python 3.14.
closes #6232
I would also like to see this addition included here as it will
introduce issues for 3.14 users if not
beetbox/pyacoustid#90
nightly-0f2bb215-ls263
CI Report:
https://ci-tests.linuxserver.io/linuxserver/beets/nightly-0f2bb215-ls263/index.html
LinuxServer Changes:
Full Changelog: nightly-72b1118a-ls262...nightly-0f2bb215-ls263
Remote Changes:
Slightly simplify lastgenre client (#6495)
Consolidate Last.fm genre fetching into a single fetch method
This PR simplifies the lastgenre client API by replacing three
separate fetch methods (fetch_track_genre, fetch_album_genre,
fetch_artist_genre) with a single unified fetch(kind, obj) method.
What changed
client.py:
- Introduces a class-level
FETCH_METHODSregistry (ClassVardict)
mapping fetch "kinds" ("track","album","artist",
"album_artist") to a(pylast_method, arg_extractor)tuple. - Replaces the three
fetch_*methods with a singlefetch(kind, obj)
that dispatches via this registry. - Removes a private
_tags_forwrapper — its logic is inlined into the
now-publicfetch_genres. - Drops the workaround for a
pylast.Album.get_top_tags()inconsistency
fixed in 2014.
__init__.py:
- All call sites updated to use
client.fetch(kind, obj)— the client
now owns field extraction (e.g.obj.artist,obj.album), removing
that concern from the plugin layer.
test_lastgenre.py:
- Test mocking simplified: a single
monkeypatchon
LastFmClient.fetchreplaces three separate method patches.
Impact
- Reduced surface area: one method to mock, test, and reason about
instead of three. - Field extraction centralised: callers no longer need to know which
fields to pass per entity type. - No behaviour change — pure refactor.
2.8.0-ls323
CI Report:
https://ci-tests.linuxserver.io/linuxserver/beets/2.8.0-ls323/index.html
LinuxServer Changes:
Full Changelog: 2.8.0-ls322...2.8.0-ls323
Remote Changes:
Updating PIP version of beets to 2.8.0
nightly-72b1118a-ls262
CI Report:
https://ci-tests.linuxserver.io/linuxserver/beets/nightly-72b1118a-ls262/index.html
LinuxServer Changes:
Full Changelog: nightly-fd586ef6-ls261...nightly-72b1118a-ls262
Remote Changes:
Support multi-valued fields in rewrite and advancedrewrite plugins (#6518)
Fix rewriting of multi-valued fields (rewrite / advancedrewrite
plugins)
Bug: Both rewrite and advancedrewrite plugins assumed all field
values are scalars, so list-type fields (e.g. genres) were not
rewritten correctly. Additionally, only the first matching rule was ever
applied to a field.
What changed
Core logic (beetsplug/rewrite.py):
- Introduced a
rewrite_valuesingledispatchfunction to handle both
strandlist[str]values. For lists, each element is rewritten
individually. - Extracted
apply_rewrite_rulesas a shared utility — now applies
all matching rules in config order (previously stopped at the first
match).
advancedrewrite plugin:
- Replaced its own inline rule-matching loop with a call to the shared
apply_rewrite_rules, fixing list field support there too.
Behaviour change — rule application order:
Previously, only the first matching rule was applied. Now, all rules run
in config order, allowing chained rewrites. For example:
rewrite:
artist .*hendrix.*: hendrix catalog
artist .*catalog.*: Experience catalogThis now produces "Experience catalog" instead of "hendrix catalog".
nightly-fd586ef6-ls261
CI Report:
https://ci-tests.linuxserver.io/linuxserver/beets/nightly-fd586ef6-ls261/index.html
LinuxServer Changes:
Full Changelog: nightly-2334b222-ls260...nightly-fd586ef6-ls261
Remote Changes:
lastgenre: Genre ignorelist (#6449)
Description
Adds a global and artist-specific genre ignorelist to lastgenre.
Ignorelist entries can use regex patterns or literal genre names and are
configurable per artist or globally. For config examples see submitted
docs and _load_ignorelist() docstring.
Additional minor refactoring
- Fixed condition in "keep original fallback stage" to use config view
object directly via.get(). - Deduplicate finding the correct artist/albumartist attribute with a
helper_artist_for_helper
nightly-2334b222-ls260
CI Report:
https://ci-tests.linuxserver.io/linuxserver/beets/nightly-2334b222-ls260/index.html
LinuxServer Changes:
Full Changelog: nightly-06512c9b-ls259...nightly-2334b222-ls260
Remote Changes:
Import MusicBrainz composer/lyricist/arranger ids (#5847)
Updates the MusicBrainz plugin to also import MBIDs for
composers/lyricists/arrangers, and adds them as multi-valued fields.
Closes #5698.
nightly-e7c7bfca-ls259
CI Report:
https://ci-tests.linuxserver.io/linuxserver/beets/nightly-e7c7bfca-ls259/index.html
LinuxServer Changes:
No changes
Remote Changes:
Swap Discogs genres and styles (#6478)
discogs: Swap genre and style field mapping
Fixes #6390
Fixes the semantic mismatch between Discogs' taxonomy and beets' fields.
In Discogs, genres are broad (e.g. "Electronic") and styles are
specific (e.g. "Techno"). Previously, beets stored them the wrong way
around.
What changed
In get_album_info, the source fields are swapped:
genres(beets) now reads from Discogsstyles— the specific
valuesstyle(beets) now reads from Discogsgenres— the broader
values
The append_style_genre config option retains its original intent:
append broader style values to genres when enabled. Only the data
source for each field changed, not the logic.
