New features
- duplicate_action: Add an
upgradeoption that replaces individual duplicate tracks only if the new copy has a higher bitrate, adds any genuinely new tracks, and keeps the album together rather than splitting it. The option is available both through configuration and from the interactive duplicate prompt. 🐛 (#4471) - list command Add
-l / --limit LIMITflag to the list command command to limit query results. 🐛 (#5076) - PlexUpdate Plugin: Add
beet plexupdate --auth, an interactive plex.tv login following Plex' traditional PIN authentication flow: the access token for the local server is stored in a token file. The manualtokenconfiguration option is now deprecated. - The interactive import prompt now offers a "Rescan directory" choice for album tasks. It re-reads the album's directory from disk and re-runs the match, so files can be cleaned up (duplicates, junk) while the import is paused at the prompt, without restarting the whole import command run.
Bug fixes
- AURA Plugin: Prevent multi-valued field filters from crashing with an
sqlite3.InterfaceError. - AURA Plugin: When sorting by
field, do not exclude resources that have no value forfield. - BPD Plugin: Fix
searchcommand whenanyfield is used. - Convert Plugin: Fixed convert plugin not taking into account the new format when determining the target path. 🐛 (#1360)
- Deezer Plugin: Detect compilations that Deezer credits to a single "main" artist instead of "Various Artists", so they are tagged as such rather than getting every track artist in the album artist field. 🐛 (#4057)
- Deezer Plugin: Singleton searches now use plain free text rather than
<title> artist:"<artist>". Deezer discards unquoted free text as soon as a query contains anyfield:"value"filter, so the old query was evaluated asartist:"<artist>"alone -- every track by the artist, in Deezer's own relevance order and truncated tosearch_limit. For artists with more releases than that window, the track being imported was never among the candidates offered. - Deezer Plugin: Track conversion no longer assumes the API sends both
contributorsandartist. The fallback toartistwas evaluated even whencontributorswas present, so a track payload withoutartistraisedKeyError. Albums were already guarded; this fixes the remaining call site. 🐛 (#4339) - Discogs Plugin: Retry a search once when Discogs returns an invalid JSON response instead of immediately discarding all Discogs candidates.
- Edit Plugin: Item-only fields rejected from the album header remain available in per-track documents during interactive import when they are also configured in
itemfields. 🐛 (#6953) - import command: Restore the ability to import from tar and 7z archives. Both failed with an
'... object has no attribute 'infolist'error becausetarfile.TarFilelost itsZipFileCompatinterface in Python 3 andpy7zr.SevenZipFileexposeslist()rather thaninfolist(). 🐛 (#5664) - IPFS Plugin: Fix
beet ipfs --playoption to invoke the Play plugin through its command interface. - Limit Query Plugin Deprecate the Limit Query Plugin plugin in favor of the new
-l/--limitflag for the list command command. - ListenBrainz Plugin and LastImport Plugin: Play counts are now matched more accurately. An exact MusicBrainz recording ID match is preferred when one exists, and titles are matched exactly (confirmed by the artist or album) before falling back to the previous substring-based matching. Previously all queries were combined, so a listen for "Song" also updated "Song (inst.)" or any other item whose title only contained the listened title.
- Lyrics Plugin:
beet lyricsno longer crashes with anAttributeErroron tracks that have no stored lyrics whenforceis enabled; a missing lyrics body is now treated as empty text. 🐛 (#6860) - Lyrics Plugin: LRCLib entries that carry no lyrics text at all, with both
plainLyricsandsyncedLyricsnull whileinstrumentalisFalse, are no longer considered matches. Previously such an entry was accepted and its null text propagated, raisingAttributeError: 'NoneType' object has no attribute 'splitlines'. During an import this aborted the whole run rather than a single track. A nullplainLyricsnow also falls back to the synced lyrics instead of discarding them. - modify command: Fix applying changes when choosing objects in interactive select mode. 🐛 (#4880)
- move command: Fix moving albums in interactive select/timid mode. 🐛 (#2802)
- Scrub Plugin: The scrub plugin now respects the
--nowrite(-W) flag during import. Previously,beet import -Wwith the scrub plugin enabled would still remove tags from imported files; the plugin now skips scrubbing whenshould_write()returnsFalse. 🐛 (#6958) - Tidal Plugin: Pass the converted track duration as
lengthso it contributes to the autotagging track-length distance instead of being silently stored as adurationflexible attribute. - Tidal Plugin: Restore catalog searches after TIDAL moved search queries from the request path to the required
filter[query]parameter. 🐛 (#6989) - Tidal Plugin: The
labelfield no longer stores Tidal's raw copyright/rights-statement text verbatim. It's now normalized to a concise label name, stripping copyright markers, years, and corporate, licensing, and territorial boilerplate. Affects both album and track metadata. 🐛 (#6796) - update command no longer crashes when a plugin-added media field is stored as a flexible attribute. 🐛 (#5580)
- A date range query written back to front (for example
added:2024..2020) no longer crashes with an uncaughtValueError. The endpoints are now swapped, so such a range means the same asadded:2020..2024. - Add
editorconfig option to allow users to permanently set their preferred editor, overriding$VISUALand$EDITORenvironment variables. 🐛 (#6641) - Autotagging distance calculations no longer treat ordinary words containing "ft" (such as "draft", "left", "gift", "craft") as a "featuring artist" suffix, which was silently making genuinely different titles/artists score as near-identical matches.
- Deduplicating a file whose name already ends in a counter of two or more digits no longer restarts the numbering:
track.10.mp3now yieldstrack.11.mp3instead oftrack.1.mp3. The counter was matched with\.(\d)+$, which captures only the final digit. - Flexible attributes whose names contain uppercase characters (for example
beet import --set Tag_With_Uppercase=true) can now be found by queries. Field names are lowercased when a query is parsed, so such attributes could never be matched; flexible attribute lookups now fall back to a case-insensitive key match. 🐛 (#4565) - Plugins built on
SearchApiMetadataSourcePlugin(MusicBrainz Plugin, Spotify Plugin, Deezer Plugin and Discogs Plugin) no longer send a search request when both the query text and the filters are empty. 🐛 (#6862)
Other changes
- BPD Plugin: Replace the bundled Bluelet scheduler with Python's standard
asyncioevent loop. - Docs: fix broken link to
test/dbcore/test_query.pyinCONTRIBUTING.rst; add crawler-blocking and unreachable sites tolinkcheck_ignoreindocs/conf.py.