CCE

Music Library Management with beets

Contents

I do not like beets, but I like everything else less, especially manual Music metadata management. Because I must, my setup is a bit non-standard, I do some hackery with paths and tags in beets to organize my music so that Syncthing can more easily sync important parts to my devices rather than fucking around with generating file-exclusion lists and such.

I install it in home-manager with the copyArtifacts plugin.

nix:tangle ~/nix/hm/beets.nix
{ pkgs, config, ... }:

let
  myBeets = pkgs.python3.pkgs.beets.override {
    pluginOverrides = {
      beetcamp = {
        propagatedBuildInputs = [ pkgs.beetcamp ];
      };
#       filetote = {
#        propagatedBuildInputs = [ pkgs.python3.pkgs.beets-filetote ];
  #    };
    };
  };
in {
  home.activation.beets-config =
    pkgs.lib.mkActivationLocalLink config
      "~/Music/beets/config.yaml"
      ".config/beets/config.yaml";

  # home.packages = [ myBeets  ];
  home.packages = [ myBeets pkgs.beetcamp ];
}

DONE check up on nixpkgs comment about copyartifacts plugin

INPROGRESS add beetcamp to package external plugins

References

The beetcamp source is pulled in as a flake input pinned by flake.lock:

nix:tangle ~/nix/snippets/beetcamp-input.nix:mkdirp yes
beetcamp = {
  url = "github:snejus/beetcamp";
  flake = false;
};
nix:tangle ~/nix/pkgs/beetcamp.nix
{ inputs,
  lib,
  callPackage,
  buildPythonPackage,
  beets,

  setuptools,
  poetry-core,
  requests,
  cached-property,
  pycountry,
  python-dateutil,
  ordered-set,

  pytest-cov,
  pytest-randomly,
  pytest-lazy-fixture,
  rich,
  tox,
  types-setuptools,
  types-requests,

  propagateBeets ? false
}:

buildPythonPackage {
  pname = "beets-beetcamp";

  version = "unstable-2026-04-17";
  src = inputs.beetcamp;

  format = "pyproject";

  propagatedBuildInputs = [ setuptools poetry-core requests cached-property pycountry python-dateutil ordered-set ]
                          ++ (lib.optional propagateBeets [ beets ]);

  postInstall = ''
    # rm $out/lib/python*/site-packages/LICENSE
    # mkdir -p $out/share/doc/beetcamp
    # mv $out/lib/python*/site-packages/README.md $out/share/doc/beetcamp/README.md
  '';

  checkInputs = [
    # pytestCheckHook
    pytest-cov
    pytest-randomly
    pytest-lazy-fixture
    rich
    tox
    types-setuptools
    types-requests
  ] ++ [
    beets
  ];

  meta = {
    homepage = "https://github.com/snejus/beetcamp";
    description = "Bandcamp autotagger plugin for beets.";
    license = lib.licenses.gpl2;
    inherit (beets.meta) platforms;
    maintainers = with lib.maintainers; [ rrix ];
  };
}

File Organization

I keep my music in ~/Music and a few subdirectories underneath it so that i can use Syncthing's filtering to narrow the Music i sync to devices with less storage like the Cosmo Communicator or whatever.

  • Albums I download go in to New, usually just unzipped and kept outside of beets.

  • High contains music I want anywhere

  • Middle contains music that is interesting but I do not listen to often

  • Low is where music I don't listen to or purchased without fully intending to listen to live

  • Archive is where music goes to die

  • Archive Old contains low quality rips and music I listened to when I was much younger.

To import an album in a particular class collection:

shell
~/.nix-profile/bin/beet -c ~/Music/beets/config.yaml import --set=class=high --write --resume $IMPORT_PATH

to modify existing and move them:

shell
~/.nix-profile/bin/beet -c ~/Music/beets/config.yaml modify class=low low:1

interactive command for importing a zip file or glob of them in to beets from Dired File Manager

I hate using unzip, such terrible ergonomics. I have this tiny helper shell-script to unzip to /tmp and then I wrap it in an interactive command (cce/import-to-beets) to make it most easy:

emacs-lisp:tangle ~/nix/lisp/beets.el
(defvar beet-command "~/.nix-profile/bin/beet -c ~/Music/beets/config.yaml import --write --resume")
(defun cce/import-to-beets (file class)
  "Import a Bandcamp zip FILE in to a CLASS in my Music folder."
  (interactive "ffile or directory to import? \nMimport-class? ")
  (let ((directory
         (if (s-ends-with? "zip" file)
             (progn
               (shell-command (format "bash ~/org/cce/beets_unzip.sh \"%s\"" (expand-file-name file)) "*beets_unzip*" "*beets_unzip_error*")
               (if (and (get-buffer "*beets_unzip_error*") (buffer-size (get-buffer "*beets_unzip_error*")))
                   (error "zip script failed")
                 (with-current-buffer (get-buffer "*beets_unzip*")
                   (buffer-string))))
           file)))
    (async-shell-command
     (format "%s --set=class=%s \"%s\""
             beet-command class
             directory)
     "*beets-import*")))
shell:tangle ~/nix/files/beets_unzip.sh:shebang #!/bin/env
TMPD=$(mktemp -d /tmp/bandcampXXXX)
cd $TMPD
unzip "$1" &>/tmp/bandcamp_unzip.log
echo $TMPD

NEXT tag out "night music", ambient sleep stuff and code drone, music to sing along

Beets Configuration

yaml:tangle ~/Music/beets/config.yaml
plugins: fetchart embedart convert scrub replaygain lastgenre web fetchart smartplaylist discogs bandcamp #fletote
directory: /home/rrix/Music
library: /home/rrix/Music/beets/musiclibrary.blb
art_filename: albumart
threaded: yes
original_date: no
per_disc_numbering: no

The class field describes the file organization hierarchy and is used in the path selection per the "Advanced Awesomeness" documentation to achieve this.

yaml:tangle ~/Music/beets/config.yaml
paths:
  default:       New/%asciify{$albumartist}/%asciify{$album}%aunique{}/$track - %asciify{$title}
  class:high:    High/%asciify{$albumartist}/%asciify{$album}%aunique{}/$track - %asciify{$title}
  class:middle:  Middle/%asciify{$albumartist}/%asciify{$album}%aunique{}/$track - %asciify{$title}
  class:low:     Low/%asciify{$albumartist}/%asciify{$album}%aunique{}/$track - %asciify{$title}
  class:archive: Archive/%asciify{$albumartist}/%asciify{$album}%aunique{}/$track - %asciify{$title}

I use filetote to copy album art, pdfs, etc

yaml:tangle ~/Music/beets/config.yaml
filetote:
  extensions: .*

import configuration is pretty simple, geared towards assuming the files are fungible, and indeed they are since i'll probably be running imports on my laptop from bandcamp downloads:

yaml:tangle ~/Music/beets/config.yaml
import:
  languages: en ja
  write: yes
  copy: no
  move: yes
  resume: ask
  incremental: yes
  quiet_fallback: skip
  timid: no
  log: /home/rrix/Music/beets/beet.log

I use beetcamp:

yaml:tangle ~/Music/beets/config.yaml
bandcamp:
  search_max: 5
  art: yes
  genre:
    capitalize: no
    mode: progressive 

Fetch genres from Last.FM:

yaml:tangle ~/Music/beets/config.yaml
lastgenre:
  auto: yes
  source: album

Embed album art in the files

yaml:tangle ~/Music/beets/config.yaml
embedart:
  auto: yes
  remove_art_file: yes

Fetch art where possible

yaml:tangle ~/Music/beets/config.yaml
fetchart:
  auto: yes
  sources: "*"
  store_source: yes

Enable and disable various other plugins:

yaml:tangle ~/Music/beets/config.yaml
chrome:
  auto: yes

replaygain:
  backend: command
  command: mp3gain
  auto: yes

scrub:
  auto: yes

replace:
  '^\.': _
  '[\x00-\x1f]': _
  '[<>:"\?\*\|]': _
  '[\xE8-\xEB]': e
  '[\xEC-\xEF]': i
  '[\xE2-\xE6]': a
  '[\xF2-\xF6]': o
  '[\xF8]': o
  '\.$': _
  '\s+$': ''

web:
  host: 0.0.0.0
  port: 8337
  reverse_proxy: true

acousticbrainz:
  auto: yes

Smart Playlists

run beet -c ~/Music/beets/config.yaml splupdate & (make sure to tangle this if you change it!):

yaml:tangle ~/Music/beets/config.yaml
smartplaylist:
  relative_to: /home/rrix/Music
  playlist_dir: /home/rrix/Music/playlists
  playlists:
  - name: upbeat.m3u
    query:
    - mood_happy::^0.99 ^genre::Rock ^artist::Vanilla ^artist:Bananarama ^album::Evangelion ^album::3.0 ^genre:Grunge ^class:archive
    - artist:"Electric Six"
    - danceable::^0.99999 ^genre::Rock ^artist::Vanilla ^artist:Bananarama ^album::Evangelion ^album::3.0 ^genre:Grunge ^class:archive

  - name: chiptunes.m3u
    query:
    - artist:Anamanaguchi
    - artist:"Bit Shifter"
    - artist:Chipzel
    - artist:chipzel
    - artist::Demoscene
    - album::Fez
    - album::"Hyper Light Drifter"
    - album::"Toffelskater"
    - artist:Fantomenk
    - artist:"Fighter X"
    - artist:"PDF Format"
    - artist:Sabrepulse
    - artist:Spamtron
    - artist:spamtron
    - genre:8-bit
    - genre:Chiptunes

  - name: j-pop.m3u
    query:
    - album::[Gg][Uu][Nn][Dd][Aa][Mm]
    - album::ガンダムユニコ
    - artist::まりや
    - artist:"Maria Asahina"
    - artist:"松原みき"
    - album::NieR
    - genre:J-pop
    - genre:J-Rock

  - name: various-and-sundry.m3u
    query:
    - album:"Birdy Nam Nam"
    - artist::Flashbulb
    - genre:vaporwave
    - genre:Ambient
    - artist:'Janel & Anthony'
    - artist:'Anthony Pirog'

  - name: music-for-sleep.m3u
    query:
    - album:CATCH‐Wave
    - album::Disassembly
    - artist:"Music For Sleep"
    - album:"Glider 10"
    - album:Liminal
    - album:"Solar Fields"
    - artist:"Matt Borghi"
    - artist:"Umber"
    - artist:"the volume settings folder"
    - artist:"Brian Eno"