Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 83ee518091 | |||
| 3ee2b1a9d8 | |||
| e9a5781944 | |||
| 1f8a34d0e8 | |||
| 1176132627 | |||
| 01b31a3493 | |||
| 28fc71dbbe | |||
| c7392f2dd9 |
@@ -1,189 +0,0 @@
|
|||||||
# Alexa Media Player Integration — Implementation Plan
|
|
||||||
|
|
||||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
||||||
|
|
||||||
**Goal:** Package the `alexa_media_player` HACS component declaratively on jupiter's Home Assistant so HA can drive the household Echo devices (TTS/announce, media, sensors).
|
|
||||||
|
|
||||||
**Architecture:** Add one `buildHomeAssistantComponent` entry to `services.home-assistant.customComponents` (same pattern as the existing `ha-sourdough`). The component has real Python deps, so the module's `let` block also defines two supporting derivations built against HA's own interpreter: a new `dictor` package and an `alexapy` version bump. No HACS runtime, no YAML config — the integration is added through the HA UI after the rebuild.
|
|
||||||
|
|
||||||
**Tech Stack:** NixOS, `pkgs.buildHomeAssistantComponent`, `buildPythonPackage`, `fetchFromGitHub`/`fetchFromGitLab`/`fetchPypi`.
|
|
||||||
|
|
||||||
## Global Constraints
|
|
||||||
|
|
||||||
- Target machine: **jupiter** only. All edits live in `modules/environments/home-assistant/default.nix`.
|
|
||||||
- Python dep derivations MUST build against Home Assistant's interpreter set — `pkgs.home-assistant.python3Packages` — the same set `buildHomeAssistantComponent` uses. Do not use top-level `pkgs.python3Packages`.
|
|
||||||
- Manifest requirements are enforced at build time by `manifestCheckPhase` and again by HA at runtime. Every requirement must resolve to an installed dist satisfying its specifier. Exact pins (verbatim from `alexa_media_player` v5.15.7 `manifest.json`):
|
|
||||||
- `alexapy==1.29.25`
|
|
||||||
- `dictor>=0.1.12,<0.2`
|
|
||||||
- `wrapt>=1.14.0` (nixpkgs 1.17.2 — already satisfied)
|
|
||||||
- `packaging>=20.3` (nixpkgs 26.1 — already satisfied)
|
|
||||||
- Pinned artifacts (all three hashes verified to build during planning):
|
|
||||||
|
|
||||||
| Artifact | Fetcher | Ref / version | Hash |
|
|
||||||
| ------------------ | ---------------- | ------------- | ------------------------------------------------- |
|
|
||||||
| alexa_media_player | fetchFromGitHub | `v5.15.7` | `sha256-1rcZVSX1xA1Lc4qSu39MOitVEciZFhoPQy2y5+PpoAI=` |
|
|
||||||
| alexapy | fetchFromGitLab | `v1.29.25` | `sha256-P/hvgqZVaBJF5dbmHrDjQMC+pwV3EEhKyFIS5KmhgD4=` |
|
|
||||||
| dictor | fetchPypi (sdist)| `0.1.12` | `sha256-bbSDda4eU9ye2EToWzj04/v79qTmC+yjd1Fa0URTuRs=` |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## File Structure
|
|
||||||
|
|
||||||
Single file touched: `modules/environments/home-assistant/default.nix`.
|
|
||||||
- Its `let` block gains `haPython`, `dictor`, and `alexapy` bindings.
|
|
||||||
- Its `services.home-assistant.customComponents` list gains a second entry (`alexa_media`) alongside the existing `sourdough` entry.
|
|
||||||
|
|
||||||
No new files. The two supporting derivations are small and specific to this component, so they live inline in the module's `let` block next to their only consumer (files that change together live together).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Task 1: Package `alexa_media_player` with its Python dependencies
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- Modify: `modules/environments/home-assistant/default.nix` (the `let` block, currently lines 9-12; and the `customComponents` list, currently lines 29-41)
|
|
||||||
|
|
||||||
**Interfaces:**
|
|
||||||
- Consumes: `pkgs.home-assistant.python3Packages` (HA interpreter set), `pkgs.buildHomeAssistantComponent`, `pkgs.fetchFromGitHub`, `pkgs.fetchFromGitLab`.
|
|
||||||
- Produces: a second `customComponents` entry with `domain = "alexa_media"`. No other module consumes these `let` bindings.
|
|
||||||
|
|
||||||
- [ ] **Step 1: Add the supporting derivations to the `let` block**
|
|
||||||
|
|
||||||
Edit the `let` block so it reads exactly:
|
|
||||||
|
|
||||||
```nix
|
|
||||||
let
|
|
||||||
cfg = config.my.profiles.home-assistant;
|
|
||||||
hostName = config.networking.hostName;
|
|
||||||
|
|
||||||
# Python deps for the alexa_media_player custom component (Task: Alexa).
|
|
||||||
# Built against Home Assistant's own interpreter — the same set
|
|
||||||
# buildHomeAssistantComponent uses — so HA's build-time and runtime
|
|
||||||
# manifest-requirement checks are satisfied.
|
|
||||||
haPython = pkgs.home-assistant.python3Packages;
|
|
||||||
|
|
||||||
# dictor is not in nixpkgs; alexa_media_player needs dictor>=0.1.12,<0.2.
|
|
||||||
# Pure-Python, no runtime deps, legacy setup.py.
|
|
||||||
dictor = haPython.buildPythonPackage {
|
|
||||||
pname = "dictor";
|
|
||||||
version = "0.1.12";
|
|
||||||
format = "setuptools";
|
|
||||||
src = haPython.fetchPypi {
|
|
||||||
pname = "dictor";
|
|
||||||
version = "0.1.12";
|
|
||||||
hash = "sha256-bbSDda4eU9ye2EToWzj04/v79qTmC+yjd1Fa0URTuRs=";
|
|
||||||
};
|
|
||||||
build-system = [ haPython.setuptools ];
|
|
||||||
doCheck = false;
|
|
||||||
pythonImportsCheck = [ "dictor" ];
|
|
||||||
};
|
|
||||||
|
|
||||||
# nixpkgs ships alexapy 1.29.22; the manifest pins ==1.29.25. Patch bump.
|
|
||||||
# nixpkgs fetches alexapy from GitLab (keatontaylor/alexapy), tag v<version>.
|
|
||||||
alexapy = haPython.alexapy.overridePythonAttrs (old: {
|
|
||||||
version = "1.29.25";
|
|
||||||
src = pkgs.fetchFromGitLab {
|
|
||||||
owner = "keatontaylor";
|
|
||||||
repo = "alexapy";
|
|
||||||
tag = "v1.29.25";
|
|
||||||
hash = "sha256-P/hvgqZVaBJF5dbmHrDjQMC+pwV3EEhKyFIS5KmhgD4=";
|
|
||||||
};
|
|
||||||
});
|
|
||||||
in
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 2: Add the `alexa_media` entry to `customComponents`**
|
|
||||||
|
|
||||||
In `services.home-assistant.customComponents`, immediately after the closing `)` of the existing `sourdough` entry (current line 40) and before the list's closing `]` (current line 41), add:
|
|
||||||
|
|
||||||
```nix
|
|
||||||
(pkgs.buildHomeAssistantComponent {
|
|
||||||
owner = "Alandtse";
|
|
||||||
domain = "alexa_media";
|
|
||||||
version = "5.15.7";
|
|
||||||
src = pkgs.fetchFromGitHub {
|
|
||||||
owner = "Alandtse";
|
|
||||||
repo = "alexa_media_player";
|
|
||||||
rev = "v5.15.7";
|
|
||||||
hash = "sha256-1rcZVSX1xA1Lc4qSu39MOitVEciZFhoPQy2y5+PpoAI=";
|
|
||||||
};
|
|
||||||
# Every manifest requirement must be importable at a satisfying
|
|
||||||
# version or manifestCheckPhase fails the build.
|
|
||||||
dependencies = [
|
|
||||||
alexapy
|
|
||||||
dictor
|
|
||||||
haPython.wrapt
|
|
||||||
haPython.packaging
|
|
||||||
];
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 3: Format the file**
|
|
||||||
|
|
||||||
Run: `nixfmt-rfc-style modules/environments/home-assistant/default.nix`
|
|
||||||
Expected: exits 0, no diff surprises (re-read the file if unsure).
|
|
||||||
|
|
||||||
- [ ] **Step 4: Build the component in isolation first (fast feedback)**
|
|
||||||
|
|
||||||
This is the real test: it runs `manifestCheckPhase`, which fails loudly if any of the four requirements is unmet. It was verified to succeed during planning.
|
|
||||||
|
|
||||||
Run:
|
|
||||||
```bash
|
|
||||||
nix build --impure --no-link --print-out-paths --expr '
|
|
||||||
let
|
|
||||||
pkgs = (builtins.getFlake (toString ./.)).nixosConfigurations.jupiter.pkgs;
|
|
||||||
py = pkgs.home-assistant.python3Packages;
|
|
||||||
dictor = py.buildPythonPackage {
|
|
||||||
pname = "dictor"; version = "0.1.12"; format = "setuptools";
|
|
||||||
src = py.fetchPypi { pname = "dictor"; version = "0.1.12"; hash = "sha256-bbSDda4eU9ye2EToWzj04/v79qTmC+yjd1Fa0URTuRs="; };
|
|
||||||
build-system = [ py.setuptools ]; doCheck = false; pythonImportsCheck = [ "dictor" ];
|
|
||||||
};
|
|
||||||
alexapy = py.alexapy.overridePythonAttrs (old: {
|
|
||||||
version = "1.29.25";
|
|
||||||
src = pkgs.fetchFromGitLab { owner = "keatontaylor"; repo = "alexapy"; tag = "v1.29.25"; hash = "sha256-P/hvgqZVaBJF5dbmHrDjQMC+pwV3EEhKyFIS5KmhgD4="; };
|
|
||||||
});
|
|
||||||
in pkgs.buildHomeAssistantComponent {
|
|
||||||
owner = "Alandtse"; domain = "alexa_media"; version = "5.15.7";
|
|
||||||
src = pkgs.fetchFromGitHub { owner = "Alandtse"; repo = "alexa_media_player"; rev = "v5.15.7"; hash = "sha256-1rcZVSX1xA1Lc4qSu39MOitVEciZFhoPQy2y5+PpoAI="; };
|
|
||||||
dependencies = [ alexapy dictor py.wrapt py.packaging ];
|
|
||||||
}'
|
|
||||||
```
|
|
||||||
Expected: prints a `/nix/store/...-Alandtse-alexa_media-5.15.7` path, exit 0. If it fails with `<pkg><specifier> not satisfied by version ...`, a dependency version drifted — re-check the manifest pin against the provided dep.
|
|
||||||
|
|
||||||
- [ ] **Step 5: Build the whole jupiter system (integration gate)**
|
|
||||||
|
|
||||||
Run: `nix build '.#nixosConfigurations.jupiter.config.system.build.toplevel'`
|
|
||||||
Expected: builds to completion, exit 0. This confirms the module edits evaluate and the component is wired into HA's package.
|
|
||||||
|
|
||||||
- [ ] **Step 6: Commit**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git add modules/environments/home-assistant/default.nix
|
|
||||||
git commit -m "$(cat <<'EOF'
|
|
||||||
feat(home-assistant): add alexa_media_player custom component
|
|
||||||
|
|
||||||
Package the Alexa Media Player HACS component declaratively (v5.15.7),
|
|
||||||
so HA can drive the Echo devices (TTS/announce, media, sensors). Needs
|
|
||||||
dictor 0.1.12 (absent from nixpkgs) and an alexapy 1.29.22 -> 1.29.25
|
|
||||||
bump to satisfy the manifest's exact requirement pins; both build
|
|
||||||
against HA's interpreter. Config-flow based: add via the HA UI and sign
|
|
||||||
in with the Amazon account after rebuild.
|
|
||||||
|
|
||||||
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
|
||||||
EOF
|
|
||||||
)"
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Post-implementation (manual, by the user — not an automatable step)
|
|
||||||
|
|
||||||
After `sudo nixos-rebuild switch --flake '.#jupiter'`:
|
|
||||||
1. Settings → Devices & Services → Add Integration → **Alexa Media Player**.
|
|
||||||
2. Sign in with the Amazon account; complete any 2FA / app-password prompt in the UI flow.
|
|
||||||
3. Confirm media_player entities and sensors appear; test a TTS/announce service call to an Echo.
|
|
||||||
|
|
||||||
## Self-Review
|
|
||||||
|
|
||||||
- **Spec coverage:** goal (alexa_media_player packaging) → Task 1; alexapy bump → Step 1; dictor packaging → Step 1; component entry → Step 2; wrapt/packaging already-satisfied → included as deps in Step 2; build-time manifest check → Steps 4-5; runtime config-flow → Post-implementation. Deferred Alexa→HA scope: intentionally absent. No gaps.
|
|
||||||
- **Placeholder scan:** none — every step has concrete code/commands and the verified hashes.
|
|
||||||
- **Type/name consistency:** `haPython`, `dictor`, `alexapy` defined in Step 1 and referenced by those exact names in Step 2; domain `alexa_media` consistent throughout; hashes identical across plan, spec, and the verified build.
|
|
||||||
@@ -0,0 +1,299 @@
|
|||||||
|
# Immich NixOS Module Implementation Plan
|
||||||
|
|
||||||
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||||
|
>
|
||||||
|
> **Note on nature:** Task 1 is repo work verifiable with `nix build` (no runtime tests exist for declarative config). Tasks 2–6 are a **manual migration runbook executed on jupiter by the operator** — they are destructive and cannot be run from the dev machine (mibook). Do not attempt to automate or execute Tasks 2–6 from an agent session; present them for the operator to run and confirm.
|
||||||
|
|
||||||
|
**Goal:** Replace jupiter's docker-compose Immich with the native `services.immich` NixOS module, preserving all data (albums, faces, shares, library).
|
||||||
|
|
||||||
|
**Architecture:** A standard `my.profiles.immich` module wraps `services.immich` (native Postgres+VectorChord over unix socket, Redis, server, machine-learning). Media stays at the default local `/var/lib/immich`. The existing docker Postgres dump is restored same-version (2.7.5 → 2.7.5, no schema/vector migration). GPU is exposed for VAAPI/QSV transcoding.
|
||||||
|
|
||||||
|
**Tech Stack:** NixOS (flake-parts), `services.immich` from nixpkgs 25.11, PostgreSQL, Intel QSV/VAAPI, docker (source only).
|
||||||
|
|
||||||
|
## Global Constraints
|
||||||
|
|
||||||
|
- Machine: **jupiter** only. Do not enable on mibook.
|
||||||
|
- Immich version: source docker == target nixpkgs == **2.7.5** (stable). No `package` override. Do NOT bump nixpkgs Immich during this work.
|
||||||
|
- Media location: default `/var/lib/immich` (local disk). Do not point at the NAS.
|
||||||
|
- Database: local PostgreSQL over **unix socket + peer auth** — no password, no sops secret.
|
||||||
|
- HW accel: **video transcoding only**. ML stays on CPU (`machine-learning.enable = true`, no OpenVINO).
|
||||||
|
- Access: LAN + VPN, `openFirewall = true`, port **2283**. No reverse proxy/TLS.
|
||||||
|
- Rebuild command: `sudo nixos-rebuild switch --flake '.#jupiter'`.
|
||||||
|
- Build-check command: `nix build '.#nixosConfigurations.jupiter.config.system.build.toplevel'`.
|
||||||
|
- Format Nix with `nixfmt-rfc-style` before committing.
|
||||||
|
- Do not delete docker DB or upload data until Task 6 sign-off.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## File Structure
|
||||||
|
|
||||||
|
- **Create** `modules/environments/immich/default.nix` — the `my.profiles.immich` module (single responsibility: declare Immich).
|
||||||
|
- **Modify** `modules/environments/default.nix` — add `./environments/immich` to the import list.
|
||||||
|
- **Modify** `machines/jupiter/environments.nix` — set `immich.enable = true`.
|
||||||
|
|
||||||
|
No other files change. The DB/media migration touches only runtime state on jupiter, not the repo.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 1: Author the `immich` profile module
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `modules/environments/immich/default.nix`
|
||||||
|
- Modify: `modules/environments/default.nix` (import list)
|
||||||
|
- Modify: `machines/jupiter/environments.nix` (`my.profiles.immich.enable`)
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Produces: NixOS option `my.profiles.immich.enable` (bool). When true, configures `services.immich`, adds `immich` user to `video`/`render` groups, and appends an entry to `my.homepage.services`.
|
||||||
|
- Consumes: existing `my.homepage.services` aggregator; `config.networking.hostName`.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Read a reference module to match repo style**
|
||||||
|
|
||||||
|
Read `modules/environments/jellyfin/default.nix` (same shape: `cfg`, `hostName`, `port`, `mkIf`, `my.homepage.services`). Match its formatting and header-comment convention.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Create the module file**
|
||||||
|
|
||||||
|
Create `modules/environments/immich/default.nix`:
|
||||||
|
|
||||||
|
```nix
|
||||||
|
# Immich self-hosted photo & video server
|
||||||
|
{
|
||||||
|
config,
|
||||||
|
lib,
|
||||||
|
pkgs,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
let
|
||||||
|
cfg = config.my.profiles.immich;
|
||||||
|
hostName = config.networking.hostName;
|
||||||
|
port = 2283;
|
||||||
|
in
|
||||||
|
{
|
||||||
|
options.my.profiles.immich = with lib; {
|
||||||
|
enable = mkEnableOption "Immich photo server";
|
||||||
|
};
|
||||||
|
|
||||||
|
config = lib.mkIf cfg.enable {
|
||||||
|
services.immich = {
|
||||||
|
enable = true;
|
||||||
|
host = "0.0.0.0";
|
||||||
|
inherit port;
|
||||||
|
openFirewall = true;
|
||||||
|
mediaLocation = "/var/lib/immich";
|
||||||
|
machine-learning.enable = true;
|
||||||
|
accelerationDevices = [ "/dev/dri/renderD128" ];
|
||||||
|
settings.server.externalDomain = "http://${hostName}:${toString port}";
|
||||||
|
};
|
||||||
|
|
||||||
|
# The native module does not add GPU groups; required for VAAPI/QSV transcoding.
|
||||||
|
users.users.immich.extraGroups = [
|
||||||
|
"video"
|
||||||
|
"render"
|
||||||
|
];
|
||||||
|
|
||||||
|
my.homepage.services = [
|
||||||
|
{
|
||||||
|
group = "Media";
|
||||||
|
name = "Immich";
|
||||||
|
description = "Photo & video server";
|
||||||
|
href = "http://${hostName}:${toString port}";
|
||||||
|
icon = "immich.png";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Register the module in the environments import list**
|
||||||
|
|
||||||
|
Open `modules/environments/default.nix` and add `./environments/immich` (or `./immich`, matching the exact relative style already used in that file — check how `jellyfin` is listed and mirror it).
|
||||||
|
|
||||||
|
- [ ] **Step 4: Enable it on jupiter**
|
||||||
|
|
||||||
|
In `machines/jupiter/environments.nix`, inside the `my.profiles = { ... }` block, add:
|
||||||
|
|
||||||
|
```nix
|
||||||
|
immich.enable = true;
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 5: Format**
|
||||||
|
|
||||||
|
Run: `nixfmt-rfc-style modules/environments/immich/default.nix`
|
||||||
|
|
||||||
|
- [ ] **Step 6: Build-check (this is the "test")**
|
||||||
|
|
||||||
|
Run: `nix build '.#nixosConfigurations.jupiter.config.system.build.toplevel'`
|
||||||
|
Expected: builds successfully. If it fails on an unknown option (e.g. `accelerationDevices`, `settings.server.externalDomain`), reconcile against the module at `$(nix eval --raw '.#nixosConfigurations.jupiter.pkgs.path')/nixos/modules/services/web-apps/immich.nix` and fix.
|
||||||
|
|
||||||
|
- [ ] **Step 7: Confirm the option evaluates on**
|
||||||
|
|
||||||
|
Run: `nix eval '.#nixosConfigurations.jupiter.config.services.immich.enable'`
|
||||||
|
Expected: `true`
|
||||||
|
|
||||||
|
- [ ] **Step 8: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add modules/environments/immich/default.nix modules/environments/default.nix machines/jupiter/environments.nix
|
||||||
|
git commit -m "feat(jupiter): add native Immich profile module"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 2: Pre-flight & backup on jupiter (operator-run)
|
||||||
|
|
||||||
|
**Files:** none (runtime state on jupiter). Run all commands on jupiter.
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Produces: `immich-db.sql` dump file and a known-good copy/snapshot of the docker upload folder; recorded `UPLOAD_LOCATION` path and DB container name.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Record docker facts**
|
||||||
|
|
||||||
|
From the docker-compose dir on jupiter, note `UPLOAD_LOCATION`, the DB service/container name, and `POSTGRES_USER`/`POSTGRES_DB` from `.env`/compose. Confirm server version is **2.7.5** (web UI footer or `docker exec <server> immich --version`). If it is not 2.7.5, STOP — this plan assumes a same-version restore.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Stop the docker stack (DB may stay up for the dump)**
|
||||||
|
|
||||||
|
Run: `docker compose stop immich-server immich-machine-learning` (leave the DB container running).
|
||||||
|
|
||||||
|
- [ ] **Step 3: Dump the database**
|
||||||
|
|
||||||
|
Run: `docker exec -t <db-container> pg_dumpall --clean --if-exists --username=<POSTGRES_USER> > ~/immich-db.sql`
|
||||||
|
Expected: a non-trivial `immich-db.sql` (check it is not near-empty: `wc -l ~/immich-db.sql`).
|
||||||
|
|
||||||
|
- [ ] **Step 4: Stop the DB and record the media size**
|
||||||
|
|
||||||
|
Run: `docker compose down` then `du -sh <UPLOAD_LOCATION>` and note the size. Do NOT copy yet. Do NOT delete anything.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 3: First switch — let the module create empty state (operator-run)
|
||||||
|
|
||||||
|
**Files:** none at runtime (repo change already committed in Task 1). Run on jupiter after pulling the committed branch.
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Consumes: `immich-db.sql`, `UPLOAD_LOCATION` from Task 2.
|
||||||
|
- Produces: an `immich` system user, an empty `immich` Postgres DB + role, and `/var/lib/immich` created with correct ownership, with services then stopped.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Deploy the config**
|
||||||
|
|
||||||
|
On jupiter, check out the branch containing Task 1's commit and run:
|
||||||
|
`sudo nixos-rebuild switch --flake '.#jupiter'`
|
||||||
|
Expected: `immich-server`, `immich-machine-learning`, postgres, and redis units come up; UI reachable at `http://jupiter:2283` showing a fresh/empty instance.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Stop immich so data can be swapped underneath**
|
||||||
|
|
||||||
|
Run: `sudo systemctl stop immich-server immich-machine-learning`
|
||||||
|
Expected: both inactive. PostgreSQL and Redis stay running.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Verify the DB and user exist**
|
||||||
|
|
||||||
|
Run: `sudo -u postgres psql -c '\l' | grep immich` and `sudo -u postgres psql -c '\du' | grep immich`
|
||||||
|
Expected: an `immich` database and `immich` role are present.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 4: Restore database and media (operator-run, destructive)
|
||||||
|
|
||||||
|
**Files:** none in repo. Run on jupiter. This overwrites the freshly-created empty DB.
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Consumes: `immich-db.sql`, `<UPLOAD_LOCATION>`, the running NixOS PostgreSQL.
|
||||||
|
- Produces: the migrated DB and populated `/var/lib/immich`.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Restore the dump into the NixOS Postgres**
|
||||||
|
|
||||||
|
`pg_dumpall` output includes role/DB creation. Load it as the `postgres` superuser over the unix socket:
|
||||||
|
Run: `sudo -u postgres psql -f ~/immich-db.sql`
|
||||||
|
Expected: completes without fatal errors. Harmless "role already exists"/"database already exists" notices are OK because of `--clean --if-exists`. If the immich DB ends up owned by the wrong role, reassign: `sudo -u postgres psql -c 'ALTER DATABASE immich OWNER TO immich;'`.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Sanity-check the restored data**
|
||||||
|
|
||||||
|
Run: `sudo -u postgres psql -d immich -c 'SELECT count(*) FROM assets;'`
|
||||||
|
Expected: a count matching your library size (non-zero). If the table name differs by version, list tables with `\dt` and check an obviously-populated one.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Move the media into the default location**
|
||||||
|
|
||||||
|
Immich's upload folder holds subdirs `library/ upload/ thumbs/ encoded-video/ profile/ backups/`. Move (not copy, if same filesystem) the contents of `<UPLOAD_LOCATION>` into `/var/lib/immich`:
|
||||||
|
Run: `sudo rsync -aHAX --info=progress2 <UPLOAD_LOCATION>/ /var/lib/immich/`
|
||||||
|
(Use `rsync` — safe if partially interrupted. Keep the source until Task 6 sign-off.)
|
||||||
|
|
||||||
|
- [ ] **Step 4: Fix ownership**
|
||||||
|
|
||||||
|
Run: `sudo chown -R immich:immich /var/lib/immich`
|
||||||
|
Expected: everything under `/var/lib/immich` owned by `immich`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 5: Start and verify (operator-run)
|
||||||
|
|
||||||
|
**Files:** none. Run on jupiter.
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Consumes: migrated DB + media from Task 4.
|
||||||
|
- Produces: a running, verified native Immich.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Start the server and watch logs**
|
||||||
|
|
||||||
|
Run: `sudo systemctl start immich-server && journalctl -u immich-server -f`
|
||||||
|
Expected: it connects to the DB, runs same-version startup checks (no destructive migration since 2.7.5==2.7.5), and reports listening on 2283. Leave the follow running through the next step.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Start machine-learning**
|
||||||
|
|
||||||
|
Run: `sudo systemctl start immich-machine-learning`
|
||||||
|
Expected: active, no crash loop in `journalctl -u immich-machine-learning`.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Functional spot-check in the web UI**
|
||||||
|
|
||||||
|
At `http://jupiter:2283`: log in with an existing account; confirm the timeline loads; open an **album**; open the **People/faces** view; open a **shared link**; open one photo so a **thumbnail and its full original both load** (this proves DB↔file paths align after the media move).
|
||||||
|
Expected: all present, images render.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Confirm homepage dashboard tile**
|
||||||
|
|
||||||
|
Open the homepage dashboard; confirm the Immich tile appears under "Media" and links to `http://jupiter:2283`.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Enable and verify hardware transcoding**
|
||||||
|
|
||||||
|
In Immich **Administration → Settings → Video Transcoding**, set hardware acceleration to **Quick Sync** (QSV) (or VAAPI). Trigger a transcode (upload/play a video that needs transcoding, or run the transcoding job). Then:
|
||||||
|
Run: `journalctl -u immich-server | grep -iE 'qsv|vaapi|hwaccel|transcode'`
|
||||||
|
Expected: log shows the hardware path in use, not a CPU-fallback error. Confirm `/dev/dri/renderD128` is accessible to the service (the `video`/`render` groups + `accelerationDevices` from Task 1 handle this).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 6: Sign-off and cleanup (operator-run)
|
||||||
|
|
||||||
|
**Files:** none in repo. Merge the branch; then, only after a confidence window, remove docker.
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Consumes: a verified running instance (Task 5).
|
||||||
|
|
||||||
|
- [ ] **Step 1: Merge the feature branch**
|
||||||
|
|
||||||
|
Open a PR from `feat/immich-nixos-module` into `main` and merge it (repo convention: PRs via the Gitea remote).
|
||||||
|
|
||||||
|
- [ ] **Step 2: Confidence window**
|
||||||
|
|
||||||
|
Use Immich normally for a few days. Keep the docker `<UPLOAD_LOCATION>` source copy and `~/immich-db.sql` untouched as the rollback path.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Rollback (only if needed, before cleanup)**
|
||||||
|
|
||||||
|
If something is wrong: `sudo systemctl stop immich-server immich-machine-learning`, set `immich.enable = false` (or check out the pre-migration commit), `sudo nixos-rebuild switch --flake '.#jupiter'`, then `docker compose up -d` in the old stack. Original docker DB + upload folder are intact until Step 4.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Cleanup (after sign-off)**
|
||||||
|
|
||||||
|
Remove the docker Immich stack (`docker compose down --rmi all --volumes` in the old dir if the DB volume is dedicated — verify first), delete the now-duplicated `<UPLOAD_LOCATION>` source, and remove `~/immich-db.sql`. Optionally disable the `docker` profile on jupiter if Immich was its only consumer (check other services first — jupiter's `docker.enable` may still be needed).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Self-Review
|
||||||
|
|
||||||
|
**Spec coverage:**
|
||||||
|
- Native `services.immich` → Task 1. ✓
|
||||||
|
- Version target 2.7.5==stable, no override → Global Constraints + Task 2 Step 1. ✓
|
||||||
|
- Media at default `/var/lib/immich` → Task 1 + Task 4 Step 3. ✓
|
||||||
|
- DB migrate keep-everything → Tasks 2–4. ✓
|
||||||
|
- HW transcoding only → Task 1 (`accelerationDevices`, groups) + Task 5 Step 5. ✓
|
||||||
|
- LAN+VPN, port 2283, openFirewall, homepage tile → Task 1 + Task 5 Steps 3–4. ✓
|
||||||
|
- Rollback path → Task 6 Step 3. ✓
|
||||||
|
- Deferred (OpenVINO/NAS/proxy) → correctly absent. ✓
|
||||||
|
|
||||||
|
**Placeholder scan:** No TBD/TODO; every command is concrete. Placeholders like `<db-container>`, `<UPLOAD_LOCATION>`, `<POSTGRES_USER>` are runtime values the operator reads in Task 2 Step 1 — intentional, not gaps.
|
||||||
|
|
||||||
|
**Type consistency:** Option name `my.profiles.immich.enable` and path `/var/lib/immich` used consistently across all tasks. Media subfolder list matches between Task 4 Step 3 and the spec.
|
||||||
@@ -1,152 +0,0 @@
|
|||||||
# Amazon Alexa integration for Home Assistant (jupiter) — design
|
|
||||||
|
|
||||||
**Date:** 2026-07-29
|
|
||||||
**Machine:** jupiter
|
|
||||||
**Status:** approved, ready for implementation plan
|
|
||||||
|
|
||||||
## Goal
|
|
||||||
|
|
||||||
Let Home Assistant control and read the household Amazon Echo devices via the
|
|
||||||
unofficial [`alexa_media_player`](https://github.com/Alandtse/alexa_media_player)
|
|
||||||
integration: text-to-speech / announcements, media control, and per-device
|
|
||||||
sensors (last-called device, next alarm/timer, DND state, etc.).
|
|
||||||
|
|
||||||
This uses the user's Amazon account through an unofficial API. It is a
|
|
||||||
config-flow integration: after the rebuild it is added through the HA UI, not
|
|
||||||
via YAML.
|
|
||||||
|
|
||||||
## Non-goals (deferred)
|
|
||||||
|
|
||||||
Alexa → HA voice control ("Alexa, turn on the light") is **out of scope**. It
|
|
||||||
would need either `emulated_hue` bound to port 80 on the LAN, or a public HTTPS
|
|
||||||
endpoint plus the AWS-Lambda Smart Home Skill. The user chose to decide on that
|
|
||||||
later. Nothing in this change touches the firewall, port 80, systemd unit
|
|
||||||
capabilities, or network exposure.
|
|
||||||
|
|
||||||
## Approach
|
|
||||||
|
|
||||||
Package the integration declaratively, following the existing `ha-sourdough`
|
|
||||||
pattern in `modules/environments/home-assistant/default.nix` (a
|
|
||||||
`buildHomeAssistantComponent` entry in `services.home-assistant.customComponents`).
|
|
||||||
No HACS runtime.
|
|
||||||
|
|
||||||
Unlike sourdough, `alexa_media_player` has real Python dependencies. Its
|
|
||||||
`manifest.json` (v5.15.7) declares:
|
|
||||||
|
|
||||||
```
|
|
||||||
alexapy==1.29.25
|
|
||||||
packaging>=20.3
|
|
||||||
wrapt>=1.14.0
|
|
||||||
dictor>=0.1.12,<0.2
|
|
||||||
```
|
|
||||||
|
|
||||||
`buildHomeAssistantComponent` runs `manifestCheckPhase` at build time
|
|
||||||
(`check_manifest.py`): every requirement must resolve to an installed
|
|
||||||
distribution **whose version satisfies the specifier**, or the build fails.
|
|
||||||
Home Assistant repeats this check at runtime. Therefore each requirement must be
|
|
||||||
satisfied exactly — the exact `==1.29.25` pin in particular.
|
|
||||||
|
|
||||||
Dependency status in the pinned nixpkgs (`nixos-25.11`):
|
|
||||||
|
|
||||||
| Requirement | nixpkgs today | Action |
|
|
||||||
| ---------------------- | ------------- | --------------------------------------- |
|
|
||||||
| `alexapy==1.29.25` | 1.29.22 | **Bump** to 1.29.25 via override |
|
|
||||||
| `dictor>=0.1.12,<0.2` | *absent* | **Package** dictor 0.1.12 (new) |
|
|
||||||
| `wrapt>=1.14.0` | 1.17.2 | none — already satisfied |
|
|
||||||
| `packaging>=20.3` | 26.1 | none — already satisfied |
|
|
||||||
|
|
||||||
`authcaptureproxy` (in nixpkgs at 1.3.7) is a transitive dependency of
|
|
||||||
`alexapy`, not listed in the manifest, so it needs no direct handling.
|
|
||||||
|
|
||||||
## Components
|
|
||||||
|
|
||||||
All changes live in `modules/environments/home-assistant/default.nix` (plus the
|
|
||||||
hashes below). Three small pieces, wired together in the module's `let` block:
|
|
||||||
|
|
||||||
### 1. `dictor` package (new)
|
|
||||||
|
|
||||||
Not in nixpkgs. Pure-Python, no runtime dependencies (`requires_dist: null`),
|
|
||||||
ships a `setup.py`. A minimal `pkgs.python3Packages.buildPythonPackage`:
|
|
||||||
|
|
||||||
- pname `dictor`, version `0.1.12`
|
|
||||||
- `src = fetchPypi { pname = "dictor"; version = "0.1.12"; hash = "sha256-bbSDda4eU9ye2EToWzj04/v79qTmC+yjd1Fa0URTuRs="; }`
|
|
||||||
- setuptools format (legacy `setup.py`); `build-system = [ setuptools ]`
|
|
||||||
- `doCheck = false` (no meaningful test suite); `pythonImportsCheck = [ "dictor" ]`
|
|
||||||
|
|
||||||
Build against the HA Python set so the version lands in HA's venv — i.e. use
|
|
||||||
`config.services.home-assistant.package.python.pkgs` (the same interpreter the
|
|
||||||
component check and HA runtime use), not the top-level `pkgs.python3Packages`.
|
|
||||||
|
|
||||||
### 2. `alexapy` 1.29.25 (override)
|
|
||||||
|
|
||||||
nixpkgs `alexapy` is fetched from **GitLab** (`keatontaylor/alexapy`, tag
|
|
||||||
`v<version>`), not PyPI. Override just the version + src via
|
|
||||||
`overridePythonAttrs`, reusing the existing build-system and dependency list:
|
|
||||||
|
|
||||||
- `version = "1.29.25"`
|
|
||||||
- `src = fetchFromGitLab { owner = "keatontaylor"; repo = "alexapy"; tag = "v1.29.25"; hash = "sha256-P/hvgqZVaBJF5dbmHrDjQMC+pwV3EEhKyFIS5KmhgD4="; }`
|
|
||||||
|
|
||||||
This is a patch bump (1.29.22 → 1.29.25); the dependency set is expected to be
|
|
||||||
unchanged. Override the HA-Python-set `alexapy` so it shares the interpreter
|
|
||||||
with dictor and the component.
|
|
||||||
|
|
||||||
### 3. `alexa_media_player` component (new `customComponents` entry)
|
|
||||||
|
|
||||||
```
|
|
||||||
buildHomeAssistantComponent {
|
|
||||||
owner = "Alandtse";
|
|
||||||
domain = "alexa_media";
|
|
||||||
version = "5.15.7";
|
|
||||||
src = fetchFromGitHub {
|
|
||||||
owner = "Alandtse";
|
|
||||||
repo = "alexa_media_player";
|
|
||||||
rev = "v5.15.7";
|
|
||||||
hash = "sha256-1rcZVSX1xA1Lc4qSu39MOitVEciZFhoPQy2y5+PpoAI=";
|
|
||||||
};
|
|
||||||
dependencies = [ alexapy' dictor' wrapt packaging ]; # HA-python-set packages
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
`dependencies` must make every manifest requirement importable at the required
|
|
||||||
version for `manifestCheckPhase` to pass. Include all four (the two custom ones
|
|
||||||
plus `wrapt` and `packaging` from the HA Python set) to be explicit.
|
|
||||||
|
|
||||||
## Data flow
|
|
||||||
|
|
||||||
1. `nixos-rebuild switch` builds the component; `manifestCheckPhase` validates
|
|
||||||
the four requirements against the provided `dependencies`.
|
|
||||||
2. HA starts; the custom component is present but not configured.
|
|
||||||
3. User adds **Alexa Media Player** in Settings → Devices & Services, signs in
|
|
||||||
with the Amazon account (email/password; 2FA or app-password handled in the
|
|
||||||
UI flow at runtime).
|
|
||||||
4. HA creates media_player entities and per-device sensors; TTS/announce
|
|
||||||
services become available for automations.
|
|
||||||
|
|
||||||
## Error handling / risks
|
|
||||||
|
|
||||||
- **Version-pin drift.** If a future `alexa_media_player` bump changes the
|
|
||||||
`alexapy==` pin, `alexapy` must be re-bumped in lockstep, or the build fails
|
|
||||||
loudly at `manifestCheckPhase` (fail-safe, not silent).
|
|
||||||
- **Amazon login fragility.** The unofficial API can break on Amazon's side
|
|
||||||
(captcha/2FA changes). This is a runtime concern, independent of packaging;
|
|
||||||
not addressed here.
|
|
||||||
- **`dictor` upper bound `<0.2`.** 0.1.12 is the current release and satisfies
|
|
||||||
it. If nixpkgs later gains a `dictor` ≥ 0.2, prefer our pinned 0.1.12 for
|
|
||||||
this component.
|
|
||||||
|
|
||||||
## Verification
|
|
||||||
|
|
||||||
- `nix build '.#nixosConfigurations.jupiter.config.system.build.toplevel'`
|
|
||||||
succeeds — this exercises the build-time manifest-requirements check for all
|
|
||||||
three new/overridden derivations.
|
|
||||||
- `nixfmt-rfc-style` clean on the edited module.
|
|
||||||
- Post-deploy (manual, by the user): the integration appears under Add
|
|
||||||
Integration, and a TTS/announce call reaches an Echo.
|
|
||||||
|
|
||||||
## Pinned artifacts
|
|
||||||
|
|
||||||
| Artifact | Source | Ref / version | Hash |
|
|
||||||
| --------------------------- | ---------- | ------------- | ------------------------------------------------- |
|
|
||||||
| alexa_media_player | GitHub | v5.15.7 | sha256-1rcZVSX1xA1Lc4qSu39MOitVEciZFhoPQy2y5+PpoAI= |
|
|
||||||
| alexapy | GitLab | v1.29.25 | sha256-P/hvgqZVaBJF5dbmHrDjQMC+pwV3EEhKyFIS5KmhgD4= |
|
|
||||||
| dictor | PyPI sdist | 0.1.12 | sha256-bbSDda4eU9ye2EToWzj04/v79qTmC+yjd1Fa0URTuRs= |
|
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
# Immich: Docker → NixOS module migration
|
||||||
|
|
||||||
|
**Date:** 2026-08-05
|
||||||
|
**Machine:** jupiter (home server, Intel iGPU)
|
||||||
|
**Status:** Design approved, pending implementation plan
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Replace the existing docker-compose Immich deployment on jupiter with the
|
||||||
|
native `services.immich` NixOS module, wrapped in the repo's standard
|
||||||
|
`my.profiles.*` pattern. Preserve all existing data (albums, faces, shared
|
||||||
|
links, metadata) and photo/video library.
|
||||||
|
|
||||||
|
## Decisions
|
||||||
|
|
||||||
|
| Topic | Decision |
|
||||||
|
|-------|----------|
|
||||||
|
| Approach | Native `services.immich` (nixpkgs), not `oci-containers` |
|
||||||
|
| Version target | **Resolved: docker runs 2.7.5 == stable nixpkgs 2.7.5.** Use the stable module as-is; no `package` override. Same-version restore, no forward schema migration |
|
||||||
|
| Media location | Default local path `/var/lib/immich`. NAS deferred to a future read-only external library |
|
||||||
|
| Database | Migrate via dump/restore — keep everything |
|
||||||
|
| HW acceleration | Video transcoding only (VAAPI/QSV via existing Intel graphics stack). ML on CPU |
|
||||||
|
| Access | LAN + VPN only: open port 2283, register on homepage dashboard. No reverse proxy/TLS |
|
||||||
|
|
||||||
|
### Deliberately deferred (YAGNI)
|
||||||
|
- OpenVINO ML acceleration
|
||||||
|
- NAS-backed external library
|
||||||
|
- Reverse proxy / TLS / public hostname
|
||||||
|
|
||||||
|
## Part 1 — The module
|
||||||
|
|
||||||
|
New file `modules/environments/immich/default.nix` following the profile
|
||||||
|
pattern; add `./environments/immich` to `modules/environments/default.nix`;
|
||||||
|
enable `my.profiles.immich.enable = true` in
|
||||||
|
`machines/jupiter/environments.nix`.
|
||||||
|
|
||||||
|
```nix
|
||||||
|
{ config, lib, pkgs, ... }:
|
||||||
|
let
|
||||||
|
cfg = config.my.profiles.immich;
|
||||||
|
hostName = config.networking.hostName;
|
||||||
|
port = 2283;
|
||||||
|
in {
|
||||||
|
options.my.profiles.immich.enable = lib.mkEnableOption "Immich photo server";
|
||||||
|
|
||||||
|
config = lib.mkIf cfg.enable {
|
||||||
|
services.immich = {
|
||||||
|
enable = true;
|
||||||
|
# package = pkgs.unstable.immich; # only if docker :release is > 2.7.5
|
||||||
|
host = "0.0.0.0";
|
||||||
|
inherit port;
|
||||||
|
openFirewall = true;
|
||||||
|
mediaLocation = "/var/lib/immich";
|
||||||
|
machine-learning.enable = true;
|
||||||
|
accelerationDevices = [ "/dev/dri/renderD128" ];
|
||||||
|
settings.server.externalDomain = "http://${hostName}:${toString port}";
|
||||||
|
};
|
||||||
|
|
||||||
|
# native module does not add GPU groups; needed for VAAPI/QSV transcoding
|
||||||
|
users.users.immich.extraGroups = [ "video" "render" ];
|
||||||
|
|
||||||
|
my.homepage.services = [{
|
||||||
|
group = "Media";
|
||||||
|
name = "Immich";
|
||||||
|
description = "Photo & video server";
|
||||||
|
href = "http://${hostName}:${toString port}";
|
||||||
|
icon = "immich.png";
|
||||||
|
}];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Provided for free by the native module:** local PostgreSQL with the required
|
||||||
|
vector extension over a **unix socket + peer auth** (so no DB password / sops
|
||||||
|
secret needed), Redis, `immich-server` and `immich-machine-learning` systemd
|
||||||
|
units, the `immich` system user, and `mediaLocation` created via tmpfiles.
|
||||||
|
|
||||||
|
**Transcoding is two parts:** (a) NixOS exposes the GPU device + `video`/`render`
|
||||||
|
groups (above); (b) the hwaccel backend (QSV/VAAPI) is chosen in Immich's
|
||||||
|
**admin → video transcoding** settings after cutover — a UI toggle, not Nix.
|
||||||
|
|
||||||
|
## Part 2 — Migration runbook (on jupiter)
|
||||||
|
|
||||||
|
### Pre-flight (hard blocker)
|
||||||
|
1. Get running docker Immich version (`docker exec <server> immich --version` or web UI footer).
|
||||||
|
2. **Resolved 2026-08-05: running version is 2.7.5, equal to stable nixpkgs.**
|
||||||
|
Use the stable module as-is (no `package` override). Kept for reference:
|
||||||
|
- running ≤ 2.7.5 → stable module as-is ← **this case**
|
||||||
|
- 2.7.6–3.0.3 → set `package = pkgs.unstable.immich`
|
||||||
|
- `> 3.0.3` → bump nixpkgs first; **stop and re-plan**
|
||||||
|
3. Record docker `UPLOAD_LOCATION` and DB container name/credentials.
|
||||||
|
|
||||||
|
### Backup (before touching anything)
|
||||||
|
4. `docker compose down` (DB may stay up for the dump).
|
||||||
|
5. Dump DB: `docker exec -t <db> pg_dumpall --clean --if-exists --username=postgres > immich-db.sql`
|
||||||
|
6. Verify upload folder intact; note size (no copy yet).
|
||||||
|
|
||||||
|
### Cutover
|
||||||
|
7. Add the module to jupiter's `environments.nix` (leave `database.createDB` default).
|
||||||
|
8. `sudo nixos-rebuild switch --flake '.#jupiter'` → creates user, empty DB + role, `mediaLocation`. Then `systemctl stop immich-server immich-machine-learning`.
|
||||||
|
9. Restore the DB into the NixOS Postgres (drop the freshly-created empty `immich` DB, load `immich-db.sql`) per Immich's restore docs.
|
||||||
|
10. Move media into `/var/lib/immich` (subfolders `library/`, `upload/`, `thumbs/`, `encoded-video/`, `profile/`); `chown -R immich:immich /var/lib/immich`.
|
||||||
|
11. `systemctl start immich-server`; it runs schema migrations forward. Watch `journalctl -u immich-server -f`.
|
||||||
|
|
||||||
|
### Verify
|
||||||
|
12. UI at `http://jupiter:2283` loads; log in; spot-check albums, faces, a shared link, and that thumbnails/originals actually load.
|
||||||
|
13. Homepage tile works.
|
||||||
|
14. Enable QSV/VAAPI in admin settings; transcode one video; confirm `journalctl` shows the hw path, not a CPU fallback error.
|
||||||
|
|
||||||
|
### Rollback
|
||||||
|
Before deleting any docker data: `systemctl stop immich-*`, disable the profile,
|
||||||
|
`nixos-rebuild switch`, `docker compose up -d`. Original docker DB + upload
|
||||||
|
folder remain untouched until explicitly removed after a few days of confidence.
|
||||||
|
|
||||||
|
## Known risk — RESOLVED
|
||||||
|
|
||||||
|
The main risk was step 9 crossing the **pgvecto.rs → VectorChord** vector-extension
|
||||||
|
boundary. With source and target both at **2.7.5**, both use VectorChord — no
|
||||||
|
boundary crossing and no forward schema migration. The restore is a same-version
|
||||||
|
dump/load. Residual risk is limited to routine dump/restore mechanics
|
||||||
|
(roles, extension availability in the NixOS Postgres, ownership on restore).
|
||||||
@@ -21,6 +21,7 @@ in
|
|||||||
sonarr.enable = true;
|
sonarr.enable = true;
|
||||||
jellyfin.enable = true;
|
jellyfin.enable = true;
|
||||||
jellyseerr.enable = true;
|
jellyseerr.enable = true;
|
||||||
|
immich.enable = true;
|
||||||
development.enable = true;
|
development.enable = true;
|
||||||
home-assistant.enable = true;
|
home-assistant.enable = true;
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
./disks.nix
|
./disks.nix
|
||||||
./hardware-configuration.nix
|
./hardware-configuration.nix
|
||||||
./environments.nix
|
./environments.nix
|
||||||
|
./network.nix
|
||||||
# ./system.nix use docker here
|
# ./system.nix use docker here
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
_: {
|
||||||
|
# Athena (local AI): allow LAN access to the Hermes web dashboard.
|
||||||
|
# Bound to 0.0.0.0:9119 in the athena docker stack; NixOS default-deny
|
||||||
|
# firewall otherwise blocks inbound connections from other devices.
|
||||||
|
networking.firewall.allowedTCPPorts = [
|
||||||
|
9119 # athena hermes dashboard
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -19,5 +19,6 @@
|
|||||||
./sonarr
|
./sonarr
|
||||||
./jellyfin
|
./jellyfin
|
||||||
./jellyseerr
|
./jellyseerr
|
||||||
|
./immich
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,39 +9,6 @@
|
|||||||
let
|
let
|
||||||
cfg = config.my.profiles.home-assistant;
|
cfg = config.my.profiles.home-assistant;
|
||||||
hostName = config.networking.hostName;
|
hostName = config.networking.hostName;
|
||||||
|
|
||||||
# Python deps for the alexa_media_player custom component. Built against
|
|
||||||
# Home Assistant's own interpreter — the same set buildHomeAssistantComponent
|
|
||||||
# uses — so HA's build-time and runtime manifest-requirement checks pass.
|
|
||||||
haPython = pkgs.home-assistant.python3Packages;
|
|
||||||
|
|
||||||
# dictor is not in nixpkgs; alexa_media_player needs dictor>=0.1.12,<0.2.
|
|
||||||
# Pure-Python, no runtime deps, legacy setup.py.
|
|
||||||
dictor = haPython.buildPythonPackage {
|
|
||||||
pname = "dictor";
|
|
||||||
version = "0.1.12";
|
|
||||||
format = "setuptools";
|
|
||||||
src = haPython.fetchPypi {
|
|
||||||
pname = "dictor";
|
|
||||||
version = "0.1.12";
|
|
||||||
hash = "sha256-bbSDda4eU9ye2EToWzj04/v79qTmC+yjd1Fa0URTuRs=";
|
|
||||||
};
|
|
||||||
build-system = [ haPython.setuptools ];
|
|
||||||
doCheck = false;
|
|
||||||
pythonImportsCheck = [ "dictor" ];
|
|
||||||
};
|
|
||||||
|
|
||||||
# nixpkgs ships alexapy 1.29.22; the manifest pins ==1.29.25. Patch bump.
|
|
||||||
# nixpkgs fetches alexapy from GitLab (keatontaylor/alexapy), tag v<version>.
|
|
||||||
alexapy = haPython.alexapy.overridePythonAttrs (old: {
|
|
||||||
version = "1.29.25";
|
|
||||||
src = pkgs.fetchFromGitLab {
|
|
||||||
owner = "keatontaylor";
|
|
||||||
repo = "alexapy";
|
|
||||||
tag = "v1.29.25";
|
|
||||||
hash = "sha256-P/hvgqZVaBJF5dbmHrDjQMC+pwV3EEhKyFIS5KmhgD4=";
|
|
||||||
};
|
|
||||||
});
|
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
|
|
||||||
@@ -56,42 +23,6 @@ in
|
|||||||
services.home-assistant = {
|
services.home-assistant = {
|
||||||
enable = true;
|
enable = true;
|
||||||
openFirewall = true;
|
openFirewall = true;
|
||||||
|
|
||||||
# HACS-style custom components, packaged declaratively (no HACS runtime).
|
|
||||||
# Config-flow based: add via Settings > Devices & Services after rebuild.
|
|
||||||
customComponents = [
|
|
||||||
(pkgs.buildHomeAssistantComponent {
|
|
||||||
owner = "Matts-Baps";
|
|
||||||
domain = "sourdough";
|
|
||||||
version = "1.1.3";
|
|
||||||
src = pkgs.fetchFromGitHub {
|
|
||||||
owner = "Matts-Baps";
|
|
||||||
repo = "ha-sourdough";
|
|
||||||
rev = "v1.1.3";
|
|
||||||
hash = "sha256-Uoid/2f6GxZMuE5Keu2VjHPYuOxnYG8hsCD6BYcaTvM=";
|
|
||||||
};
|
|
||||||
})
|
|
||||||
(pkgs.buildHomeAssistantComponent {
|
|
||||||
owner = "Alandtse";
|
|
||||||
domain = "alexa_media";
|
|
||||||
version = "5.15.7";
|
|
||||||
src = pkgs.fetchFromGitHub {
|
|
||||||
owner = "Alandtse";
|
|
||||||
repo = "alexa_media_player";
|
|
||||||
rev = "v5.15.7";
|
|
||||||
hash = "sha256-1rcZVSX1xA1Lc4qSu39MOitVEciZFhoPQy2y5+PpoAI=";
|
|
||||||
};
|
|
||||||
# Every manifest requirement must be importable at a satisfying
|
|
||||||
# version or manifestCheckPhase fails the build.
|
|
||||||
dependencies = [
|
|
||||||
alexapy
|
|
||||||
dictor
|
|
||||||
haPython.wrapt
|
|
||||||
haPython.packaging
|
|
||||||
];
|
|
||||||
})
|
|
||||||
];
|
|
||||||
|
|
||||||
extraComponents = [
|
extraComponents = [
|
||||||
"matter"
|
"matter"
|
||||||
"mobile_app"
|
"mobile_app"
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
# Immich self-hosted photo & video server
|
||||||
|
{
|
||||||
|
config,
|
||||||
|
lib,
|
||||||
|
pkgs,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
let
|
||||||
|
cfg = config.my.profiles.immich;
|
||||||
|
hostName = config.networking.hostName;
|
||||||
|
port = 2283;
|
||||||
|
in
|
||||||
|
{
|
||||||
|
options.my.profiles.immich = with lib; {
|
||||||
|
enable = mkEnableOption "Immich photo server";
|
||||||
|
};
|
||||||
|
|
||||||
|
config = lib.mkIf cfg.enable {
|
||||||
|
services.immich = {
|
||||||
|
enable = true;
|
||||||
|
host = "0.0.0.0";
|
||||||
|
inherit port;
|
||||||
|
openFirewall = true;
|
||||||
|
mediaLocation = "/var/lib/immich";
|
||||||
|
machine-learning.enable = true;
|
||||||
|
accelerationDevices = [ "/dev/dri/renderD128" ];
|
||||||
|
# Setting `settings` puts Immich in config-file mode: the admin settings
|
||||||
|
# UI becomes read-only and system config is managed declaratively here.
|
||||||
|
settings = {
|
||||||
|
server.externalDomain = "http://${hostName}:${toString port}";
|
||||||
|
# Intel Quick Sync hardware transcoding (jupiter's iGPU).
|
||||||
|
ffmpeg.accel = "qsv";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
# The native module does not add GPU groups; required for VAAPI/QSV transcoding.
|
||||||
|
users.users.immich.extraGroups = [
|
||||||
|
"video"
|
||||||
|
"render"
|
||||||
|
];
|
||||||
|
|
||||||
|
my.homepage.services = [
|
||||||
|
{
|
||||||
|
group = "Media";
|
||||||
|
name = "Immich";
|
||||||
|
description = "Photo & video server";
|
||||||
|
href = "http://${hostName}:${toString port}";
|
||||||
|
icon = "immich.png";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user