Commit 01b46cc2d64b

Vincent Demeester <vincent@sbr.pm>
2025-12-17 12:19:03
feat(music): Add beets music library manager with Lidarr integration
Add comprehensive beets configuration with custom plugins: - Package beets-lidarr-fields (v1.1.2) for Lidarr-compatible path formatting - Package beets-filetote (v1.1.1) for managing non-music files during import - Configure beets with Lidarr-compatible paths using lidarr fields plugin - Enable filetote plugin for handling album art, lyrics, cue sheets, and logs - Set up smart playlists, artwork fetching, and ReplayGain - Configure test environment in ~/desktop/music/test - Create directory structure for library, soundtracks, compilations, singles, and podcasts Both custom plugins use pythonRemoveDeps to exclude beets from dependency checks, avoiding circular dependencies while using the official pluginOverrides mechanism for integration. Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: Vincent Demeester <vincent@sbr.pm>
1 parent c7bcfb5
Changed files (5)
home
common
services
pkgs
beets-filetote
beets-lidarr-fields
systems
kyushu
home/common/services/beets.nix
@@ -1,15 +1,29 @@
 { config, pkgs, ... }:
+let
+  # Override beets with our custom plugins using the official pluginOverrides mechanism
+  beetsWithPlugins = pkgs.beets.override {
+    python3 = pkgs.python3.override {
+      packageOverrides = _self: super: {
+        beets = super.beets.override {
+          pluginOverrides = {
+            lidarrfields = {
+              enable = true;
+              propagatedBuildInputs = [ pkgs.beets-lidarr-fields ];
+            };
+            filetote = {
+              enable = true;
+              propagatedBuildInputs = [ pkgs.beets-filetote ];
+            };
+          };
+        };
+      };
+    };
+  };
+in
 {
   programs.beets = {
     enable = true;
-
-    # Add extra Python packages to beets' environment
-    package = pkgs.beets.override {
-      extraPackages = [
-        pkgs.beets-lidarr-fields
-        pkgs.python3Packages.beets-filetote
-      ];
-    };
+    package = beetsWithPlugins;
 
     settings = {
       # Library paths
@@ -35,6 +49,7 @@
         "discogs"
         "fromfilename"
         "edit"
+        "musicbrainz"
         "smartplaylist"
         "lidarrfields" # Nix-packaged plugin for Lidarr-compatible paths
         "filetote" # Manage non-music files (art, cue, logs, lyrics)
@@ -42,25 +57,30 @@
 
       # Path formats using Lidarr-compatible fields
       paths = {
-        # Regular albums - using Lidarr fields for compatibility
-        default = "library/$lidarr_albumartist/$lidarr_album_title/$lidarr_track_title";
+        # Regular albums - using lidarr fields plugin format
+        # Format: Artist/Album (Year)/[Disc-]Track.Title
+        default = "library/$releasegroupartist/$lidarralbum%aunique{}/%if{$audiodisctotal,$disc-}$track.$title";
 
-        # Soundtracks - using Lidarr fields
-        "albumtype:soundtrack" = "soundtrack/$lidarr_album_title/$lidarr_track_title";
+        # Soundtracks - using lidarr fields
+        "albumtype:soundtrack" =
+          "soundtrack/$lidarralbum%aunique{}/%if{$audiodisctotal,$disc-}$track.$title";
 
         # Singletons (podcasts, DJ sets, etc)
-        singleton = "single/$lidarr_artist - $lidarr_title";
+        singleton = "single/$artist - $title";
 
-        # Compilations - using Lidarr fields
-        comp = "compilation/$lidarr_album_title/$lidarr_track_title";
+        # Compilations - using lidarr fields
+        comp = "compilation/$lidarralbum%aunique{}/%if{$audiodisctotal,$disc-}$track.$title";
 
         # Podcasts get their own path (not using Lidarr fields since not from Lidarr)
         "albumtype:podcast" = "podcasts/$album/$track - $title";
       };
 
+      # Enable per-disc track numbering for multi-disc albums
+      per_disc_numbering = true;
+
       # Alternative metadata sources
-      discogs.source_weight = 0.0;
-      musicbrainz.source_weight = 0.5;
+      discogs.data_source_mismatch_penalty = 0.5;
+      musicbrainz.data_source_mismatch_penalty = 0.0;
 
       # Artwork
       fetchart = {
@@ -73,6 +93,20 @@
         maxwidth = 1000;
       };
 
+      # Lyrics fetching
+      lyrics = {
+        auto = true;
+        sources = [
+          "lrclib"
+          "genius"
+          "musixmatch"
+        ];
+        # Prefer synced lyrics from lrclib
+        synced = true;
+        # Fall back to plain text if synced not available
+        fallback = "";
+      };
+
       # ReplayGain for volume normalization
       replaygain = {
         auto = true;
@@ -101,13 +135,30 @@
         # Covers: album art, scans, booklets
         # Audio related: cue sheets, ripping logs, accuracy logs
         # Text: lyrics, metadata files
-        extensions = ".cue .log .txt .jpg .jpeg .png .webp .gif .pdf .nfo .m3u .sfv .md5";
+        extensions = [
+          ".cue"
+          ".log"
+          ".txt"
+          ".jpg"
+          ".jpeg"
+          ".png"
+          ".webp"
+          ".gif"
+          ".pdf"
+          ".nfo"
+          ".m3u"
+          ".sfv"
+          ".md5"
+        ];
 
         # Pairing configuration - for lyrics files
         pairing = {
           enabled = true;
           pairing_only = false; # Also process non-paired files
-          extensions = ".lrc .txt"; # Paired lyrics files
+          extensions = [
+            ".lrc"
+            ".txt"
+          ]; # Paired lyrics files
         };
 
         # Exclude unwanted files
@@ -119,7 +170,13 @@
             "desktop.ini"
             ".directory"
           ];
-          extensions = ".torrent .url .htm .html"; # Download artifacts
+          # Download artifacts
+          extensions = [
+            ".torrent"
+            ".url"
+            ".htm"
+            ".html"
+          ];
         };
 
         # Path configuration for different file types
@@ -133,20 +190,20 @@
           "ext:.webp" = "$albumpath/cover";
 
           # Cue sheets and logs in album directory (using lidarr fields)
-          "ext:.cue" = "$albumpath/$lidarr_albumartist - $lidarr_album_title";
-          "ext:.log" = "$albumpath/$lidarr_albumartist - $lidarr_album_title";
-          "ext:.txt" = "$albumpath/$lidarr_albumartist - $lidarr_album_title";
+          "ext:.cue" = "$albumpath/$releasegroupartist - $lidarralbum";
+          "ext:.log" = "$albumpath/$releasegroupartist - $lidarralbum";
+          "ext:.txt" = "$albumpath/$releasegroupartist - $lidarralbum";
 
           # PDFs (booklets/scans) in album directory (using lidarr fields)
-          "ext:.pdf" = "$albumpath/$lidarr_albumartist - $lidarr_album_title";
+          "ext:.pdf" = "$albumpath/$releasegroupartist - $lidarralbum";
 
           # Paired lyrics files next to tracks
-          # $medianame_new is the new filename of the paired track (already using lidarr fields)
+          # $medianame_new is the new filename of the paired track
           "paired_ext:.lrc" = "$albumpath/$medianame_new";
           "paired_ext:.txt" = "$albumpath/$medianame_new";
 
           # NFO files in album directory (using lidarr fields)
-          "ext:.nfo" = "$albumpath/$lidarr_albumartist - $lidarr_album_title";
+          "ext:.nfo" = "$albumpath/$releasegroupartist - $lidarralbum";
         };
 
         # Print ignored files for debugging
pkgs/beets-filetote/default.nix
@@ -0,0 +1,59 @@
+{
+  lib,
+  buildPythonPackage,
+  fetchPypi,
+  poetry-core,
+  mediafile,
+  reflink,
+  toml,
+  typeguard,
+}:
+
+buildPythonPackage rec {
+  pname = "beets-filetote";
+  version = "1.1.1";
+  pyproject = true;
+
+  src = fetchPypi {
+    pname = "beets_filetote";
+    inherit version;
+    hash = "sha256-2u9Zhlwr/R7Q2Vxr4bs0B58lADg1n7qao8WRwRttCnk=";
+  };
+
+  postPatch = ''
+    substituteInPlace pyproject.toml --replace-fail "poetry-core<2.0.0" "poetry-core"
+  '';
+
+  build-system = [
+    poetry-core
+  ];
+
+  dependencies = [
+    mediafile
+    reflink
+    toml
+    typeguard
+  ];
+
+  # Don't check for beets dependency - it will be provided by the beets package
+  pythonRemoveDeps = [ "beets" ];
+
+  # Remove conflicting files from namespace package
+  postInstall = ''
+    rm -rf $out/lib/python*/site-packages/beetsplug/__init__.py
+    rm -rf $out/lib/python*/site-packages/beetsplug/__pycache__
+  '';
+
+  # Tests require a running beets setup
+  doCheck = false;
+
+  # Skip imports check - beets tries to create config dirs during import
+  pythonImportsCheck = [ ];
+
+  meta = with lib; {
+    description = "Beets plugin to copy/move non-music extra files, attachments, and artifacts during import";
+    homepage = "https://github.com/gtronset/beets-filetote";
+    license = licenses.mit;
+    maintainers = with maintainers; [ ];
+  };
+}
pkgs/beets-lidarr-fields/default.nix
@@ -1,10 +1,10 @@
 {
   lib,
-  python3Packages,
+  buildPythonPackage,
   fetchPypi,
 }:
 
-python3Packages.buildPythonPackage rec {
+buildPythonPackage rec {
   pname = "beets-lidarr-fields";
   version = "1.1.2";
   format = "setuptools";
@@ -14,16 +14,20 @@ python3Packages.buildPythonPackage rec {
     hash = "sha256-wlybw3v0QLfeIgbezz5PQdbGFsu9KLI1AkVQD4CrgI4=";
   };
 
-  propagatedBuildInputs = with python3Packages; [
-    beets-minimal
-  ];
+  # Don't check for beets dependency - it will be provided by the beets package
+  pythonRemoveDeps = [ "beets" ];
+
+  # Remove conflicting files from namespace package
+  postInstall = ''
+    rm -rf $out/lib/python*/site-packages/beetsplug/__init__.py
+    rm -rf $out/lib/python*/site-packages/beetsplug/__pycache__
+  '';
 
   # Tests require a running beets setup
   doCheck = false;
 
-  pythonImportsCheck = [
-    "beetsplug.lidarrfields"
-  ];
+  # Skip imports check to avoid needing beets at build time
+  pythonImportsCheck = [ ];
 
   meta = with lib; {
     description = "Beets plugin that defines useful template fields to customize path formats in a Lidarr-compatible way";
pkgs/default.nix
@@ -31,6 +31,7 @@ in
   jellyfin-auto-collections = pkgs.callPackage ./jellyfin-auto-collections { };
   music-playlist-dl = pkgs.callPackage ../tools/music-playlist-dl { };
   beets-lidarr-fields = pkgs.python3Packages.callPackage ./beets-lidarr-fields { };
+  beets-filetote = pkgs.python3Packages.callPackage ./beets-filetote { };
 
   chmouzies-ai = pkgs.callPackage ./chmouzies/ai.nix { };
   chmouzies-git = pkgs.callPackage ./chmouzies/git.nix { };
systems/kyushu/home.nix
@@ -53,6 +53,7 @@
     transmission_4-gtk
 
     forgejo-cli
+    jira-cli-go
 
     # lisp
     roswell