The Complete Computing Environment

Music Library Management with beets

LifeTechEmacsTopicsArcology

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.

{ pkgs, config, ... }:

let
  myBeets = with pkgs; beets.override {
    pluginOverrides = {
      beetcamp = {
        propagatedBuildInputs = [ pkgs.beetcamp ];
      };
      copyartifacts = {
        propagatedBuildInputs = [ beetsPackages.copyartifacts ];
      };
    };
  };
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

{ lib,
  fetchFromGitHub,
  python3Packages,
  beets,

  propagateBeets ? false
}:

python3Packages.buildPythonPackage {
  pname = "beets-beetcamp";
  version = "unstable-2022-06-07";

  src = fetchFromGitHub {
    repo = "beetcamp";
    owner = "snejus";
    rev = "118d4239bd570a59997f13ac0920e6e92890ac67";
    sha256 = "sha256-yrlpgLdNEzlWMY7Cns0UE93oEbpkOoYZHGLpui6MfC0=";
  };

  format = "pyproject";

  propagatedBuildInputs = with python3Packages; [ setuptools poetry 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 = with python3Packages; [
    # 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.

To import an album in a particular class collection:

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

to modify existing and move them:

~/.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 elisp:(cce/import-to-beets) to make it most easy:

(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*")))
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

plugins: fetchart embedart convert scrub replaygain lastgenre web acousticbrainz fetchart smartplaylist copyartifacts discogs bandcamp
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.

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 copyartifacts to copy album art, pdfs, etc

copyartifacts:
  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:

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:

bandcamp:
  search_max: 5
  art: yes
  genre:
    capitalize: no
    mode: progressive 

Fetch genres from Last.FM:

lastgenre:
  auto: yes
  source: album

Embed album art in the files

embedart:
  auto: yes
  remove_art_file: yes

Fetch art where possible

fetchart:
  auto: yes
  sources: "*"
  store_source: yes

Enable and disable various other plugins:

chrome:
  auto: yes

replaygain:
  auto: no

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 shell:beet -c ~/Music/beets/config.yaml splupdate & (make sure to tangle this if you change it!):

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"