dewey/beets-importer

A simple interactive beets importer wrapper with file picker and identification of upgrade possibilities.

Go

2

19 commits

updated Oct 3, 2026

See the code

README

beets-importer

A command line tool for beets. It adds batch import, upgrade finding, library repair and a statistics report on top of beet. It only reads your beets database. Every change is made by calling beet.

beets imports albums one by one. If your inbox is large, you can not look at everything at once, pick what you want now and skip the rest. beets-importer adds a terminal UI in front of beet import, so you can select many albums before anything is imported.

upgrades -i matches your inbox against your library and shows upgrade candidates next to the library copy:

Upgrade picker

report writes a static HTML page about your library. Click the image to watch a short demo:

Library report

Road to all lossless

Why use it with beets

beets does the tagging. beets-importer adds the tools around it:

  1. Import in batches. beets goes through a folder one album at a time. Here you see all new folders, tick the ones you want, and they import as a queue. Folders you skipped do not come back.
  2. Find upgrades. upgrades -i compares your inbox with your library and shows which albums are better, for example FLAC instead of MP3. You see both copies side by side and import your picks.
  3. Track your library over time. report writes one HTML page with formats, bitrates, size and gaps. Every run saves a snapshot, so you can see your progress to an all-lossless library.
  4. Find problems. doctor checks for split albums, artists spelled in different ways, untracked or empty folders, missing years and artwork, and more.
  5. Fix them in a queue. maintenance turns what doctor finds into work. You pick the cases, and the fixes run one by one. Finished albums are marked, so you can stop and continue later.

It reads the beets database and calls beet for every change. Your plugins and config stay as they are.

Features

  • Bring in new music
    • import: pick albums from your source folder and import them
      • The picker lists new folders, newest first. Folders beets already processed are left out
      • --since, --limit and --reimport make the list shorter or longer
      • --from-file: import a list of paths (a text file, or the CSV from upgrades --csv)
    • upgrades: find albums in the source folder that are better than your library copy
      • Finds format upgrades (MP3 to FLAC) and higher bitrates
      • Filters: --library-format, --source-format, --lossy-to-lossless, --threshold, --min-bitrate-delta, --require-year-match
      • -i: pick candidates, compare source and library side by side, ignore wrong matches
      • --csv: write the candidates to a file for import --from-file
  • Fix your library
    • doctor: find problems (read-only)
      • Checks for empty and untracked folders, low bitrate, DRM files, missing artwork or year, split albums, different artist spellings and duplicate Unicode names
      • --json, --paths and --ids print results for scripts
    • maintenance: pick linters and fix what they find, one by one
    • import --retag: retag albums that are already in the library with beet import -L
      • --from-file: album IDs from a file (doctor --ids makes one)
      • --from-playlist: album IDs from a Navidrome playlist
  • Understand your library
    • report: one HTML page with formats, bitrates, years, gaps and your progress towards an all-lossless library
      • Saves a snapshot on every run, so the page can show progress over time
  • Setup
    • config init: write an example config file
    • config show: show the values in use

Requirements

  • beets, installed and set up
  • ffmpeg (ffprobe): needed by upgrades to read bitrates

Getting started

1. Install

Homebrew (macOS/Linux):

brew tap dewey/beets-importer https://github.com/dewey/beets-importer
brew install dewey/beets-importer/beets-importer

Go:

go install github.com/dewey/beets-importer@latest

From source:

make build

2. Run

If beet is on your PATH, you do not need a config file for report and doctor. beets-importer asks beet where your database is.

# Write an HTML report about your library
beets-importer report

# Check the health of your library
beets-importer doctor

import and upgrades also need to know your source folder, so they need a config file.

3. Create a config file (for import and upgrades)

beets-importer config init

This copies internal/config/config.example.yaml to ~/.config/beets-importer/config.yaml and prints the path. Set at least source:

# Folder with new albums
source: ~/Music/Inbox

Then check the result:

beets-importer config show

Common commands

Import the newest albums

beets-importer import --limit 10
beets-importer import --limit 10 --source ~/Downloads/music

Looks at the source folder, leaves out folders beets already processed, and opens a picker with the 10 newest ones. You select albums, and beet import runs for each of them.

  • --limit 10: show at most 10 albums. Without it you get all new folders.
  • --source: scan this folder instead of source from the config.

Replace lossy albums with lossless ones

beets-importer upgrades --lossy-to-lossless -i --limit 10

Scans the source folder, finds albums that are in your library as MP3, AAC, OGG or OPUS and in the source folder as FLAC, ALAC, WAV, AIFF or APE, and opens a picker. You see both copies side by side and import the ones you pick.

  • --lossy-to-lossless: only look at lossy library albums that have a lossless copy in the source folder.
  • -i (--interactive): open the picker. Without it you get a table.
  • --limit 10: stop after 10 candidates. This makes the scan quick.

Save lossy albums to a file

beets-importer upgrades --library-format MP3,AAC --csv lossy.csv

Finds upgrade candidates where the library copy is MP3 or AAC, and writes them to a CSV file instead of printing a table. You can import from the file later with import --from-file lossy.csv.

  • --library-format MP3,AAC: only look at library albums in these formats. The source copy can be any better format or bitrate.
  • --csv lossy.csv: write the candidates to this file.

Check the library and fix what is found

beets-importer doctor
beets-importer maintenance

doctor checks your library and shows the problems it finds. It changes nothing. maintenance runs the same checks, then lets you pick which problems to fix. The fixes run one by one and stop at the first error.

  • maintenance --folders: also run the checks that walk the library folder. They are slow on a network share.
  • maintenance --limit 20: run at most 20 fixes.

Retag albums that are spelled in different ways

beets-importer doctor --linter split_albums --ids > split.txt
beets-importer import --retag --from-file split.txt --limit 10

The first command writes the beets IDs of albums that players show twice into a file. The second retags 10 of them with beet import -L. Run it again to get the next 10, because retagged albums are marked and left out.

  • --linter split_albums: run only this check.
  • --ids: print only album IDs, one per line.
  • --retag: retag albums that are already in the library, instead of importing new ones.
  • --from-file split.txt: read the album IDs from this file.
  • --limit 10: retag at most 10 albums in this run.

Retag the albums of a Navidrome playlist

beets-importer import --retag --from-playlist <playlist-id> --limit 10

Takes every album that has a track in the playlist and retags it. The playlist ID is the last part of the playlist URL. It needs the navidrome section in the config.

  • --from-playlist: the Navidrome playlist to read the albums from. Needs --retag.

Write the library report

beets-importer report

Writes index.html with statistics about your library to the report folder in the data folder, and saves a snapshot for the progress charts.

  • --output ~/Music/report: write the page to this folder instead.
  • --no-snapshot: do not save a snapshot of this run.
  • --refresh-disk: read all file sizes and covers from disk again.

Configuration

The config file is ~/.config/beets-importer/config.yaml. Use --config /path/to/file.yaml to load another file.

Defaults taken from beets

If you do not set these, beets-importer finds them:

SettingDefault
beetThe first beet on your PATH
dbThe library option of beets (from beet config)
state_fileThe statefile option of beets (from beet config)
data_dirA beets-importer folder next to the beets database
report_outputA report folder in the data folder

A value in the config file or on the command line always wins. source has no default, because beets does not know it.

Beet wrapper script

If you run beets with uv and a config file in your project, point beet at a wrapper script. For example ~/Music/Music Library Beets/beet.sh:

#!/bin/bash
DIR="$(dirname "$(realpath "$0")")"
exec uv run --project "$DIR" beet -c "$DIR/plugins/config.yaml" "$@"
beet: ~/Music/Music Library Beets/beet.sh

Ignoring albums

List album names under ignore.albums to hide them everywhere: the import picker, import --retag, upgrades and the doctor linters that read the beets database. Names are compared without case. untracked_dirs still counts their tracks, so their folders are not reported as untracked.

ignore:
  albums:
    - "! random !"

Shared flags

Each command only has the flags it uses. Set a flag in the config file or on the command line. The command line wins.

FlagConfig keyCommandsDescription
--configallPath to the config file (default: ~/.config/beets-importer/config.yaml)
--dbdbimport, upgrades, doctor, maintenance, reportPath to the beets SQLite database
--sourcesourceimport, upgrades, doctor, maintenanceFolder with new albums
--beetbeetimport, upgrades, doctor, maintenancePath to the beet binary or a wrapper script
--state-filestate_fileimportPath to the beets incremental state file (state.pickle)
--data-dirdata_dirimport, upgrades, reportFolder for the files beets-importer keeps. See Where files are stored
--verboseverboseimport, upgradesPrint a warning for every folder that could not be scanned, not only a count
--no-cacheno_cacheimport, upgradesDo not use the scan cache and scan everything again

beets-importer --version prints the installed version.

Where files are stored

FileWritten byPurpose
~/.config/beets-importer/config.yamlconfig initYour settings. Use --config to put it elsewhere
<data dir>/ignore.jsonupgrades -iSource and library pairs you ignored
<data dir>/store.dbreportReport snapshots, and the file sizes and cover folders read from disk
<data dir>/scan-cache.jsonimport, upgradesScan results of the source folder
<data dir>/report/index.htmlreportThe report, unless you set --output

The data dir is a beets-importer folder next to your beets database. For example, if your database is ~/Music/Library/musiclibrary.db, the data dir is ~/Music/Library/beets-importer/. It moves with your library, so you can back both up together. Set data_dir in the config or pass --data-dir to use another folder.

ignore.json and store.db are your data. scan-cache.json is only a cache. You can delete it at any time and the next run builds it again. Pass --no-cache to skip it for one run.

beets-importer never writes to the beets database or the beets config.


upgrades: find upgrade candidates

Scans the source folder and compares each album with your beets library. It lists albums where the source copy is better: a better format (for example FLAC replacing MP3), or a clearly higher bitrate in the same format.

Albums are matched by artist and album name, with some tolerance for small differences. Tags in the first audio file are used before the folder name. The score is 35% artist, 55% album name and 10% release year (when both sides have a year). If both years are known and differ by more than 3 years, the albums are not a match.

Example commands

# Show a table of all upgrade candidates
beets-importer upgrades

# Stop after 10 candidates (a quick check)
beets-importer upgrades --limit 10

# Only candidates where the library copy is MP3 or AAC
beets-importer upgrades --library-format MP3,AAC

# Only candidates where the source copy is FLAC
beets-importer upgrades --source-format FLAC

# Combine both and open the picker
beets-importer upgrades --library-format MP3,AAC --source-format FLAC -i

# Shortcut for lossy library copies replaced by lossless sources
# (same as --library-format MP3,AAC,OGG,OPUS --source-format FLAC,ALAC,WAV,AIFF,APE)
beets-importer upgrades --lossy-to-lossless -i

# Write the candidates to a CSV file, then import from it later
beets-importer upgrades --csv /tmp/upgrades.csv
beets-importer import --from-file /tmp/upgrades.csv

--interactive / -i: picker

With -i, a picker opens after the scan instead of a table. Each candidate has two rows, so you can compare the source and the library copy:

  [ ] Source:   Blumentopf          Kein Zufall             1999  16   FLAC   Similarity 0.90
      Library:  Blumentopf          Kein Zufall             1999  16   MP3    FLAC replaces MP3 (192kbps)
  [ ] Source:   Curren$y            Pilot Talk II           2010  6    FLAC   Similarity 0.86
      Library:  Curren$y            Pilot Talk III          2015  14   MP3    FLAC replaces MP3 (256kbps)

Columns: artist, album, year, track count, format. Fields that match are green. Fields that differ (year, track count) are orange.

To hide a wrong match (for example "Square One" matched to "Square Two"), select it and press x ("X ignore in upgrades"). The row is unselected and greyed out ([⊘]) but stays in the list. To undo it, select the greyed row and press x again. Ignored rows are never imported, and CTRL+A skips them. They are saved when the picker closes, also on ESC.

Ignores are saved per feature in ignore.json in the data folder, so an ignore in upgrades does not change other commands. Only that exact source and library pair is ignored. Ignored pairs are hidden on later runs and do not count toward --limit. To undo an ignore later, run upgrades -i --show-ignored. It shows ignored pairs greyed out at the bottom.

Press I on a candidate to compare the files of the source and library folders side by side. Use it to check a match before you import:

Inspector

Keys: SPACE toggle, CTRL+A select all, j/k or arrows to move, ENTER confirm, ESC cancel, I inspect

Flags

FlagDefaultDescription
--interactive, -ifalseOpen a picker after the scan to select candidates to import
--limit0Stop after this many candidates (0 = scan everything)
--threshold0.70Lowest score (0 to 1) for a source and library pair to count as a match
--min-bitrate-delta32Smallest bitrate gain in kbps that counts as an upgrade in the same format
--library-formatnoneOnly look at library albums in these formats, comma-separated (for example MP3,AAC)
--source-formatnoneOnly look at source albums in these formats, comma-separated (for example FLAC)
--lossy-to-losslessfalseSame as --library-format MP3,AAC,OGG,OPUS --source-format FLAC,ALAC,WAV,AIFF,APE. Can not be used with those flags
--show-ignoredfalseWith -i, also list ignored candidates, greyed out, so you can unignore them
--require-year-matchfalseSkip candidates where both sides have a year and the years differ
--csvnoneWrite the candidates to this CSV file instead of printing a table
--allfalseShow all matched pairs, not only upgrade candidates

Output table columns

ColumnDescription
Source DirectoryFolder name of the source album
Library MatchThe matching library album (Artist / Album (Year))
ScoreSimilarity from 0 to 1
Year✓ if both sides have the same year, ✗ if they differ
FormatThe format change, for example MP3→FLAC
Avg BitrateThe bitrate change, for example 192→870 kbps
Upgrade ReasonFor example FLAC replaces MP3 (192kbps) or 385kbps replaces 320kbps

import: import albums

import opens a picker by default. Pass --from-file to import a list of paths instead.

Picker (default)

Lists the newest folders in the source folder that are not in your beets library yet. You select what you want, then beet import runs for each selection.

# Pick from everything new in the source folder
beets-importer import

# Only show the 20 newest
beets-importer import --limit 20

# Only show albums added on or after a date
beets-importer import --since 2024-01-01

Folders that beets already processed (applied or skipped) are left out. The list comes from the beets state file, so beets must run with incremental: yes. Paths are compared exactly. If you run import --limit 5 twice, the second run shows the next 5 folders. Pass --reimport to list processed folders too. It also runs beet import --noincremental, because beets would skip them otherwise.

--from-file: import from a file

Runs beet import for each path in a file. The format depends on the file extension:

  • .csv: reads the source_path column. The CSV from upgrades --csv works as is
  • anything else: one path per line. Empty lines and lines starting with # are ignored
beets-importer import --from-file /tmp/upgrades.csv
beets-importer import --from-file /tmp/my-list.txt --limit 5

--retag: retag albums from a list of album IDs

Runs beet import -L for each beets album ID in a file, one ID per line. Empty lines and lines starting with # are ignored. Use it to fix albums that are already in your library, for example artist names that are spelled in different ways.

# Retag the next 10 albums from the list
beets-importer import --retag --from-file split-albums.txt --limit 10

Albums are retagged in batches of 20, with one beet import -L call per batch. beets looks up the next albums while you decide on the current one. The albums of a batch are listed with their IDs before it starts. Every album gets the field retagged set to today's date (beet import --set). Albums you skip at the beets prompt get retag_skipped instead, read from the beets import log. Run the same command again to get the next 10, because albums with either field are left out. When beets applies a match, the album gets a new ID, so IDs that are no longer in the library also count as done. If beets stops before it applied or skipped every album of a batch (after aBort, or after "Merge all" on a duplicate), those albums are not marked and you are asked if you want to go on.

# What was retagged or skipped
beet ls -a retagged:2026-09-27
beet ls -a retag_skipped::.

# Offer a skipped album again
beet modify -a id:1234 retag_skipped!

--from-playlist: retag the albums of a Navidrome playlist

With --retag, --from-playlist takes the album IDs from a Navidrome playlist instead of a file. Every album with a track in the playlist is retagged once. The playlist ID is the last part of the playlist URL, for example 1LWVZhA0vqU46FAJmzEzWG in https://music.example.com/app/#/playlist/1LWVZhA0vqU46FAJmzEzWG/show.

beets-importer import --retag --from-playlist 1LWVZhA0vqU46FAJmzEzWG --limit 10

Tracks are matched by file path, so the music folder of Navidrome must be the same as directory: in beets. Tracks that beets does not know are listed. This happens when beets moved files and Navidrome did not scan again, or when a folder is not in beets at all (see doctor --linter untracked_dirs). It needs the navidrome section in the config:

navidrome:
  url: https://music.example.com
  username: me
  password_command: op read op://Private/Navidrome/password

Flags

FlagDefaultDescription
--from-filenoneImport the paths in this file instead of opening the picker
--retagfalseRetag library albums with beet import -L. Album IDs come from --from-file or --from-playlist
--from-playlistnoneWith --retag, retag the albums of this Navidrome playlist ID
--limit0Most albums to process (0 = no limit)
--sincenoneOnly show albums added on or after this date (YYYY-MM-DD). Not with --from-file
--reimportfalseAlso list folders beets already processed, and import them again with --noincremental

maintenance: fix what the linters find

Runs the doctor linters, shows how many cases each one found, and lets you fix them one by one:

  1. Pick linters with SPACE and press ENTER.
  2. For each linter, pick the cases to fix. CTRL+A selects all, I shows the folder.
  3. The fixes run one by one, like the import queue. The run stops at the first error. Retags that follow each other run in batches of 20, like import --retag.
beets-importer maintenance
beets-importer maintenance --folders --limit 20
LinterFix
split_albums, artist_variants, missing_year, lowercase_metadataRetag the album with beet import -L, like import --retag. Albums marked retagged or retag_skipped are hidden
split_importsShows the albums of the group and asks. On yes, it merges them into one album (beet modify album_id=…, then removes the empty album rows) and retags it. After a skip, the files are still moved together with beet move
missing_artworkbeet fetchart for the album
untracked_dirsbeet import -m --noincremental on the folder, which moves it into place
empty_dirsRemove the folder
protected_audioDelete the file with beet remove -d. The album goes away with its last track. Before a queue with deletions starts, you are asked once to confirm
low_quality, duplicate_namesReport only. Use upgrades, or fix it on the file server

Track linters are grouped by album. An album with ten lowercase tracks is retagged once. An album found by two linters is also fixed once. Nothing is saved between runs, because fixed cases are gone from the next run.

FlagDefaultDescription
--foldersfalseAlso run the linters that walk the library folder (empty_dirs, untracked_dirs, duplicate_names). They can take minutes on a network share
--limit0Most fixes to run (0 = no limit)

doctor: check library health

Runs linters against your beets library and shows the results in a view you can scroll.

doctor reads the beets database. Some linters also walk the folder that beets moves music into. That folder comes from directory: in your beets config (read with beet config), so you do not need to set it.

LinterNeeds library folderWhat it finds
empty_dirsyesFolders with no entries at all
untracked_dirsyesFolders with audio files but no track in the beets database
low_qualitynoTracks below doctor.low_quality_threshold_kbps (default 128), and AAC below 256 kbps
lowercase_metadatanoTracks where artist, album and title are all lowercase
protected_audionoTracks with iTunes FairPlay DRM (.m4p). Only iTunes can play them
missing_artworknoAlbums without an art path
missing_yearnoAlbums without a year
split_importsnoAlbums that beets split into several albums on import. This is albums with the same name in one folder (for example split by featured artist, unless two tracks have the same title, which means two copies), and one-track albums with the same album artist and name in different folders (a compilation imported track by track)
split_albumsnoAlbums whose tracks do not agree on album artist, album name or MusicBrainz album ID, or do not agree with the album itself. Navidrome and other players show these twice
artist_variantsnoAlbums whose album artist is written in different ways elsewhere ("Lady GaGa" and "Lady Gaga"), or has the same name with a missing or different artist ID. The spelling used on albums with a MusicBrainz artist ID is taken as correct. "Various Artists" is skipped
duplicate_namesyesFolders with two entries that have the same name in different Unicode forms (NFC and NFD). Also checks --source

All linters run by default. Turn one off in the config:

doctor:
  linters:
    lowercase_metadata: false

Example commands

# Interactive report
beets-importer doctor

# Full report as JSON
beets-importer doctor --json

# Only some linters
beets-importer doctor --linter empty_dirs,untracked_dirs

# Paths for one linter, for example to remove empty folders
beets-importer doctor --linter empty_dirs --paths --print0 | xargs -0 rmdir

# Album IDs for one linter, then retag them 10 at a time
beets-importer doctor --linter split_albums --ids > split-albums.txt
beets-importer import --retag --from-file split-albums.txt --limit 10

Duplicate Unicode names

A name like "gehört" can be stored with "ö" as one code point (NFC) or as "o" plus a combining mark (NFD). macOS used to write NFD. Linux tools such as torrent clients write NFC. If a Linux tool writes a file next to an NFD copy from a Mac, the folder has both. Over SMB, macOS lists both entries but can only open one of them, so beets reports extra "unmatched tracks".

duplicate_names finds these folders. You must fix them on the file server, where the two names really are different. The Mac can not tell which copy it deletes.

Flags

FlagDefaultDescription
--jsonfalsePrint results as JSON instead of the interactive view
--linternoneRun only these linters, comma-separated
--pathsfalsePrint only the issue paths of the selected linters, one per line (needs --linter)
--idsfalsePrint only the beets album IDs of the selected linters, one per line, for import --retag (needs --linter, not for folder linters)
--print0, -0falseWith --paths, separate paths with NUL (for xargs -0)

report: library statistics as a web page

Watch the demo video

Reads the beets database (read-only) and writes one index.html. You do not need a server. The page loads Carbon Charts and the IBM Plex font from jsDelivr (fixed versions, with integrity hashes where it matters), so your browser needs internet access. Covers of recent imports are linked from the file system at full size. A smaller copy is embedded in case the file can not be opened.

# Writes <data dir>/report/index.html
beets-importer report

# Write it somewhere else
beets-importer report --output ~/Music/report

More screenshots

Formats, lossy bitrates, lossless resolution and how formats add up to lossless and lossy.

Report: formats

Size on disk by format, and library growth by import month.

Report: storage

Albums by release year (lossless and lossy) and the import calendar.

Report: collection

Albums per genre, grouped by lossless and lossy. The lossy blocks are your upgrade backlog by genre, next to the tracks per album.

Report: genres

What is in the beets database, including the fields your plugins fill.

Report: internal

The page shows:

  • Recently imported: the last 10 albums with cover art, format and import date
  • Road to all lossless: albums and tracks split into lossless (FLAC, ALAC and others), lossy and mixed
  • Formats and quality: tracks per format (FLAC and ALAC apart), lossy bitrates, lossless bit depth and sample rate, and a flow from format to class
  • Storage: size on disk by format, and library size per import month
  • Collection: albums per release year, an import calendar, top artists, albums per month, genres, tracks per album, album types and labels
  • Upgrade list: the artists with the most lossy albums
  • Gaps: how complete the metadata is, and a list you can filter for each gap (lossy and mixed albums, duplicate albums, files missing on disk, no album name, no year, no cover art, cover in folder only, no genre, no MusicBrainz or Discogs ID, tracks without title or number, singletons). Lists show the first 500 rows. Counts are exact
  • Beets internal: database file size, row counts, which optional track fields plugins filled (MusicBrainz IDs, ReplayGain, lyrics and more), the flexible attributes in use and duplicate album names. This part reads the raw database, so ignored albums are included

Every list of albums has a Copy button that puts Artist - Album on the clipboard, and a link to the MusicBrainz or Discogs release when beets knows its ID. The lists also have buttons to copy all rows, with or without links. Use them to search for lossless copies.

Details:

  • An album has cover art when beets has an art path, or when its folder has cover, folder, front, album, albumart, art or artwork as .jpg, .jpeg, .png or .webp. Art inside the audio files is not detected.
  • File sizes and cover folders are read from the music folder, which is slow on a network share. They are cached in store.db and read again in full every 30 days. Files that are new since the last scan are read on the next run. Pass --refresh-disk to read everything again. If more than half of the files are missing, the run stops and saves nothing, because the music folder is probably not mounted.
  • Albums in ignore.albums are left out. Years before 1900 count as missing. "Various Artists" is left out of the top artists and the upgrade list.

Every run saves a snapshot to store.db in the data folder. It has the lossless and lossy album counts, the lossless track count and the size on disk. There is one snapshot per day, and the last run of a day wins. The report draws the lossless share of albums and tracks, and the library size, over time from the snapshots. Pass --no-snapshot to make a report without saving a snapshot.

FlagConfig keyDescription
--dbdbPath to the beets SQLite database
--data-dirdata_dirFolder for store.db and the default report folder
--outputreport_outputFolder to write index.html to (default: report in the data folder)
--no-snapshotDo not save a snapshot of this run
--refresh-diskRead all file sizes and cover images from disk again

dewey/beets-importer

A simple interactive beets importer wrapper with file picker and identification of upgrade possibilities.

Go

2

19 commits

updated Oct 3, 2026

See the code

README

beets-importer

A command line tool for beets. It adds batch import, upgrade finding, library repair and a statistics report on top of beet. It only reads your beets database. Every change is made by calling beet.

beets imports albums one by one. If your inbox is large, you can not look at everything at once, pick what you want now and skip the rest. beets-importer adds a terminal UI in front of beet import, so you can select many albums before anything is imported.

upgrades -i matches your inbox against your library and shows upgrade candidates next to the library copy:

Upgrade picker

report writes a static HTML page about your library. Click the image to watch a short demo:

Library report

Road to all lossless

Why use it with beets

beets does the tagging. beets-importer adds the tools around it:

  1. Import in batches. beets goes through a folder one album at a time. Here you see all new folders, tick the ones you want, and they import as a queue. Folders you skipped do not come back.
  2. Find upgrades. upgrades -i compares your inbox with your library and shows which albums are better, for example FLAC instead of MP3. You see both copies side by side and import your picks.
  3. Track your library over time. report writes one HTML page with formats, bitrates, size and gaps. Every run saves a snapshot, so you can see your progress to an all-lossless library.
  4. Find problems. doctor checks for split albums, artists spelled in different ways, untracked or empty folders, missing years and artwork, and more.
  5. Fix them in a queue. maintenance turns what doctor finds into work. You pick the cases, and the fixes run one by one. Finished albums are marked, so you can stop and continue later.

It reads the beets database and calls beet for every change. Your plugins and config stay as they are.

Features

  • Bring in new music
    • import: pick albums from your source folder and import them
      • The picker lists new folders, newest first. Folders beets already processed are left out
      • --since, --limit and --reimport make the list shorter or longer
      • --from-file: import a list of paths (a text file, or the CSV from upgrades --csv)
    • upgrades: find albums in the source folder that are better than your library copy
      • Finds format upgrades (MP3 to FLAC) and higher bitrates
      • Filters: --library-format, --source-format, --lossy-to-lossless, --threshold, --min-bitrate-delta, --require-year-match
      • -i: pick candidates, compare source and library side by side, ignore wrong matches
      • --csv: write the candidates to a file for import --from-file
  • Fix your library
    • doctor: find problems (read-only)
      • Checks for empty and untracked folders, low bitrate, DRM files, missing artwork or year, split albums, different artist spellings and duplicate Unicode names
      • --json, --paths and --ids print results for scripts
    • maintenance: pick linters and fix what they find, one by one
    • import --retag: retag albums that are already in the library with beet import -L
      • --from-file: album IDs from a file (doctor --ids makes one)
      • --from-playlist: album IDs from a Navidrome playlist
  • Understand your library
    • report: one HTML page with formats, bitrates, years, gaps and your progress towards an all-lossless library
      • Saves a snapshot on every run, so the page can show progress over time
  • Setup
    • config init: write an example config file
    • config show: show the values in use

Requirements

  • beets, installed and set up
  • ffmpeg (ffprobe): needed by upgrades to read bitrates

Getting started

1. Install

Homebrew (macOS/Linux):

brew tap dewey/beets-importer https://github.com/dewey/beets-importer
brew install dewey/beets-importer/beets-importer

Go:

go install github.com/dewey/beets-importer@latest

From source:

make build

2. Run

If beet is on your PATH, you do not need a config file for report and doctor. beets-importer asks beet where your database is.

# Write an HTML report about your library
beets-importer report

# Check the health of your library
beets-importer doctor

import and upgrades also need to know your source folder, so they need a config file.

3. Create a config file (for import and upgrades)

beets-importer config init

This copies internal/config/config.example.yaml to ~/.config/beets-importer/config.yaml and prints the path. Set at least source:

# Folder with new albums
source: ~/Music/Inbox

Then check the result:

beets-importer config show

Common commands

Import the newest albums

beets-importer import --limit 10
beets-importer import --limit 10 --source ~/Downloads/music

Looks at the source folder, leaves out folders beets already processed, and opens a picker with the 10 newest ones. You select albums, and beet import runs for each of them.

  • --limit 10: show at most 10 albums. Without it you get all new folders.
  • --source: scan this folder instead of source from the config.

Replace lossy albums with lossless ones

beets-importer upgrades --lossy-to-lossless -i --limit 10

Scans the source folder, finds albums that are in your library as MP3, AAC, OGG or OPUS and in the source folder as FLAC, ALAC, WAV, AIFF or APE, and opens a picker. You see both copies side by side and import the ones you pick.

  • --lossy-to-lossless: only look at lossy library albums that have a lossless copy in the source folder.
  • -i (--interactive): open the picker. Without it you get a table.
  • --limit 10: stop after 10 candidates. This makes the scan quick.

Save lossy albums to a file

beets-importer upgrades --library-format MP3,AAC --csv lossy.csv

Finds upgrade candidates where the library copy is MP3 or AAC, and writes them to a CSV file instead of printing a table. You can import from the file later with import --from-file lossy.csv.

  • --library-format MP3,AAC: only look at library albums in these formats. The source copy can be any better format or bitrate.
  • --csv lossy.csv: write the candidates to this file.

Check the library and fix what is found

beets-importer doctor
beets-importer maintenance

doctor checks your library and shows the problems it finds. It changes nothing. maintenance runs the same checks, then lets you pick which problems to fix. The fixes run one by one and stop at the first error.

  • maintenance --folders: also run the checks that walk the library folder. They are slow on a network share.
  • maintenance --limit 20: run at most 20 fixes.

Retag albums that are spelled in different ways

beets-importer doctor --linter split_albums --ids > split.txt
beets-importer import --retag --from-file split.txt --limit 10

The first command writes the beets IDs of albums that players show twice into a file. The second retags 10 of them with beet import -L. Run it again to get the next 10, because retagged albums are marked and left out.

  • --linter split_albums: run only this check.
  • --ids: print only album IDs, one per line.
  • --retag: retag albums that are already in the library, instead of importing new ones.
  • --from-file split.txt: read the album IDs from this file.
  • --limit 10: retag at most 10 albums in this run.

Retag the albums of a Navidrome playlist

beets-importer import --retag --from-playlist <playlist-id> --limit 10

Takes every album that has a track in the playlist and retags it. The playlist ID is the last part of the playlist URL. It needs the navidrome section in the config.

  • --from-playlist: the Navidrome playlist to read the albums from. Needs --retag.

Write the library report

beets-importer report

Writes index.html with statistics about your library to the report folder in the data folder, and saves a snapshot for the progress charts.

  • --output ~/Music/report: write the page to this folder instead.
  • --no-snapshot: do not save a snapshot of this run.
  • --refresh-disk: read all file sizes and covers from disk again.

Configuration

The config file is ~/.config/beets-importer/config.yaml. Use --config /path/to/file.yaml to load another file.

Defaults taken from beets

If you do not set these, beets-importer finds them:

SettingDefault
beetThe first beet on your PATH
dbThe library option of beets (from beet config)
state_fileThe statefile option of beets (from beet config)
data_dirA beets-importer folder next to the beets database
report_outputA report folder in the data folder

A value in the config file or on the command line always wins. source has no default, because beets does not know it.

Beet wrapper script

If you run beets with uv and a config file in your project, point beet at a wrapper script. For example ~/Music/Music Library Beets/beet.sh:

#!/bin/bash
DIR="$(dirname "$(realpath "$0")")"
exec uv run --project "$DIR" beet -c "$DIR/plugins/config.yaml" "$@"
beet: ~/Music/Music Library Beets/beet.sh

Ignoring albums

List album names under ignore.albums to hide them everywhere: the import picker, import --retag, upgrades and the doctor linters that read the beets database. Names are compared without case. untracked_dirs still counts their tracks, so their folders are not reported as untracked.

ignore:
  albums:
    - "! random !"

Shared flags

Each command only has the flags it uses. Set a flag in the config file or on the command line. The command line wins.

FlagConfig keyCommandsDescription
--configallPath to the config file (default: ~/.config/beets-importer/config.yaml)
--dbdbimport, upgrades, doctor, maintenance, reportPath to the beets SQLite database
--sourcesourceimport, upgrades, doctor, maintenanceFolder with new albums
--beetbeetimport, upgrades, doctor, maintenancePath to the beet binary or a wrapper script
--state-filestate_fileimportPath to the beets incremental state file (state.pickle)
--data-dirdata_dirimport, upgrades, reportFolder for the files beets-importer keeps. See Where files are stored
--verboseverboseimport, upgradesPrint a warning for every folder that could not be scanned, not only a count
--no-cacheno_cacheimport, upgradesDo not use the scan cache and scan everything again

beets-importer --version prints the installed version.

Where files are stored

FileWritten byPurpose
~/.config/beets-importer/config.yamlconfig initYour settings. Use --config to put it elsewhere
<data dir>/ignore.jsonupgrades -iSource and library pairs you ignored
<data dir>/store.dbreportReport snapshots, and the file sizes and cover folders read from disk
<data dir>/scan-cache.jsonimport, upgradesScan results of the source folder
<data dir>/report/index.htmlreportThe report, unless you set --output

The data dir is a beets-importer folder next to your beets database. For example, if your database is ~/Music/Library/musiclibrary.db, the data dir is ~/Music/Library/beets-importer/. It moves with your library, so you can back both up together. Set data_dir in the config or pass --data-dir to use another folder.

ignore.json and store.db are your data. scan-cache.json is only a cache. You can delete it at any time and the next run builds it again. Pass --no-cache to skip it for one run.

beets-importer never writes to the beets database or the beets config.


upgrades: find upgrade candidates

Scans the source folder and compares each album with your beets library. It lists albums where the source copy is better: a better format (for example FLAC replacing MP3), or a clearly higher bitrate in the same format.

Albums are matched by artist and album name, with some tolerance for small differences. Tags in the first audio file are used before the folder name. The score is 35% artist, 55% album name and 10% release year (when both sides have a year). If both years are known and differ by more than 3 years, the albums are not a match.

Example commands

# Show a table of all upgrade candidates
beets-importer upgrades

# Stop after 10 candidates (a quick check)
beets-importer upgrades --limit 10

# Only candidates where the library copy is MP3 or AAC
beets-importer upgrades --library-format MP3,AAC

# Only candidates where the source copy is FLAC
beets-importer upgrades --source-format FLAC

# Combine both and open the picker
beets-importer upgrades --library-format MP3,AAC --source-format FLAC -i

# Shortcut for lossy library copies replaced by lossless sources
# (same as --library-format MP3,AAC,OGG,OPUS --source-format FLAC,ALAC,WAV,AIFF,APE)
beets-importer upgrades --lossy-to-lossless -i

# Write the candidates to a CSV file, then import from it later
beets-importer upgrades --csv /tmp/upgrades.csv
beets-importer import --from-file /tmp/upgrades.csv

--interactive / -i: picker

With -i, a picker opens after the scan instead of a table. Each candidate has two rows, so you can compare the source and the library copy:

  [ ] Source:   Blumentopf          Kein Zufall             1999  16   FLAC   Similarity 0.90
      Library:  Blumentopf          Kein Zufall             1999  16   MP3    FLAC replaces MP3 (192kbps)
  [ ] Source:   Curren$y            Pilot Talk II           2010  6    FLAC   Similarity 0.86
      Library:  Curren$y            Pilot Talk III          2015  14   MP3    FLAC replaces MP3 (256kbps)

Columns: artist, album, year, track count, format. Fields that match are green. Fields that differ (year, track count) are orange.

To hide a wrong match (for example "Square One" matched to "Square Two"), select it and press x ("X ignore in upgrades"). The row is unselected and greyed out ([⊘]) but stays in the list. To undo it, select the greyed row and press x again. Ignored rows are never imported, and CTRL+A skips them. They are saved when the picker closes, also on ESC.

Ignores are saved per feature in ignore.json in the data folder, so an ignore in upgrades does not change other commands. Only that exact source and library pair is ignored. Ignored pairs are hidden on later runs and do not count toward --limit. To undo an ignore later, run upgrades -i --show-ignored. It shows ignored pairs greyed out at the bottom.

Press I on a candidate to compare the files of the source and library folders side by side. Use it to check a match before you import:

Inspector

Keys: SPACE toggle, CTRL+A select all, j/k or arrows to move, ENTER confirm, ESC cancel, I inspect

Flags

FlagDefaultDescription
--interactive, -ifalseOpen a picker after the scan to select candidates to import
--limit0Stop after this many candidates (0 = scan everything)
--threshold0.70Lowest score (0 to 1) for a source and library pair to count as a match
--min-bitrate-delta32Smallest bitrate gain in kbps that counts as an upgrade in the same format
--library-formatnoneOnly look at library albums in these formats, comma-separated (for example MP3,AAC)
--source-formatnoneOnly look at source albums in these formats, comma-separated (for example FLAC)
--lossy-to-losslessfalseSame as --library-format MP3,AAC,OGG,OPUS --source-format FLAC,ALAC,WAV,AIFF,APE. Can not be used with those flags
--show-ignoredfalseWith -i, also list ignored candidates, greyed out, so you can unignore them
--require-year-matchfalseSkip candidates where both sides have a year and the years differ
--csvnoneWrite the candidates to this CSV file instead of printing a table
--allfalseShow all matched pairs, not only upgrade candidates

Output table columns

ColumnDescription
Source DirectoryFolder name of the source album
Library MatchThe matching library album (Artist / Album (Year))
ScoreSimilarity from 0 to 1
Year✓ if both sides have the same year, ✗ if they differ
FormatThe format change, for example MP3→FLAC
Avg BitrateThe bitrate change, for example 192→870 kbps
Upgrade ReasonFor example FLAC replaces MP3 (192kbps) or 385kbps replaces 320kbps

import: import albums

import opens a picker by default. Pass --from-file to import a list of paths instead.

Picker (default)

Lists the newest folders in the source folder that are not in your beets library yet. You select what you want, then beet import runs for each selection.

# Pick from everything new in the source folder
beets-importer import

# Only show the 20 newest
beets-importer import --limit 20

# Only show albums added on or after a date
beets-importer import --since 2024-01-01

Folders that beets already processed (applied or skipped) are left out. The list comes from the beets state file, so beets must run with incremental: yes. Paths are compared exactly. If you run import --limit 5 twice, the second run shows the next 5 folders. Pass --reimport to list processed folders too. It also runs beet import --noincremental, because beets would skip them otherwise.

--from-file: import from a file

Runs beet import for each path in a file. The format depends on the file extension:

  • .csv: reads the source_path column. The CSV from upgrades --csv works as is
  • anything else: one path per line. Empty lines and lines starting with # are ignored
beets-importer import --from-file /tmp/upgrades.csv
beets-importer import --from-file /tmp/my-list.txt --limit 5

--retag: retag albums from a list of album IDs

Runs beet import -L for each beets album ID in a file, one ID per line. Empty lines and lines starting with # are ignored. Use it to fix albums that are already in your library, for example artist names that are spelled in different ways.

# Retag the next 10 albums from the list
beets-importer import --retag --from-file split-albums.txt --limit 10

Albums are retagged in batches of 20, with one beet import -L call per batch. beets looks up the next albums while you decide on the current one. The albums of a batch are listed with their IDs before it starts. Every album gets the field retagged set to today's date (beet import --set). Albums you skip at the beets prompt get retag_skipped instead, read from the beets import log. Run the same command again to get the next 10, because albums with either field are left out. When beets applies a match, the album gets a new ID, so IDs that are no longer in the library also count as done. If beets stops before it applied or skipped every album of a batch (after aBort, or after "Merge all" on a duplicate), those albums are not marked and you are asked if you want to go on.

# What was retagged or skipped
beet ls -a retagged:2026-09-27
beet ls -a retag_skipped::.

# Offer a skipped album again
beet modify -a id:1234 retag_skipped!

--from-playlist: retag the albums of a Navidrome playlist

With --retag, --from-playlist takes the album IDs from a Navidrome playlist instead of a file. Every album with a track in the playlist is retagged once. The playlist ID is the last part of the playlist URL, for example 1LWVZhA0vqU46FAJmzEzWG in https://music.example.com/app/#/playlist/1LWVZhA0vqU46FAJmzEzWG/show.

beets-importer import --retag --from-playlist 1LWVZhA0vqU46FAJmzEzWG --limit 10

Tracks are matched by file path, so the music folder of Navidrome must be the same as directory: in beets. Tracks that beets does not know are listed. This happens when beets moved files and Navidrome did not scan again, or when a folder is not in beets at all (see doctor --linter untracked_dirs). It needs the navidrome section in the config:

navidrome:
  url: https://music.example.com
  username: me
  password_command: op read op://Private/Navidrome/password

Flags

FlagDefaultDescription
--from-filenoneImport the paths in this file instead of opening the picker
--retagfalseRetag library albums with beet import -L. Album IDs come from --from-file or --from-playlist
--from-playlistnoneWith --retag, retag the albums of this Navidrome playlist ID
--limit0Most albums to process (0 = no limit)
--sincenoneOnly show albums added on or after this date (YYYY-MM-DD). Not with --from-file
--reimportfalseAlso list folders beets already processed, and import them again with --noincremental

maintenance: fix what the linters find

Runs the doctor linters, shows how many cases each one found, and lets you fix them one by one:

  1. Pick linters with SPACE and press ENTER.
  2. For each linter, pick the cases to fix. CTRL+A selects all, I shows the folder.
  3. The fixes run one by one, like the import queue. The run stops at the first error. Retags that follow each other run in batches of 20, like import --retag.
beets-importer maintenance
beets-importer maintenance --folders --limit 20
LinterFix
split_albums, artist_variants, missing_year, lowercase_metadataRetag the album with beet import -L, like import --retag. Albums marked retagged or retag_skipped are hidden
split_importsShows the albums of the group and asks. On yes, it merges them into one album (beet modify album_id=…, then removes the empty album rows) and retags it. After a skip, the files are still moved together with beet move
missing_artworkbeet fetchart for the album
untracked_dirsbeet import -m --noincremental on the folder, which moves it into place
empty_dirsRemove the folder
protected_audioDelete the file with beet remove -d. The album goes away with its last track. Before a queue with deletions starts, you are asked once to confirm
low_quality, duplicate_namesReport only. Use upgrades, or fix it on the file server

Track linters are grouped by album. An album with ten lowercase tracks is retagged once. An album found by two linters is also fixed once. Nothing is saved between runs, because fixed cases are gone from the next run.

FlagDefaultDescription
--foldersfalseAlso run the linters that walk the library folder (empty_dirs, untracked_dirs, duplicate_names). They can take minutes on a network share
--limit0Most fixes to run (0 = no limit)

doctor: check library health

Runs linters against your beets library and shows the results in a view you can scroll.

doctor reads the beets database. Some linters also walk the folder that beets moves music into. That folder comes from directory: in your beets config (read with beet config), so you do not need to set it.

LinterNeeds library folderWhat it finds
empty_dirsyesFolders with no entries at all
untracked_dirsyesFolders with audio files but no track in the beets database
low_qualitynoTracks below doctor.low_quality_threshold_kbps (default 128), and AAC below 256 kbps
lowercase_metadatanoTracks where artist, album and title are all lowercase
protected_audionoTracks with iTunes FairPlay DRM (.m4p). Only iTunes can play them
missing_artworknoAlbums without an art path
missing_yearnoAlbums without a year
split_importsnoAlbums that beets split into several albums on import. This is albums with the same name in one folder (for example split by featured artist, unless two tracks have the same title, which means two copies), and one-track albums with the same album artist and name in different folders (a compilation imported track by track)
split_albumsnoAlbums whose tracks do not agree on album artist, album name or MusicBrainz album ID, or do not agree with the album itself. Navidrome and other players show these twice
artist_variantsnoAlbums whose album artist is written in different ways elsewhere ("Lady GaGa" and "Lady Gaga"), or has the same name with a missing or different artist ID. The spelling used on albums with a MusicBrainz artist ID is taken as correct. "Various Artists" is skipped
duplicate_namesyesFolders with two entries that have the same name in different Unicode forms (NFC and NFD). Also checks --source

All linters run by default. Turn one off in the config:

doctor:
  linters:
    lowercase_metadata: false

Example commands

# Interactive report
beets-importer doctor

# Full report as JSON
beets-importer doctor --json

# Only some linters
beets-importer doctor --linter empty_dirs,untracked_dirs

# Paths for one linter, for example to remove empty folders
beets-importer doctor --linter empty_dirs --paths --print0 | xargs -0 rmdir

# Album IDs for one linter, then retag them 10 at a time
beets-importer doctor --linter split_albums --ids > split-albums.txt
beets-importer import --retag --from-file split-albums.txt --limit 10

Duplicate Unicode names

A name like "gehört" can be stored with "ö" as one code point (NFC) or as "o" plus a combining mark (NFD). macOS used to write NFD. Linux tools such as torrent clients write NFC. If a Linux tool writes a file next to an NFD copy from a Mac, the folder has both. Over SMB, macOS lists both entries but can only open one of them, so beets reports extra "unmatched tracks".

duplicate_names finds these folders. You must fix them on the file server, where the two names really are different. The Mac can not tell which copy it deletes.

Flags

FlagDefaultDescription
--jsonfalsePrint results as JSON instead of the interactive view
--linternoneRun only these linters, comma-separated
--pathsfalsePrint only the issue paths of the selected linters, one per line (needs --linter)
--idsfalsePrint only the beets album IDs of the selected linters, one per line, for import --retag (needs --linter, not for folder linters)
--print0, -0falseWith --paths, separate paths with NUL (for xargs -0)

report: library statistics as a web page

Watch the demo video

Reads the beets database (read-only) and writes one index.html. You do not need a server. The page loads Carbon Charts and the IBM Plex font from jsDelivr (fixed versions, with integrity hashes where it matters), so your browser needs internet access. Covers of recent imports are linked from the file system at full size. A smaller copy is embedded in case the file can not be opened.

# Writes <data dir>/report/index.html
beets-importer report

# Write it somewhere else
beets-importer report --output ~/Music/report

More screenshots

Formats, lossy bitrates, lossless resolution and how formats add up to lossless and lossy.

Report: formats

Size on disk by format, and library growth by import month.

Report: storage

Albums by release year (lossless and lossy) and the import calendar.

Report: collection

Albums per genre, grouped by lossless and lossy. The lossy blocks are your upgrade backlog by genre, next to the tracks per album.

Report: genres

What is in the beets database, including the fields your plugins fill.

Report: internal

The page shows:

  • Recently imported: the last 10 albums with cover art, format and import date
  • Road to all lossless: albums and tracks split into lossless (FLAC, ALAC and others), lossy and mixed
  • Formats and quality: tracks per format (FLAC and ALAC apart), lossy bitrates, lossless bit depth and sample rate, and a flow from format to class
  • Storage: size on disk by format, and library size per import month
  • Collection: albums per release year, an import calendar, top artists, albums per month, genres, tracks per album, album types and labels
  • Upgrade list: the artists with the most lossy albums
  • Gaps: how complete the metadata is, and a list you can filter for each gap (lossy and mixed albums, duplicate albums, files missing on disk, no album name, no year, no cover art, cover in folder only, no genre, no MusicBrainz or Discogs ID, tracks without title or number, singletons). Lists show the first 500 rows. Counts are exact
  • Beets internal: database file size, row counts, which optional track fields plugins filled (MusicBrainz IDs, ReplayGain, lyrics and more), the flexible attributes in use and duplicate album names. This part reads the raw database, so ignored albums are included

Every list of albums has a Copy button that puts Artist - Album on the clipboard, and a link to the MusicBrainz or Discogs release when beets knows its ID. The lists also have buttons to copy all rows, with or without links. Use them to search for lossless copies.

Details:

  • An album has cover art when beets has an art path, or when its folder has cover, folder, front, album, albumart, art or artwork as .jpg, .jpeg, .png or .webp. Art inside the audio files is not detected.
  • File sizes and cover folders are read from the music folder, which is slow on a network share. They are cached in store.db and read again in full every 30 days. Files that are new since the last scan are read on the next run. Pass --refresh-disk to read everything again. If more than half of the files are missing, the run stops and saves nothing, because the music folder is probably not mounted.
  • Albums in ignore.albums are left out. Years before 1900 count as missing. "Various Artists" is left out of the top artists and the upgrade list.

Every run saves a snapshot to store.db in the data folder. It has the lossless and lossy album counts, the lossless track count and the size on disk. There is one snapshot per day, and the last run of a day wins. The report draws the lossless share of albums and tracks, and the library size, over time from the snapshots. Pass --no-snapshot to make a report without saving a snapshot.

FlagConfig keyDescription
--dbdbPath to the beets SQLite database
--data-dirdata_dirFolder for store.db and the default report folder
--outputreport_outputFolder to write index.html to (default: report in the data folder)
--no-snapshotDo not save a snapshot of this run
--refresh-diskRead all file sizes and cover images from disk again