Conversation
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## master #6367 +/- ##
==========================================
+ Coverage 69.18% 69.33% +0.15%
==========================================
Files 140 141 +1
Lines 18686 18782 +96
Branches 3053 3059 +6
==========================================
+ Hits 12927 13023 +96
Misses 5114 5114
Partials 645 645
🚀 New features to boost your workflow:
|
6f18c92 to
8c0820b
Compare
JOJ0
left a comment
There was a problem hiding this comment.
supported separators in docs correct?
I don't have much to say on the sql implementation except that it works when switching back and forth between this branch and current master and it's super-fast 🤩 👍
beetsplug/beatport.py
Outdated
| if "genres" in data: | ||
| self.genres = [str(x["name"]) for x in data["genres"]] | ||
| genre_list = [str(x["name"]) for x in data["genres"]] | ||
| self.genres = unique_list(genre_list) |
There was a problem hiding this comment.
nope, separators are correct, sorry my bad
https://github.com/beetbox/beets/pull/6367/changes#diff-ae385b0e7d1b2ea018b059b900c8d3c44554954bec89513a7ebe54d9c50cd503R90
let's finally get this done! ❤️
8c0820b to
fbf7085
Compare
fbf7085 to
a8101c1
Compare
There was a problem hiding this comment.
Pull request overview
This pull request implements native multi-value genre support in beets by replacing the singular genre field with a plural genres field across the codebase. This addresses long-standing issues with maintaining synchronization between singular and plural fields that caused repeated retagging operations.
Changes:
- Introduces database migration infrastructure to support automatic data migrations across schema changes
- Migrates existing
genrestring values togenresmulti-value field, splitting on common separators (, ; /) - Updates metadata source plugins (MusicBrainz, Beatport, Discogs, LastGenre) to populate
genresas a list rather thangenreas a string - Removes the
separatorconfiguration option from LastGenre plugin since genres are now stored as lists - Updates all tests to use
genresfield instead ofgenre
Reviewed changes
Copilot reviewed 38 out of 40 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| beets/dbcore/db.py | Adds Migration base class and infrastructure for tracking migration state, adds mutate_many for batch operations, adds db_tables cached property for schema introspection |
| beets/dbcore/types.py | Exports MULTI_VALUE_DELIMITER constant for reuse in migrations |
| beets/library/library.py | Registers MultiGenreFieldMigration to run on library initialization |
| beets/library/migrations.py | Implements genre→genres migration logic with automatic separator detection |
| beets/library/models.py | Changes genre field from STRING to genres field with MULTI_VALUE_DSV type in both Item and Album models |
| beets/autotag/hooks.py | Adds deprecation warning for genre parameter in AlbumInfo constructor with automatic conversion to genres list |
| beetsplug/musicbrainz.py | Updates to populate genres list instead of genre string |
| beetsplug/lastgenre/init.py | Refactors to work with genres as lists, removes separator config option, updates type hints |
| beetsplug/discogs/init.py | Updates to populate genres list and simplifies genre/style handling |
| beetsplug/beatport.py | Updates to populate genres list for both releases and tracks |
| beetsplug/bpd/init.py | Maps MPD "Genre" tag type to "genres" field |
| beetsplug/aura.py | Adds mapping for both "genre" and "genres" to "genres" field for compatibility |
| test/* | Updates all tests to use genres field instead of genre, adds migration tests |
| docs/* | Updates documentation to reflect genres field usage and documents migration behavior |
| setup.cfg | Adds follow_untyped_imports = true to mypy configuration |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
@JOJ0 given that Copilot also raised a question regarding separators, I've now documented the precedence and the fact that |
151468e to
241d335
Compare
fbe848a to
80d08bc
Compare
03114e1 to
54a46bd
Compare
80d08bc to
ce5214a
Compare
Co-authored-by: Šarūnas Nejus <snejus@protonmail.com>
Co-authored-by: Šarūnas Nejus <snejus@protonmail.com>
Co-authored-by: Šarūnas Nejus <snejus@protonmail.com>
- Add Library._make_table() override to automatically migrate genres when database schema is updated - Migration splits comma/semicolon/slash-separated genre strings into genres list - Writes changes to both database and media files with progress reporting - Remove lazy migration from correct_list_fields() - now handled at database level - Remove migration-specific tests (migration is now automatic, not lazy) - Update changelog to reflect automatic migration behavior Related PR review comment changes: - Replace _is_valid with _filter_valid method in lastgenre plugin - Use unique_list and remove genre field from Beatport plugin - Simplify LastGenre tests - remove separator logic - Document separator deprecation in lastgenre plugin - Add deprecation warning for genre parameter in Info.__init__()
Remove intermediate variable and assign directly to info.genres. Addresses PR review comment.
Migration now happens automatically when the database schema is updated (in Library._make_table()), so the manual 'beet migrate' command is no longer needed. Addresses PR review comment.
* Move genre-to-genres migration into a dedicated Migration class and wire it into Library._migrations for items and albums. * Add batched SQL updates via mutate_many and share the multi-value delimiter as a constant. * Cover migration behavior with new tests. I initially attempted to migrate using our model infrastructure / Model.store(), see the comparison below: Durations migrating my library of ~9000 items and ~2300 albums: 1. Using our Python logic: 11 minutes 2. Using SQL directly: 4 seconds That's why I've gone ahead with option 2.
09fac2f to
cc5c589
Compare
| def test_store_only_writes_dirty_fields(self): | ||
| original_genre = self.i.genre | ||
| self.i._values_fixed["genre"] = "beatboxing" # change w/o dirtying | ||
| original_genres = self.i.genres | ||
| self.i._values_fixed["genres"] = ["beatboxing"] # change w/o dirtying | ||
| self.i.store() | ||
| new_genre = ( | ||
| self.lib._connection() | ||
| .execute("select genre from items where title = ?", (self.i.title,)) | ||
| .fetchone()["genre"] | ||
| .execute( | ||
| "select genres from items where title = ?", (self.i.title,) | ||
| ) | ||
| .fetchone()["genres"] | ||
| ) | ||
| assert new_genre == original_genre | ||
| assert [new_genre] == original_genres |
There was a problem hiding this comment.
grug confused by test logic! new_genre is raw database string value (with delimiters, like "beatboxing"), but test compare [new_genre] (string wrapped in list) to original_genres (which already list of strings like ["the genre"]).
this comparison wrong! should either:
- Compare
new_genredirectly to delimited string likeMULTI_VALUE_DELIMITER.join(original_genres), or - Parse
new_genrefirst, then compare parsed list tooriginal_genres
currently test checking if ["beatboxing"] equals ["the genre"] which will pass (both different) but for wrong reason!
Add support for a multi-valued
genresfieldgenresinstead ofgenre:musicbrainz,beatport,discogs.separatorconfiguration fromlastgenre.%firsttemplate function to split by\␀by default.Context
We previously had multiple issues with maintaining both singular and plural fields:
fields must be carefully synchronised, otherwise we see these fields being repeatedly
retagged / rewritten using commands such as
beet write. See related issuesimported as-is could not be fixed. See Stop perpetually writing
mb_artistid,mb_albumartistidandalbumtypesfields #5540, for example.Therefore, this PR replaces a singular
genrefield by pluralgenresfor good:genre->genresimmediately on the firstbeetsinvocationgenrefield is removed andgenresis addedgenrecolumn in the database is left in place - these values will be ignoredby beets.
beets, their
genrevalues are still in place.Migration
This PR creates a new DB table
migrations(name TEXT, table TEXT)CTRL-C during the migration, the migration will continue on the next beets invocation,
see:
Implemented using SQL due to:
genresfield:genrefield is only accessible by querying thedatabase directly.
Supersedes: #6169