diff --git a/nixos/doc/manual/release-notes/rl-2605.section.md b/nixos/doc/manual/release-notes/rl-2605.section.md index 9cfb4e223933..4fee9fb92e5e 100644 --- a/nixos/doc/manual/release-notes/rl-2605.section.md +++ b/nixos/doc/manual/release-notes/rl-2605.section.md @@ -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} diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index c18a632f9c47..430872242d57 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -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 diff --git a/nixos/modules/services/web-apps/immichframe.nix b/nixos/modules/services/web-apps/immichframe.nix new file mode 100644 index 000000000000..cbe7d57bab7b --- /dev/null +++ b/nixos/modules/services/web-apps/immichframe.nix @@ -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 + + 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 + + for details on what permissions this key needs. + ''; + }; + }; + } + ); + + description = '' + Accounts configuration, multiple are permitted. See + . + ''; + }; + }; + }; + default = { }; + description = '' + Configuration for ImmichFrame. See + 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 ]; +} diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index 90e53e45500e..515d834237fa 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -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; diff --git a/nixos/tests/web-apps/immichframe.nix b/nixos/tests/web-apps/immichframe.nix new file mode 100644 index 000000000000..f2f590470163 --- /dev/null +++ b/nixos/tests/web-apps/immichframe.nix @@ -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") + ''; +} diff --git a/pkgs/by-name/im/immichframe/deps.json b/pkgs/by-name/im/immichframe/deps.json new file mode 100644 index 000000000000..abf5e32343d8 --- /dev/null +++ b/pkgs/by-name/im/immichframe/deps.json @@ -0,0 +1,467 @@ +[ + { + "pname": "AwesomeAssertions", + "version": "9.2.0", + "hash": "sha256-s5j46b6KchKwjQgiXvCYUZLk5Xtk4sb1wSh25f8ShFc=" + }, + { + "pname": "BloomFilter.NetCore", + "version": "2.5.3", + "hash": "sha256-UfH3JZgUh8zj34+E1DLZdKo9Dyqb/Ek8qmg4kx5N818=" + }, + { + "pname": "Castle.Core", + "version": "5.1.1", + "hash": "sha256-oVkQB+ON7S6Q27OhXrTLaxTL0kWB58HZaFFuiw4iTrE=" + }, + { + "pname": "coverlet.collector", + "version": "6.0.4", + "hash": "sha256-ieiUl7G5pVKQ4V6rxhEe0ehep0/u1RBD3EAI63AQTI0=" + }, + { + "pname": "Ical.Net", + "version": "4.3.1", + "hash": "sha256-jpohvCiUBe61A3O+BJwCqsPW+3jUt4cKeCqZb/0cw0M=" + }, + { + "pname": "Microsoft.ApplicationInsights", + "version": "2.23.0", + "hash": "sha256-5sf3bg7CZZjHseK+F3foOchEhmVeioePxMZVvS6Rjb0=" + }, + { + "pname": "Microsoft.AspNetCore.Mvc.Testing", + "version": "8.0.20", + "hash": "sha256-jSWZfbewG7LqrqVQ4wmwj4K6OPGslm0wt4D5wK2TlFc=" + }, + { + "pname": "Microsoft.AspNetCore.SpaProxy", + "version": "8.0.20", + "hash": "sha256-kjvVZQBbE6fjKdbZsg+BNCRHh3d5fvxNNFaCF5fxQSI=" + }, + { + "pname": "Microsoft.AspNetCore.TestHost", + "version": "8.0.20", + "hash": "sha256-M/MTiN0nNQ8eKQn9r8TnIXYWxOYgBHNG/PZtvnInmDw=" + }, + { + "pname": "Microsoft.CodeCoverage", + "version": "17.14.1", + "hash": "sha256-f8QytG8GvRoP47rO2KEmnDLxIpyesaq26TFjDdW40Gs=" + }, + { + "pname": "Microsoft.Extensions.ApiDescription.Client", + "version": "8.0.20", + "hash": "sha256-BcgOnv5xXl42ZocyzNrgSiUA6DbGoseTCV8RDi/n8Eo=" + }, + { + "pname": "Microsoft.Extensions.ApiDescription.Server", + "version": "6.0.5", + "hash": "sha256-RJjBWz+UHxkQE2s7CeGYdTZ218mCufrxl0eBykZdIt4=" + }, + { + "pname": "Microsoft.Extensions.Caching.Abstractions", + "version": "9.0.5", + "hash": "sha256-TUsEWQLao2UCQi/Lqqlc5wnjMCI52tJ1A6NlODdRLOU=" + }, + { + "pname": "Microsoft.Extensions.Caching.Memory", + "version": "9.0.5", + "hash": "sha256-tCNn5RSGZ+J3gd68cDGaHofB3ZTcvOlLXeY/4TG5INU=" + }, + { + "pname": "Microsoft.Extensions.Configuration", + "version": "8.0.0", + "hash": "sha256-9BPsASlxrV8ilmMCjdb3TiUcm5vFZxkBnAI/fNBSEyA=" + }, + { + "pname": "Microsoft.Extensions.Configuration.Abstractions", + "version": "6.0.0", + "hash": "sha256-Evg+Ynj2QUa6Gz+zqF+bUyfGD0HI5A2fHmxZEXbn3HA=" + }, + { + "pname": "Microsoft.Extensions.Configuration.Abstractions", + "version": "8.0.0", + "hash": "sha256-4eBpDkf7MJozTZnOwQvwcfgRKQGcNXe0K/kF+h5Rl8o=" + }, + { + "pname": "Microsoft.Extensions.Configuration.Binder", + "version": "6.0.0", + "hash": "sha256-7NZcKkiXWSuhhVcA/fXHPY/62aGUyMsRdiHm91cWC5Y=" + }, + { + "pname": "Microsoft.Extensions.Configuration.Binder", + "version": "8.0.0", + "hash": "sha256-GanfInGzzoN2bKeNwON8/Hnamr6l7RTpYLA49CNXD9Q=" + }, + { + "pname": "Microsoft.Extensions.Configuration.Binder", + "version": "8.0.2", + "hash": "sha256-aGB0VuoC34YadAEqrwoaXLc5qla55pswDV2xLSmR7SE=" + }, + { + "pname": "Microsoft.Extensions.Configuration.CommandLine", + "version": "8.0.0", + "hash": "sha256-fmPC/o8S+weTtQJWykpnGHm6AKVU21xYE/CaHYU7zgg=" + }, + { + "pname": "Microsoft.Extensions.Configuration.EnvironmentVariables", + "version": "8.0.0", + "hash": "sha256-+bjFZvqCsMf2FRM2olqx/fub+QwfM1kBhjGVOT5HC48=" + }, + { + "pname": "Microsoft.Extensions.Configuration.FileExtensions", + "version": "8.0.1", + "hash": "sha256-iRA8L7BX/fe5LHCVOhzBSk30GfshP7V2Qj2nxpEvStA=" + }, + { + "pname": "Microsoft.Extensions.Configuration.Json", + "version": "8.0.1", + "hash": "sha256-J8EK/yhsfTpeSUY8F81ZTBV9APHiPUliN7d+n2OX9Ig=" + }, + { + "pname": "Microsoft.Extensions.Configuration.UserSecrets", + "version": "8.0.1", + "hash": "sha256-yGvWfwBhyFudcIv96pKWaQ1MIMOiv5LHSCn+9J7Doz0=" + }, + { + "pname": "Microsoft.Extensions.DependencyInjection", + "version": "8.0.1", + "hash": "sha256-O9g0jWS+jfGoT3yqKwZYJGL+jGSIeSbwmvomKDC3hTU=" + }, + { + "pname": "Microsoft.Extensions.DependencyInjection.Abstractions", + "version": "6.0.0", + "hash": "sha256-SZke0jNKIqJvvukdta+MgIlGsrP2EdPkkS8lfLg7Ju4=" + }, + { + "pname": "Microsoft.Extensions.DependencyInjection.Abstractions", + "version": "8.0.0", + "hash": "sha256-75KzEGWjbRELczJpCiJub+ltNUMMbz5A/1KQU+5dgP8=" + }, + { + "pname": "Microsoft.Extensions.DependencyInjection.Abstractions", + "version": "8.0.2", + "hash": "sha256-UfLfEQAkXxDaVPC7foE/J3FVEXd31Pu6uQIhTic3JgY=" + }, + { + "pname": "Microsoft.Extensions.DependencyInjection.Abstractions", + "version": "9.0.5", + "hash": "sha256-JSGmzV9CdZaRIBDbnUXCMlrIk2oVnrQSCL79xoNmjeY=" + }, + { + "pname": "Microsoft.Extensions.DependencyModel", + "version": "8.0.2", + "hash": "sha256-PyuO/MyCR9JtYqpA1l/nXGh+WLKCq34QuAXN9qNza9Q=" + }, + { + "pname": "Microsoft.Extensions.Diagnostics", + "version": "8.0.1", + "hash": "sha256-CraHNCaVlMiYx6ff9afT6U7RC/MoOCXM3pn2KrXkiLc=" + }, + { + "pname": "Microsoft.Extensions.Diagnostics.Abstractions", + "version": "8.0.1", + "hash": "sha256-d5DVXhA8qJFY9YbhZjsTqs5w5kDuxF5v+GD/WZR1QL0=" + }, + { + "pname": "Microsoft.Extensions.FileProviders.Abstractions", + "version": "8.0.0", + "hash": "sha256-uQSXmt47X2HGoVniavjLICbPtD2ReQOYQMgy3l0xuMU=" + }, + { + "pname": "Microsoft.Extensions.FileProviders.Physical", + "version": "8.0.0", + "hash": "sha256-29y5ZRQ1ZgzVOxHktYxyiH40kVgm5un2yTGdvuSWnRc=" + }, + { + "pname": "Microsoft.Extensions.FileSystemGlobbing", + "version": "8.0.0", + "hash": "sha256-+Oz41JR5jdcJlCJOSpQIL5OMBNi+1Hl2d0JUHfES7sU=" + }, + { + "pname": "Microsoft.Extensions.Hosting", + "version": "8.0.1", + "hash": "sha256-FFLo6em0N2vaWg6//vaQhxoOgT9LLH5Y2KWkCeX5xQ4=" + }, + { + "pname": "Microsoft.Extensions.Hosting.Abstractions", + "version": "8.0.1", + "hash": "sha256-/bIVL9uvBQhV/KQmjA1ZjR74sMfaAlBb15sVXsGDEVA=" + }, + { + "pname": "Microsoft.Extensions.Http", + "version": "8.0.1", + "hash": "sha256-ScPwhBvD3Jd4S0E7JQ18+DqY3PtQvdFLbkohUBbFd3o=" + }, + { + "pname": "Microsoft.Extensions.Logging", + "version": "8.0.1", + "hash": "sha256-vkfVw4tQEg86Xg18v6QO0Qb4Ysz0Njx57d1XcNuj6IU=" + }, + { + "pname": "Microsoft.Extensions.Logging.Abstractions", + "version": "6.0.1", + "hash": "sha256-v3FWpuKXlBIW5NwqQx0Ffb6y58RlevIyO/byqeLphJ8=" + }, + { + "pname": "Microsoft.Extensions.Logging.Abstractions", + "version": "8.0.2", + "hash": "sha256-cHpe8X2BgYa5DzulZfq24rg8O2K5Lmq2OiLhoyAVgJc=" + }, + { + "pname": "Microsoft.Extensions.Logging.Abstractions", + "version": "9.0.5", + "hash": "sha256-ILcK06TEbVwxUZ3PCpGkjg69H6PGwTXnN512JJ8F9KI=" + }, + { + "pname": "Microsoft.Extensions.Logging.Configuration", + "version": "8.0.1", + "hash": "sha256-E2JbJG2EXlv2HUWLi17kIkAL6RC9rC2E18C3gAyOuaE=" + }, + { + "pname": "Microsoft.Extensions.Logging.Console", + "version": "8.0.1", + "hash": "sha256-2thhF1JbDNj3Bx2fcH7O26uHGNeMd9MYah6N60lIpIU=" + }, + { + "pname": "Microsoft.Extensions.Logging.Debug", + "version": "8.0.1", + "hash": "sha256-gKFqBg5lbjy5VBEcAuoQ/SsXAxvrYdBYOu9dV60eJKg=" + }, + { + "pname": "Microsoft.Extensions.Logging.EventLog", + "version": "8.0.1", + "hash": "sha256-1UkEOwl3Op2b3jTvpI10hHxIe9FqeVVy+VB1tZp6Lc8=" + }, + { + "pname": "Microsoft.Extensions.Logging.EventSource", + "version": "8.0.1", + "hash": "sha256-EINT/PgfB4Dvf+1JBzL1plPT35ezT7kyS8y/XMMgYxA=" + }, + { + "pname": "Microsoft.Extensions.Options", + "version": "6.0.0", + "hash": "sha256-DxnEgGiCXpkrxFkxXtOXqwaiAtoIjA8VSSWCcsW0FwE=" + }, + { + "pname": "Microsoft.Extensions.Options", + "version": "8.0.2", + "hash": "sha256-AjcldddddtN/9aH9pg7ClEZycWtFHLi9IPe1GGhNQys=" + }, + { + "pname": "Microsoft.Extensions.Options", + "version": "9.0.5", + "hash": "sha256-iz/ox4wpQBX0Qk97qRYlmmJPuuoV2urN03uGxAkp/SM=" + }, + { + "pname": "Microsoft.Extensions.Options.ConfigurationExtensions", + "version": "6.0.0", + "hash": "sha256-au0Y13cGk/dQFKuvSA5NnP/++bErTk0oOTlgmHdI2Mw=" + }, + { + "pname": "Microsoft.Extensions.Options.ConfigurationExtensions", + "version": "8.0.0", + "hash": "sha256-A5Bbzw1kiNkgirk5x8kyxwg9lLTcSngojeD+ocpG1RI=" + }, + { + "pname": "Microsoft.Extensions.Primitives", + "version": "6.0.0", + "hash": "sha256-AgvysszpQ11AiTBJFkvSy8JnwIWTj15Pfek7T7ThUc4=" + }, + { + "pname": "Microsoft.Extensions.Primitives", + "version": "8.0.0", + "hash": "sha256-FU8qj3DR8bDdc1c+WeGZx/PCZeqqndweZM9epcpXjSo=" + }, + { + "pname": "Microsoft.Extensions.Primitives", + "version": "9.0.5", + "hash": "sha256-3ctJMOwnEsBSNqcrh77IItX2wtOmjM/b8OTXJUZ/P0o=" + }, + { + "pname": "Microsoft.NET.Test.Sdk", + "version": "17.14.1", + "hash": "sha256-mZUzDFvFp7x1nKrcnRd0hhbNu5g8EQYt8SKnRgdhT/A=" + }, + { + "pname": "Microsoft.OpenApi", + "version": "1.6.23", + "hash": "sha256-YD2oxM/tlNpK5xUeHF85xdqcpBzHioUSyRjpN2A7KcY=" + }, + { + "pname": "Microsoft.Testing.Extensions.Telemetry", + "version": "1.9.0", + "hash": "sha256-JT91ThKLEyoRS/8ZJqZwlSTT7ofC2QhNqPFI3pYmMaw=" + }, + { + "pname": "Microsoft.Testing.Extensions.TrxReport.Abstractions", + "version": "1.9.0", + "hash": "sha256-oscZOEKw7gM6eRdDrOS3x+CwqIvXWRmfmi0ugCxBRw0=" + }, + { + "pname": "Microsoft.Testing.Extensions.VSTestBridge", + "version": "1.9.0", + "hash": "sha256-CadXLWD093sUDaWhnppzD9LvpxSRqqt93ZEOFiIAPyw=" + }, + { + "pname": "Microsoft.Testing.Platform", + "version": "1.9.0", + "hash": "sha256-6nzjoYbJOh7v/GB7d+TDuM0l/xglCshFX6KWjg7+cFI=" + }, + { + "pname": "Microsoft.Testing.Platform.MSBuild", + "version": "1.9.0", + "hash": "sha256-/bileP4b+9RZp8yjgS6eynXwc2mohyyzf6p/0LZJd8I=" + }, + { + "pname": "Microsoft.TestPlatform.AdapterUtilities", + "version": "17.13.0", + "hash": "sha256-Vr+3Tad/h/nk7f/5HMExn3HvCGFCarehFAzJSfCBaOc=" + }, + { + "pname": "Microsoft.TestPlatform.ObjectModel", + "version": "17.13.0", + "hash": "sha256-6S0fjfj8vA+h6dJVNwLi6oZhYDO/I/6hBZaq2VTW+Uk=" + }, + { + "pname": "Microsoft.TestPlatform.ObjectModel", + "version": "17.14.1", + "hash": "sha256-QMf6O+w0IT+16Mrzo7wn+N20f3L1/mDhs/qjmEo1rYs=" + }, + { + "pname": "Microsoft.TestPlatform.TestHost", + "version": "17.14.1", + "hash": "sha256-1cxHWcvHRD7orQ3EEEPPxVGEkTpxom1/zoICC9SInJs=" + }, + { + "pname": "Moq", + "version": "4.20.72", + "hash": "sha256-+uAc/6xtzij9YnmZrhZwc+4vUgx6cppZsWQli3CGQ8o=" + }, + { + "pname": "Newtonsoft.Json", + "version": "13.0.3", + "hash": "sha256-hy/BieY4qxBWVVsDqqOPaLy1QobiIapkbrESm6v2PHc=" + }, + { + "pname": "Newtonsoft.Json", + "version": "13.0.4", + "hash": "sha256-8JCB1FdAW681qXP6DFDWvycu1oPyVoxaYgpJ2pUvZSk=" + }, + { + "pname": "NodaTime", + "version": "3.2.0", + "hash": "sha256-kt59MWEzmMFBgpw5tOPlcYwZKe74WkA9N5ggrLS3tUM=" + }, + { + "pname": "NSwag.ApiDescription.Client", + "version": "13.20.0", + "hash": "sha256-r/BtE46yFdM9MuoaVYNhO7vvdBICDsM8ROXZldxFIeY=" + }, + { + "pname": "NSwag.MSBuild", + "version": "13.20.0", + "hash": "sha256-HarUF6peOsd12saco8+Na4TnOEh+Gl2MJelSfnBVRhQ=" + }, + { + "pname": "NUnit", + "version": "4.4.0", + "hash": "sha256-5geF5QOF+X/WkuCEgkPVKH4AdKx4U0olpU07S8+G3nU=" + }, + { + "pname": "NUnit.Analyzers", + "version": "4.10.0", + "hash": "sha256-wkThcI2ratJoCnstOIj/jB5muGQbdDPFbRK1r7PHEBU=" + }, + { + "pname": "NUnit3TestAdapter", + "version": "5.2.0", + "hash": "sha256-ybTutL4VkX/fq61mS+O3Ruh+adic4fpv+MKgQ0IZvGg=" + }, + { + "pname": "OpenWeatherMap.API", + "version": "2.1.2", + "hash": "sha256-rosFFZzqPKNPuItlxGjeeCv+jqDzVvyiKy4vxmUFP5A=" + }, + { + "pname": "Swashbuckle.AspNetCore", + "version": "8.1.4", + "hash": "sha256-gZVM5/6HMBAuuQI2uXdoAUJOG8BkGuh/3ZuWlGME8vk=" + }, + { + "pname": "Swashbuckle.AspNetCore.Swagger", + "version": "8.1.4", + "hash": "sha256-6iqC/F581my20C+/3ORTM6uVUj+M095cRQyCIAo9AyA=" + }, + { + "pname": "Swashbuckle.AspNetCore.SwaggerGen", + "version": "8.1.4", + "hash": "sha256-m0ixgc45HCX2xrvrnhy3WYU/r/basjat535n4rN+vOY=" + }, + { + "pname": "Swashbuckle.AspNetCore.SwaggerUI", + "version": "8.1.4", + "hash": "sha256-zkVzBxkxGh5WyhFEOiZknez9GBeBKXTH2ySf2/BnSV4=" + }, + { + "pname": "System.Collections.Immutable", + "version": "8.0.0", + "hash": "sha256-F7OVjKNwpqbUh8lTidbqJWYi476nsq9n+6k0+QVRo3w=" + }, + { + "pname": "System.Diagnostics.DiagnosticSource", + "version": "5.0.0", + "hash": "sha256-6mW3N6FvcdNH/pB58pl+pFSCGWgyaP4hfVtC/SMWDV4=" + }, + { + "pname": "System.Diagnostics.DiagnosticSource", + "version": "9.0.5", + "hash": "sha256-fB8970CWgKWI1UwAaDa9D2yaMUiQ74ULGU7GMsr2u/o=" + }, + { + "pname": "System.Diagnostics.EventLog", + "version": "6.0.0", + "hash": "sha256-zUXIQtAFKbiUMKCrXzO4mOTD5EUphZzghBYKXprowSM=" + }, + { + "pname": "System.Diagnostics.EventLog", + "version": "8.0.1", + "hash": "sha256-zvqd72pwgcGoa1nH3ZT1C0mP9k53vFLJ69r5MCQ1saA=" + }, + { + "pname": "System.IO.Pipelines", + "version": "8.0.0", + "hash": "sha256-LdpB1s4vQzsOODaxiKstLks57X9DTD5D6cPx8DE1wwE=" + }, + { + "pname": "System.Reflection.Metadata", + "version": "1.6.0", + "hash": "sha256-JJfgaPav7UfEh4yRAQdGhLZF1brr0tUWPl6qmfNWq/E=" + }, + { + "pname": "System.Reflection.Metadata", + "version": "8.0.0", + "hash": "sha256-dQGC30JauIDWNWXMrSNOJncVa1umR1sijazYwUDdSIE=" + }, + { + "pname": "System.Runtime.CompilerServices.Unsafe", + "version": "6.0.0", + "hash": "sha256-bEG1PnDp7uKYz/OgLOWs3RWwQSVYm+AnPwVmAmcgp2I=" + }, + { + "pname": "ThumbHash", + "version": "2.1.1", + "hash": "sha256-pLlNMjUvCHZ2cJi2nco1ikFq/plIklA3x8Cgcag3x44=" + }, + { + "pname": "UnitsNet", + "version": "5.29.0", + "hash": "sha256-LdHK42YM1X3Hq3VWWh9slhgEpUsQa2kM335bAitpT+A=" + }, + { + "pname": "YamlDotNet", + "version": "16.3.0", + "hash": "sha256-4Gi8wSQ8Rsi/3+LyegJr//A83nxn2fN8LN1wvSSp39Q=" + } +] diff --git a/pkgs/by-name/im/immichframe/package.nix b/pkgs/by-name/im/immichframe/package.nix new file mode 100644 index 000000000000..dbc88fa3b41e --- /dev/null +++ b/pkgs/by-name/im/immichframe/package.nix @@ -0,0 +1,86 @@ +{ + buildDotnetModule, + buildNpmPackage, + dotnet-sdk, + fetchFromGitHub, + fetchpatch, + lib, + nixosTests, + writeShellApplication, +}: + +let + version = "1.0.29.0"; + src = fetchFromGitHub { + owner = "immichFrame"; + repo = "immichFrame"; + rev = "v${version}"; + hash = "sha256-YFh+/QWYYtQzBVJUyUuhhKqi9/5waWVX+lw/tov++ws="; + }; + + publishApi = buildDotnetModule { + pname = "immichframe"; + inherit version src; + projectFile = "ImmichFrame.WebApi/ImmichFrame.WebApi.csproj"; + 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"; + }; + + frontend = buildNpmPackage { + pname = "immichframe-frontend"; + inherit version src; + + sourceRoot = "${src.name}/immichFrame.Web"; + + npmBuildScript = "build"; + npmDepsHash = "sha256-eOv3DlmHaI6hVCYTBzCtLWKD72/RM/KjCUDVUgb9jcg="; + + installPhase = '' + runHook preInstall + + mkdir $out + cp -r build/ $out/wwwroot + + runHook postInstall + ''; + }; +in +writeShellApplication { + name = "ImmichFrame"; + + text = '' + cd ${frontend} + 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"; + license = lib.licenses.gpl3Only; + maintainers = with lib.maintainers; [ jfly ]; + platforms = lib.platforms.all; + }; +}