Fixes#5207.
This PR replaces the `munkres` library with `lap` (Linear Assignment
Problem solver) for computing optimal track assignments during the
auto-tagging process. The main changes are:
- Remove `munkres` dependency and add `lap` and `numpy` dependencies
- Refactor the track assignment code to use `lap.lapjv()` instead of
`Munkres().compute()`
- Simplify cost matrix construction using list comprehension
- Move config value reading outside of `track_distance` function to
improve performance
The motivation for this change comes from benchmark comparisons showing
that LAPJV (implemented in the `lap` library) significantly outperforms
the Munkres/Hungarian algorithm for the linear assignment problem. See
detailed benchmarks at: https://github.com/berhane/LAP-solvers
The change should provide better performance for track matching,
especially with larger albums, while maintaining the same assignment
results.
## Testing Notes
- All existing tests pass without modification
- Track assignments produce identical results
- No behavioral changes in auto-tagging
Fixes#4622.
I don't think this changes other behavior of the function, but it fixes
the problem with the moved files being created with the wrong
permissions for me.
Thanks to @wisp3rwind's suggestion this PR adds types to the
relationship between `Model`, `Database` and `Library`.
Then I worked through the rest of the issues found in the edited files.
Most of this involved providing type parameters for generic types (or
defining defaults, rather 😉).
There `queryparse` module had a somewhat significant issue where the
sorting construction logic only expected to receive `FieldSort`
subclasses, while `SmartArtistSort` was not one. Thus `SmartArtistSort`
has now been forced to behave and is a `FieldSort` subclass. It's also
been moved to `query.py` module which is where the rest of sorts are
defined.
This PR fixes an issue where the `beet write` command repeatedly shows
differences for certain fields (`mb_artistid`, `mb_albumartistid`,
`albumtype`) even after writing the tags.
This happens because these fields are actually stored as lists in the
media files (`mb_artistids`, `mb_albumartistids`, `albumtypes`), but
beets maintains both single and list versions in its database.
This PR addresses this issue in a non-invasive way: the fix ensures
consistency between single fields and their list counterparts by:
1. When setting a single field value, making it the first element of the
corresponding list
2. When setting a list, using its first element as the single field
value
This resolves long-standing issues #5265, #5371, and #4715 where users
experienced persistent "differences" in these fields despite writing
tags multiple times.
Changes:
- Added `ensure_consistent_list_fields()` function to synchronize field
pairs
- Applied this function during metadata application
- Added tests for all field combinations
Fixes#5265, #5371, #4715
**Note**: your music needs to be reimported with `beet import -LI` or
synchronised with `beet mbsync` in order to fix these fields!
- Drop support for EOL Python 3.8 making Python 3.9 the minimum
supported version
- Take advantage of Python 3.9+ type hint syntax by:
- Using `list[T]` instead of `List[T]` etc. from typing module
- Using `Type | None` syntax for unions instead of `Union[Type, None]`
- Moving collection type hints from `typing` to `collections.abc`
- Using `TYPE_CHECKING` guard for runtime import optimization
Note: in #5503 we found that we cannot support Python 3.12 unless we
upgrade our minimum support Python to 3.9.
Fixes#5526Fixes#5531Fixes#5539
### Package contents
See #5526 where a package maintainer fails running plugin tests. I found that before
introduction of Poetry `beets` never bundled plugin tests, therefore I now excluded them.
I also remembered that previously `MANIFEST.in` file was used to specify which files get
included in the package, so I mirrored the same configuration. This includes zsh
completion in `extra/_beet` which fixes#5531.
I removed `MANIFEST.in` file since it has no use anymore.
### Release workflow
The last release workflow run failed to pick up the commit with the version updates and
tagged an outdated commit (#5539). I simplified the workflow to create the tag at the same
time the version upgrade is committed.
The `synced` config flag was not working the way the docs described it
for the LRCLIB source. With `synced: yes`, if a track does not have
synced lyrics, but does have plain lyrics, the plugin would return no
lyrics. The docs imply that, with the flag enabled, it would still use
plain lyrics if there are no synced lyrics.
LRCLIB API call that returns plain lyrics can be found
[here](https://lrclib.net/api/get?artist_name=Hania%20Rani&track_name=Moans&album_name=Ghosts&duration=274).
I've spent 2 hours troubleshooting why none of my music had genre tag.
It was because the single `genre`, without `s` doesn't seem to cover any
good ganre tags... at least it didn't on my opus files
looking at the code:
7ecd86101e/mediafile.py (L1669-L2167)
i don't honestly know why anyone created the single `ganre` field in the
first place
* Fixes#5513
* Fixes#5518
This PR adds the `test` folder and manuals to the package source tarball
and upgrades the dependencies to fix a few vulnerabilities.
I also added a fix for the changelog not making it to the release notes.
Fixes#5148.
When importing, the code that matches tracks does not consider the
medium number. This causes problems on Hybrid SACDs (and other releases)
where the artists, track numbers, titles, and lengths are the same on
both layers.
I added a distance penalty for mismatching medium numbers.
Before:
```
$ beet imp .
/Volumes/Music/ti/Red Garland/1958 - All Mornin' Long - 1 (6 items)
Match (95.4%):
The Red Garland Quintet - All Mornin' Long
≠ media, year
MusicBrainz, 2xHybrid SACD (CD layer), 2013, US, Analogue Productions, CPRJ 7130 SA, mono
https://musicbrainz.org/release/6a584522-58ea-470b-81fb-e60e5cd7b21e
* Artist: The Red Garland Quintet
* Album: All Mornin' Long
* Hybrid SACD (CD layer) 1
≠ (#2-1) All Mornin' Long (20:21) -> (#1-1) All Mornin' Long (20:21)
≠ (#2-2) They Can't Take That Away From Me (10:24) -> (#1-2) They Can't Take That Away From Me (10:27)
≠ (#2-3) Our Delight (6:23) -> (#1-3) Our Delight (6:23)
* Hybrid SACD (CD layer) 2
≠ (#1-1) All mornin' long (20:21) -> (#2-1) All Mornin' Long (20:21)
≠ (#1-2) They can't take that away from me (10:27) -> (#2-2) They Can't Take That Away From Me (10:25)
≠ (#1-3) Our delight (6:23) -> (#2-3) Our Delight (6:23)
➜ [A]pply, More candidates, Skip, Use as-is, as Tracks, Group albums,
Enter search, enter Id, aBort, eDit, edit Candidates?
```
Note that all tracks tagged with disc 1 get moved to disc 2 and vice
versa.
After:
```
$ beet-test imp .
/Volumes/Music/ti/Red Garland/1958 - All Mornin' Long - 1 (6 items)
Match (95.4%):
The Red Garland Quintet - All Mornin' Long
≠ media, year
MusicBrainz, 2xMedia, 2013, US, Analogue Productions, CPRJ 7130 SA, mono
https://musicbrainz.org/release/6a584522-58ea-470b-81fb-e60e5cd7b21e
* Artist: The Red Garland Quintet
* Album: All Mornin' Long
* Hybrid SACD (CD layer) 1
≠ (#1-1) All mornin' long (20:21) -> (#1-1) All Mornin' Long (20:21)
≠ (#1-2) They can't take that away from me (10:27) -> (#1-2) They Can't Take That Away From Me (10:27)
≠ (#1-3) Our delight (6:23) -> (#1-3) Our Delight (6:23)
* Hybrid SACD (SACD layer) 2
* (#2-1) All Mornin' Long (20:21)
* (#2-2) They Can't Take That Away From Me (10:24)
* (#2-3) Our Delight (6:23)
➜ [A]pply, More candidates, Skip, Use as-is, as Tracks, Group albums,
Enter search, enter Id, aBort, eDit, edit Candidates?
```
Yay!
Quick fix for #5467.
Checks if the path for python is under the windows store folder then
error and point the user to the beets
[documentation](https://beets.readthedocs.io/en/stable/guides/main.html).
Happy for feedback to improve, but thought it best to exit as early as
possible.