Skip to content

Releases: linuxserver/docker-beets

nightly-c9cee0d1-ls272

20 Apr 16:58
66bf953

Choose a tag to compare

Pre-release

CI Report:

https://ci-tests.linuxserver.io/linuxserver/beets/nightly-c9cee0d1-ls272/index.html

LinuxServer Changes:

No changes

Remote Changes:

fromfilename: Support 'track' prefix when parsing track number (#6557)

fromfilename: Support "track" prefix in track number parsing

The fromfilename plugin's filename-matching regex is extended to
recognise filenames like track01.m4a or track 2.m4a, where a literal
"track" prefix precedes the track number.

nightly-b13a4fa7-ls272

20 Apr 20:40
66bf953

Choose a tag to compare

Pre-release

CI Report:

https://ci-tests.linuxserver.io/linuxserver/beets/nightly-b13a4fa7-ls272/index.html

LinuxServer Changes:

No changes

Remote Changes:

Tidal Metadata Plugin (#6520)

Description

This PR introduces tidal as metadatasource. It add both an minimal api
layer and the typical metadata source plugin capabilities.

Details

The implementation provides a small API layer consisting of TidalAPI
for high-level album and track fetching, and TidalSession which
extends requests.Session with token authentication, automatic rate
limiting (~4 req/s via RateLimitAdapter), and pagination resolution
following the JSON:API spec.

Authentication is handled through an OAuth2 PKCE flow accessible via
beet tidal --auth, with automatic token refresh when the access token
expires.

Metadata parsing handles Tidal's JSON:API response format, extracting
album and track information including ISO 8601 duration conversion,
artist relationships, and copyright/label data.

Input wanted

The API layer currently lacks comprehensive test coverage. Setting up
proper tests would require either mocking all outgoing requests or
creating a dedicated test token (which necessitates an account and might
require read/write to github secrets).

Are we comfortable with the current approach of unit testing the plugin
itself while mocking all requests?

TODOs

  • Documentation
  • candidate and item_candidates lookup
  • It should be possible to optimize batched lookups
  • Add tests for candidates and item_candidates
  • Implement batching for more than 20 filters

Refs

thanks to @jcjordyn130 for his initial implementations in #5637 and
#4641

nightly-61a4ba9a-ls272

19 Apr 21:32
66bf953

Choose a tag to compare

Pre-release

CI Report:

https://ci-tests.linuxserver.io/linuxserver/beets/nightly-61a4ba9a-ls272/index.html

LinuxServer Changes:

No changes

Remote Changes:

smartplaylist: Enrich original "Creating output" with track counts; CLI output overhaul (#6493)

Description

  • The per-playlist summary now includes a track count (Creating playlist X: N tracks.) at INFO level.
  • The --pretend flag produces the same output but reports "N
    playlists would be updated"
    instead of "N playlists updated".
  • Per-track details are shown only in DEBUG log level.
  • The --format option allows customizing the track line format.
  • The __apply_opts_to_config helper was removed and instead defaults
    provided when defining CLI options
  • Fix a tiny bug: Deduplicate playlist entries by URI and only increment
    matched counts when a new entry is added.
  • The --pretend-paths option was removed since now the per-track log
    format is configurable anyway.

nightly-5f6b2d35-ls272

19 Apr 19:48
66bf953

Choose a tag to compare

Pre-release

CI Report:

https://ci-tests.linuxserver.io/linuxserver/beets/nightly-5f6b2d35-ls272/index.html

LinuxServer Changes:

Full Changelog: nightly-06f5e14b-ls271...nightly-5f6b2d35-ls272

Remote Changes:

Increment version to 2.10.0

nightly-44ffbdf6-ls272

19 Apr 22:28
66bf953

Choose a tag to compare

Pre-release

CI Report:

https://ci-tests.linuxserver.io/linuxserver/beets/nightly-44ffbdf6-ls272/index.html

LinuxServer Changes:

No changes

Remote Changes:

Remove archive on import during move mode (#6551)

Fixes #1663.

This modifies the cleanup method on the ArchiveImportTask to remove
the original archive file on successful import when move: yes.

I decided to do this such that partial imports do not destructively
remove the archive. Perhaps this would be better as a configuration
option, but I think that may go beyond the scope for this initial
feature.

I am unsure whether my method for "complete import" is strictly correct,
I reasoned that we would check that all files had been moved out, but
surely this data exists more concretely somewhere. Let me know if this
should be polished, or there's more nuance I'm not seeing!

Additionally, I added unit tests for this behavior under the old test
framework since existing helpers are present for the archive backend
setup. This way we minimize the diff and maximize probability that my
test is behaving correctly. I will gladly port this entire archive test
section to pytest in a future PR if this is a dealbreaker.

nightly-406db7cc-ls272

21 Apr 10:16
66bf953

Choose a tag to compare

Pre-release

CI Report:

https://ci-tests.linuxserver.io/linuxserver/beets/nightly-406db7cc-ls272/index.html

LinuxServer Changes:

No changes

Remote Changes:

Filter out zero penalty for cli display (#6556)

Fixes #6555

generic_penalty_keys at beets/autotag/distance.py:145 returns all
keys in _penalties without filtering for non-zero values.

I think this is a regression.

nightly-e3e8793d-ls271

19 Apr 16:31
6adee69

Choose a tag to compare

Pre-release

CI Report:

https://ci-tests.linuxserver.io/linuxserver/beets/nightly-e3e8793d-ls271/index.html

LinuxServer Changes:

No changes

Remote Changes:

Centralise field type defs (#6550)

Summary

This PR eliminates duplicated field-to-type mappings across Album and
Item model classes. Previously, each model class maintained its own
_fields: dict[str, types.Type] — a verbose, hand-written dictionary
that encoded both which fields exist and what type each one has.
This duplication made it easy for the two definitions to drift out of
sync.


Architecture Change

Before: Each model class owned a full inline _fields dict mapping
field names to type instances.

Album._fields = {"id": types.PRIMARY_ID, "artpath": types.NullPathType(), "added": types.DATE, ...}  # ~40 entries
Item._fields  = {"id": types.PRIMARY_ID, "path": types.PathType(), "album_id": types.FOREIGN_ID, ...}  # ~80 entries

After: Field names are declared as a set[str], and the type
mapping is derived lazily from a single centralised TYPE_BY_FIELD
registry in beets.dbcore.fields.

Album._field_names = {"id", "artpath", "added", ...}        # plain set of names
Item._field_names  = Album._field_names - {"artpath"} | {...} # extends Album's set

LibModel._fields (cached_classproperty) = {f: TYPE_BY_FIELD[f] for f in cls._field_names}

This creates a clean separation of concerns:

  • What fields belong to a model → declared in the model
    (_field_names)
  • What type a field has → declared once in
    dbcore.fields.TYPE_BY_FIELD

Key Impacts

Area Before After
Type definitions Inline per model Single registry in
dbcore.fields
Item._fields Fully independent dict Derived from
Album._field_names
Album.item_keys Hard-coded list of 40+ strings `_field_names -
{"artpath", "id"}`
_media_fields / _media_tag_fields Intersected with
_fields.keys() Intersected with _field_names directly
Coverage measurement --cov=beets --cov=beetsplug --cov=.
(fixes missing beetsplug/musicbrainz.py)

Risk Areas to Review

  • TYPE_BY_FIELD completeness: all field names in _field_names
    must have a corresponding entry in the registry — a missing key will
    raise at class definition time via cached_classproperty.
  • Album.item_keys semantics: changed from list to set and is
    now derived automatically. Any code that relied on ordering or specific
    list membership should be checked.
  • Item._field_names inheritance expression: Album._field_names - {"artpath"} | {...} — operator precedence here is `(Album._field_names
  • {"artpath"}) | {...}`, which is the intended behaviour, but worth a
    close read.

nightly-be33782f-ls271

19 Apr 12:37
6adee69

Choose a tag to compare

Pre-release

CI Report:

https://ci-tests.linuxserver.io/linuxserver/beets/nightly-be33782f-ls271/index.html

LinuxServer Changes:

No changes

Remote Changes:

Use aliases for artist credit (#6552)

The *_credit fields for album and track are also aliased.

Follow up of beetbox/beets#6231. I wanted to
mbsync my library but then I realized that *_credit fields were
reverted to non-alias if already aliased.

I don't how I got those aliased in the first place as the code is not
using them since years. Also, I had 2 albums added the same day and one
was with aliased credit and the other one not.

Discussed here:
beetbox/beets#6358 (comment)

nightly-82384dab-ls271

19 Apr 13:58
6adee69

Choose a tag to compare

Pre-release

CI Report:

https://ci-tests.linuxserver.io/linuxserver/beets/nightly-82384dab-ls271/index.html

LinuxServer Changes:

No changes

Remote Changes:

Parallelize candidate lookup for metadata plugins. (#6546)

This pull request introduces concurrent execution of metadata source
plugin searches and lookups, which significantly improves performance
when multiple plugins are enabled. Instead of running each plugin
sequentially, the code now uses threads to perform plugin lookups in
parallel, reducing overall wait time for I/O-bound operations.

For me this improved lookup times by up to 5s each! The improvement
should roughly scale linear with the number of enabled metadata plugins.

nightly-2f3efaea-ls271

18 Apr 19:40
6adee69

Choose a tag to compare

Pre-release

CI Report:

https://ci-tests.linuxserver.io/linuxserver/beets/nightly-2f3efaea-ls271/index.html

LinuxServer Changes:

No changes

Remote Changes:

Refactor musicbrainz to reduce cognitive complexity (#6530)

Summary

A broad internal refactor of beetsplug/musicbrainz.py and its test
suite. No user-facing behaviour changes are intended. The goal is to
make parsing logic easier to understand, test, and maintain.

This PR halves the cognitive complexity in musicbrainz plugin:

Before

$ complexipy beetsplug/musicbrainz.py
🧠 Total Cognitive Complexity: 181
1 file analyzed in 0.0116 seconds

After

$ complexipy beetsplug/musicbrainz.py
🧠 Total Cognitive Complexity: 93
1 file analyzed in 0.0097 seconds

Production Code Changes

Parsing Decomposition

The monolithic album_info method is broken into focused
@staticmethod helpers, each returning a typed TypedDict:

Method Responsibility
_parse_artist_credits Replaces _flatten_artist_credit /
_multi_artist_credit
_parse_release_group albumtype, original_year, etc.
_parse_label_infos label, catalognum
_parse_genres Genre list
_parse_external_ids Third-party IDs from URL relations
_parse_work_relations Composer / lyricist credits
_parse_artist_relations Arranger / remixer credits
get_tracks_from_medium Per-medium track iteration

Each helper returns a typed TypedDict, making the data contract
explicit and the code easier to compose with **kwargs unpacking into
AlbumInfo / TrackInfo.

Other Simplifications

  • _set_date_str (mutable, side-effecting) is replaced by _get_date,
    a pure function returning (year, month, day).
  • _preferred_release_event and _preferred_alias are simplified.
  • Three @cached_property entries (ignored_media,
    ignore_data_tracks, ignore_video_tracks) are moved up to sit with
    other class-level properties.

Test Infrastructure Changes

Factory Layer (test/plugins/factories/musicbrainz.py)

factory-boy / pytest-factoryboy are added as test dependencies. A
new factory module introduces composable, deterministic factories:

  • AliasFactory, ArtistFactory, ArtistCreditFactory
  • RecordingFactory, TrackFactory, MediumFactory
  • ReleaseGroupFactory, ReleaseFactory

Factories use a shared _IdFactory base class that generates stable,
predictable UUIDs (00000000-0000-0000-0000-000000001001, etc.) based
on an id_base + index pair. This makes assertions on IDs readable
and deterministic without hard-coded magic strings.

Before:

recording = {
    "title": "foo",
    "id": "bar",
    "length": 42,
    "artist_credit": [{"artist": {"name": "some-artist", "id": "some-id", ...}, ...}],
    ...
}

After:

recording = recording_factory(title="foo", length=42)

Test Consolidation

Many small single-field assertion tests that each constructed an
identical release are collapsed into a single test_parse_release
snapshot assertion, covering all AlbumInfo fields at once. This
reduces redundant fixture setup and makes it immediately obvious what
the full output of album_info looks like for a default release.

The hand-rolled _make_release / _make_recording helpers are removed
entirely, replaced by composable factory calls using factory-boy's __
double-underscore traversal syntax:

# Before
release = self._make_release(date="1987-03-31")

# After
release = release_factory(release_group__first_release_date="1987-03-31")