diff --git a/nix/hosts.nix b/nix/hosts.nix index 0996036b..8ff7d6a4 100644 --- a/nix/hosts.nix +++ b/nix/hosts.nix @@ -31,6 +31,7 @@ in cfg.modules.darwin.user-primary cfg.modules.darwin.agent-browser-shared cfg.modules.darwin.rosetta-builder + cfg.modules.darwin.slskd ]; }; kikir = { @@ -133,6 +134,7 @@ in cfg.modules.nixos.xremap cfg.modules.nixos.steam cfg.modules.nixos.prismlauncher + cfg.modules.nixos.slskd cfg.modules.nixos.gtnh-server cfg.modules.nixos.gtnh-backups ( diff --git a/nix/modules/slskd.nix b/nix/modules/slskd.nix new file mode 100644 index 00000000..59903089 --- /dev/null +++ b/nix/modules/slskd.nix @@ -0,0 +1,152 @@ +{ config, ... }: +let + cfg = config.flake; + inherit (cfg.paths) root; + sopsFile = root + "/sops/slskd.yaml"; +in +{ + config.flake.modules = { + darwin.slskd = + { + config, + pkgs, + primaryUser, + ... + }: + let + version = "0.26.0"; + stateDir = "${config.users.users.${primaryUser.name}.home}/Library/Application Support/slskd"; + directories = { + downloads = "${stateDir}/downloads"; + incomplete = "${stateDir}/incomplete"; + }; + package = pkgs.stdenvNoCC.mkDerivation { + pname = "slskd"; + inherit version; + src = pkgs.fetchzip { + url = "https://github.com/slskd/slskd/releases/download/${version}/slskd-${version}-osx-arm64.zip"; + hash = "sha256-vdoJy2ZpElYJA43mNZfExNK4Mpnw8ApVxVDzPq1s97I="; + stripRoot = false; + }; + installPhase = '' + runHook preInstall + mkdir -p "$out/libexec/slskd" + cp -R . "$out/libexec/slskd/" + chmod +x "$out/libexec/slskd/slskd" + runHook postInstall + ''; + meta = { + description = "Modern client-server application for the Soulseek file-sharing network"; + homepage = "https://github.com/slskd/slskd"; + license = pkgs.lib.licenses.agpl3Only; + platforms = [ "aarch64-darwin" ]; + }; + }; + settings = (pkgs.formats.yaml { }).generate "slskd.yml" { + inherit directories; + flags.no_version_check = true; + shares.directories = [ directories.downloads ]; + }; + in + { + launchd.user.agents.slskd = { + serviceConfig = { + ProgramArguments = [ + "${pkgs.writeShellScript "slskd" '' + set -euo pipefail + umask 0077 + mkdir -p ${pkgs.lib.escapeShellArg directories.incomplete} ${pkgs.lib.escapeShellArg directories.downloads} + export SLSKD_SLSK_USERNAME="$(<${ + config.sops.secrets."slskd/soulseek-username".path + })-${config.networking.hostName}" + export SLSKD_SLSK_PASSWORD="$(<${config.sops.secrets."slskd/soulseek-password".path})" + export SLSKD_USERNAME="$(<${config.sops.secrets."slskd/web-username".path})" + export SLSKD_PASSWORD="$(<${config.sops.secrets."slskd/web-password".path})" + cd ${package}/libexec/slskd + exec ${package}/libexec/slskd/slskd \ + --app-dir ${pkgs.lib.escapeShellArg stateDir} \ + --config ${settings} + ''}" + ]; + RunAtLoad = true; + KeepAlive.SuccessfulExit = false; + # Change the plist when credentials change so launchd restarts slskd with the new values. + EnvironmentVariables.SLSKD_SECRETS_HASH = + config.sops.secrets."slskd/soulseek-username".sopsFileHash; + }; + }; + + sops.secrets = + let + mkSecret = _: { + inherit sopsFile; + owner = primaryUser.name; + }; + in + pkgs.lib.genAttrs [ + "slskd/soulseek-username" + "slskd/soulseek-password" + "slskd/web-username" + "slskd/web-password" + ] mkSecret; + }; + + nixos.slskd = + { config, lib, ... }: + let + directories = { + downloads = "/var/lib/slskd/downloads"; + incomplete = "/var/lib/slskd/incomplete"; + }; + in + { + services.slskd = { + enable = true; + # The default group is slskd. Use users so the primary user and MusicGrabber can access completed files. + group = "users"; + environmentFile = config.sops.templates."slskd.env".path; + openFirewall = true; + settings = { + inherit directories; + shares.directories = [ directories.downloads ]; + }; + }; + + sops = { + secrets = { + "slskd/soulseek-username".sopsFile = sopsFile; + "slskd/soulseek-password".sopsFile = sopsFile; + "slskd/web-username".sopsFile = sopsFile; + "slskd/web-password".sopsFile = sopsFile; + }; + templates."slskd.env" = { + owner = "slskd"; + # Restart slskd after credential changes so it reloads the rendered environment file. + restartUnits = [ "slskd.service" ]; + content = '' + SLSKD_SLSK_USERNAME=${ + config.sops.placeholder."slskd/soulseek-username" + }-${config.networking.hostName} + SLSKD_SLSK_PASSWORD=${config.sops.placeholder."slskd/soulseek-password"} + SLSKD_USERNAME=${config.sops.placeholder."slskd/web-username"} + SLSKD_PASSWORD=${config.sops.placeholder."slskd/web-password"} + ''; + }; + }; + + systemd = { + services.slskd.serviceConfig = { + # Nixpkgs makes every shared directory read-only, but this directory also receives completed downloads. + ReadOnlyPaths = lib.mkForce [ ]; + StateDirectoryMode = "0750"; + UMask = "0007"; + WorkingDirectory = "${config.services.slskd.package}/lib/slskd"; + }; + tmpfiles.rules = [ + "d ${directories.incomplete} 0770 slskd users -" + "d ${directories.downloads} 0770 slskd users -" + ]; + }; + }; + }; +} diff --git a/sops/slskd.yaml b/sops/slskd.yaml new file mode 100644 index 00000000..3676e4fc --- /dev/null +++ b/sops/slskd.yaml @@ -0,0 +1,20 @@ +slskd: + soulseek-username: ENC[AES256_GCM,data:feMMR/aqs/EnAkKPPUuDGrM69UE=,iv:qwI7hMw//4YhfMQhhnWCMA2+Ak2NweDn5D1foVfWc4M=,tag:YcvfRSgo1ohnhJNBCB4Psw==,type:str] + soulseek-password: ENC[AES256_GCM,data:uVBxu5dsn/QsIC38oBCTLabtMiRs0/dh2iSAcxPdEB2O,iv:6/Zm3JMoAUqljezcEii3slPD+nbfsyrNj2mQJDjF2u4=,tag:0Pg/mjAQf1FKcQNEQg8IVQ==,type:str] + web-username: ENC[AES256_GCM,data:IaDZYxQ=,iv:FrgChB4v/5ul7BC8GqweGl//LCOc3xrHKMEY2d9pDP8=,tag:vE3+BvgJo42rjrsSuRprGw==,type:str] + web-password: ENC[AES256_GCM,data:vO5DhpcHgDwmolACPiyixZg1Jm10sp0BJHagL2zt8zDpQT1/gn2OSpz7D58=,iv:AD//s4t9PnUQ7xgLDbD1v3UXSf9eHLLzGKmUzN0tD1E=,tag:dOc5qbax30zOwoTC5Lk7uw==,type:str] +sops: + age: + - enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSArWUl5dTVDWFNhK003Q3Bu + bWY4RzN6dFUrMzVBSC9LUFF4bW1XQ1BVL1VnClpzZTUxWjhyblUySnFDUVNCRGow + S2lLWGdwdE9adEoyMzZXbE5WSTdqeTQKLS0tIGE1ZUl5UnZvWjNZSGVZQ282dmh0 + Nkg5SGJDQ1NIYzJtbnU5dXFWQ3lQZ28KZER5mRr650FJ2fHipze2KmFkvZ7hMJBw + cieRZ8yRR3NFskMFDI+wp8zga+zK/P97sf11zni5YjmsW+8Pr7iUZw== + -----END AGE ENCRYPTED FILE----- + recipient: age12l33pas8eptwjc7ewux3d8snyzfzwz0tn9qg5kw8le79fswmjgjqdjgyy6 + lastmodified: "2026-10-25T03:16:54Z" + mac: ENC[AES256_GCM,data:h0mRaqJNO9+osHptEtnQRfK1letH0juPFL8D4pEi42yDMRpiVwUs+dJEzyc+K2z9fYNRlQ1z29gQgOet0hVIJsmGsHeL6Z4THYGCdhIB2hY1+XB0F+ONTj/AQvZXBEzcJkzFwI0UiJtE0ZEDL4QRPIMrusVd8InmODvp3LKhhj8=,iv:g2XiVPR32lpBivGIcO4qTeQxxaZLmoU7xXW1TcDhnqg=,tag:J3CY/y/eeJ7VZfjyN0TeZA==,type:str] + unencrypted_suffix: _unencrypted + version: 3.13.3