Skip to content

Releases: linuxserver/docker-beets

nightly-c1dc7332-ls316

Pre-release

Choose a tag to compare

@LinuxServer-CI LinuxServer-CI released this 21 Jul 11:14
7e96fae

CI Report:

https://ci-tests.linuxserver.io/linuxserver/beets/nightly-c1dc7332-ls316/index.html

LinuxServer Changes:

Full Changelog: nightly-17d67980-ls315...nightly-c1dc7332-ls316

Remote Changes:

Fix logging in tests (#6853)

Summary

  • Add a CLI logging handler that resolves sys.stderr when each log
    record is emitted.
  • Keep CLI logs attached to the current stderr stream when callers
    temporarily replace it for capture or redirection.
  • Avoid retaining pytest's closed per-test capture stream across
    repeated CLI invocations.

Problem

The issue is that the first CLI command run under pytest can install a
StreamHandler bound to pytest's temporary stderr capture stream.
Pytest later closes that stream, but the global beets logger keeps the
handler around for the next CLI invocation.

This was introduced by #6755, which removed logging setup as an import
side effect and moved CLI logging bootstrap into command startup.

Before #6755, the handler was created when beets.ui was imported. In
tests, that usually happened before pytest swapped in its per-test
stderr capture stream. After #6755, the handler is created when
_raw_main() runs, which means it can be created while pytest capture
is active.

The important bit is that logging.StreamHandler() stores the stream it
sees at creation time. So it ends up holding on to pytest's temporary
stderr stream from the first command. Once pytest closes that stream,
later debug/info logs try to write to a closed file and Python's logging
module prints the huge --- Logging error --- blocks.

This keeps the useful part of #6755 - no logging setup as an import side
effect - but avoids retaining whichever stderr stream happened to be
active during the first CLI run.

Before

I include the output I'm seeing where I have two failing tests in
test_convert.py. See the amount of noise this generates - this also
hides the actual test failures under repeated logging-internal
tracebacks and captured debug logs.

very verbose output
Poe => uv run pytest -p no:cov test/plugins/test_convert.py --lf --maxfail=2
=============================================================================== test session starts ================================================================================
platform linux -- Python 3.10.18, pytest-9.0.3, pluggy-1.6.0
cachedir: /tmp/pytest_cache
rootdir: /home/sarunas/repo/beets
configfile: setup.cfg
plugins: factoryboy-2.8.1, requests-mock-1.12.1, Faker-40.15.0, flask-1.3.0, anyio-4.13.0, xdist-3.8.0
collected 36 items / 15 deselected / 21 selected
run-last-failure: rerun previous 21 failures

test/plugins/test_convert.py FF

===================================================================================== FAILURES =====================================================================================
___________________________________________________________________________ TestConvertCli.test_convert ____________________________________________________________________________

self = <test.plugins.test_convert.TestConvertCli object at 0x7efd27567df0>

    def test_convert(self):
        self.io.addinput("y")
        self.run_convert()
>       assert self.file_endswith(self.converted_mp3, "mp3")

test/plugins/test_convert.py:127: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <test.plugins.test_convert.TestConvertCli object at 0x7efd27567df0>, path = PosixPath('/tmp/tmpxyo_pvcw/convert_dest/converted.mp3'), tag = 'mp3'

    def file_endswith(self, path: Path, tag: str):
        """Check the path is a file and if its content ends with `tag`."""
>       assert path.exists()
E       AssertionError: assert False
E        +  where False = exists()
E        +    where exists = PosixPath('/tmp/tmpxyo_pvcw/convert_dest/converted.mp3').exists

test/plugins/test_convert.py:48: AssertionError
-------------------------------------------------------------------------------- Captured log setup --------------------------------------------------------------------------------
DEBUG    beets:plugins.py:376 plugin paths: []
DEBUG    beets:plugins.py:458 Loading plugins: convert
DEBUG    beets:plugins.py:622 Sending event: pluginload
DEBUG    beets:plugins.py:622 Sending event: database_change
DEBUG    beets:queries.py:50 Parsed query: AndQuery([NoneQuery('album_id', True)])
DEBUG    beets:queries.py:51 Parsed sort: NullSort()
DEBUG    beets:plugins.py:622 Sending event: item_copied
DEBUG    beets:plugins.py:622 Sending event: database_change
DEBUG    beets:plugins.py:622 Sending event: database_change
DEBUG    beets:plugins.py:622 Sending event: database_change
DEBUG    beets:plugins.py:622 Sending event: database_change
------------------------------------------------------------------------------- Captured stdout call -------------------------------------------------------------------------------
the artist - älbum - tïtle 0
Convert? (Y/n) 
------------------------------------------------------------------------------- Captured stderr call -------------------------------------------------------------------------------
data directory: /tmp/tmpxyo_pvcw
Sending event: library_opened
Parsed query: AndQuery([PathQuery('path', b'Non-Album/the artist/t\xc3\xaftle 0.ogg', fast=True, case_sensitive=True)])
Parsed sort: NullSort()
convert: Encoding /tmp/tmpxyo_pvcw/libdir/Non-Album/the artist/tïtle 0.ogg
convert: Finished encoding /tmp/tmpxyo_pvcw/libdir/Non-Album/the artist/tïtle 0.ogg
Sending event: write
Sending event: after_write
Sending event: after_convert
Sending event: cli_exit
data directory: /tmp/tmpxyo_pvcw
Sending event: library_opened
Parsed query: AndQuery([PathQuery('path', b'Non-Album/the artist/t\xc3\xaftle 0.ogg', fast=True, case_sensitive=True)])
Parsed sort: NullSort()
convert: Encoding /tmp/tmpxyo_pvcw/libdir/Non-Album/the artist/tïtle 0.ogg
convert: Finished encoding /tmp/tmpxyo_pvcw/libdir/Non-Album/the artist/tïtle 0.ogg
Sending event: write
Sending event: after_write
Sending event: after_convert
Sending event: cli_exit
-------------------------------------------------------------------------------- Captured log call ---------------------------------------------------------------------------------
DEBUG    beets:__init__.py:953 data directory: /tmp/tmpxyo_pvcw
DEBUG    beets:plugins.py:622 Sending event: library_opened
DEBUG    beets:queries.py:50 Parsed query: AndQuery([PathQuery('path', b'Non-Album/the artist/t\xc3\xaftle 0.ogg', fast=True, case_sensitive=True)])
DEBUG    beets:queries.py:51 Parsed sort: NullSort()
INFO     beets.convert:convert.py:318 Encoding /tmp/tmpxyo_pvcw/libdir/Non-Album/the artist/tïtle 0.ogg
INFO     beets.convert:convert.py:354 Finished encoding /tmp/tmpxyo_pvcw/libdir/Non-Album/the artist/tïtle 0.ogg
DEBUG    beets:plugins.py:622 Sending event: write
DEBUG    beets:plugins.py:622 Sending event: after_write
DEBUG    beets:plugins.py:622 Sending event: after_convert
DEBUG    beets:plugins.py:622 Sending event: cli_exit
________________________________________________________________ TestConvertCli.test_convert_with_auto_confirmation ________________________________________________________________

self = <test.plugins.test_convert.TestConvertCli object at 0x7efd27565f30>

    def test_convert_with_auto_confirmation(self):
        self.run_convert("--yes")
>       assert self.file_endswith(self.converted_mp3, "mp3")

test/plugins/test_convert.py:131: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <test.plugins.test_convert.TestConvertCli object at 0x7efd27565f30>, path = PosixPath('/tmp/tmpvslyu88g/convert_dest/converted.mp3'), tag = 'mp3'

    def file_endswith(self, path: Path, tag: str):
        """Check the path is a file and if its content ends with `tag`."""
>       assert path.exists()
E       AssertionError: assert False
E        +  where False = exists()
E        +    where exists = PosixPath('/tmp/tmpvslyu88g/convert_dest/converted.mp3').exists

test/plugins/test_convert.py:48: AssertionError
------------------------------------------------------------------------------ Captured stderr setup -------------------------------------------------------------------------------
--- Logging error ---
Traceback (most recent call last):
  File "/home/sarunas/.local/share/uv/python/cpython-3.10.18-linux-x86_64-gnu/lib/python3.10/logging/__init__.py", line 1103, in emit
    stream.write(msg + self.terminator)
  File "/media/poetry/virtualenvs/beets-yAypcYUQ-py3.10/lib/python3.10/site-packages/_pytest/capture.py", line 218, in write
    super().write(s)
ValueError: I/O operation on closed file.
Call stack:
  File "/media/poetry/virtualenvs/beets-yAypcYUQ-py3.10/bin/pytest", line 10, in <module>
    sys.exit(console_main())
  File "/media/poetry/virtualenvs/beets-yAypcYUQ-py3.10/lib/python3.10/site-packages/_pytest/config/__init__.py", line 223, in console_main

  ...
  Another 1200 lines of this stuff
  ...

  File "/media/poetry/virtualenvs/beets-yAypcYUQ-py3.10/lib/python3.10/site-packages/_pytest/fixtures.py", line 1202, in pytest_fixture_setup
    result = call_fixture_func(fixturefunc, request, kwargs)
  File "/media/poetry/virtualenvs/beets-yAypcYUQ-py3.10/lib/python3.10/site-packages/_pytest/fixtures.py", line 908, in call_fixture_func
    fixture_result = next(generator)
  File "/home/sarunas/repo/beets/beets/test/helper.py", line 209, in setup
    self.setup_beets()
  File "/home/sarunas/repo/beets/test/plugins/test_convert.py", line 107, in setup_beets
    self.album = self.add_album_fixture(ext="ogg")
  File "/home/sarunas/repo/beets/beets/test/helper.py", line 376, in add_album_fixture
    return self.lib.add...
Read more

nightly-8778d0ef-ls317

Pre-release

Choose a tag to compare

@LinuxServer-CI LinuxServer-CI released this 21 Jul 13:30
012eae5

CI Report:

https://ci-tests.linuxserver.io/linuxserver/beets/nightly-8778d0ef-ls317/index.html

LinuxServer Changes:

Full Changelog: nightly-c1dc7332-ls316...nightly-8778d0ef-ls317

Remote Changes:

use pathlib in tests: use only Path-based attributes on PathsMixin/TestHelper (#6847)

Part of #6807

  • This change standardizes the test helper path API around
    pathlib.Path. Main shared helpers now use temp_path, lib_path, and
    import_path instead of older mixed names like temp_dir,
    temp_dir_path, libdir, and import_dir.

  • Architecturally, path handling is pushed toward one clear model: use
    Path objects inside helpers and tests, and only encode to bytes at
    boundaries that still require it, like importer session setup.

  • beets.dbcore.types also now accepts Path values for item.path,
    so code can pass Path objects directly without manual conversion.

  • High-level impact: the test infrastructure becomes more consistent,
    path-related code gets easier to read, and there is less
    bytes/string/path conversion scattered across the suite. This is mostly
    a cleanup and API consistency change, not a behavior change.

2.12.0-ls341

Choose a tag to compare

@LinuxServer-CI LinuxServer-CI released this 17 Jul 19:55
639c0ab

CI Report:

https://ci-tests.linuxserver.io/linuxserver/beets/2.12.0-ls341/index.html

LinuxServer Changes:

Full Changelog: 2.12.0-ls340...2.12.0-ls341

Remote Changes:

Updating PIP version of beets to 2.12.0

nightly-db9e0441-ls315

Pre-release

Choose a tag to compare

@LinuxServer-CI LinuxServer-CI released this 15 Jul 20:13
6895718

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, 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.

nightly-86f0f0a7-ls315

Pre-release

Choose a tag to compare

@LinuxServer-CI LinuxServer-CI released this 16 Jul 17:09
6895718

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

Pre-release

Choose a tag to compare

@LinuxServer-CI LinuxServer-CI released this 17 Jul 09:44
6895718

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

Pre-release

Choose a tag to compare

@LinuxServer-CI LinuxServer-CI released this 16 Jul 13:16
6895718

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.py and beets/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 through Library._fetch().

  • The PR also cleans up how template uniqueness helpers work by giving
    _memotable and related keys a stable shape, and by tightening the
    logic around album-aware formatting and disambiguation.

  • A small architectural rename in dbcore changes Database._fetch()
    to Database.get_results(), which makes the lower-level database API
    clearer and avoids confusion with Library._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

Pre-release

Choose a tag to compare

@LinuxServer-CI LinuxServer-CI released this 16 Jul 11:39
6895718

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.py now models Discogs tracklist entries as
    distinct shapes: AudioTrack, IndexTrack, and HeadingTrack. This
    shifts the plugin from "guessing by position" to handling each entry
    by its declared type_.

  • beetsplug/discogs/__init__.py refactors _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.py now advances track state only for real
    audio entries (type_ == "track"), so structural entries no longer
    affect track counting through indirect position checks.

  • 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 whose sub_tracks are 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_tracks is 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 as 22a and 22b representing one CD track.
  • PR #2222 added both flat
    and
    nested subtrack support. Its author explicitly noted that type_ might
    identify index tracks robustly, but avoided the larger refactor.
  • Issue #2318
    demonstrated that
    nested sub_tracks can 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, with Ba,
    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 | HeadingTrack

With 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: str

Then test using _track(), _index(), and _heading() factories.

The eventual normalization can dispatch structurally:

  1. track without subindex: pass through.
  2. Flat subindexed track group: apply the flat-fragment policy.
  3. index: normalize its children as logical or physical.
  4. heading: preserve as structural metadata.
  5. 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

Pre-release

Choose a tag to compare

@LinuxServer-CI LinuxServer-CI released this 16 Jul 07:22
6895718

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

Pre-release

Choose a tag to compare

@LinuxServer-CI LinuxServer-CI released this 15 Jul 21:56
6895718

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.