Implements native multi-value genre support following the same pattern
as multi-value artists. Adds a 'genres' field that stores genres as a
list and writes them as multiple individual genre tags to files.
Features:
- New 'genres' field (MULTI_VALUE_DSV) for albums and tracks
- Bidirectional sync between 'genre' (string) and 'genres' (list)
- Config option 'multi_value_genres' (default: yes) to enable/disable
- Config option 'genre_separator' (default: ', ') for joining genres
into the single 'genre' field - matches lastgenre's default separator
- Updated MusicBrainz, Beatport, and LastGenre plugins to populate
'genres' field
- LastGenre plugin now uses global genre_separator when
multi_value_genres is enabled for consistency
- Comprehensive test coverage (10 tests for sync logic)
- Full documentation in changelog and reference/config.rst
Backward Compatibility:
- When multi_value_genres=yes: 'genre' field maintained as joined
string for backward compatibility, 'genres' is the authoritative list
- When multi_value_genres=no: Preserves old behavior (only first genre)
- Default separator matches lastgenre's default for seamless migration
Migration:
- Most users (using lastgenre's default) need no configuration changes
- Users with custom lastgenre separator should set genre_separator to
match their existing data
- Users can opt-out entirely with multi_value_genres: no
Code Review Feedback Addressed:
- Extracted genre separator into configurable option (not hardcoded)
- Fixed Beatport plugin to always populate genres field consistently
- Added tests for None values and edge cases
- Handle None values gracefully in sync logic
- Added migration documentation for smooth user experience
- Made separator user-configurable instead of constant
- Changed default to ', ' for seamless migration (matches lastgenre)
## Description
Fixes#5560. Also a couple other incidental changes / improvements:
* Add `EventType` that holds the actual string literals used for event
sending. With type checking, this can prevent subtle bugs resulting from
misspelled event names.
* Fix `HiddenFileTest` by using `bytestring_path()`
## To Do
- [x] ~Documentation.~
- [x] Changelog.
- [x] Tests.
---------
Co-authored-by: J0J0 Todos <jojo@peek-a-boo.at>
Co-authored-by: J0J0 Todos <2733783+JOJ0@users.noreply.github.com>
## Description
Fixes#5802.
Today, tests fail on most Windows machines because we hard-code `D:` as
the root drive, but most machines use `C:`. This change uses the same
normalization function in the test assertion to ensure the drives match.
## To Do
- [ ] ~~Documentation.~~
- [x] Changelog.
- [x] Tests. (this is a tests change)
## What changed?
* Updated tests to generate the drive name via normalization, instead of
hard-coding `D:`.
* Updated the `Item::destination()` method to document the
`relative_to_libdir` param.
## How tested?
* [x] Tests pass locally.
Background
The `_legalize_stage` function was causing issues with Mypy due to
inconsistent type usage between the `path` and `extension` parameters.
This inconsistency stemmed from the `fragment` parameter influencing the
types of these variables.
Key issues
1. `path` was defined as `str`, while `extension` was `bytes`.
2. Depending on `fragment`, `extension` could be either `str` or `bytes`.
3. `path` was sometimes converted to `bytes` within `_legalize_stage`.
Item.destination` method
- The `fragment` parameter determined the output format:
- `False`: Returned absolute path as bytes (default)
- `True`: Returned path relative to library directory as str
Thus
- Rename `fragment` parameter to `relative_to_libdir` for clarity
- Ensure `Item.destination` returns `bytes` in all cases
- Code expecting strings now converts the output to `str`
- Use only `str` type in `_legalize_stage` and `_legalize_path`
functions
- These functions are no longer dependent on `relative_to_libdir`
External Python packages interfacing beets may want to use an in-memory
beets library instance for testing beets-related code.
The `TestHelper` class is very helpful for this purpose.
Previously `TestHelper` was located in the `test/` directory.
Now it is part of `beets` itself (`beets.test.helper.TestHelper`) and
can be easily imported.
Adds the following fields with id3v2.4 multi-valued tag support to autotag:
- artists, artists_sort, artists_credit
- albumartists, albumartists_sort, albumartists_credit
- mb_artistids, mb_albumartistids
MusicBrainz support to populate + write the above multi-valued tags by default. Can be toggled to use id3v2.3 or id3v2.4 tags via the existing beets configuration option `id3v23`.
Big thanks to @JOJ0, @OxygenCobalt, @arsaboo for testing + @sampsyo for the initial code review .
these are mostly in the tests, which didn't cause issues since the
affected directories usually have nice ASCII paths. For consistency, it
is nicer to always invoke syspath. That also avoids deprecation warnings
for the bytestring interfaces on Python <= 3.5. The bytestring
interfaces were undeprecated with PEP 529 in Python 3.6, such that we
didn't observe any actual failures.
Unidecode 1.3.5 (a yanked PyPI version) changed the behavior of
Unidecode for some specific characters:
> Remove trailing space in replacements for vulgar fractions.
As luck would have it, our tests used the 1/2 character specifically to
test the behavior when these characters decoded to contain slashes. We
now pin a sufficiently recent version of Unidecode and adapt the tests
to match the new behavior.
Unit test may fails when path to temprorary library contains `.`; to
garantue that bug wasn't here, it forces to use one more `.` inside path.
Fixes: https://github.com/beetbox/beets/issues/4151
This allows for the use of differing replacements for destinations other than
the library, which is useful for beets-alternatives in the case where
filesystem requirements differ between the two paths.
Signed-off-by: Christopher Larson <kergoth@gmail.com>