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 = 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
Linked from
References
The beetcamp source is pulled in as a flake input pinned by flake.lock:
beetcamp = {
url = "github:snejus/beetcamp";
flake = false;
};{ 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:
~/.nix-profile/bin/beet -c ~/Music/beets/config.yaml import --set=class=high --write --resume $IMPORT_PATHto modify existing and move them:
~/.nix-profile/bin/beet -c ~/Music/beets/config.yaml modify class=low low:1interactive 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:
(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 $TMPDNEXT tag out "night music", ambient sleep stuff and code drone, music to sing along
Beets Configuration
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: noThe 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 filetote to copy album art, pdfs, etc
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:
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.logI use beetcamp:
bandcamp:
search_max: 5
art: yes
genre:
capitalize: no
mode: progressive Fetch genres from Last.FM:
lastgenre:
auto: yes
source: albumEmbed album art in the files
embedart:
auto: yes
remove_art_file: yesFetch art where possible
fetchart:
auto: yes
sources: "*"
store_source: yesEnable and disable various other plugins:
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: yesSmart Playlists
run 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"