github beetbox/beets v2.14.0
Release v2.14.0

2 hours ago

New features

  • duplicate_action: Add an upgrade option 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 LIMIT flag 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 manual token configuration 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 for field.
  • BPD Plugin: Fix search command when any field 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 any field:"value" filter, so the old query was evaluated as artist:"<artist>" alone -- every track by the artist, in Deezer's own relevance order and truncated to search_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 contributors and artist. The fallback to artist was evaluated even when contributors was present, so a track payload without artist raised KeyError. 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 because tarfile.TarFile lost its ZipFileCompat interface in Python 3 and py7zr.SevenZipFile exposes list() rather than infolist(). 🐛 (#5664)
  • IPFS Plugin: Fix beet ipfs --play option to invoke the Play plugin through its command interface.
  • Limit Query Plugin Deprecate the Limit Query Plugin plugin in favor of the new -l / --limit flag 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 lyrics no longer crashes with an AttributeError on tracks that have no stored lyrics when force is 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 plainLyrics and syncedLyrics null while instrumental is False, are no longer considered matches. Previously such an entry was accepted and its null text propagated, raising AttributeError: 'NoneType' object has no attribute 'splitlines'. During an import this aborted the whole run rather than a single track. A null plainLyrics now 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 -W with the scrub plugin enabled would still remove tags from imported files; the plugin now skips scrubbing when should_write() returns False. 🐛 (#6958)
  • Tidal Plugin: Pass the converted track duration as length so it contributes to the autotagging track-length distance instead of being silently stored as a duration flexible 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 label field 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 uncaught ValueError. The endpoints are now swapped, so such a range means the same as added:2020..2024.
  • Add editor config option to allow users to permanently set their preferred editor, overriding $VISUAL and $EDITOR environment 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.mp3 now yields track.11.mp3 instead of track.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 asyncio event loop.
  • Docs: fix broken link to test/dbcore/test_query.py in CONTRIBUTING.rst; add crawler-blocking and unreachable sites to linkcheck_ignore in docs/conf.py.

Don't miss a new beets release

NewReleases is sending notifications on new releases.