diff --git a/default.nix b/default.nix new file mode 100644 index 0000000..01c82f4 --- /dev/null +++ b/default.nix @@ -0,0 +1,9 @@ +{ system ? builtins.currentSystem, pkgs ? import { inherit system; } +}: +let + flake = import ./flake.nix; + outputs = flake.outputs { + self = outputs; + nixpkgs = pkgs.path or ; + }; +in outputs.packages.${system}.default diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..2e5f9bf --- /dev/null +++ b/flake.lock @@ -0,0 +1,27 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1767116409, + "narHash": "sha256-5vKw92l1GyTnjoLzEagJy5V5mDFck72LiQWZSOnSicw=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "cad22e7d996aea55ecab064e84834289143e44a0", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..30316e7 --- /dev/null +++ b/flake.nix @@ -0,0 +1,70 @@ +{ + description = "Piper - A teal.fm scrobbler service for ATProto"; + inputs = { nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; }; + outputs = { self, nixpkgs }: + let + forAllSystems = nixpkgs.lib.genAttrs [ + "x86_64-linux" + "aarch64-linux" + "x86_64-darwin" + "aarch64-darwin" + ]; + nixpkgsFor = forAllSystems (system: import nixpkgs { inherit system; }); + + mkPiper = pkgs: + pkgs.buildGoModule { + pname = "teal-piper"; + version = "0.0.2"; + src = ./.; + vendorHash = "sha256-gYlVWk1TOUOB2J49smq9TyGw/6AQdyP/A6tzJsfe3kI="; + + nativeBuildInputs = [ pkgs.pkg-config ]; + buildInputs = [ pkgs.sqlite ]; + + env.CGO_ENABLED = 1; + subPackages = [ "cmd" ]; + ldflags = [ "-s" "-w" ]; + + postInstall = '' + mv $out/bin/cmd $out/bin/piper + ''; + + meta = with pkgs.lib; { + description = "A teal.fm tool for scrobbling music to ATProto PDSs"; + homepage = "https://github.com/teal-fm/piper"; + license = licenses.mit; + maintainers = [ ]; + mainProgram = "piper"; + }; + }; + in { + packages = forAllSystems (system: + let + pkgs = nixpkgsFor.${system}; + piper = mkPiper pkgs; + in { + default = piper; + teal-piper = piper; + }); + + apps = forAllSystems (system: + let piper = self.packages.${system}.default; + in { + default = { + type = "app"; + program = "${piper}/bin/piper"; + }; + }); + + devShells = forAllSystems (system: + let pkgs = nixpkgsFor.${system}; + in { + default = pkgs.mkShell { buildInputs = with pkgs; [ go air ]; }; + }); + + nixosModules.default = import ./nixos-module.nix; + nixosModules.teal-piper = import ./nixos-module.nix; + + overlays.default = final: prev: { teal-piper = mkPiper final; }; + }; +} diff --git a/nixos-module.nix b/nixos-module.nix new file mode 100644 index 0000000..10e8dd4 --- /dev/null +++ b/nixos-module.nix @@ -0,0 +1,187 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + cfg = config.services.teal-piper; + + # Helper function to generate environment variables + settingsFormat = pkgs.formats.keyValue { }; + + # Auto-derive URLs from SERVER_ROOT_URL if not explicitly set + defaultSettings = { + SERVER_PORT = cfg.settings.SERVER_PORT or 8080; + SERVER_HOST = cfg.settings.SERVER_HOST or "localhost"; + DB_PATH = cfg.settings.DB_PATH or "${cfg.dataDir}/piper.db"; + TRACKER_INTERVAL = cfg.settings.TRACKER_INTERVAL or 30; + + # Spotify defaults + SPOTIFY_AUTH_URL = + cfg.settings.SPOTIFY_AUTH_URL or "https://accounts.spotify.com/authorize"; + SPOTIFY_TOKEN_URL = + cfg.settings.SPOTIFY_TOKEN_URL or "https://accounts.spotify.com/api/token"; + SPOTIFY_SCOPES = + cfg.settings.SPOTIFY_SCOPES or "user-read-currently-playing user-read-email"; + }; + + # Auto-derive callback URLs if SERVER_ROOT_URL is set + derivedSettings = optionalAttrs (cfg.settings ? SERVER_ROOT_URL) { + ATPROTO_CLIENT_ID = + cfg.settings.ATPROTO_CLIENT_ID or "${cfg.settings.SERVER_ROOT_URL}/oauth-client-metadata.json"; + ATPROTO_METADATA_URL = + cfg.settings.ATPROTO_METADATA_URL or "${cfg.settings.SERVER_ROOT_URL}/oauth-client-metadata.json"; + ATPROTO_CALLBACK_URL = + cfg.settings.ATPROTO_CALLBACK_URL or "${cfg.settings.SERVER_ROOT_URL}/callback/atproto"; + CALLBACK_SPOTIFY = + cfg.settings.CALLBACK_SPOTIFY or "${cfg.settings.SERVER_ROOT_URL}/callback/spotify"; + }; + + # Merge user settings with defaults and derived settings + finalSettings = defaultSettings // cfg.settings // derivedSettings; + + # Create environment file + settingsFile = settingsFormat.generate "teal-piper.env" finalSettings; + +in { + options.services.teal-piper = { + enable = mkEnableOption "Piper - teal.fm scrobbler service"; + + package = mkOption { + type = types.package; + default = pkgs.teal-piper or (throw + "teal-piper package not found. Please add it to your nixpkgs overlay."); + defaultText = literalExpression "pkgs.teal-piper"; + description = "The piper package to use."; + }; + + user = mkOption { + type = types.str; + default = "teal-piper"; + description = "User account under which piper runs."; + }; + + group = mkOption { + type = types.str; + default = "teal-piper"; + description = "Group under which piper runs."; + }; + + dataDir = mkOption { + type = types.path; + default = "/var/lib/teal-piper"; + description = "Directory where piper stores its database and data."; + }; + + settings = mkOption { + type = types.attrsOf types.str; + default = { }; + example = literalExpression '' + { + SERVER_PORT = "8080"; + SERVER_HOST = "0.0.0.0"; + SERVER_ROOT_URL = "https://piper.teal.fm"; + TRACKER_INTERVAL = "30"; + } + ''; + description = '' + Configuration for piper. These will be converted to environment variables. + + Required settings: + - ATPROTO_CLIENT_SECRET_KEY (generate with: goat key generate -t P-256) + - ATPROTO_CLIENT_SECRET_KEY_ID + - SERVER_ROOT_URL + + Optional settings: + - SPOTIFY_CLIENT_SECRET + - LASTFM_API_KEY + - APPLE_MUSIC_TEAM_ID + - APPLE_MUSIC_KEY_ID + - APPLE_MUSIC_PRIVATE_KEY_PATH + + URLs are auto-derived from SERVER_ROOT_URL: + - ATPROTO_CLIENT_ID + - ATPROTO_METADATA_URL + - ATPROTO_CALLBACK_URL + - CALLBACK_SPOTIFY + ''; + }; + + environmentFile = mkOption { + type = types.nullOr types.path; + default = null; + example = "/run/secrets/teal-piper.env"; + description = '' + Path to a file containing environment variables for secrets. + This file should contain: + + SPOTIFY_CLIENT_ID=your_spotify_client_id + SPOTIFY_CLIENT_SECRET=your_spotify_client_secret + ATPROTO_CLIENT_SECRET_KEY=your_p256_private_key + ATPROTO_CLIENT_SECRET_KEY_ID=1758199756 + LASTFM_API_KEY=your_lastfm_key # optional + + This is the recommended way to configure secrets instead of putting them in settings. + ''; + }; + }; + + config = mkIf cfg.enable { + # Create user and group + users.users.${cfg.user} = { + isSystemUser = true; + group = cfg.group; + home = cfg.dataDir; + description = "Piper service user"; + }; + + users.groups.${cfg.group} = { }; + + # Systemd service + systemd.services.teal-piper = { + description = "Piper - teal.fm scrobbler service"; + after = [ "network-online.target" ]; + wants = [ "network-online.target" ]; + wantedBy = [ "multi-user.target" ]; + + serviceConfig = { + Type = "simple"; + User = cfg.user; + Group = cfg.group; + + # Security hardening + NoNewPrivileges = true; + PrivateTmp = true; + PrivateDevices = true; + ProtectSystem = "strict"; + ProtectHome = true; + ProtectKernelTunables = true; + ProtectKernelModules = true; + ProtectControlGroups = true; + RestrictAddressFamilies = [ "AF_INET" "AF_INET6" "AF_UNIX" ]; + RestrictNamespaces = true; + RestrictRealtime = true; + RestrictSUIDSGID = true; + LockPersonality = true; + + # Allow write access to data directory + ReadWritePaths = [ cfg.dataDir ]; + StateDirectory = "teal-piper"; + StateDirectoryMode = "0700"; + + # Working directory + WorkingDirectory = cfg.dataDir; + + # Load environment from generated file + EnvironmentFile = [ settingsFile ] + ++ optional (cfg.environmentFile != null) cfg.environmentFile; + + # Start the service + ExecStart = "${cfg.package}/bin/piper"; + + # Restart policy + Restart = "on-failure"; + RestartSec = "10s"; + }; + }; + }; +} -- 2.51.2 From edddc9dacedfee982f330688af5775ec57a8c3f8 Mon Sep 17 00:00:00 2001 From: ptdewey <57921252+ptdewey@users.noreply.github.com> Date: Sat, 3 Jan 2026 10:31:04 -0500 Subject: [PATCH 02/13] refactor: nixos module fixes --- default.nix | 12 ++--- flake.nix | 49 +++++------------ nixos-module.nix => module.nix | 99 ++++++++++++++++++++-------------- package.nix | 46 ++++++++++++++++ 4 files changed, 120 insertions(+), 86 deletions(-) rename nixos-module.nix => module.nix (64%) create mode 100644 package.nix diff --git a/default.nix b/default.nix index 01c82f4..7e97d10 100644 --- a/default.nix +++ b/default.nix @@ -1,9 +1,3 @@ -{ system ? builtins.currentSystem, pkgs ? import { inherit system; } -}: -let - flake = import ./flake.nix; - outputs = flake.outputs { - self = outputs; - nixpkgs = pkgs.path or ; - }; -in outputs.packages.${system}.default +{ pkgs ? import { } }: + +pkgs.callPackage ./package.nix { } diff --git a/flake.nix b/flake.nix index 30316e7..d0e17fd 100644 --- a/flake.nix +++ b/flake.nix @@ -1,6 +1,8 @@ { description = "Piper - A teal.fm scrobbler service for ATProto"; + inputs = { nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; }; + outputs = { self, nixpkgs }: let forAllSystems = nixpkgs.lib.genAttrs [ @@ -10,41 +12,12 @@ "aarch64-darwin" ]; nixpkgsFor = forAllSystems (system: import nixpkgs { inherit system; }); - - mkPiper = pkgs: - pkgs.buildGoModule { - pname = "teal-piper"; - version = "0.0.2"; - src = ./.; - vendorHash = "sha256-gYlVWk1TOUOB2J49smq9TyGw/6AQdyP/A6tzJsfe3kI="; - - nativeBuildInputs = [ pkgs.pkg-config ]; - buildInputs = [ pkgs.sqlite ]; - - env.CGO_ENABLED = 1; - subPackages = [ "cmd" ]; - ldflags = [ "-s" "-w" ]; - - postInstall = '' - mv $out/bin/cmd $out/bin/piper - ''; - - meta = with pkgs.lib; { - description = "A teal.fm tool for scrobbling music to ATProto PDSs"; - homepage = "https://github.com/teal-fm/piper"; - license = licenses.mit; - maintainers = [ ]; - mainProgram = "piper"; - }; - }; in { packages = forAllSystems (system: - let - pkgs = nixpkgsFor.${system}; - piper = mkPiper pkgs; + let pkgs = nixpkgsFor.${system}; in { - default = piper; - teal-piper = piper; + default = pkgs.callPackage ./package.nix { }; + teal-piper = pkgs.callPackage ./package.nix { }; }); apps = forAllSystems (system: @@ -59,12 +32,16 @@ devShells = forAllSystems (system: let pkgs = nixpkgsFor.${system}; in { - default = pkgs.mkShell { buildInputs = with pkgs; [ go air ]; }; + default = pkgs.mkShell { + buildInputs = with pkgs; [ go air nodejs sqlite pkg-config ]; + }; }); - nixosModules.default = import ./nixos-module.nix; - nixosModules.teal-piper = import ./nixos-module.nix; + nixosModules.default = import ./module.nix; + nixosModules.teal-piper = import ./module.nix; - overlays.default = final: prev: { teal-piper = mkPiper final; }; + overlays.default = final: prev: { + teal-piper = final.callPackage ./package.nix { }; + }; }; } diff --git a/nixos-module.nix b/module.nix similarity index 64% rename from nixos-module.nix rename to module.nix index 10e8dd4..f70de96 100644 --- a/nixos-module.nix +++ b/module.nix @@ -1,7 +1,5 @@ { config, lib, pkgs, ... }: -with lib; - let cfg = config.services.teal-piper; @@ -25,7 +23,7 @@ let }; # Auto-derive callback URLs if SERVER_ROOT_URL is set - derivedSettings = optionalAttrs (cfg.settings ? SERVER_ROOT_URL) { + derivedSettings = lib.optionalAttrs (cfg.settings ? SERVER_ROOT_URL) { ATPROTO_CLIENT_ID = cfg.settings.ATPROTO_CLIENT_ID or "${cfg.settings.SERVER_ROOT_URL}/oauth-client-metadata.json"; ATPROTO_METADATA_URL = @@ -43,39 +41,38 @@ let settingsFile = settingsFormat.generate "teal-piper.env" finalSettings; in { + meta = { + maintainers = with lib.maintainers; + [ ]; # Add maintainer info when upstreaming + }; + options.services.teal-piper = { - enable = mkEnableOption "Piper - teal.fm scrobbler service"; - - package = mkOption { - type = types.package; - default = pkgs.teal-piper or (throw - "teal-piper package not found. Please add it to your nixpkgs overlay."); - defaultText = literalExpression "pkgs.teal-piper"; - description = "The piper package to use."; - }; + enable = lib.mkEnableOption "Piper - teal.fm scrobbler service"; - user = mkOption { - type = types.str; + package = lib.mkPackageOption pkgs "teal-piper" { }; + + user = lib.mkOption { + type = lib.types.str; default = "teal-piper"; description = "User account under which piper runs."; }; - group = mkOption { - type = types.str; + group = lib.mkOption { + type = lib.types.str; default = "teal-piper"; description = "Group under which piper runs."; }; - dataDir = mkOption { - type = types.path; + dataDir = lib.mkOption { + type = lib.types.path; default = "/var/lib/teal-piper"; description = "Directory where piper stores its database and data."; }; - settings = mkOption { - type = types.attrsOf types.str; + settings = lib.mkOption { + type = lib.types.attrsOf lib.types.str; default = { }; - example = literalExpression '' + example = lib.literalExpression '' { SERVER_PORT = "8080"; SERVER_HOST = "0.0.0.0"; @@ -86,17 +83,21 @@ in { description = '' Configuration for piper. These will be converted to environment variables. - Required settings: - - ATPROTO_CLIENT_SECRET_KEY (generate with: goat key generate -t P-256) - - ATPROTO_CLIENT_SECRET_KEY_ID - - SERVER_ROOT_URL + Required settings (set via environmentFile for security): + - SERVER_ROOT_URL: Public URL for OAuth callbacks + - SPOTIFY_CLIENT_ID: From Spotify Developer Dashboard + - SPOTIFY_CLIENT_SECRET: From Spotify Developer Dashboard + - ATPROTO_CLIENT_SECRET_KEY: P-256 private key (generate with: goat key generate -t P-256) + - ATPROTO_CLIENT_SECRET_KEY_ID: Unique persistent identifier Optional settings: - - SPOTIFY_CLIENT_SECRET - - LASTFM_API_KEY - - APPLE_MUSIC_TEAM_ID - - APPLE_MUSIC_KEY_ID - - APPLE_MUSIC_PRIVATE_KEY_PATH + - SERVER_PORT: Port to listen on (default: 8080) + - SERVER_HOST: Host to bind to (default: localhost) + - TRACKER_INTERVAL: Seconds between music checks (default: 30) + - LASTFM_API_KEY: For Last.fm integration + - APPLE_MUSIC_TEAM_ID: For Apple Music integration + - APPLE_MUSIC_KEY_ID: For Apple Music integration + - APPLE_MUSIC_PRIVATE_KEY_PATH: Path to Apple Music private key URLs are auto-derived from SERVER_ROOT_URL: - ATPROTO_CLIENT_ID @@ -106,27 +107,24 @@ in { ''; }; - environmentFile = mkOption { - type = types.nullOr types.path; + environmentFile = lib.mkOption { + type = lib.types.nullOr lib.types.path; default = null; example = "/run/secrets/teal-piper.env"; description = '' Path to a file containing environment variables for secrets. - This file should contain: - + ``` SPOTIFY_CLIENT_ID=your_spotify_client_id SPOTIFY_CLIENT_SECRET=your_spotify_client_secret ATPROTO_CLIENT_SECRET_KEY=your_p256_private_key ATPROTO_CLIENT_SECRET_KEY_ID=1758199756 - LASTFM_API_KEY=your_lastfm_key # optional - - This is the recommended way to configure secrets instead of putting them in settings. + LASTFM_API_KEY=your_lastfm_key + ``` ''; }; }; - config = mkIf cfg.enable { - # Create user and group + config = lib.mkIf cfg.enable { users.users.${cfg.user} = { isSystemUser = true; group = cfg.group; @@ -136,7 +134,6 @@ in { users.groups.${cfg.group} = { }; - # Systemd service systemd.services.teal-piper = { description = "Piper - teal.fm scrobbler service"; after = [ "network-online.target" ]; @@ -173,7 +170,7 @@ in { # Load environment from generated file EnvironmentFile = [ settingsFile ] - ++ optional (cfg.environmentFile != null) cfg.environmentFile; + ++ lib.optional (cfg.environmentFile != null) cfg.environmentFile; # Start the service ExecStart = "${cfg.package}/bin/piper"; @@ -183,5 +180,25 @@ in { RestartSec = "10s"; }; }; + + assertions = [ + { + assertion = cfg.environmentFile != null + || (cfg.settings ? ATPROTO_CLIENT_SECRET_KEY); + message = + "services.teal-piper: ATPROTO_CLIENT_SECRET_KEY must be set via settings or environmentFile"; + } + { + assertion = cfg.environmentFile != null + || (cfg.settings ? ATPROTO_CLIENT_SECRET_KEY_ID); + message = + "services.teal-piper: ATPROTO_CLIENT_SECRET_KEY_ID must be set via settings or environmentFile"; + } + { + assertion = cfg.settings ? SERVER_ROOT_URL; + message = + "services.teal-piper: SERVER_ROOT_URL must be set in settings (e.g., https://piper.teal.fm)"; + } + ]; }; } diff --git a/package.nix b/package.nix new file mode 100644 index 0000000..6559122 --- /dev/null +++ b/package.nix @@ -0,0 +1,46 @@ +{ lib, buildGoModule, pkg-config, sqlite }: + +buildGoModule rec { + pname = "teal-piper"; + version = "0.0.2"; + + src = ./.; + + vendorHash = "sha256-gYlVWk1TOUOB2J49smq9TyGw/6AQdyP/A6tzJsfe3kI="; + + nativeBuildInputs = [ pkg-config ]; + buildInputs = [ sqlite ]; + + # CGO is required for sqlite3 support + env.CGO_ENABLED = 1; + + # Only build the cmd package + subPackages = [ "cmd" ]; + + # Strip debug symbols for smaller binary size + ldflags = [ "-s" "-w" ]; + + # Rename binary from 'cmd' to 'piper' for better UX + postInstall = '' + mv $out/bin/cmd $out/bin/piper + ''; + + meta = { + description = "Music scrobbler service for ATProto"; + longDescription = '' + Piper is a teal.fm tool that scrobbles music plays from various + music providers (Spotify, Apple Music, Last.fm) to ATProto Personal + Data Servers using the teal.fm lexicons. + + It runs as a web service that periodically checks configured music + services for currently playing tracks and submits them to your + ATProto PDS for social music listening features. + ''; + homepage = "https://github.com/teal-fm/piper"; + changelog = "https://github.com/teal-fm/piper/releases/tag/v${version}"; + license = lib.licenses.mit; + maintainers = [ ]; + mainProgram = "piper"; + platforms = lib.platforms.unix; + }; +} -- 2.51.2 From 98db17c56be4ee92fba89b4315bdad7f9eb14899 Mon Sep 17 00:00:00 2001 From: ptdewey <57921252+ptdewey@users.noreply.github.com> Date: Sat, 3 Jan 2026 11:33:01 -0500 Subject: [PATCH 03/13] fix: port type --- module.nix | 164 ++++++++++++++++++++++++++++++++--------------------- 1 file changed, 100 insertions(+), 64 deletions(-) diff --git a/module.nix b/module.nix index f70de96..3f16eb9 100644 --- a/module.nix +++ b/module.nix @@ -1,29 +1,15 @@ { config, lib, pkgs, ... }: let + inherit (lib) + mkEnableOption mkIf mkOption mkPackageOption types literalExpression; + cfg = config.services.teal-piper; - # Helper function to generate environment variables settingsFormat = pkgs.formats.keyValue { }; - # Auto-derive URLs from SERVER_ROOT_URL if not explicitly set - defaultSettings = { - SERVER_PORT = cfg.settings.SERVER_PORT or 8080; - SERVER_HOST = cfg.settings.SERVER_HOST or "localhost"; - DB_PATH = cfg.settings.DB_PATH or "${cfg.dataDir}/piper.db"; - TRACKER_INTERVAL = cfg.settings.TRACKER_INTERVAL or 30; - - # Spotify defaults - SPOTIFY_AUTH_URL = - cfg.settings.SPOTIFY_AUTH_URL or "https://accounts.spotify.com/authorize"; - SPOTIFY_TOKEN_URL = - cfg.settings.SPOTIFY_TOKEN_URL or "https://accounts.spotify.com/api/token"; - SPOTIFY_SCOPES = - cfg.settings.SPOTIFY_SCOPES or "user-read-currently-playing user-read-email"; - }; - # Auto-derive callback URLs if SERVER_ROOT_URL is set - derivedSettings = lib.optionalAttrs (cfg.settings ? SERVER_ROOT_URL) { + derivedSettings = lib.optionalAttrs (cfg.settings.SERVER_ROOT_URL != null) { ATPROTO_CLIENT_ID = cfg.settings.ATPROTO_CLIENT_ID or "${cfg.settings.SERVER_ROOT_URL}/oauth-client-metadata.json"; ATPROTO_METADATA_URL = @@ -34,97 +20,147 @@ let cfg.settings.CALLBACK_SPOTIFY or "${cfg.settings.SERVER_ROOT_URL}/callback/spotify"; }; - # Merge user settings with defaults and derived settings - finalSettings = defaultSettings // cfg.settings // derivedSettings; + dbPathDefault = lib.optionalAttrs (cfg.settings.DB_PATH == null) { + DB_PATH = "${cfg.dataDir}/piper.db"; + }; - # Create environment file + finalSettings = lib.filterAttrs (_: v: v != null) + (cfg.settings // derivedSettings // dbPathDefault); settingsFile = settingsFormat.generate "teal-piper.env" finalSettings; in { meta = { - maintainers = with lib.maintainers; - [ ]; # Add maintainer info when upstreaming + maintainers = with lib.maintainers; [ ]; # TODO: }; options.services.teal-piper = { - enable = lib.mkEnableOption "Piper - teal.fm scrobbler service"; + enable = mkEnableOption "Piper - teal.fm scrobbler service"; - package = lib.mkPackageOption pkgs "teal-piper" { }; + package = mkPackageOption pkgs "teal-piper" { }; - user = lib.mkOption { - type = lib.types.str; + user = mkOption { + type = types.str; default = "teal-piper"; description = "User account under which piper runs."; }; - group = lib.mkOption { - type = lib.types.str; + group = mkOption { + type = types.str; default = "teal-piper"; description = "Group under which piper runs."; }; - dataDir = lib.mkOption { - type = lib.types.path; + dataDir = mkOption { + type = types.path; default = "/var/lib/teal-piper"; description = "Directory where piper stores its database and data."; }; - settings = lib.mkOption { - type = lib.types.attrsOf lib.types.str; + settings = mkOption { + type = types.submodule { + freeformType = types.attrsOf + (types.oneOf [ (types.nullOr types.str) types.int types.port ]); + + options = { + SERVER_PORT = mkOption { + type = types.port; + default = 8080; + description = "Port to listen on."; + }; + + SERVER_HOST = mkOption { + type = types.str; + default = "localhost"; + description = "Host to bind to."; + }; + + SERVER_ROOT_URL = mkOption { + type = types.nullOr types.str; + default = null; + example = "https://piper.teal.fm"; + description = '' + Public URL for OAuth callbacks. Required for OAuth flows. + + Auto-derives the following URLs if not explicitly set: + - ATPROTO_CLIENT_ID + - ATPROTO_METADATA_URL + - ATPROTO_CALLBACK_URL + - CALLBACK_SPOTIFY + ''; + }; + + DB_PATH = mkOption { + type = types.nullOr types.str; + default = null; + description = '' + Path to SQLite database file. + Defaults to {dataDir}/piper.db if not set. + ''; + }; + + TRACKER_INTERVAL = mkOption { + type = types.int; + default = 30; + description = "Seconds between music playback checks."; + }; + + # Spotify defaults + SPOTIFY_AUTH_URL = mkOption { + type = types.str; + default = "https://accounts.spotify.com/authorize"; + description = "Spotify authorization endpoint."; + }; + + SPOTIFY_TOKEN_URL = mkOption { + type = types.str; + default = "https://accounts.spotify.com/api/token"; + description = "Spotify token endpoint."; + }; + + SPOTIFY_SCOPES = mkOption { + type = types.str; + default = "user-read-currently-playing user-read-email"; + description = "Spotify OAuth scopes to request."; + }; + }; + }; + default = { }; - example = lib.literalExpression '' + + example = literalExpression '' { - SERVER_PORT = "8080"; - SERVER_HOST = "0.0.0.0"; + SERVER_PORT = 8080; + SERVER_HOST = "localhost"; SERVER_ROOT_URL = "https://piper.teal.fm"; - TRACKER_INTERVAL = "30"; + TRACKER_INTERVAL = 30; } ''; + description = '' Configuration for piper. These will be converted to environment variables. - - Required settings (set via environmentFile for security): - - SERVER_ROOT_URL: Public URL for OAuth callbacks - - SPOTIFY_CLIENT_ID: From Spotify Developer Dashboard - - SPOTIFY_CLIENT_SECRET: From Spotify Developer Dashboard - - ATPROTO_CLIENT_SECRET_KEY: P-256 private key (generate with: goat key generate -t P-256) - - ATPROTO_CLIENT_SECRET_KEY_ID: Unique persistent identifier - - Optional settings: - - SERVER_PORT: Port to listen on (default: 8080) - - SERVER_HOST: Host to bind to (default: localhost) - - TRACKER_INTERVAL: Seconds between music checks (default: 30) - - LASTFM_API_KEY: For Last.fm integration - - APPLE_MUSIC_TEAM_ID: For Apple Music integration - - APPLE_MUSIC_KEY_ID: For Apple Music integration - - APPLE_MUSIC_PRIVATE_KEY_PATH: Path to Apple Music private key - - URLs are auto-derived from SERVER_ROOT_URL: - - ATPROTO_CLIENT_ID - - ATPROTO_METADATA_URL - - ATPROTO_CALLBACK_URL - - CALLBACK_SPOTIFY ''; }; - environmentFile = lib.mkOption { - type = lib.types.nullOr lib.types.path; + environmentFile = mkOption { + type = types.nullOr types.path; default = null; example = "/run/secrets/teal-piper.env"; description = '' Path to a file containing environment variables for secrets. + + Example content: ``` SPOTIFY_CLIENT_ID=your_spotify_client_id SPOTIFY_CLIENT_SECRET=your_spotify_client_secret ATPROTO_CLIENT_SECRET_KEY=your_p256_private_key - ATPROTO_CLIENT_SECRET_KEY_ID=1758199756 + ATPROTO_CLIENT_SECRET_KEY_ID=1234567890 LASTFM_API_KEY=your_lastfm_key ``` ''; }; }; - config = lib.mkIf cfg.enable { + config = mkIf cfg.enable { users.users.${cfg.user} = { isSystemUser = true; group = cfg.group; @@ -195,7 +231,7 @@ in { "services.teal-piper: ATPROTO_CLIENT_SECRET_KEY_ID must be set via settings or environmentFile"; } { - assertion = cfg.settings ? SERVER_ROOT_URL; + assertion = cfg.settings.SERVER_ROOT_URL != null; message = "services.teal-piper: SERVER_ROOT_URL must be set in settings (e.g., https://piper.teal.fm)"; } -- 2.51.2 From bd25c561e82428d8b8fb06cfb9a4a12edd82239a Mon Sep 17 00:00:00 2001 From: ptdewey <57921252+ptdewey@users.noreply.github.com> Date: Sat, 3 Jan 2026 13:16:49 -0500 Subject: [PATCH 04/13] refactor: nix derivation cleanup/consolidation --- default.nix | 37 +++++++++++++++++++++++++++++++++++-- flake.nix | 12 +++++------- module.nix | 1 + package.nix | 46 ---------------------------------------------- 4 files changed, 41 insertions(+), 55 deletions(-) delete mode 100644 package.nix diff --git a/default.nix b/default.nix index 7e97d10..c3bb9dd 100644 --- a/default.nix +++ b/default.nix @@ -1,3 +1,36 @@ -{ pkgs ? import { } }: +{ lib, buildGoModule, sqlite }: -pkgs.callPackage ./package.nix { } +buildGoModule rec { + pname = "teal-piper"; + version = "0.0.3"; + src = ./.; + vendorHash = "sha256-gYlVWk1TOUOB2J49smq9TyGw/6AQdyP/A6tzJsfe3kI="; + buildInputs = [ sqlite ]; + + env.CGO_ENABLED = 1; + subPackages = [ "cmd" ]; + ldflags = [ "-s" "-w" ]; + + postInstall = '' + mv $out/bin/cmd $out/bin/piper + ''; + + meta = { + description = "Music scrobbler service for ATProto"; + longDescription = '' + Piper is a teal.fm tool that scrobbles music plays from various + music providers (Spotify, Apple Music, Last.fm) to ATProto Personal + Data Servers using the teal.fm lexicons. + + It runs as a web service that periodically checks configured music + services for currently playing tracks and submits them to your + ATProto PDS for social music listening features. + ''; + homepage = "https://github.com/teal-fm/piper"; + changelog = "https://github.com/teal-fm/piper/releases/tag/v${version}"; + license = lib.licenses.mit; + maintainers = [ ]; + mainProgram = "piper"; + platforms = lib.platforms.unix; + }; +} diff --git a/flake.nix b/flake.nix index d0e17fd..02c0702 100644 --- a/flake.nix +++ b/flake.nix @@ -1,6 +1,5 @@ { description = "Piper - A teal.fm scrobbler service for ATProto"; - inputs = { nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; }; outputs = { self, nixpkgs }: @@ -16,8 +15,8 @@ packages = forAllSystems (system: let pkgs = nixpkgsFor.${system}; in { - default = pkgs.callPackage ./package.nix { }; - teal-piper = pkgs.callPackage ./package.nix { }; + default = pkgs.callPackage ./default.nix { }; + teal-piper = pkgs.callPackage ./default.nix { }; }); apps = forAllSystems (system: @@ -32,16 +31,15 @@ devShells = forAllSystems (system: let pkgs = nixpkgsFor.${system}; in { - default = pkgs.mkShell { - buildInputs = with pkgs; [ go air nodejs sqlite pkg-config ]; - }; + default = + pkgs.mkShell { buildInputs = with pkgs; [ go air nodejs sqlite ]; }; }); nixosModules.default = import ./module.nix; nixosModules.teal-piper = import ./module.nix; overlays.default = final: prev: { - teal-piper = final.callPackage ./package.nix { }; + teal-piper = final.callPackage ./default.nix { }; }; }; } diff --git a/module.nix b/module.nix index 3f16eb9..12e1274 100644 --- a/module.nix +++ b/module.nix @@ -141,6 +141,7 @@ in { ''; }; + # TODO: maybe change to `environmentFiles` environmentFile = mkOption { type = types.nullOr types.path; default = null; diff --git a/package.nix b/package.nix deleted file mode 100644 index 6559122..0000000 --- a/package.nix +++ /dev/null @@ -1,46 +0,0 @@ -{ lib, buildGoModule, pkg-config, sqlite }: - -buildGoModule rec { - pname = "teal-piper"; - version = "0.0.2"; - - src = ./.; - - vendorHash = "sha256-gYlVWk1TOUOB2J49smq9TyGw/6AQdyP/A6tzJsfe3kI="; - - nativeBuildInputs = [ pkg-config ]; - buildInputs = [ sqlite ]; - - # CGO is required for sqlite3 support - env.CGO_ENABLED = 1; - - # Only build the cmd package - subPackages = [ "cmd" ]; - - # Strip debug symbols for smaller binary size - ldflags = [ "-s" "-w" ]; - - # Rename binary from 'cmd' to 'piper' for better UX - postInstall = '' - mv $out/bin/cmd $out/bin/piper - ''; - - meta = { - description = "Music scrobbler service for ATProto"; - longDescription = '' - Piper is a teal.fm tool that scrobbles music plays from various - music providers (Spotify, Apple Music, Last.fm) to ATProto Personal - Data Servers using the teal.fm lexicons. - - It runs as a web service that periodically checks configured music - services for currently playing tracks and submits them to your - ATProto PDS for social music listening features. - ''; - homepage = "https://github.com/teal-fm/piper"; - changelog = "https://github.com/teal-fm/piper/releases/tag/v${version}"; - license = lib.licenses.mit; - maintainers = [ ]; - mainProgram = "piper"; - platforms = lib.platforms.unix; - }; -} -- 2.51.2 From 342241cd2294c90038a9b9073c6c0014d0ad68ef Mon Sep 17 00:00:00 2001 From: ptdewey <57921252+ptdewey@users.noreply.github.com> Date: Sat, 3 Jan 2026 13:29:19 -0500 Subject: [PATCH 05/13] refactor: rename nix package -> 'tealfm-piper' --- default.nix | 15 +++++---------- flake.nix | 8 ++++---- module.nix | 30 ++++++++++++++---------------- 3 files changed, 23 insertions(+), 30 deletions(-) diff --git a/default.nix b/default.nix index c3bb9dd..7086a20 100644 --- a/default.nix +++ b/default.nix @@ -1,12 +1,11 @@ { lib, buildGoModule, sqlite }: buildGoModule rec { - pname = "teal-piper"; + pname = "tealfm-piper"; version = "0.0.3"; src = ./.; vendorHash = "sha256-gYlVWk1TOUOB2J49smq9TyGw/6AQdyP/A6tzJsfe3kI="; buildInputs = [ sqlite ]; - env.CGO_ENABLED = 1; subPackages = [ "cmd" ]; ldflags = [ "-s" "-w" ]; @@ -15,21 +14,17 @@ buildGoModule rec { mv $out/bin/cmd $out/bin/piper ''; - meta = { - description = "Music scrobbler service for ATProto"; + meta = with lib; { + description = "Music scrobbler service for teal.fm"; longDescription = '' Piper is a teal.fm tool that scrobbles music plays from various music providers (Spotify, Apple Music, Last.fm) to ATProto Personal Data Servers using the teal.fm lexicons. - - It runs as a web service that periodically checks configured music - services for currently playing tracks and submits them to your - ATProto PDS for social music listening features. ''; homepage = "https://github.com/teal-fm/piper"; changelog = "https://github.com/teal-fm/piper/releases/tag/v${version}"; - license = lib.licenses.mit; - maintainers = [ ]; + license = licenses.mit; + maintainers = with maintainers; [ ptdewey ]; mainProgram = "piper"; platforms = lib.platforms.unix; }; diff --git a/flake.nix b/flake.nix index 02c0702..7ac48a6 100644 --- a/flake.nix +++ b/flake.nix @@ -1,5 +1,5 @@ { - description = "Piper - A teal.fm scrobbler service for ATProto"; + description = "Piper - A scrobbler service for teal.fm"; inputs = { nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; }; outputs = { self, nixpkgs }: @@ -16,7 +16,7 @@ let pkgs = nixpkgsFor.${system}; in { default = pkgs.callPackage ./default.nix { }; - teal-piper = pkgs.callPackage ./default.nix { }; + tealfm-piper = pkgs.callPackage ./default.nix { }; }); apps = forAllSystems (system: @@ -36,10 +36,10 @@ }); nixosModules.default = import ./module.nix; - nixosModules.teal-piper = import ./module.nix; + nixosModules.tealfm-piper = import ./module.nix; overlays.default = final: prev: { - teal-piper = final.callPackage ./default.nix { }; + tealfm-piper = final.callPackage ./default.nix { }; }; }; } diff --git a/module.nix b/module.nix index 12e1274..6c2cc33 100644 --- a/module.nix +++ b/module.nix @@ -4,7 +4,7 @@ let inherit (lib) mkEnableOption mkIf mkOption mkPackageOption types literalExpression; - cfg = config.services.teal-piper; + cfg = config.services.tealfm-piper; settingsFormat = pkgs.formats.keyValue { }; @@ -26,33 +26,31 @@ let finalSettings = lib.filterAttrs (_: v: v != null) (cfg.settings // derivedSettings // dbPathDefault); - settingsFile = settingsFormat.generate "teal-piper.env" finalSettings; + settingsFile = settingsFormat.generate "tealfm-piper.env" finalSettings; in { - meta = { - maintainers = with lib.maintainers; [ ]; # TODO: - }; + meta = { maintainers = with lib.maintainers; [ ptdewey ]; }; - options.services.teal-piper = { + options.services.tealfm-piper = { enable = mkEnableOption "Piper - teal.fm scrobbler service"; - package = mkPackageOption pkgs "teal-piper" { }; + package = mkPackageOption pkgs "tealfm-piper" { }; user = mkOption { type = types.str; - default = "teal-piper"; + default = "tealfm-piper"; description = "User account under which piper runs."; }; group = mkOption { type = types.str; - default = "teal-piper"; + default = "tealfm-piper"; description = "Group under which piper runs."; }; dataDir = mkOption { type = types.path; - default = "/var/lib/teal-piper"; + default = "/var/lib/tealfm-piper"; description = "Directory where piper stores its database and data."; }; @@ -145,7 +143,7 @@ in { environmentFile = mkOption { type = types.nullOr types.path; default = null; - example = "/run/secrets/teal-piper.env"; + example = "/run/secrets/tealfm-piper.env"; description = '' Path to a file containing environment variables for secrets. @@ -171,7 +169,7 @@ in { users.groups.${cfg.group} = { }; - systemd.services.teal-piper = { + systemd.services.tealfm-piper = { description = "Piper - teal.fm scrobbler service"; after = [ "network-online.target" ]; wants = [ "network-online.target" ]; @@ -199,7 +197,7 @@ in { # Allow write access to data directory ReadWritePaths = [ cfg.dataDir ]; - StateDirectory = "teal-piper"; + StateDirectory = "tealfm-piper"; StateDirectoryMode = "0700"; # Working directory @@ -223,18 +221,18 @@ in { assertion = cfg.environmentFile != null || (cfg.settings ? ATPROTO_CLIENT_SECRET_KEY); message = - "services.teal-piper: ATPROTO_CLIENT_SECRET_KEY must be set via settings or environmentFile"; + "services.tealfm-piper: ATPROTO_CLIENT_SECRET_KEY must be set via settings or environmentFile"; } { assertion = cfg.environmentFile != null || (cfg.settings ? ATPROTO_CLIENT_SECRET_KEY_ID); message = - "services.teal-piper: ATPROTO_CLIENT_SECRET_KEY_ID must be set via settings or environmentFile"; + "services.tealfm-piper: ATPROTO_CLIENT_SECRET_KEY_ID must be set via settings or environmentFile"; } { assertion = cfg.settings.SERVER_ROOT_URL != null; message = - "services.teal-piper: SERVER_ROOT_URL must be set in settings (e.g., https://piper.teal.fm)"; + "services.tealfm-piper: SERVER_ROOT_URL must be set in settings (e.g., https://piper.teal.fm)"; } ]; }; -- 2.51.2 From cfa864c918b29fec67b3a22fcf01e0f5a7978862 Mon Sep 17 00:00:00 2001 From: pdewey Date: Sat, 3 Jan 2026 17:00:57 -0500 Subject: [PATCH 06/13] chore: update nix go modules vendor hash --- default.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/default.nix b/default.nix index 7086a20..8ec5c8f 100644 --- a/default.nix +++ b/default.nix @@ -4,7 +4,7 @@ buildGoModule rec { pname = "tealfm-piper"; version = "0.0.3"; src = ./.; - vendorHash = "sha256-gYlVWk1TOUOB2J49smq9TyGw/6AQdyP/A6tzJsfe3kI="; + vendorHash = "sha256-poQutY1V8X6BdmPMXdQuPWIWE/j3xNoEp4PKSimj2bA="; buildInputs = [ sqlite ]; env.CGO_ENABLED = 1; subPackages = [ "cmd" ]; -- 2.51.2 From a7688a6fc76b652aed5f8a91da9a0c194848c4c8 Mon Sep 17 00:00:00 2001 From: pdewey Date: Sun, 4 Jan 2026 15:27:55 -0500 Subject: [PATCH 07/13] refactor: swap to fetchFromGitHub for package install - still builds from local source for local dev with nix run --- default.nix | 31 ------------------------------- flake.nix | 8 ++++---- package.nix | 45 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 49 insertions(+), 35 deletions(-) delete mode 100644 default.nix create mode 100644 package.nix diff --git a/default.nix b/default.nix deleted file mode 100644 index 8ec5c8f..0000000 --- a/default.nix +++ /dev/null @@ -1,31 +0,0 @@ -{ lib, buildGoModule, sqlite }: - -buildGoModule rec { - pname = "tealfm-piper"; - version = "0.0.3"; - src = ./.; - vendorHash = "sha256-poQutY1V8X6BdmPMXdQuPWIWE/j3xNoEp4PKSimj2bA="; - buildInputs = [ sqlite ]; - env.CGO_ENABLED = 1; - subPackages = [ "cmd" ]; - ldflags = [ "-s" "-w" ]; - - postInstall = '' - mv $out/bin/cmd $out/bin/piper - ''; - - meta = with lib; { - description = "Music scrobbler service for teal.fm"; - longDescription = '' - Piper is a teal.fm tool that scrobbles music plays from various - music providers (Spotify, Apple Music, Last.fm) to ATProto Personal - Data Servers using the teal.fm lexicons. - ''; - homepage = "https://github.com/teal-fm/piper"; - changelog = "https://github.com/teal-fm/piper/releases/tag/v${version}"; - license = licenses.mit; - maintainers = with maintainers; [ ptdewey ]; - mainProgram = "piper"; - platforms = lib.platforms.unix; - }; -} diff --git a/flake.nix b/flake.nix index 7ac48a6..e28c49f 100644 --- a/flake.nix +++ b/flake.nix @@ -15,8 +15,9 @@ packages = forAllSystems (system: let pkgs = nixpkgsFor.${system}; in { - default = pkgs.callPackage ./default.nix { }; - tealfm-piper = pkgs.callPackage ./default.nix { }; + tealfm-piper = pkgs.callPackage ./package.nix { }; + # Local development build + default = pkgs.callPackage ./package.nix { source = ./.; }; }); apps = forAllSystems (system: @@ -36,10 +37,9 @@ }); nixosModules.default = import ./module.nix; - nixosModules.tealfm-piper = import ./module.nix; overlays.default = final: prev: { - tealfm-piper = final.callPackage ./default.nix { }; + tealfm-piper = final.callPackage ./package.nix { }; }; }; } diff --git a/package.nix b/package.nix new file mode 100644 index 0000000..9363189 --- /dev/null +++ b/package.nix @@ -0,0 +1,45 @@ +{ lib, buildGoModule, tailwindcss_4, fetchFromGitHub, source ? null }: + +let + # Default to GitHub source for builds. + # Override with `source = ./.` for local development. + defaultSource = fetchFromGitHub { + owner = "teal-fm"; + repo = "piper"; + rev = "ccb72442021bd9f6ed20acc63f9703cf475b0f51"; + hash = "sha256-wXA2RnvQ0J0QwUeDIg2gLRI2DNjgu07+QYjw5pRmyyI="; + }; +in buildGoModule { + pname = "tealfm-piper"; + version = "0.0.3"; + + src = if source != null then source else defaultSource; + + vendorHash = "sha256-poQutY1V8X6BdmPMXdQuPWIWE/j3xNoEp4PKSimj2bA="; + + nativeBuildInputs = [ tailwindcss_4 ]; + + env.CGO_ENABLED = 1; + + subPackages = [ "cmd" ]; + + ldflags = [ "-s" "-w" ]; + + postBuild = '' + cp -r ./pages/templates $out/ + cp -r ./pages/static $out/ + tailwindcss -i $out/static/base.css -o $out/static/main.css -m + ''; + + postInstall = '' + mv $out/bin/cmd $out/bin/piper + ''; + + meta = with lib; { + description = "Music scrobbler service for teal.fm"; + homepage = "https://github.com/teal-fm/piper"; + license = licenses.mit; + maintainers = with maintainers; [ ptdewey ]; + mainProgram = "piper"; + }; +} -- 2.51.2 From ee07d25de6e0a115ac2c30cc15342a5c866e19c2 Mon Sep 17 00:00:00 2001 From: pdewey Date: Sun, 4 Jan 2026 16:04:51 -0500 Subject: [PATCH 08/13] feat: change to environmentFiles in module.nix --- module.nix | 51 ++++++++++++++++----------------------------------- 1 file changed, 16 insertions(+), 35 deletions(-) diff --git a/module.nix b/module.nix index 6c2cc33..30ed464 100644 --- a/module.nix +++ b/module.nix @@ -8,7 +8,6 @@ let settingsFormat = pkgs.formats.keyValue { }; - # Auto-derive callback URLs if SERVER_ROOT_URL is set derivedSettings = lib.optionalAttrs (cfg.settings.SERVER_ROOT_URL != null) { ATPROTO_CLIENT_ID = cfg.settings.ATPROTO_CLIENT_ID or "${cfg.settings.SERVER_ROOT_URL}/oauth-client-metadata.json"; @@ -102,7 +101,6 @@ in { description = "Seconds between music playback checks."; }; - # Spotify defaults SPOTIFY_AUTH_URL = mkOption { type = types.str; default = "https://accounts.spotify.com/authorize"; @@ -139,22 +137,18 @@ in { ''; }; - # TODO: maybe change to `environmentFiles` - environmentFile = mkOption { - type = types.nullOr types.path; - default = null; - example = "/run/secrets/tealfm-piper.env"; + environmentFiles = mkOption { + type = types.listOf types.path; + default = [ ]; + example = literalExpression '' + [ + "/run/secrets/tealfm-piper.env" + "/run/secrets/tealfm-piper-apple-music.env" + ] + ''; description = '' - Path to a file containing environment variables for secrets. - - Example content: - ``` - SPOTIFY_CLIENT_ID=your_spotify_client_id - SPOTIFY_CLIENT_SECRET=your_spotify_client_secret - ATPROTO_CLIENT_SECRET_KEY=your_p256_private_key - ATPROTO_CLIENT_SECRET_KEY_ID=1234567890 - LASTFM_API_KEY=your_lastfm_key - ``` + List of files containing environment variables for secrets. + Files are loaded in order, with later files overriding earlier ones. ''; }; }; @@ -179,8 +173,6 @@ in { Type = "simple"; User = cfg.user; Group = cfg.group; - - # Security hardening NoNewPrivileges = true; PrivateTmp = true; PrivateDevices = true; @@ -194,23 +186,12 @@ in { RestrictRealtime = true; RestrictSUIDSGID = true; LockPersonality = true; - - # Allow write access to data directory ReadWritePaths = [ cfg.dataDir ]; StateDirectory = "tealfm-piper"; StateDirectoryMode = "0700"; - - # Working directory WorkingDirectory = cfg.dataDir; - - # Load environment from generated file - EnvironmentFile = [ settingsFile ] - ++ lib.optional (cfg.environmentFile != null) cfg.environmentFile; - - # Start the service + EnvironmentFile = [ settingsFile ] ++ cfg.environmentFiles; ExecStart = "${cfg.package}/bin/piper"; - - # Restart policy Restart = "on-failure"; RestartSec = "10s"; }; @@ -218,16 +199,16 @@ in { assertions = [ { - assertion = cfg.environmentFile != null + assertion = (cfg.environmentFiles != [ ]) || (cfg.settings ? ATPROTO_CLIENT_SECRET_KEY); message = - "services.tealfm-piper: ATPROTO_CLIENT_SECRET_KEY must be set via settings or environmentFile"; + "services.tealfm-piper: ATPROTO_CLIENT_SECRET_KEY must be set via settings or environmentFiles"; } { - assertion = cfg.environmentFile != null + assertion = (cfg.environmentFiles != [ ]) || (cfg.settings ? ATPROTO_CLIENT_SECRET_KEY_ID); message = - "services.tealfm-piper: ATPROTO_CLIENT_SECRET_KEY_ID must be set via settings or environmentFile"; + "services.tealfm-piper: ATPROTO_CLIENT_SECRET_KEY_ID must be set via settings or environmentFiles"; } { assertion = cfg.settings.SERVER_ROOT_URL != null; -- 2.51.2 From b58b01dd9f7fe42e790f49be9ab2ca38185aef14 Mon Sep 17 00:00:00 2001 From: pdewey Date: Sun, 4 Jan 2026 16:13:32 -0500 Subject: [PATCH 09/13] feat: remove need for importing overlay --- flake.nix | 9 ++++++++- module.nix | 12 ++++++++++-- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/flake.nix b/flake.nix index e28c49f..f83cddf 100644 --- a/flake.nix +++ b/flake.nix @@ -36,7 +36,14 @@ pkgs.mkShell { buildInputs = with pkgs; [ go air nodejs sqlite ]; }; }); - nixosModules.default = import ./module.nix; + nixosModules.default = { config, lib, pkgs, ... }: + let + piperPackage = self.packages.${pkgs.stdenv.hostPlatform.system}.tealfm-piper; + in { + imports = [ + (import ./module.nix { defaultPackage = piperPackage; }) + ]; + }; overlays.default = final: prev: { tealfm-piper = final.callPackage ./package.nix { }; diff --git a/module.nix b/module.nix index 30ed464..7781a91 100644 --- a/module.nix +++ b/module.nix @@ -1,8 +1,9 @@ +{ defaultPackage ? null }: { config, lib, pkgs, ... }: let inherit (lib) - mkEnableOption mkIf mkOption mkPackageOption types literalExpression; + mkEnableOption mkIf mkOption types literalExpression; cfg = config.services.tealfm-piper; @@ -33,7 +34,14 @@ in { options.services.tealfm-piper = { enable = mkEnableOption "Piper - teal.fm scrobbler service"; - package = mkPackageOption pkgs "tealfm-piper" { }; + package = mkOption { + type = types.package; + default = if defaultPackage != null + then defaultPackage + else pkgs.tealfm-piper; + defaultText = literalExpression "pkgs.tealfm-piper"; + description = "The piper package to use."; + }; user = mkOption { type = types.str; -- 2.51.2 From c96ed11215d42a99ed4782fd88c56d992ae94bf7 Mon Sep 17 00:00:00 2001 From: pdewey Date: Sun, 4 Jan 2026 16:26:57 -0500 Subject: [PATCH 10/13] refactor: general nix cleanup --- .gitignore | 3 ++- flake.nix | 34 ++++++++++++---------------------- module.nix | 6 +++--- package.nix | 21 ++++++++------------- 4 files changed, 25 insertions(+), 39 deletions(-) diff --git a/.gitignore b/.gitignore index 6243d21..a8282e2 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,5 @@ jwk*.json **.bak .idea AM_AUTHKEY.p8 -.DS_Store \ No newline at end of file +.DS_Store +result diff --git a/flake.nix b/flake.nix index f83cddf..9c9c3a1 100644 --- a/flake.nix +++ b/flake.nix @@ -10,40 +10,30 @@ "x86_64-darwin" "aarch64-darwin" ]; - nixpkgsFor = forAllSystems (system: import nixpkgs { inherit system; }); in { packages = forAllSystems (system: - let pkgs = nixpkgsFor.${system}; + let pkgs = import nixpkgs { inherit system; }; in { tealfm-piper = pkgs.callPackage ./package.nix { }; - # Local development build default = pkgs.callPackage ./package.nix { source = ./.; }; }); - apps = forAllSystems (system: - let piper = self.packages.${system}.default; - in { - default = { - type = "app"; - program = "${piper}/bin/piper"; - }; - }); + apps = forAllSystems (system: { + default = { + type = "app"; + program = "${self.packages.${system}.default}/bin/piper"; + }; + }); devShells = forAllSystems (system: - let pkgs = nixpkgsFor.${system}; + let pkgs = import nixpkgs { inherit system; }; in { - default = - pkgs.mkShell { buildInputs = with pkgs; [ go air nodejs sqlite ]; }; + default = pkgs.mkShell { + buildInputs = with pkgs; [ go air nodejs sqlite ]; + }; }); - nixosModules.default = { config, lib, pkgs, ... }: - let - piperPackage = self.packages.${pkgs.stdenv.hostPlatform.system}.tealfm-piper; - in { - imports = [ - (import ./module.nix { defaultPackage = piperPackage; }) - ]; - }; + nixosModules.default = import ./module.nix { inherit self; }; overlays.default = final: prev: { tealfm-piper = final.callPackage ./package.nix { }; diff --git a/module.nix b/module.nix index 7781a91..8035332 100644 --- a/module.nix +++ b/module.nix @@ -1,4 +1,4 @@ -{ defaultPackage ? null }: +{ self ? null }: { config, lib, pkgs, ... }: let @@ -36,8 +36,8 @@ in { package = mkOption { type = types.package; - default = if defaultPackage != null - then defaultPackage + default = if self != null + then self.packages.${pkgs.stdenv.hostPlatform.system}.tealfm-piper else pkgs.tealfm-piper; defaultText = literalExpression "pkgs.tealfm-piper"; description = "The piper package to use."; diff --git a/package.nix b/package.nix index 9363189..c3da375 100644 --- a/package.nix +++ b/package.nix @@ -1,19 +1,14 @@ -{ lib, buildGoModule, tailwindcss_4, fetchFromGitHub, source ? null }: - -let - # Default to GitHub source for builds. - # Override with `source = ./.` for local development. - defaultSource = fetchFromGitHub { - owner = "teal-fm"; - repo = "piper"; - rev = "ccb72442021bd9f6ed20acc63f9703cf475b0f51"; - hash = "sha256-wXA2RnvQ0J0QwUeDIg2gLRI2DNjgu07+QYjw5pRmyyI="; - }; -in buildGoModule { +{ lib, buildGoModule, tailwindcss_4, fetchFromGitHub, source ? fetchFromGitHub { + owner = "teal-fm"; + repo = "piper"; + rev = "ccb72442021bd9f6ed20acc63f9703cf475b0f51"; + hash = "sha256-wXA2RnvQ0J0QwUeDIg2gLRI2DNjgu07+QYjw5pRmyyI="; +} }: +buildGoModule { pname = "tealfm-piper"; version = "0.0.3"; - src = if source != null then source else defaultSource; + src = source; vendorHash = "sha256-poQutY1V8X6BdmPMXdQuPWIWE/j3xNoEp4PKSimj2bA="; -- 2.51.2 From bf80a89def70b1d520140a1863bed57128cba036 Mon Sep 17 00:00:00 2001 From: pdewey Date: Sun, 4 Jan 2026 16:33:35 -0500 Subject: [PATCH 11/13] feat: add `ALLOWED_DIDS` support to nix module --- flake.nix | 5 ++--- module.nix | 28 ++++++++++++++++++++++------ 2 files changed, 24 insertions(+), 9 deletions(-) diff --git a/flake.nix b/flake.nix index 9c9c3a1..e3c02a6 100644 --- a/flake.nix +++ b/flake.nix @@ -28,9 +28,8 @@ devShells = forAllSystems (system: let pkgs = import nixpkgs { inherit system; }; in { - default = pkgs.mkShell { - buildInputs = with pkgs; [ go air nodejs sqlite ]; - }; + default = + pkgs.mkShell { buildInputs = with pkgs; [ go air nodejs sqlite ]; }; }); nixosModules.default = import ./module.nix { inherit self; }; diff --git a/module.nix b/module.nix index 8035332..aa6f87e 100644 --- a/module.nix +++ b/module.nix @@ -2,8 +2,7 @@ { config, lib, pkgs, ... }: let - inherit (lib) - mkEnableOption mkIf mkOption types literalExpression; + inherit (lib) mkEnableOption mkIf mkOption types literalExpression; cfg = config.services.tealfm-piper; @@ -24,8 +23,12 @@ let DB_PATH = "${cfg.dataDir}/piper.db"; }; + allowedDidsString = lib.optionalAttrs (cfg.settings.ALLOWED_DIDS != null) { + ALLOWED_DIDS = lib.concatStringsSep " " cfg.settings.ALLOWED_DIDS; + }; + finalSettings = lib.filterAttrs (_: v: v != null) - (cfg.settings // derivedSettings // dbPathDefault); + (cfg.settings // derivedSettings // dbPathDefault // allowedDidsString); settingsFile = settingsFormat.generate "tealfm-piper.env" finalSettings; in { @@ -36,9 +39,10 @@ in { package = mkOption { type = types.package; - default = if self != null - then self.packages.${pkgs.stdenv.hostPlatform.system}.tealfm-piper - else pkgs.tealfm-piper; + default = if self != null then + self.packages.${pkgs.stdenv.hostPlatform.system}.tealfm-piper + else + pkgs.tealfm-piper; defaultText = literalExpression "pkgs.tealfm-piper"; description = "The piper package to use."; }; @@ -126,6 +130,18 @@ in { default = "user-read-currently-playing user-read-email"; description = "Spotify OAuth scopes to request."; }; + + ALLOWED_DIDS = mkOption { + type = types.nullOr (types.listOf types.str); + default = null; + example = + literalExpression ''[ "did:plc:abcdefg" "did:web:example.com" ]''; + description = '' + List of ATProto DIDs allowed to sign in. + When set, restricts instance access to only these accounts. + Leave null to allow any ATProto account to sign in. + ''; + }; }; }; -- 2.51.2 From 4ffb3ad3b5b2d8ed182d6541734323de69ec4b3b Mon Sep 17 00:00:00 2001 From: pdewey Date: Fri, 16 Jan 2026 19:41:44 -0500 Subject: [PATCH 12/13] docs: document nix usage --- README.md | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/README.md b/README.md index 7da0c41..d07589c 100644 --- a/README.md +++ b/README.md @@ -112,3 +112,40 @@ We also provide a docker compose file to use to run piper locally. There are a f `DB_PATH` = `/db/piper.db` to persist your piper db through container restarts Make sure you have docker and docker compose installed, then you can run piper with `docker compose up` + +#### nix + +For local development, the flake provides a dev shell with all dependencies: `nix develop`. +You can also run piper directly with `nix run`. + +Piper can also be installed as a NixOS module. +Sensitive environment variables (Spotify, ATProto, Last.fm credentials) should be securely managed. + +Example nix module configuration (using [sops](https://github.com/getsops/sops) for secrets): + +```nix +{ config, ... }: + +{ + sops.secrets.piper = { + owner = "tealfm-piper"; + group = "tealfm-piper"; + sopsFile = ./sops/piper.env; + format = "dotenv"; + }; + + services.tealfm-piper = { + enable = true; + environmentFiles = [ config.sops.secrets.piper.path ]; + settings = { + SERVER_PORT = 19990; + SERVER_HOST = "localhost"; + SERVER_ROOT_URL = "https://piper.example.com"; + # Optional: DID allow list + ALLOWED_DIDS = [ "did:plc:tas6hj2xjrqben5653v5kohk" ]; + }; + }; +} +``` + +See [module.nix](./module.nix) for additional configuration options. -- 2.51.2 From 7c11e7cb8b1357a3a85efbe0a84f66106bdacd61 Mon Sep 17 00:00:00 2001 From: pdewey Date: Fri, 16 Jan 2026 19:46:36 -0500 Subject: [PATCH 13/13] docs: add nix instructions for installation with flake --- README.md | 29 ++++++++++++++++++++++++----- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index d07589c..28da5fb 100644 --- a/README.md +++ b/README.md @@ -115,15 +115,34 @@ Make sure you have docker and docker compose installed, then you can run piper w #### nix -For local development, the flake provides a dev shell with all dependencies: `nix develop`. -You can also run piper directly with `nix run`. +For local development, the flake provides a dev shell with all dependencies: `nix develop`. You can also run piper directly with `nix run`. -Piper can also be installed as a NixOS module. -Sensitive environment variables (Spotify, ATProto, Last.fm credentials) should be securely managed. +Piper can be installed as a NixOS module. Add the flake to your system configuration: -Example nix module configuration (using [sops](https://github.com/getsops/sops) for secrets): +```nix +# flake.nix +{ + inputs = { + # ... + tealfm-piper.url = "github:teal-fm/piper"; + }; + outputs = { self, nixpkgs, tealfm-piper, ... }: { + nixosConfigurations.nixos = nixpkgs.lib.nixosSystem { + # ... + modules = [ + # ... + ./modules/piper.nix # piper config + tealfm-piper.nixosModules.default # import piper module + ]; + }; + }; +} +``` + +Sensitive environment variables (Spotify, ATProto, Last.fm credentials) should be securely managed with a tool like [sops](https://github.com/getsops/sops): ```nix +# modules/piper.nix { config, ... }: {