Releases: linuxserver/docker-beets
Release list
nightly-db9e0441-ls315
CI Report:
https://ci-tests.linuxserver.io/linuxserver/beets/nightly-db9e0441-ls315/index.html
LinuxServer Changes:
Full Changelog: nightly-35e48114-ls314...nightly-db9e0441-ls315
Remote Changes:
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, andtypes.py. - The main architecture change is in
db.py: model materialization is
now centralized inDatabase._make_model(), whileResultsonly
handles lazy row iteration and uses a materializer callback. - Reloading is also cleaner now:
Model.get_fresh_from_db()goes
throughDatabase._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 inResults, while row-to-model construction and reload
behavior live inDatabase.
High-level impact
dbcorehas 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 returnsstr.
PathType.from_sql()only expands paths when the normalized value is
actuallybytes.- Several
__eq__implementations now use explicitisinstance(...)
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.
nightly-86f0f0a7-ls315
CI Report:
https://ci-tests.linuxserver.io/linuxserver/beets/nightly-86f0f0a7-ls315/index.html
LinuxServer Changes:
No changes
Remote Changes:
Ignore empty deprecated genres (#6848)
While testing my custom out of date soundcloud autotagger which still
sets genre instead of genres, I realised that empty (genre = '')
is converted to genres = [''] list by
Info._get_list_from_string_value.
nightly-4e3884a6-ls315
CI Report:
https://ci-tests.linuxserver.io/linuxserver/beets/nightly-4e3884a6-ls315/index.html
LinuxServer Changes:
No changes
Remote Changes:
Document art_set event (#6851)
Document art_set event
Summary
Documents the missing art_set plugin event in the developer event
reference. The entry records that the event fires after album cover art
is copied or moved into place and that handlers receive the affected
album.
Fixes #6845
AI assistance disclosure
This contribution was produced by an autonomous AI coding agent (Claude
Code) that @Dodothereal operates and monitors. @Dodothereal is
accountable for it, will address review feedback promptly, and will
close this PR immediately if this kind of contribution is unwelcome in
this project. Commits carry an Assisted-by: Claude Code trailer.
nightly-311662bd-ls315
CI Report:
https://ci-tests.linuxserver.io/linuxserver/beets/nightly-311662bd-ls315/index.html
LinuxServer Changes:
No changes
Remote Changes:
typing: type beets.library (#6825)
-
This PR finishes a broad typing pass across
beets.library, mainly in
beets/library/models.pyandbeets/library/library.py, and tightens a
few related plugin and dbcore interfaces. -
Architecturally, the change makes the
Library,Item,Album,
query parsing, and template helper layers agree on clearer contracts:
nullable values are explicit, collection and query return types are
defined, and cross-layer APIs now use more precise types. -
The main behavioral surface is still the same, but several weak spots
are now modeled directly in the code, such as optional albums on items,
optional art paths, library-backed template memoization, and query
objects flowing throughLibrary._fetch(). -
The PR also cleans up how template uniqueness helpers work by giving
_memotableand related keys a stable shape, and by tightening the
logic around album-aware formatting and disambiguation. -
A small architectural rename in
dbcorechangesDatabase._fetch()
toDatabase.get_results(), which makes the lower-level database API
clearer and avoids confusion withLibrary._fetch(). -
High-level impact: better static type safety, clearer boundaries
between database, library, model, and templating code, and less
ambiguity for future refactors without intentionally changing
user-facing behavior.
nightly-2cf20aa6-ls315
CI Report:
https://ci-tests.linuxserver.io/linuxserver/beets/nightly-2cf20aa6-ls315/index.html
LinuxServer Changes:
No changes
Remote Changes:
discogs: refactor _coalesce_tracks around more specific track types (#6818)
-
beetsplug/discogs/types.pynow models Discogs tracklist entries as
distinct shapes:AudioTrack,IndexTrack, andHeadingTrack. This
shifts the plugin from "guessing byposition" to handling each entry
by its declaredtype_. -
beetsplug/discogs/__init__.pyrefactors_coalesce_tracks()into
smaller helpers for flat subtracks and index tracks. The normalization
flow is now type-driven, which makes the rules for "one physical track"
vs "many physical tracks" more explicit. -
beetsplug/discogs/states.pynow advances track state only for real
audio entries (type_ == "track"), so structural entries no longer
affect track counting through indirectpositionchecks. -
Tests now use dedicated Discogs track factories and add coverage for
logical index tracks. This gives the refactor a clearer test model and
protects the new coalescing behavior. -
High-level impact: Discogs imports should now treat headings, index
containers, and subtracks more consistently, especially for releases
where one displayed work contains multiple logical parts but maps to a
single playable track.
This change is based on the following research (thanks, Codex!):
Discogs track coalescing
DiscogsPlugin._coalesce_tracks() handles three distinct notions all
called
"subtracks." This is the main source of its complexity.
Discogs distinguishes:
track: an audio entry.index: a titled container whosesub_tracksare parts or movements.heading: descriptive text applying to following tracks.
That distinction is documented in the current
Discogs tracklisting
guidelines.
Positions such as 3.1, 3.2 or 3a, 3b separately indicate
multiple
musical pieces inside one physical track.
Observed input shapes
| Case | API shape | Current result | Example |
|---|---|---|---|
| Normal track | type_="track", nonempty position |
Passed through | |
| Most releases | |||
| Flat virtual subtracks | Multiple top-level track entries with |
||
1.1, 1.2 or 22a, 22b |
Merged into a copy of the first track | ||
| This Is Not, [Eclectic Beatz | |||
| 8](https://www.discogs.com/release/2885582) | |||
| Nested logical subtracks | index with nested children sharing one |
||
| physical position | Parent index becomes the physical track; children | ||
| discarded | [Defqon.1 Weekend | ||
| Festival](https://www.discogs.com/release/7168134) | |||
| Nested physical subtracks | index with children numbered as |
||
| independent tracks | Index discarded; children promoted | [Classical | |
| Hollywood III](https://www.discogs.com/release/3647530) | |||
| Heading | heading, empty position, no sub_tracks |
Passed through | |
| as structural metadata | Defqon.1 CD headings | ||
| Ambiguous/nonstandard index | index with positions the regex cannot |
||
| classify | Usually promoted incorrectly as separate tracks | [King | |
| Crimson - Lizard](https://www.discogs.com/release/1156598) |
Flat virtual subtracks
This Is Not returns nine top-level entries:
{"type_": "track", "position": "1.1", "title": "Intro", "duration": "1:49"}
{"type_": "track", "position": "1.2", "title": "Untitled", "duration": "6:31"}
# ...
{"type_": "track", "position": "1.9", "title": "2 Bad ...", "duration": "10:18"}There is no index container. _coalesce_tracks() detects the suffixes
and
_add_merged_subtracks() produces:
{
"type_": "track",
"position": "1.1",
"title": "Intro / Untitled / ... / 2 Bad ...",
"duration": "1:49",
"artists": ["Unknown Artist"],
}It copies all metadata from the first entry and only combines titles.
This
explains the incorrect duration and artist.
Eclectic Beatz 8 has the same flat shape:
{"type_": "track", "position": "22a", "title": "Amplifier", "artists": ["Fedde Le Grand"]}
{"type_": "track", "position": "22b", "title": "Autograph ...", "artists": ["Hatiras", "MC Flipside"]}Those become one track, retaining only the first artist.
Nested logical subtracks
Defqon.1 contains:
{
"type_": "index",
"position": "",
"title": "Eat This / God! What The Hell!",
"duration": "3:06",
"sub_tracks": [
{"type_": "track", "position": "2-20A", "title": "Eat This"},
{"type_": "track", "position": "2-20B", "title": "God! What The Hell!"},
],
}Because 2-20A has a parsed subindex, the helper changes the parent to:
{
"type_": "index",
"position": "2-20",
"title": "Eat This / God! What The Hell!",
"duration": "3:06",
}The children disappear. Downstream code treats this as audio solely
because
its position is now nonempty. It never checks that type_ remains
"index".
Nested physical subtracks
Classical Hollywood III contains index containers such as:
{
"type_": "index",
"position": "",
"title": "Auld Lang Syne Variations For Piano Quartet",
"sub_tracks": [
{"type_": "track", "position": "1", "title": "Eine Kleine Nichtmusik ..."},
{"type_": "track", "position": "2", "title": "L.V.B. - Adagio"},
{"type_": "track", "position": "3", "title": "Chaconne ..."},
{"type_": "track", "position": "4", "title": "Homage ..."},
],
}Because position 1 has no subindex:
- the index container is removed;
- its children become top-level physical tracks;
- parent artists are copied to children missing artists;
- when
index_tracksis enabled, the parent title prefixes every child
title.
This behavior was added for
issue #2318 by
PR #2355.
Headings
Defqon.1 also contains:
{
"type_": "heading",
"position": "",
"title": "CD2 ⚡ Mixed By Partyraiser",
"duration": "",
}This is not coalesced. It passes through, but TracklistState
subsequently
treats every positionless object as structural metadata without checking
type_.
The code therefore cannot distinguish:
position == "" and type_ == "heading"
position == "" and type_ == "index"Worse, _add_merged_subtracks() identifies a parent using only:
tracklist and not tracklist[-1]["position"]A heading can therefore theoretically be mistaken for an index parent.
History
- Issue #1543 reported
flat
positions such as22aand22brepresenting one CD track. - PR #2222 added both flat
and
nested subtrack support. Its author explicitly noted thattype_might
identify index tracks robustly, but avoided the larger refactor. - Issue #2318
demonstrated that
nestedsub_trackscan instead be independent physical tracks. - PR #2355 added the current heuristic:
- parsed child subindex means logical fragments of one physical track;
- no parsed child subindex means independent physical tracks.
- That PR documented the heuristic as imperfect.
Lizard, withBa,
Bb,
Bc, and related positions, was already a known failure.
Test data does not match the API shapes
The current test helper always creates:
"type_": "track"Consequently, test inputs such as:
_track("TRACK GROUP TITLE", sub_tracks=[...])are not realistic. Real API data uses:
"type_": "index"Likewise, test medium titles are manufactured as positionless track
objects
rather than heading or index objects.
Recommended direction
Before refactoring production code, introduce accurate input types and
factories:
TrackEntry = AudioTrack | IndexTrack | HeadingTrackWith approximate shapes:
class AudioTrack(TypedDict):
type_: Literal["track"]
position: str
title: str
duration: str
artists: NotRequired[list[Artist]]
class IndexTrack(TypedDict):
type_: Literal["index"]
position: Literal[""]
title: str
duration: str
sub_tracks: list[AudioTrack]
artists: NotRequired[list[Artist]]
class HeadingTrack(TypedDict):
type_: Literal["heading"]
position: Literal[""]
title: str
duration: strThen test using _track(), _index(), and _heading() factories.
The eventual normalization can dispatch structurally:
trackwithout subindex: pass through.- Flat subindexed
trackgroup: apply the flat-fragment policy. index: normalize its children as logical or physical.heading: preserve as structural metadata.- Never infer an index merely from
position == "".
This removes the most dangerous ambiguity while preserving the
positional
heuristic where Discogs genuinely requires it.
nightly-26b26c4b-ls315
CI Report:
https://ci-tests.linuxserver.io/linuxserver/beets/nightly-26b26c4b-ls315/index.html
LinuxServer Changes:
No changes
Remote Changes:
lastgenre: Fix whitelist/tree some disco/funk genres (#6841)
Description
Fixes two issues in whitelist and genre tree.
nightly-17d67980-ls315
CI Report:
https://ci-tests.linuxserver.io/linuxserver/beets/nightly-17d67980-ls315/index.html
LinuxServer Changes:
No changes
Remote Changes:
replaygain: add metaflac backend (#6800)
Fixes #1203.
The replaygain plugin can't compute ReplayGain when metaflac is the only
tool available. The existing backends (command, gstreamer, audiotools,
ffmpeg) each need something heavier installed, which doesn't work on
minimal setups like a NAS where those won't install. Issue #1203 asks
for a metaflac based option so FLAC users on those setups can still tag
ReplayGain.
This adds a metaflac backend for that case. It runs metaflac --add-replay-gain to compute the gain and reads the values back with
metaflac --show-tag. I modeled it on the existing CommandBackend
since both wrap an external tool. It only handles FLAC, skips other
formats, and shifts the gain to the configured targetlevel like the
other backends.
One limitation: metaflac scans a whole album in one pass, so the files
of an album need the same sample rate and channel layout.
Before, selecting the metaflac backend failed:
$ beet replaygain
UserError: Selected ReplayGain backend metaflac is not supported.
After, the backend works and the plugin tests pass:
$ python -m pytest test/plugins/test_replaygain.py
7 passed, 4 skipped
The 4 skipped are the Opus R128 cases, which don't apply to the metaflac
path.
nightly-a85068be-ls313
CI Report:
https://ci-tests.linuxserver.io/linuxserver/beets/nightly-a85068be-ls313/index.html
LinuxServer Changes:
No changes
Remote Changes:
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_ihatetest_thetest_substitutetest_m3utest_acousticbrainztest_advancedwritetest_tidaltest_inlinetest_mpdstatstest_replacetest_rewrite
related to #5361
nightly-95abdfb5-ls314
CI Report:
https://ci-tests.linuxserver.io/linuxserver/beets/nightly-95abdfb5-ls314/index.html
LinuxServer Changes:
Full Changelog: nightly-15949c86-ls313...nightly-95abdfb5-ls314
Remote Changes:
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.
nightly-4775599c-ls314
CI Report:
https://ci-tests.linuxserver.io/linuxserver/beets/nightly-4775599c-ls314/index.html
LinuxServer Changes:
No changes
Remote Changes:
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: addInQuery.__hash__.test/dbcore/test_query.py: regression test that anInQuerywith 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.