summary refs log tree commit diff
path: root/pkgs/tools/audio/beets
diff options
context:
space:
mode:
authorBernardo Meurer <bernardo@meurer.org>2022-05-07 13:42:01 -0700
committerBernardo Meurer <bernardo@meurer.org>2022-05-08 14:52:16 -0700
commitd9910fc92819cc8290da84bdcddf991dacf06cf6 (patch)
tree66b281db3682771c56398ec4a294cf544c2b171c /pkgs/tools/audio/beets
parentd4aca509e6820203ce4ef71695d80bfe498393ed (diff)
downloadnixpkgs-d9910fc92819cc8290da84bdcddf991dacf06cf6.tar
nixpkgs-d9910fc92819cc8290da84bdcddf991dacf06cf6.tar.gz
nixpkgs-d9910fc92819cc8290da84bdcddf991dacf06cf6.tar.bz2
nixpkgs-d9910fc92819cc8290da84bdcddf991dacf06cf6.tar.lz
nixpkgs-d9910fc92819cc8290da84bdcddf991dacf06cf6.tar.xz
nixpkgs-d9910fc92819cc8290da84bdcddf991dacf06cf6.tar.zst
nixpkgs-d9910fc92819cc8290da84bdcddf991dacf06cf6.zip
beets: refactor
Highlights:

* We now have a beets-unstable
* We now run tests for beets-minimal
* We're much more strict about the effects of enabling/disabling plugins
* All patches but one were dropped
* Removal of deprecated nose in favor of pytest
* Art resizing is working better now
Diffstat (limited to 'pkgs/tools/audio/beets')
-rw-r--r--pkgs/tools/audio/beets/badfiles-plugin-nix-paths.patch21
-rw-r--r--pkgs/tools/audio/beets/builtin-plugins.nix94
-rw-r--r--pkgs/tools/audio/beets/common.nix147
-rw-r--r--pkgs/tools/audio/beets/convert-plugin-ffmpeg-path.patch34
-rw-r--r--pkgs/tools/audio/beets/default.nix302
-rw-r--r--pkgs/tools/audio/beets/imagemagick-nix-path.patch20
-rw-r--r--pkgs/tools/audio/beets/patches/bash-completion-always-print.patch (renamed from pkgs/tools/audio/beets/bash-completion-always-print.patch)0
-rw-r--r--pkgs/tools/audio/beets/plugins/alternatives.nix6
-rw-r--r--pkgs/tools/audio/beets/plugins/copyartifacts.nix6
-rw-r--r--pkgs/tools/audio/beets/plugins/extrafiles.nix6
-rw-r--r--pkgs/tools/audio/beets/replaygain-default-ffmpeg.patch26
11 files changed, 280 insertions, 382 deletions
diff --git a/pkgs/tools/audio/beets/badfiles-plugin-nix-paths.patch b/pkgs/tools/audio/beets/badfiles-plugin-nix-paths.patch
deleted file mode 100644
index 6956183344c..00000000000
--- a/pkgs/tools/audio/beets/badfiles-plugin-nix-paths.patch
+++ /dev/null
@@ -1,21 +0,0 @@
-diff --git i/beetsplug/badfiles.py w/beetsplug/badfiles.py
-index 36b45de3..5208b696 100644
---- i/beetsplug/badfiles.py
-+++ w/beetsplug/badfiles.py
-@@ -71,14 +71,14 @@ class BadFiles(BeetsPlugin):
-         return status, errors, [line for line in output.split("\n") if line]
- 
-     def check_mp3val(self, path):
--        status, errors, output = self.run_command(["mp3val", path])
-+        status, errors, output = self.run_command(["@mp3val@/bin/mp3val", path])
-         if status == 0:
-             output = [line for line in output if line.startswith("WARNING:")]
-             errors = len(output)
-         return status, errors, output
- 
-     def check_flac(self, path):
--        return self.run_command(["flac", "-wst", path])
-+        return self.run_command(["@flac@/bin/flac", "-wst", path])
- 
-     def check_custom(self, command):
-         def checker(path):
diff --git a/pkgs/tools/audio/beets/builtin-plugins.nix b/pkgs/tools/audio/beets/builtin-plugins.nix
new file mode 100644
index 00000000000..ee7da8d2033
--- /dev/null
+++ b/pkgs/tools/audio/beets/builtin-plugins.nix
@@ -0,0 +1,94 @@
+{ stdenv, lib
+, aacgain ? null
+, essentia-extractor ? null
+, ffmpeg ? null
+, flac ? null
+, imagemagick ? null
+, keyfinder-cli ? null
+, mp3gain ? null
+, mp3val ? null
+, python3Packages
+, ...
+}: {
+  absubmit = {
+    enable = lib.elem stdenv.hostPlatform essentia-extractor.meta.platforms;
+    wrapperBins = [ essentia-extractor ];
+  };
+  acousticbrainz.propagatedBuildInputs = [ python3Packages.requests ];
+  albumtypes = { };
+  aura.propagatedBuildInputs = with python3Packages; [ flask pillow ];
+  badfiles.wrapperBins = [ mp3val flac ];
+  bareasc = { };
+  beatport.propagatedBuildInputs = [ python3Packages.requests-oauthlib ];
+  bench = { };
+  bpd = { };
+  bpm = { };
+  bpsync = { };
+  bucket = { };
+  chroma.propagatedBuildInputs = [ python3Packages.pyacoustid ];
+  convert.wrapperBins = [ ffmpeg ];
+  deezer.propagatedBuildInputs = [ python3Packages.requests ];
+  discogs.propagatedBuildInputs = with python3Packages; [ discogs-client requests ];
+  duplicates = { };
+  edit = { };
+  embedart = {
+    propagatedBuildInputs = with python3Packages; [ pillow ];
+    wrapperBins = [ imagemagick ];
+  };
+  embyupdate.propagatedBuildInputs = [ python3Packages.requests ];
+  export = { };
+  fetchart = {
+    propagatedBuildInputs = with python3Packages; [ requests pillow ];
+    wrapperBins = [ imagemagick ];
+  };
+  filefilter = { };
+  fish = { };
+  freedesktop = { };
+  fromfilename = { };
+  ftintitle = { };
+  fuzzy = { };
+  gmusic = { };
+  hook = { };
+  ihate = { };
+  importadded = { };
+  importfeeds = { };
+  info = { };
+  inline = { };
+  ipfs = { };
+  keyfinder.wrapperBins = [ keyfinder-cli ];
+  kodiupdate.propagatedBuildInputs = [ python3Packages.requests ];
+  lastgenre.propagatedBuildInputs = [ python3Packages.pylast ];
+  lastimport.propagatedBuildInputs = [ python3Packages.pylast ];
+  loadext.propagatedBuildInputs = [ python3Packages.requests ];
+  lyrics.propagatedBuildInputs = [ python3Packages.beautifulsoup4 ];
+  mbcollection = { };
+  mbsubmit = { };
+  mbsync = { };
+  metasync = { };
+  missing = { };
+  mpdstats.propagatedBuildInputs = [ python3Packages.mpd2 ];
+  mpdupdate.propagatedBuildInputs = [ python3Packages.mpd2 ];
+  parentwork = { };
+  permissions = { };
+  play = { };
+  playlist.propagatedBuildInputs = [ python3Packages.requests ];
+  plexupdate = { };
+  random = { };
+  replaygain.wrapperBins = [ aacgain ffmpeg mp3gain ];
+  rewrite = { };
+  scrub = { };
+  smartplaylist = { };
+  sonosupdate.propagatedBuildInputs = [ python3Packages.soco ];
+  spotify = { };
+  subsonicplaylist.propagatedBuildInputs = [ python3Packages.requests ];
+  subsonicupdate.propagatedBuildInputs = [ python3Packages.requests ];
+  the = { };
+  thumbnails = {
+    propagatedBuildInputs = with python3Packages; [ pillow pyxdg ];
+    wrapperBins = [ imagemagick ];
+  };
+  types.testPaths = [ "test/test_types_plugin.py" ];
+  unimported = { };
+  web.propagatedBuildInputs = [ python3Packages.flask ];
+  zero = { };
+}
diff --git a/pkgs/tools/audio/beets/common.nix b/pkgs/tools/audio/beets/common.nix
new file mode 100644
index 00000000000..51391a639cc
--- /dev/null
+++ b/pkgs/tools/audio/beets/common.nix
@@ -0,0 +1,147 @@
+{ stdenv, lib
+, bashInteractive
+, diffPlugins
+, glibcLocales
+, gobject-introspection
+, gst_all_1
+, python3Packages
+, runtimeShell
+, writeScript
+
+  # plugin deps
+, aacgain ? null
+, essentia-extractor ? null
+, ffmpeg ? null
+, flac ? null
+, imagemagick ? null
+, keyfinder-cli ? null
+, mp3gain ? null
+, mp3val ? null
+
+, src
+, version
+, pluginOverrides ? { }
+, disableAllPlugins ? false
+}@inputs:
+let
+  inherit (lib) attrNames attrValues concatMap;
+
+  builtinPlugins = import ./builtin-plugins.nix inputs;
+
+  mkPlugin = { enable ? !disableAllPlugins, propagatedBuildInputs ? [ ], testPaths ? [ ], wrapperBins ? [ ] }: {
+    inherit enable propagatedBuildInputs testPaths wrapperBins;
+  };
+
+  allPlugins = lib.mapAttrs (_: mkPlugin) (lib.recursiveUpdate builtinPlugins pluginOverrides);
+  enabledPlugins = lib.filterAttrs (_: p: p.enable) allPlugins;
+  disabledPlugins = lib.filterAttrs (_: p: !p.enable) allPlugins;
+
+  pluginWrapperBins = concatMap (p: p.wrapperBins) (attrValues enabledPlugins);
+in
+python3Packages.buildPythonApplication rec {
+  pname = "beets";
+  inherit src version;
+
+  patches = [
+    # Bash completion fix for Nix
+    ./patches/bash-completion-always-print.patch
+  ];
+
+  propagatedBuildInputs = with python3Packages; [
+    confuse
+    enum34
+    gobject-introspection
+    gst-python
+    jellyfish
+    mediafile
+    munkres
+    musicbrainzngs
+    mutagen
+    pygobject3
+    pyyaml
+    reflink
+    unidecode
+  ] ++ (concatMap (p: p.propagatedBuildInputs) (attrValues enabledPlugins));
+
+  buildInputs = [
+  ] ++ (with gst_all_1; [
+    gst-plugins-base
+    gst-plugins-good
+    gst-plugins-ugly
+  ]);
+
+  postInstall = ''
+    mkdir -p $out/share/zsh/site-functions
+    cp extra/_beet $out/share/zsh/site-functions/
+  '';
+
+  doInstallCheck = true;
+
+  installCheckPhase = ''
+    runHook preInstallCheck
+
+    tmphome="$(mktemp -d)"
+
+    EDITOR="${writeScript "beetconfig.sh" ''
+      #!${runtimeShell}
+      cat > "$1" <<CFG
+      plugins: ${lib.concatStringsSep " " (attrNames enabledPlugins)}
+      CFG
+    ''}" HOME="$tmphome" "$out/bin/beet" config -e
+    EDITOR=true HOME="$tmphome" "$out/bin/beet" config -e
+
+    runHook postInstallCheck
+  '';
+
+  makeWrapperArgs = [
+    "--set GI_TYPELIB_PATH \"$GI_TYPELIB_PATH\""
+    "--set GST_PLUGIN_SYSTEM_PATH_1_0 \"$GST_PLUGIN_SYSTEM_PATH_1_0\""
+    "--prefix PATH : ${lib.makeBinPath pluginWrapperBins}"
+  ];
+
+  checkInputs = with python3Packages; [
+    pytest
+    mock
+    rarfile
+    responses
+  ] ++ pluginWrapperBins;
+
+  disabledTestPaths = lib.flatten (attrValues (lib.mapAttrs (n: v: v.testPaths ++ [ "test/test_${n}.py" ]) disabledPlugins));
+
+  checkPhase = ''
+    runHook preCheck
+
+    # Check for undefined plugins
+    find beetsplug -mindepth 1 \
+      \! -path 'beetsplug/__init__.py' -a \
+      \( -name '*.py' -o -path 'beetsplug/*/__init__.py' \) -print \
+      | sed -n -re 's|^beetsplug/([^/.]+).*|\1|p' \
+      | sort -u > plugins_available
+    ${diffPlugins (attrNames allPlugins) "plugins_available"}
+
+    export BEETS_TEST_SHELL="${bashInteractive}/bin/bash --norc"
+    export HOME="$(mktemp -d)"
+
+    args=" -m pytest -r fEs"
+    eval "disabledTestPaths=($disabledTestPaths)"
+    for path in ''${disabledTestPaths[@]}; do
+      if [ -e "$path" ]; then
+        args+=" --ignore \"$path\""
+      else
+        echo "Skipping non-existent test path '$path'"
+      fi
+    done
+
+    eval "python $args"
+
+    runHook postCheck
+  '';
+
+  meta = with lib; {
+    description = "Music tagger and library organizer";
+    homepage = "https://beets.io";
+    license = licenses.mit;
+    maintainers = with maintainers; [ aszlig doronbehar lovesegfault pjones ];
+    platforms = platforms.linux;
+  };
+}
diff --git a/pkgs/tools/audio/beets/convert-plugin-ffmpeg-path.patch b/pkgs/tools/audio/beets/convert-plugin-ffmpeg-path.patch
deleted file mode 100644
index 1bc17893448..00000000000
--- a/pkgs/tools/audio/beets/convert-plugin-ffmpeg-path.patch
+++ /dev/null
@@ -1,34 +0,0 @@
-diff --git i/beetsplug/convert.py w/beetsplug/convert.py
-index 6bc07c28..039fb452 100644
---- i/beetsplug/convert.py
-+++ w/beetsplug/convert.py
-@@ -118,22 +118,22 @@ class ConvertPlugin(BeetsPlugin):
-             'id3v23': 'inherit',
-             'formats': {
-                 'aac': {
--                    'command': 'ffmpeg -i $source -y -vn -acodec aac '
-+                    'command': '@ffmpeg@/bin/ffmpeg -i $source -y -vn -acodec aac '
-                     '-aq 1 $dest',
-                     'extension': 'm4a',
-                 },
-                 'alac': {
--                    'command': 'ffmpeg -i $source -y -vn -acodec alac $dest',
-+                    'command': '@ffmpeg@/bin/ffmpeg -i $source -y -vn -acodec alac $dest',
-                     'extension': 'm4a',
-                 },
--                'flac': 'ffmpeg -i $source -y -vn -acodec flac $dest',
--                'mp3': 'ffmpeg -i $source -y -vn -aq 2 $dest',
-+                'flac': '@ffmpeg@/bin/ffmpeg -i $source -y -vn -acodec flac $dest',
-+                'mp3': '@ffmpeg@/bin/ffmpeg -i $source -y -vn -aq 2 $dest',
-                 'opus':
--                    'ffmpeg -i $source -y -vn -acodec libopus -ab 96k $dest',
-+                    '@ffmpeg@/bin/ffmpeg -i $source -y -vn -acodec libopus -ab 96k $dest',
-                 'ogg':
--                    'ffmpeg -i $source -y -vn -acodec libvorbis -aq 3 $dest',
-+                    '@ffmpeg@/bin/ffmpeg -i $source -y -vn -acodec libvorbis -aq 3 $dest',
-                 'wma':
--                    'ffmpeg -i $source -y -vn -acodec wmav2 -vn $dest',
-+                    '@ffmpeg@/bin/ffmpeg -i $source -y -vn -acodec wmav2 -vn $dest',
-             },
-             'max_bitrate': 500,
-             'auto': False,
diff --git a/pkgs/tools/audio/beets/default.nix b/pkgs/tools/audio/beets/default.nix
index 0bdbe9d345d..d312ec9b627 100644
--- a/pkgs/tools/audio/beets/default.nix
+++ b/pkgs/tools/audio/beets/default.nix
@@ -1,279 +1,37 @@
-{ stdenv
-, lib
+{ lib
+, callPackage
 , fetchFromGitHub
-, writeScript
-, glibcLocales
-, diffPlugins
-, substituteAll
-, pythonPackages
-# can be null, if you wish to disable a reference to it. It's needed for the
-# artresizer, see:
-# https://beets.readthedocs.io/en/v1.6.0/plugins/fetchart.html#image-resizing
-, imagemagick
-, gobject-introspection
-, gst_all_1
-, runtimeShell
-
-# external plugins package set
-, beetsExternalPlugins
-
-, enableAbsubmit         ? lib.elem stdenv.hostPlatform.system essentia-extractor.meta.platforms, essentia-extractor
-, enableAcousticbrainz   ? true
-, enableAcoustid         ? true
-, enableAura             ? true
-, enableBadfiles         ? true, flac, mp3val
-, enableBeatport         ? true
-, enableBpsync           ? true
-, enableConvert          ? true, ffmpeg
-, enableDeezer           ? true
-, enableDiscogs          ? true
-, enableEmbyupdate       ? true
-, enableFetchart         ? true
-, enableKeyfinder        ? true, keyfinder-cli
-, enableKodiupdate       ? true
-, enableLastfm           ? true
-, enableLoadext          ? true
-, enableLyrics           ? true
-, enableMpd              ? true
-, enablePlaylist         ? true
-, enableReplaygain       ? true
-, enableSonosUpdate      ? true
-, enableSubsonicplaylist ? true
-, enableSubsonicupdate   ? true
-, enableThumbnails       ? true
-, enableWeb              ? true
-
-# External plugins
-, enableAlternatives     ? false
-, enableCopyArtifacts    ? false
-, enableExtraFiles       ? false
-
-, bashInteractive, bash-completion
 }:
 
-assert enableBpsync      -> enableBeatport;
-
-let
-  optionalPlugins = {
-    absubmit = enableAbsubmit;
-    acousticbrainz = enableAcousticbrainz;
-    aura = enableAura;
-    badfiles = enableBadfiles;
-    beatport = enableBeatport;
-    bpsync = enableBpsync;
-    chroma = enableAcoustid;
-    convert = enableConvert;
-    deezer = enableDeezer;
-    discogs = enableDiscogs;
-    embyupdate = enableEmbyupdate;
-    fetchart = enableFetchart;
-    keyfinder = enableKeyfinder;
-    kodiupdate = enableKodiupdate;
-    lastgenre = enableLastfm;
-    lastimport = enableLastfm;
-    loadext = enableLoadext;
-    lyrics = enableLyrics;
-    mpdstats = enableMpd;
-    mpdupdate = enableMpd;
-    playlist = enablePlaylist;
-    replaygain = enableReplaygain;
-    sonosupdate = enableSonosUpdate;
-    subsonicplaylist = enableSubsonicplaylist;
-    subsonicupdate = enableSubsonicupdate;
-    thumbnails = enableThumbnails;
-    web = enableWeb;
-  };
-
-  pluginsWithoutDeps = [
-    "albumtypes" "bareasc" "bench" "bpd" "bpm" "bucket" "duplicates" "edit" "embedart"
-    "export" "filefilter" "fish" "freedesktop" "fromfilename" "ftintitle" "fuzzy"
-    "hook" "ihate" "importadded" "importfeeds" "info" "inline" "ipfs" "gmusic"
-    "mbcollection" "mbsubmit" "mbsync" "metasync" "missing" "parentwork" "permissions" "play"
-    "plexupdate" "random" "rewrite" "scrub" "smartplaylist" "spotify" "the"
-    "types" "unimported" "zero"
-  ];
-
-  enabledOptionalPlugins = lib.attrNames (lib.filterAttrs (_: lib.id) optionalPlugins);
-
-  allPlugins = pluginsWithoutDeps ++ lib.attrNames optionalPlugins;
-  allEnabledPlugins = pluginsWithoutDeps ++ enabledOptionalPlugins;
-
-  testShell = "${bashInteractive}/bin/bash --norc";
-  completion = "${bash-completion}/share/bash-completion/bash_completion";
-
-in pythonPackages.buildPythonApplication rec {
-  pname = "beets";
-  version = "1.6.0";
-
-  src = fetchFromGitHub {
-    owner = "beetbox";
-    repo = "beets";
-    rev = "v${version}";
-    sha256 = "sha256-fT+rCJJQR7bdfAcmeFRaknmh4ZOP4RCx8MXpq7/D8tM=";
+lib.makeExtensible (self: {
+  beets = self.beets-stable;
+
+  beets-stable = callPackage ./common.nix rec {
+    version = "1.6.0";
+    src = fetchFromGitHub {
+      owner = "beetbox";
+      repo = "beets";
+      rev = "v${version}";
+      hash = "sha256-fT+rCJJQR7bdfAcmeFRaknmh4ZOP4RCx8MXpq7/D8tM=";
+    };
   };
 
-  propagatedBuildInputs = [
-    pythonPackages.six
-    pythonPackages.enum34
-    pythonPackages.jellyfish
-    pythonPackages.munkres
-    pythonPackages.musicbrainzngs
-    pythonPackages.mutagen
-    pythonPackages.pyyaml
-    pythonPackages.unidecode
-    pythonPackages.gst-python
-    pythonPackages.pygobject3
-    pythonPackages.reflink
-    pythonPackages.confuse
-    pythonPackages.mediafile
-    gobject-introspection
-  ] ++ lib.optional enableAbsubmit         essentia-extractor
-    ++ lib.optional enableAcoustid         pythonPackages.pyacoustid
-    ++ lib.optional enableBeatport         pythonPackages.requests-oauthlib
-    ++ lib.optional enableConvert          ffmpeg
-    ++ lib.optional enableDiscogs          pythonPackages.discogs-client
-    ++ lib.optional (enableFetchart
-                  || enableDeezer
-                  || enableEmbyupdate
-                  || enableKodiupdate
-                  || enableLoadext
-                  || enablePlaylist
-                  || enableSubsonicplaylist
-                  || enableSubsonicupdate
-                  || enableAcousticbrainz) pythonPackages.requests
-    ++ lib.optional enableKeyfinder        keyfinder-cli
-    ++ lib.optional enableLastfm           pythonPackages.pylast
-    ++ lib.optional enableLyrics           pythonPackages.beautifulsoup4
-    ++ lib.optional enableMpd              pythonPackages.mpd2
-    ++ lib.optional enableSonosUpdate      pythonPackages.soco
-    ++ lib.optional enableThumbnails       pythonPackages.pyxdg
-    ++ lib.optional (enableAura
-                  || enableWeb)            pythonPackages.flask
-    ++ lib.optional enableAlternatives     beetsExternalPlugins.alternatives
-    ++ lib.optional enableCopyArtifacts    beetsExternalPlugins.copyartifacts
-    ++ lib.optional enableExtraFiles       beetsExternalPlugins.extrafiles
-  ;
-
-  buildInputs = [
-  ] ++ (with gst_all_1; [
-    gst-plugins-base
-    gst-plugins-good
-    gst-plugins-ugly
-  ]);
-
-  checkInputs = with pythonPackages; [
-    beautifulsoup4
-    mock
-    nose
-    rarfile
-    responses
-    # Although considered as plugin dependencies, they are needed for the
-    # tests, for disabling them via an override makes the build fail. see:
-    # https://github.com/beetbox/beets/blob/v1.6.0/setup.py
-    pylast
-    mpd2
-    discogs-client
-    pyxdg
-  ];
-
-  patches = [
-    # Bash completion fix for Nix
-    ./bash-completion-always-print.patch
-  ]
-    # Fix path to imagemagick, used for the artresizer.py file. This reference
-    # to imagemagick might be expensive for some people, so the patch can be
-    # disabled if imagemagick is set to null
-    ++ lib.optional (imagemagick != null) (substituteAll {
-      src = ./imagemagick-nix-path.patch;
-      inherit imagemagick;
-    })
-    # We need to force ffmpeg as the default, since we do not package
-    # bs1770gain, and set the absolute path there, to avoid impurities.
-    ++ lib.optional enableReplaygain (substituteAll {
-      src = ./replaygain-default-ffmpeg.patch;
-      ffmpeg = lib.getBin ffmpeg;
-    })
-    # Put absolute Nix paths in place
-    ++ lib.optional enableConvert (substituteAll {
-      src = ./convert-plugin-ffmpeg-path.patch;
-      ffmpeg = lib.getBin ffmpeg;
-    })
-    ++ lib.optional enableBadfiles (substituteAll {
-      src = ./badfiles-plugin-nix-paths.patch;
-      inherit mp3val flac;
-    })
-  ;
-
-  # Disable failing tests
-  postPatch = ''
-    echo echo completion tests passed > test/rsrc/test_completion.sh
-
-    # https://github.com/beetbox/beets/issues/1187
-    sed -i -e 's/len(mf.images)/0/' test/test_zero.py
-  '';
-
-  postInstall = ''
-    mkdir -p $out/share/zsh/site-functions
-    cp extra/_beet $out/share/zsh/site-functions/
-  '';
-
-  doCheck = true;
-
-  preCheck = ''
-    find beetsplug -mindepth 1 \
-      \! -path 'beetsplug/__init__.py' -a \
-      \( -name '*.py' -o -path 'beetsplug/*/__init__.py' \) -print \
-      | sed -n -re 's|^beetsplug/([^/.]+).*|\1|p' \
-      | sort -u > plugins_available
-
-     ${diffPlugins allPlugins "plugins_available"}
-  '';
-
-  checkPhase = ''
-    runHook preCheck
-
-    LANG=en_US.UTF-8 \
-    LOCALE_ARCHIVE=${assert stdenv.isLinux; glibcLocales}/lib/locale/locale-archive \
-    BEETS_TEST_SHELL="${testShell}" \
-    BASH_COMPLETION_SCRIPT="${completion}" \
-    HOME="$(mktemp -d)" nosetests -v
-
-    runHook postCheck
-  '';
-
-  doInstallCheck = true;
-
-  installCheckPhase = ''
-    runHook preInstallCheck
-
-    tmphome="$(mktemp -d)"
-
-    EDITOR="${writeScript "beetconfig.sh" ''
-      #!${runtimeShell}
-      cat > "$1" <<CFG
-      plugins: ${lib.concatStringsSep " " allEnabledPlugins}
-      CFG
-    ''}" HOME="$tmphome" "$out/bin/beet" config -e
-    EDITOR=true HOME="$tmphome" "$out/bin/beet" config -e
-
-    runHook postInstallCheck
-  '';
-
-  makeWrapperArgs = [
-    "--set GI_TYPELIB_PATH \"$GI_TYPELIB_PATH\""
-    "--set GST_PLUGIN_SYSTEM_PATH_1_0 \"$GST_PLUGIN_SYSTEM_PATH_1_0\""
-  ];
-
-  passthru = {
-    # FIXME: remove in favor of pkgs.beetsExternalPlugins
-    externalPlugins = beetsExternalPlugins;
+  beets-minimal = self.beets.override { disableAllPlugins = true; };
+
+  beets-unstable = callPackage ./common.nix {
+    version = "unstable-2022-05-08";
+    src = fetchFromGitHub {
+      owner = "beetbox";
+      repo = "beets";
+      rev = "e06cf7969bfdfa4773049699320471be45d56054";
+      hash = "sha256-yWwxYSzSSmx2UfCn0EBH23hQGZKSRn/c8ryvxLUeHdM=";
+    };
+    pluginOverrides = {
+      limit = { };
+    };
   };
 
-  meta = with lib; {
-    description = "Music tagger and library organizer";
-    homepage = "https://beets.io";
-    license = licenses.mit;
-    maintainers = with maintainers; [ aszlig doronbehar lovesegfault pjones ];
-    platforms = platforms.linux;
-  };
-}
+  beets-alternatives = callPackage ./plugins/alternatives.nix { beets = self.beets-minimal; };
+  beets-copyartifacts = callPackage ./plugins/copyartifacts.nix { beets = self.beets-minimal; };
+  beets-extrafiles = callPackage ./plugins/extrafiles.nix { beets = self.beets-minimal; };
+})
diff --git a/pkgs/tools/audio/beets/imagemagick-nix-path.patch b/pkgs/tools/audio/beets/imagemagick-nix-path.patch
deleted file mode 100644
index 9a77703ede7..00000000000
--- a/pkgs/tools/audio/beets/imagemagick-nix-path.patch
+++ /dev/null
@@ -1,20 +0,0 @@
-diff --git i/beets/util/artresizer.py w/beets/util/artresizer.py
-index 8683e228..2f38b4d6 100644
---- i/beets/util/artresizer.py
-+++ w/beets/util/artresizer.py
-@@ -334,13 +334,8 @@ class ArtResizer(metaclass=Shareable):
-         # not, fall back to the older, separate convert and identify
-         # commands.
-         if self.method[0] == IMAGEMAGICK:
--            self.im_legacy = self.method[2]
--            if self.im_legacy:
--                self.im_convert_cmd = ['convert']
--                self.im_identify_cmd = ['identify']
--            else:
--                self.im_convert_cmd = ['magick']
--                self.im_identify_cmd = ['magick', 'identify']
-+            self.im_convert_cmd = ['@imagemagick@/bin/magick']
-+            self.im_identify_cmd = ['@imagemagick@/bin/magick', 'identify']
- 
-     def resize(
-         self, maxwidth, path_in, path_out=None, quality=0, max_filesize=0
diff --git a/pkgs/tools/audio/beets/bash-completion-always-print.patch b/pkgs/tools/audio/beets/patches/bash-completion-always-print.patch
index 8a31fe22f72..8a31fe22f72 100644
--- a/pkgs/tools/audio/beets/bash-completion-always-print.patch
+++ b/pkgs/tools/audio/beets/patches/bash-completion-always-print.patch
diff --git a/pkgs/tools/audio/beets/plugins/alternatives.nix b/pkgs/tools/audio/beets/plugins/alternatives.nix
index 146e9f50664..635854d4576 100644
--- a/pkgs/tools/audio/beets/plugins/alternatives.nix
+++ b/pkgs/tools/audio/beets/plugins/alternatives.nix
@@ -1,6 +1,6 @@
-{ lib, fetchFromGitHub, beets, pythonPackages }:
+{ lib, fetchFromGitHub, beets, python3Packages }:
 
-pythonPackages.buildPythonApplication rec {
+python3Packages.buildPythonApplication rec {
   pname = "beets-alternatives";
   version = "unstable-2021-02-01";
 
@@ -18,7 +18,7 @@ pythonPackages.buildPythonApplication rec {
 
   nativeBuildInputs = [ beets ];
 
-  checkInputs = with pythonPackages; [
+  checkInputs = with python3Packages; [
     pytestCheckHook
     mock
   ];
diff --git a/pkgs/tools/audio/beets/plugins/copyartifacts.nix b/pkgs/tools/audio/beets/plugins/copyartifacts.nix
index 2f1ecdfc369..a1e8470c4e5 100644
--- a/pkgs/tools/audio/beets/plugins/copyartifacts.nix
+++ b/pkgs/tools/audio/beets/plugins/copyartifacts.nix
@@ -1,6 +1,6 @@
-{ lib, fetchFromGitHub, beets, pythonPackages, glibcLocales }:
+{ lib, fetchFromGitHub, beets, python3Packages, glibcLocales }:
 
-pythonPackages.buildPythonApplication {
+python3Packages.buildPythonApplication {
   pname = "beets-copyartifacts";
   version = "unstable-2020-02-15";
 
@@ -22,7 +22,7 @@ pythonPackages.buildPythonApplication {
            tests/test_reimport.py
   '';
 
-  nativeBuildInputs = [ beets pythonPackages.nose glibcLocales ];
+  nativeBuildInputs = [ beets python3Packages.nose glibcLocales ];
 
   checkPhase = "LANG=en_US.UTF-8 nosetests";
 
diff --git a/pkgs/tools/audio/beets/plugins/extrafiles.nix b/pkgs/tools/audio/beets/plugins/extrafiles.nix
index 0d3ccc0d7a7..a6a5eb1e1bd 100644
--- a/pkgs/tools/audio/beets/plugins/extrafiles.nix
+++ b/pkgs/tools/audio/beets/plugins/extrafiles.nix
@@ -1,6 +1,6 @@
-{ lib, fetchFromGitHub, beets, pythonPackages }:
+{ lib, fetchFromGitHub, beets, python3Packages }:
 
-pythonPackages.buildPythonApplication rec {
+python3Packages.buildPythonApplication rec {
   pname = "beets-extrafiles";
   version = "unstable-2020-12-13";
 
@@ -19,7 +19,7 @@ pythonPackages.buildPythonApplication rec {
 
   nativeBuildInputs = [ beets ];
 
-  propagatedBuildInputs = with pythonPackages; [ mediafile ];
+  propagatedBuildInputs = with python3Packages; [ mediafile ];
 
   preCheck = ''
     HOME=$TEMPDIR
diff --git a/pkgs/tools/audio/beets/replaygain-default-ffmpeg.patch b/pkgs/tools/audio/beets/replaygain-default-ffmpeg.patch
deleted file mode 100644
index e441997cae5..00000000000
--- a/pkgs/tools/audio/beets/replaygain-default-ffmpeg.patch
+++ /dev/null
@@ -1,26 +0,0 @@
-diff --git i/beetsplug/replaygain.py w/beetsplug/replaygain.py
-index b6297d93..5c1cbbc0 100644
---- i/beetsplug/replaygain.py
-+++ w/beetsplug/replaygain.py
-@@ -139,7 +139,7 @@ class FfmpegBackend(Backend):
- 
-     def __init__(self, config, log):
-         super().__init__(config, log)
--        self._ffmpeg_path = "ffmpeg"
-+        self._ffmpeg_path = "@ffmpeg@/bin/ffmpeg"
- 
-         # check that ffmpeg is installed
-         try:
-@@ -975,11 +975,10 @@ class ReplayGainPlugin(BeetsPlugin):
-     def __init__(self):
-         super().__init__()
- 
--        # default backend is 'command' for backward-compatibility.
-         self.config.add({
-             'overwrite': False,
-             'auto': True,
--            'backend': 'command',
-+            'backend': 'ffmpeg',
-             'threads': cpu_count(),
-             'parallel_on_import': False,
-             'per_disc': False,