mirror of
https://github.com/NixOS/nixpkgs.git
synced 2026-07-06 17:13:24 -05:00
nixos/immichframe: init module
This commit is contained in:
parent
ffb50aa8b3
commit
20aa784406
6 changed files with 314 additions and 0 deletions
|
|
@ -12,6 +12,8 @@
|
|||
|
||||
- [knot-resolver](https://www.knot-resolver.cz/) in version 6. Available as `services.knot-resolver`. A module for knot-resolver 5 was already available as `services.kresd`.
|
||||
|
||||
- [ImmichFrame](https://immichframe.dev/), display your photos from Immich as a digital photo frame. Available as `services.immichframe`.
|
||||
|
||||
- [udp-over-tcp](https://github.com/mullvad/udp-over-tcp), a tunnel for proxying UDP traffic over a TCP stream. Available as `services.udp-over-tcp`.
|
||||
|
||||
## Backward Incompatibilities {#sec-release-26.05-incompatibilities}
|
||||
|
|
|
|||
|
|
@ -1637,6 +1637,7 @@
|
|||
./services/web-apps/immich-kiosk.nix
|
||||
./services/web-apps/immich-public-proxy.nix
|
||||
./services/web-apps/immich.nix
|
||||
./services/web-apps/immichframe.nix
|
||||
./services/web-apps/invidious.nix
|
||||
./services/web-apps/isso.nix
|
||||
./services/web-apps/jirafeau.nix
|
||||
|
|
|
|||
142
nixos/modules/services/web-apps/immichframe.nix
Normal file
142
nixos/modules/services/web-apps/immichframe.nix
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
{
|
||||
config,
|
||||
lib,
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
let
|
||||
cfg = config.services.immichframe;
|
||||
format = pkgs.formats.json { };
|
||||
inherit (lib)
|
||||
types
|
||||
mkIf
|
||||
mkOption
|
||||
mkEnableOption
|
||||
;
|
||||
in
|
||||
{
|
||||
options.services.immichframe = {
|
||||
enable = mkEnableOption "ImmichFrame";
|
||||
package = lib.mkPackageOption pkgs "immichframe" { };
|
||||
|
||||
port = mkOption {
|
||||
type = types.port;
|
||||
default = 3000;
|
||||
description = "The port that ImmichFrame will listen on.";
|
||||
};
|
||||
|
||||
settings = mkOption {
|
||||
type = types.submodule {
|
||||
freeformType = format.type;
|
||||
options = {
|
||||
Accounts = mkOption {
|
||||
type = types.listOf (
|
||||
types.submodule {
|
||||
freeformType = format.type;
|
||||
options = {
|
||||
ImmichServerUrl = mkOption {
|
||||
type = types.str;
|
||||
example = "http://photos.example.com";
|
||||
description = "The URL of your Immich server.";
|
||||
};
|
||||
ApiKey = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
default = null;
|
||||
description = ''
|
||||
API key to talk to the Immich server.
|
||||
Warning: it will be world-readable in /nix/store.
|
||||
Consider using {option}`ApiKeyFile` instead.
|
||||
|
||||
See
|
||||
<https://immichframe.online/docs/getting-started/configuration#api-key-permissions>
|
||||
for details on what permissions this key needs.
|
||||
'';
|
||||
};
|
||||
ApiKeyFile = mkOption {
|
||||
type = types.nullOr types.externalPath;
|
||||
default = null;
|
||||
description = ''
|
||||
File containing an API key to talk to the Immich server.
|
||||
|
||||
See
|
||||
<https://immichframe.online/docs/getting-started/configuration#api-key-permissions>
|
||||
for details on what permissions this key needs.
|
||||
'';
|
||||
};
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
description = ''
|
||||
Accounts configuration, multiple are permitted. See
|
||||
<https://immichframe.online/docs/getting-started/configuration>.
|
||||
'';
|
||||
};
|
||||
};
|
||||
};
|
||||
default = { };
|
||||
description = ''
|
||||
Configuration for ImmichFrame. See
|
||||
<https://immichframe.online/docs/getting-started/configuration> for
|
||||
options and defaults.
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
assertions = lib.imap0 (i: account: {
|
||||
assertion = lib.xor (account.ApiKey == null) (account.ApiKeyFile == null);
|
||||
message = "Exactly one of {option}`services.immichframe.settings.Accounts[${toString i}].ApiKey` and {option}`services.immichframe.settings.Accounts[${toString i}].ApiKeyFile` must be specified";
|
||||
}) cfg.settings.Accounts;
|
||||
|
||||
systemd.services.immichframe =
|
||||
let
|
||||
accountsWithApiKeyFiles = lib.filter (account: account.ApiKeyFile != null) cfg.settings.Accounts;
|
||||
apiKeyFileToId = lib.listToAttrs (
|
||||
lib.imap0 (
|
||||
index: account: lib.nameValuePair account.ApiKeyFile "api-key-${toString index}"
|
||||
) accountsWithApiKeyFiles
|
||||
);
|
||||
settingsPatchWithCredentialPaths = {
|
||||
Accounts = map (
|
||||
account:
|
||||
account
|
||||
// (
|
||||
if account.ApiKeyFile != null then
|
||||
{
|
||||
ApiKeyFile = "/run/credentials/${config.systemd.services.immichframe.name}/${
|
||||
apiKeyFileToId.${account.ApiKeyFile}
|
||||
}";
|
||||
}
|
||||
else
|
||||
{ }
|
||||
)
|
||||
) cfg.settings.Accounts;
|
||||
};
|
||||
settingsWithFixedSecretPaths = lib.recursiveUpdate cfg.settings settingsPatchWithCredentialPaths;
|
||||
in
|
||||
{
|
||||
description = "Display your photos from Immich as a digital photo frame";
|
||||
after = [ "network.target" ];
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
environment = {
|
||||
IMMICHFRAME_CONFIG_PATH = pkgs.runCommand "Config" { } ''
|
||||
mkdir $out
|
||||
ln -s ${format.generate "Settings.json" settingsWithFixedSecretPaths} $out/Settings.json
|
||||
'';
|
||||
};
|
||||
serviceConfig = {
|
||||
ExecStart = "${lib.getExe cfg.package} --urls=http://localhost:${toString cfg.port}";
|
||||
LoadCredential = lib.concatMapAttrsStringSep ":" (
|
||||
apiKeyFile: id: "${id}:${apiKeyFile}"
|
||||
) apiKeyFileToId;
|
||||
DynamicUser = true;
|
||||
Type = "simple";
|
||||
Restart = "on-failure";
|
||||
RestartSec = 3;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
meta.maintainers = with lib.maintainers; [ jfly ];
|
||||
}
|
||||
|
|
@ -756,6 +756,7 @@ in
|
|||
immich-public-proxy = runTest ./web-apps/immich-public-proxy.nix;
|
||||
immich-vectorchord-migration = runTest ./web-apps/immich-vectorchord-migration.nix;
|
||||
immich-vectorchord-reindex = runTest ./web-apps/immich-vectorchord-reindex.nix;
|
||||
immichframe = runTest ./web-apps/immichframe.nix;
|
||||
incron = runTest ./incron.nix;
|
||||
incus = import ./incus {
|
||||
inherit runTestOn;
|
||||
|
|
|
|||
145
nixos/tests/web-apps/immichframe.nix
Normal file
145
nixos/tests/web-apps/immichframe.nix
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
let
|
||||
apiKeyFile = "/tmp/immich-api.key";
|
||||
customInterval = 5;
|
||||
in
|
||||
{
|
||||
name = "immichframe";
|
||||
|
||||
enableOCR = true;
|
||||
|
||||
nodes.machine =
|
||||
{
|
||||
config,
|
||||
pkgs,
|
||||
lib,
|
||||
...
|
||||
}:
|
||||
{
|
||||
imports = [ ../common/x11.nix ];
|
||||
|
||||
# When setting this to 2500 I got "Kernel panic - not syncing: Out of
|
||||
# memory: compulsory panic_on_oom is enabled".
|
||||
virtualisation.memorySize = 3000;
|
||||
|
||||
environment.systemPackages = with pkgs; [
|
||||
imagemagick
|
||||
immich-cli
|
||||
firefox
|
||||
xdotool
|
||||
];
|
||||
|
||||
fonts.packages = [ pkgs.liberation_ttf ];
|
||||
|
||||
services.immich = {
|
||||
enable = true;
|
||||
port = 2283;
|
||||
openFirewall = true;
|
||||
# Disable a bunch of features that aren't needed for this test.
|
||||
machine-learning.enable = false;
|
||||
settings = {
|
||||
backup.database.enabled = false;
|
||||
machineLearning.enabled = false;
|
||||
map.enabled = false;
|
||||
reverseGeocoding.enabled = false;
|
||||
metadata.faces.import = false;
|
||||
newVersionCheck.enabled = false;
|
||||
notifications.smtp.enabled = false;
|
||||
};
|
||||
};
|
||||
|
||||
services.immichframe = {
|
||||
enable = true;
|
||||
port = 8002;
|
||||
settings = {
|
||||
General.Interval = customInterval;
|
||||
Accounts = [
|
||||
{
|
||||
ImmichServerUrl = "http://localhost:${toString config.services.immich.port}";
|
||||
ApiKeyFile = apiKeyFile;
|
||||
}
|
||||
];
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
testScript = /* python */ ''
|
||||
import json
|
||||
import tempfile
|
||||
from shlex import quote
|
||||
|
||||
custom_interval = ${toString customInterval}
|
||||
|
||||
machine.wait_for_unit("immich-server.service")
|
||||
machine.wait_for_open_port(2283)
|
||||
|
||||
# Log in to Immich and create an access key.
|
||||
machine.succeed("""
|
||||
curl --no-progress-meter -f --json '{ "email": "test@example.com", "name": "Admin", "password": "admin" }' http://localhost:2283/api/auth/admin-sign-up
|
||||
""")
|
||||
res = machine.succeed("""
|
||||
curl --no-progress-meter -f --json '{ "email": "test@example.com", "password": "admin" }' http://localhost:2283/api/auth/login
|
||||
""")
|
||||
token = json.loads(res)['accessToken']
|
||||
res = machine.succeed("""
|
||||
curl --no-progress-meter -f -H 'Cookie: immich_access_token=%s' --json '{ "name": "API Key", "permissions": ["all"] }' http://localhost:2283/api/api-keys
|
||||
""" % token)
|
||||
key = json.loads(res)['secret']
|
||||
machine.succeed(f"immich login http://localhost:2283/api {key}")
|
||||
res = machine.succeed("immich server-info")
|
||||
print(res)
|
||||
|
||||
# Create the API key so ImmichFrame can start.
|
||||
with tempfile.NamedTemporaryFile("w", delete_on_close=False) as f:
|
||||
f.write(key)
|
||||
f.close()
|
||||
machine.copy_from_host(f.name, ${builtins.toJSON apiKeyFile})
|
||||
|
||||
# We finally have an api key! Make sure ImmichFrame starts up.
|
||||
machine.wait_for_unit("immichframe.service")
|
||||
machine.wait_for_open_port(8002)
|
||||
|
||||
# Check ImmichFrame's configuration.
|
||||
res = machine.succeed("curl --no-progress-meter -f http://localhost:8002/api/Config")
|
||||
frame_config = json.loads(res)
|
||||
assert frame_config['interval'] == custom_interval, frame_config
|
||||
|
||||
# At this point, ImmichFrame should not find any images to serve.
|
||||
res = machine.succeed("curl -f http://localhost:8002/api/Asset")
|
||||
assets = json.loads(res)
|
||||
assert len(assets) == 0, assets
|
||||
|
||||
# Repeat to make it easier for OCR to pick up given overlays
|
||||
image_text = '\\n'.join(['reproduce this moment', 'with NixOS tests <3'] * 6)
|
||||
|
||||
# These settings make it display fine given potential cropping.
|
||||
common_args = f"-gravity center -font Liberation-Mono -pointsize 50 -annotate 0 {quote(image_text)}"
|
||||
|
||||
# Upload some images to a new album.
|
||||
album_title = '✨ Reproducible Moments ✨'
|
||||
machine.succeed(f"magick -size 1920x1080 canvas:white -fill black {common_args} /tmp/white.png")
|
||||
machine.succeed(f"immich upload -A {quote(album_title)} /tmp/white.png")
|
||||
machine.succeed(f"magick -size 1920x1080 canvas:black -fill white {common_args} /tmp/black.png")
|
||||
machine.succeed(f"immich upload -A {quote(album_title)} /tmp/black.png")
|
||||
res = machine.succeed("immich server-info")
|
||||
print(res)
|
||||
|
||||
# ImmichFrame should now find some assets.
|
||||
# Note: we restart ImmichFrame because it has some caching that prevents it
|
||||
# from immediately seeing the newly uploaded photos.
|
||||
machine.succeed("systemctl restart immichframe.service")
|
||||
machine.wait_for_open_port(8002)
|
||||
res = machine.succeed("curl --no-progress-meter -f http://localhost:8002/api/Asset")
|
||||
assets = json.loads(res)
|
||||
assert len(assets) == 2, assets
|
||||
|
||||
# Wait for a photo to be displayed.
|
||||
machine.wait_for_x()
|
||||
machine.execute("xterm -e 'firefox --kiosk http://localhost:8002' >&2 &")
|
||||
machine.wait_for_window("immichFrame")
|
||||
_, active_window = machine.execute("xdotool getactivewindow")
|
||||
machine.succeed(f"xdotool windowsize {quote(active_window.strip())} 100% 100%")
|
||||
machine.wait_for_text('reproduce this moment')
|
||||
machine.wait_for_text('with NixOS tests')
|
||||
machine.screenshot("screen")
|
||||
'';
|
||||
}
|
||||
|
|
@ -3,7 +3,9 @@
|
|||
buildNpmPackage,
|
||||
dotnet-sdk,
|
||||
fetchFromGitHub,
|
||||
fetchpatch,
|
||||
lib,
|
||||
nixosTests,
|
||||
writeShellApplication,
|
||||
}:
|
||||
|
||||
|
|
@ -23,6 +25,25 @@ let
|
|||
nugetDeps = ./deps.json;
|
||||
dotnet-runtime = dotnet-sdk.aspnetcore;
|
||||
|
||||
patches = [
|
||||
# This not-yet-released commit has landed upstream. It adds a
|
||||
# `IMMICHFRAME_CONFIG_PATH` environment variable for a "configurable"
|
||||
# config path.
|
||||
(fetchpatch {
|
||||
name = "Configurable config path";
|
||||
url = "https://github.com/immichFrame/ImmichFrame/commit/f6680f23bcf107ce27372dfb37809c0f92ebb2f2.patch";
|
||||
hash = "sha256-dQnspQEKixQgBpCvNxrYL51z5wg5BhdN0uTuaXgKQZU=";
|
||||
})
|
||||
# This patch adds an `ApiKeyFile` option, which makes it possible to
|
||||
# configure ImmichFrame without leaking secrets into your configuration.
|
||||
# See [upstream PR](https://github.com/immichFrame/ImmichFrame/pull/511)
|
||||
(fetchpatch {
|
||||
name = "Add a `ApiKeyFile` option";
|
||||
url = "https://github.com/immichFrame/ImmichFrame/commit/f5bb164170460b1020bfe6bce8e8abb3315e32e3.diff";
|
||||
hash = "sha256-F3BVIxcu8Hm6wbWmzVnfgm6XvqdBw4IiS61CDQiMRVg=";
|
||||
})
|
||||
];
|
||||
|
||||
meta.mainProgram = "ImmichFrame.WebApi";
|
||||
};
|
||||
|
||||
|
|
@ -53,6 +74,8 @@ writeShellApplication {
|
|||
exec ${lib.getExe publishApi} "$@"
|
||||
'';
|
||||
|
||||
passthru.tests = { inherit (nixosTests) immichframe; };
|
||||
|
||||
meta = {
|
||||
description = "Display your photos from Immich as a digital photo frame";
|
||||
homepage = "https://immichframe.dev";
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue