Add NixOS module and deployment guide for lidarr-mb-gap

- Introduced a new NixOS module for the lidarr-mb-gap service, allowing users to configure and manage the report generation process through NixOS.
- Added a comprehensive deployment guide in `nixos/DEPLOYMENT.md`, detailing setup instructions, configuration options, and troubleshooting tips for deploying the service on NixOS and serving reports via Caddy.
- Updated `flake.nix` to export the new NixOS module.
- Enhanced the report generation scripts to support customizable output paths for generated reports.
This commit is contained in:
Danilo Reyes
2025-11-11 11:04:01 -06:00
parent d38fb12e17
commit e6f96107aa
5 changed files with 438 additions and 7 deletions

View File

@@ -7,7 +7,17 @@
}; };
outputs = { self, nixpkgs, flake-utils }: outputs = { self, nixpkgs, flake-utils }:
flake-utils.lib.eachDefaultSystem (system: let
# NixOS module output (not system-specific)
# The module accepts a package option, which can be set from the flake's packages
nixosModules = {
lidarr-mb-gap = import ./nixos/lidarr-mb-gap.nix;
};
in
{
# Export NixOS modules
nixosModules = nixosModules;
} // flake-utils.lib.eachDefaultSystem (system:
let let
pkgs = import nixpkgs { inherit system; }; pkgs = import nixpkgs { inherit system; };
lib = pkgs.lib; lib = pkgs.lib;

292
nixos/DEPLOYMENT.md Normal file
View File

@@ -0,0 +1,292 @@
# Deployment Guide
This guide explains how to deploy the Lidarr MusicBrainz gap reporter using NixOS on your server and serve it with Caddy on your VPS.
## Architecture
- **Server (NixOS)**: Runs the report generation script periodically via systemd timer
- **VPS**: Serves the generated HTML report via Caddy
## Setup on NixOS Server
### 1. Add the NixOS Module
Add the module to your NixOS configuration. You have two options:
#### Option A: Import directly with source path
```nix
# In your configuration.nix or flake.nix
{ config, pkgs, ... }:
{
imports = [
./path/to/lidarr-musicbrainz/nixos/lidarr-mb-gap.nix
];
services.lidarr-mb-gap = {
enable = true;
src = /path/to/lidarr-musicbrainz/src; # Source path - package will be built from this
reportDir = "/var/lib/lidarr-mb-gap/reports";
envFile = "/var/lib/lidarr-mb-gap/.env";
runInterval = "daily"; # Or "hourly", or "*-*-* 02:00:00" for specific time
# Optional: Auto-sync to VPS
syncToVPS = true;
vpsHost = "user@vps"; # Your SSH host alias
vpsPath = "/var/www/html";
};
}
```
#### Option B: Use as a flake input (Recommended)
```nix
# In your flake.nix
{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
lidarr-mb-gap.url = "path:/path/to/lidarr-musicbrainz";
# or
# lidarr-mb-gap.url = "github:yourusername/lidarr-musicbrainz";
};
outputs = { self, nixpkgs, lidarr-mb-gap, ... }: {
nixosConfigurations.your-server = nixpkgs.lib.nixosSystem {
system = "x86_64-linux"; # or your system
modules = [
lidarr-mb-gap.nixosModules.lidarr-mb-gap
{
# Reference the package from the flake
services.lidarr-mb-gap.package = lidarr-mb-gap.packages.${system}.lidarr-mb-gap;
services.lidarr-mb-gap.enable = true;
services.lidarr-mb-gap.reportDir = "/var/lib/lidarr-mb-gap/reports";
services.lidarr-mb-gap.envFile = "/var/lib/lidarr-mb-gap/.env";
services.lidarr-mb-gap.runInterval = "daily";
}
./configuration.nix
];
};
};
}
```
Note: When using the flake module, you can reference the package directly from `lidarr-mb-gap.packages.${system}.lidarr-mb-gap`, which is more efficient than building from source.
### 2. Create Environment File
Create `/var/lib/lidarr-mb-gap/.env` with your Lidarr credentials:
```bash
sudo mkdir -p /var/lib/lidarr-mb-gap
sudo nano /var/lib/lidarr-mb-gap/.env
```
Add:
```env
LIDARR_URL=http://your-lidarr-instance:8686
LIDARR_API_KEY=your-api-key-here
SAMBL_URL=https://sambl.lioncat6.com
MAX_ARTISTS=0 # 0 = no limit
OUTPUT_DIR=/var/lib/lidarr-mb-gap/reports # Optional: defaults to current directory
```
Note: The `OUTPUT_DIR` is automatically set by the systemd service, but you can override it in the `.env` file if needed.
Set proper permissions:
```bash
sudo chown -R lidarr-mb-gap:lidarr-mb-gap /var/lib/lidarr-mb-gap
sudo chmod 600 /var/lib/lidarr-mb-gap/.env
```
### 3. Configure SSH for rsync (if using auto-sync)
If you enabled `syncToVPS`, set up SSH key authentication:
```bash
# On the server
ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519_lidarr
ssh-copy-id -i ~/.ssh/id_ed25519_lidarr.pub user@vps
# Add to ~/.ssh/config
Host vps
HostName your-vps-ip-or-domain
User your-username
IdentityFile ~/.ssh/id_ed25519_lidarr
```
### 4. Build and Switch
```bash
sudo nixos-rebuild switch
```
### 5. Test the Service
```bash
# Run manually once
sudo systemctl start lidarr-mb-gap
# Check status
sudo systemctl status lidarr-mb-gap
# View logs
sudo journalctl -u lidarr-mb-gap -f
# Check timer
sudo systemctl status lidarr-mb-gap.timer
sudo systemctl list-timers lidarr-mb-gap
```
## Setup on VPS (Caddy)
### Option 1: Manual Sync (Recommended for initial setup)
If you didn't enable auto-sync, manually copy files:
```bash
# On server
scp /var/lib/lidarr-mb-gap/reports/missing_albums.html user@vps:/var/www/html/
# Or use rsync
rsync -avz /var/lib/lidarr-mb-gap/reports/ user@vps:/var/www/html/
```
### Option 2: Auto-sync via rsync
If you enabled `syncToVPS` in the NixOS config, files will sync automatically after each report generation.
### Configure Caddy
1. **Install Caddy** (if not already installed):
```bash
# On Debian/Ubuntu
sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list
sudo apt update
sudo apt install caddy
# Or using Nix
nix-env -iA nixpkgs.caddy
```
2. **Create web directory**:
```bash
sudo mkdir -p /var/www/html
sudo chown -R www-data:www-data /var/www/html
```
3. **Configure Caddy**:
Copy the `Caddyfile` to your Caddy config location:
```bash
# For systemd service
sudo cp Caddyfile /etc/caddy/Caddyfile
# Or for user service
mkdir -p ~/.config/caddy
cp Caddyfile ~/.config/caddy/
```
4. **Start Caddy**:
```bash
# Systemd service
sudo systemctl enable caddy
sudo systemctl start caddy
sudo systemctl status caddy
# Or run directly
caddy file-server --listen :80 --root /var/www/html
```
5. **Access the report**:
- If using a domain: `https://your-domain.com/missing_albums.html`
- If using IP: `http://your-vps-ip/missing_albums.html`
## Customization
### Change Report Generation Frequency
Edit the `runInterval` option:
```nix
services.lidarr-mb-gap.runInterval = "hourly"; # Every hour
services.lidarr-mb-gap.runInterval = "*-*-* 02:00:00"; # Daily at 2 AM
services.lidarr-mb-gap.runInterval = "Mon *-*-* 00:00:00"; # Weekly on Monday
```
### Add Authentication to Caddy
Uncomment the `basicauth` section in `Caddyfile` and generate a password:
```bash
caddy hash-password
# Enter your password, copy the hash
```
Then update the Caddyfile with your username and hash.
### Use a Custom Domain with HTTPS
Update the Caddyfile:
```
your-domain.com {
root * /var/www/html
file_server
encode gzip
}
```
Caddy will automatically obtain and renew SSL certificates via Let's Encrypt.
## Troubleshooting
### Report not generating
```bash
# Check service logs
sudo journalctl -u lidarr-mb-gap -n 50
# Check if .env file exists and has correct permissions
sudo ls -la /var/lib/lidarr-mb-gap/.env
# Test manually
sudo -u lidarr-mb-gap nix run /path/to/flake#lidarr-mb-gap -- --output-dir /var/lib/lidarr-mb-gap/reports
```
### Files not syncing to VPS
```bash
# Test SSH connection
ssh user@vps
# Test rsync manually
rsync -avz /var/lib/lidarr-mb-gap/reports/ user@vps:/var/www/html/
```
### Caddy not serving files
```bash
# Check Caddy logs
sudo journalctl -u caddy -f
# Verify file permissions
ls -la /var/www/html/
# Test Caddy config
sudo caddy validate --config /etc/caddy/Caddyfile
```
## Maintenance
- Reports are generated in `/var/lib/lidarr-mb-gap/reports/`
- Old reports are overwritten on each run
- To keep history, modify the service to add timestamps to filenames
- Monitor disk space if keeping history

119
nixos/lidarr-mb-gap.nix Normal file
View File

@@ -0,0 +1,119 @@
{ config, lib, pkgs, ... }:
let
reportDir = "/var/lib/lidarr-mb-gap/reports";
envFile = "/var/lib/lidarr-mb-gap/.env";
in
{
options.services.lidarr-mb-gap = {
enable = lib.mkEnableOption "Lidarr MusicBrainz Gap Reporter";
package = lib.mkOption {
type = lib.types.nullOr lib.types.package;
default = null;
description = "The lidarr-mb-gap package to use. If null, will be built from src.";
};
src = lib.mkOption {
type = lib.types.nullOr lib.types.path;
default = null;
description = "Source path for building the package. Required if package is not explicitly set.";
};
reportDir = lib.mkOption {
type = lib.types.str;
default = reportDir;
description = "Directory where reports will be generated";
};
envFile = lib.mkOption {
type = lib.types.str;
default = envFile;
description = "Path to .env file with LIDARR_URL and LIDARR_API_KEY";
};
runInterval = lib.mkOption {
type = lib.types.str;
default = "daily";
description = "systemd timer interval (e.g., 'daily', 'hourly', '*-*-* 02:00:00')";
};
syncToVPS = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Whether to sync reports to VPS via rsync";
};
vpsHost = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
description = "VPS hostname or IP for rsync (e.g., 'vps' or 'user@vps.example.com')";
};
vpsPath = lib.mkOption {
type = lib.types.str;
default = "/var/www/html";
description = "Path on VPS where reports should be synced";
};
};
config = lib.mkIf config.services.lidarr-mb-gap.enable {
systemd.tmpfiles.rules = [
"d ${config.services.lidarr-mb-gap.reportDir} 0755 lidarr-mb-gap lidarr-mb-gap -"
];
users.users.lidarr-mb-gap = {
isSystemUser = true;
group = "lidarr-mb-gap";
home = "/var/lib/lidarr-mb-gap";
createHome = true;
};
users.groups.lidarr-mb-gap = {};
systemd.services.lidarr-mb-gap = {
description = "Generate Lidarr MusicBrainz Gap Report";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
Type = "oneshot";
User = "lidarr-mb-gap";
Group = "lidarr-mb-gap";
WorkingDirectory = config.services.lidarr-mb-gap.reportDir;
EnvironmentFile = config.services.lidarr-mb-gap.envFile;
ExecStart = pkgs.writeShellScript "lidarr-mb-gap-run" ''
set -euo pipefail
# Export OUTPUT_DIR to environment
export OUTPUT_DIR=${config.services.lidarr-mb-gap.reportDir}
# Run the binary
${lib.getExe lidarrMbGapPackage} || {
echo "Failed to generate report"
exit 1
}
# Sync to VPS if enabled
${lib.optionalString (config.services.lidarr-mb-gap.syncToVPS && config.services.lidarr-mb-gap.vpsHost != null) ''
${pkgs.rsync}/bin/rsync -avz --delete \
${config.services.lidarr-mb-gap.reportDir}/ \
${config.services.lidarr-mb-gap.vpsHost}:${config.services.lidarr-mb-gap.vpsPath}/
''}
'';
StandardOutput = "journal";
StandardError = "journal";
};
};
systemd.timers.lidarr-mb-gap = {
description = "Timer for Lidarr MusicBrainz Gap Report";
wantedBy = [ "timers.target" ];
timerConfig = {
OnCalendar = config.services.lidarr-mb-gap.runInterval;
Persistent = true;
};
};
};
}

View File

@@ -2,12 +2,15 @@
import logging import logging
from html import escape from html import escape
from pathlib import Path
from typing import Dict, List from typing import Dict, List
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def generate_html_report(albums_to_add: List[Dict], albums_to_update: List[Dict]): def generate_html_report(
albums_to_add: List[Dict], albums_to_update: List[Dict], output_path: Path = Path("missing_albums.html")
):
"""Generate an HTML report with clickable submission links""" """Generate an HTML report with clickable submission links"""
html_content = """<!DOCTYPE html> html_content = """<!DOCTYPE html>
<html lang="en"> <html lang="en">
@@ -355,6 +358,6 @@ def generate_html_report(albums_to_add: List[Dict], albums_to_update: List[Dict]
html_footer = "\n</body>\n</html>\n" html_footer = "\n</body>\n</html>\n"
html_content = html_header + albums_html + html_footer html_content = html_header + albums_html + html_footer
with open("missing_albums.html", "w", encoding="utf-8") as f: with open(output_path, "w", encoding="utf-8") as f:
f.write(html_content) f.write(html_content)
logger.info("HTML report saved to missing_albums.html") logger.info(f"HTML report saved to {output_path}")

View File

@@ -8,6 +8,7 @@ import json
import logging import logging
import os import os
import sys import sys
from pathlib import Path
from typing import Dict, List, Optional, Tuple from typing import Dict, List, Optional, Tuple
from urllib.parse import quote from urllib.parse import quote
@@ -260,6 +261,10 @@ def main():
LIDARR_API_KEY = os.getenv("LIDARR_API_KEY") LIDARR_API_KEY = os.getenv("LIDARR_API_KEY")
SAMBL_URL = os.getenv("SAMBL_URL") or None SAMBL_URL = os.getenv("SAMBL_URL") or None
MAX_ARTISTS = int(os.getenv("MAX_ARTISTS", "5")) MAX_ARTISTS = int(os.getenv("MAX_ARTISTS", "5"))
OUTPUT_DIR = os.getenv("OUTPUT_DIR", ".")
output_dir = Path(OUTPUT_DIR)
output_dir.mkdir(parents=True, exist_ok=True)
if not LIDARR_URL: if not LIDARR_URL:
logger.error("LIDARR_URL not set") logger.error("LIDARR_URL not set")
@@ -347,11 +352,13 @@ def main():
"total": len(all_albums), "total": len(all_albums),
}, },
} }
with open("missing_albums.json", "w", encoding="utf-8") as f: json_path = output_dir / "missing_albums.json"
with open(json_path, "w", encoding="utf-8") as f:
json.dump(output_data, f, indent=2, ensure_ascii=False) json.dump(output_data, f, indent=2, ensure_ascii=False)
logger.info("Results saved to missing_albums.json") logger.info(f"Results saved to {json_path}")
generate_html_report(all_albums_to_add, all_albums_to_update) html_path = output_dir / "missing_albums.html"
generate_html_report(all_albums_to_add, all_albums_to_update, html_path)
if __name__ == "__main__": if __name__ == "__main__":