diff --git a/flake.nix b/flake.nix index 59e73ee..f6e9053 100644 --- a/flake.nix +++ b/flake.nix @@ -7,7 +7,17 @@ }; 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 pkgs = import nixpkgs { inherit system; }; lib = pkgs.lib; diff --git a/nixos/DEPLOYMENT.md b/nixos/DEPLOYMENT.md new file mode 100644 index 0000000..e9c67ed --- /dev/null +++ b/nixos/DEPLOYMENT.md @@ -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 + diff --git a/nixos/lidarr-mb-gap.nix b/nixos/lidarr-mb-gap.nix new file mode 100644 index 0000000..2059d51 --- /dev/null +++ b/nixos/lidarr-mb-gap.nix @@ -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; + }; + }; + }; +} + diff --git a/src/html_report.py b/src/html_report.py index 2ad0295..f00a01d 100644 --- a/src/html_report.py +++ b/src/html_report.py @@ -2,12 +2,15 @@ import logging from html import escape +from pathlib import Path from typing import Dict, List 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""" html_content = """ @@ -355,6 +358,6 @@ def generate_html_report(albums_to_add: List[Dict], albums_to_update: List[Dict] html_footer = "\n\n\n" 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) - logger.info("HTML report saved to missing_albums.html") + logger.info(f"HTML report saved to {output_path}") diff --git a/src/main.py b/src/main.py index 7e57713..cedf481 100755 --- a/src/main.py +++ b/src/main.py @@ -8,6 +8,7 @@ import json import logging import os import sys +from pathlib import Path from typing import Dict, List, Optional, Tuple from urllib.parse import quote @@ -260,6 +261,10 @@ def main(): LIDARR_API_KEY = os.getenv("LIDARR_API_KEY") SAMBL_URL = os.getenv("SAMBL_URL") or None 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: logger.error("LIDARR_URL not set") @@ -347,11 +352,13 @@ def main(): "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) - 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__":