From bb1e3215f8c6228ebca0e13ac39ed0f085cc4a07 Mon Sep 17 00:00:00 2001 From: Gabriel Volpe Date: Wed, 12 Aug 2026 09:46:38 +0200 Subject: [PATCH 1/2] feat: add weather api key file configuration option --- .../Interfaces/IServerBehaviorSettings.cs | 1 + .../Controllers/ConfigControllerTests.cs | 3 + .../Helpers/Config/ConfigLoaderTest.cs | 63 +++++++++++++++++++ .../Resources/TestV1.json | 1 + .../Resources/TestV2.json | 1 + ImmichFrame.WebApi.Tests/Resources/TestV2.yml | 1 + .../Helpers/Config/ServerSettingsV1.cs | 15 ++++- ImmichFrame.WebApi/Models/ServerSettings.cs | 14 ++++- Install_Web.md | 1 + docker/Settings.example.json | 1 + docker/Settings.example.yml | 2 + docker/example.env | 1 + docs/docs/getting-started/configuration.md | 6 +- docs/docs/getting-started/configurationV1.md | 3 +- 14 files changed, 108 insertions(+), 5 deletions(-) diff --git a/ImmichFrame.Core/Interfaces/IServerBehaviorSettings.cs b/ImmichFrame.Core/Interfaces/IServerBehaviorSettings.cs index 100abf39..02b7e379 100644 --- a/ImmichFrame.Core/Interfaces/IServerBehaviorSettings.cs +++ b/ImmichFrame.Core/Interfaces/IServerBehaviorSettings.cs @@ -5,6 +5,7 @@ public interface IServerBehaviorSettings public List Webcalendars { get; } public int RefreshAlbumPeopleInterval { get; } public string? WeatherApiKey { get; } + public string? WeatherApiKeyFile { get; } public string? WeatherLatLong { get; } public string? UnitSystem { get; } public string? Webhook { get; } diff --git a/ImmichFrame.WebApi.Tests/Controllers/ConfigControllerTests.cs b/ImmichFrame.WebApi.Tests/Controllers/ConfigControllerTests.cs index de31232a..a4075834 100644 --- a/ImmichFrame.WebApi.Tests/Controllers/ConfigControllerTests.cs +++ b/ImmichFrame.WebApi.Tests/Controllers/ConfigControllerTests.cs @@ -53,6 +53,7 @@ public void Setup() Language = "de", // Server-only values that must never surface in the client config: WeatherApiKey = "secret-weather-key", + WeatherApiKeyFile = "/run/secrets/secret-weather-key-file", WeatherLatLong = "1.23,4.56", UnitSystem = "metric", Webhook = "https://webhook.example.com/secret-hook", @@ -128,11 +129,13 @@ public async Task GetConfig_ContainsNoSecrets() { Assert.That(json, Does.Not.Contain("secret-auth-token")); Assert.That(json, Does.Not.Contain("secret-weather-key")); + Assert.That(json, Does.Not.Contain("secret-weather-key-file")); Assert.That(json, Does.Not.Contain("secret-api-key")); Assert.That(json, Does.Not.Contain("secret-hook")); Assert.That(json, Does.Not.Contain("secret.ics")); Assert.That(json, Does.Not.Contain("authenticationSecret")); Assert.That(json, Does.Not.Contain("weatherApiKey")); + Assert.That(json, Does.Not.Contain("weatherApiKeyFile")); Assert.That(json, Does.Not.Contain("apiKey")); Assert.That(json, Does.Not.Contain("webhook")); Assert.That(json, Does.Not.Contain("webcalendars")); diff --git a/ImmichFrame.WebApi.Tests/Helpers/Config/ConfigLoaderTest.cs b/ImmichFrame.WebApi.Tests/Helpers/Config/ConfigLoaderTest.cs index 15c3254b..a974103d 100644 --- a/ImmichFrame.WebApi.Tests/Helpers/Config/ConfigLoaderTest.cs +++ b/ImmichFrame.WebApi.Tests/Helpers/Config/ConfigLoaderTest.cs @@ -68,6 +68,69 @@ public void TestLoadConfigV2Yaml() VerifyConfig(config, true, false); } + [Test] + public void GeneralSettingsValidate_ReadsWeatherApiKeyFile() + { + var apiKeyFile = Path.GetTempFileName(); + File.WriteAllText(apiKeyFile, "weather-api-key\n"); + + try + { + var settings = new GeneralSettings + { + WeatherApiKey = "", + WeatherApiKeyFile = apiKeyFile, + }; + + settings.Validate(); + + settings.WeatherApiKey.Should().Be("weather-api-key"); + } + finally + { + File.Delete(apiKeyFile); + } + } + + [Test] + public void GeneralSettingsValidate_ThrowsWhenWeatherApiKeyAndWeatherApiKeyFileAreBothSet() + { + var settings = new GeneralSettings + { + WeatherApiKey = "weather-api-key", + WeatherApiKeyFile = "/path/to/weather-api-key", + }; + + var exception = Assert.Throws(() => settings.Validate()); + + exception!.Message.Should().Contain("Cannot specify both WeatherApiKey and WeatherApiKeyFile"); + } + + [Test] + public void ServerSettingsV1AdapterValidate_ReadsWeatherApiKeyFile() + { + var apiKeyFile = Path.GetTempFileName(); + File.WriteAllText(apiKeyFile, "weather-api-key\n"); + + try + { + var settings = new ServerSettingsV1 + { + WeatherApiKey = "", + WeatherApiKeyFile = apiKeyFile, + }; + + var adapter = new ServerSettingsV1Adapter(settings); + adapter.Validate(); + + adapter.GeneralSettings.WeatherApiKey.Should().Be("weather-api-key"); + } + finally + { + File.Delete(apiKeyFile); + } + } + private void VerifyConfig(IServerSettings serverSettings, bool usePrefix, bool expectNullApiKeyFile) { VerifyProperties(serverSettings.GeneralSettings); diff --git a/ImmichFrame.WebApi.Tests/Resources/TestV1.json b/ImmichFrame.WebApi.Tests/Resources/TestV1.json index e6c49102..309d0a01 100644 --- a/ImmichFrame.WebApi.Tests/Resources/TestV1.json +++ b/ImmichFrame.WebApi.Tests/Resources/TestV1.json @@ -50,6 +50,7 @@ "Style": "Style_TEST", "BaseFontSize": "BaseFontSize_TEST", "WeatherApiKey": "WeatherApiKey_TEST", + "WeatherApiKeyFile": "WeatherApiKeyFile_TEST", "ShowWeatherDescription": true, "WeatherIconUrl": "WeatherIconUrl_TEST", "UnitSystem": "UnitSystem_TEST", diff --git a/ImmichFrame.WebApi.Tests/Resources/TestV2.json b/ImmichFrame.WebApi.Tests/Resources/TestV2.json index 4d603dc9..33ed6e62 100644 --- a/ImmichFrame.WebApi.Tests/Resources/TestV2.json +++ b/ImmichFrame.WebApi.Tests/Resources/TestV2.json @@ -10,6 +10,7 @@ "PhotoDateFormat": "PhotoDateFormat_TEST", "ImageLocationFormat": "ImageLocationFormat_TEST", "WeatherApiKey": "WeatherApiKey_TEST", + "WeatherApiKeyFile": "WeatherApiKeyFile_TEST", "UnitSystem": "UnitSystem_TEST", "WeatherLatLong": "WeatherLatLong_TEST", "Language": "Language_TEST", diff --git a/ImmichFrame.WebApi.Tests/Resources/TestV2.yml b/ImmichFrame.WebApi.Tests/Resources/TestV2.yml index 47f45947..8deac2c1 100644 --- a/ImmichFrame.WebApi.Tests/Resources/TestV2.yml +++ b/ImmichFrame.WebApi.Tests/Resources/TestV2.yml @@ -9,6 +9,7 @@ General: PhotoDateFormat: PhotoDateFormat_TEST ImageLocationFormat: ImageLocationFormat_TEST WeatherApiKey: WeatherApiKey_TEST + WeatherApiKeyFile: WeatherApiKeyFile_TEST UnitSystem: UnitSystem_TEST WeatherLatLong: WeatherLatLong_TEST Language: Language_TEST diff --git a/ImmichFrame.WebApi/Helpers/Config/ServerSettingsV1.cs b/ImmichFrame.WebApi/Helpers/Config/ServerSettingsV1.cs index 076f36da..4d8f6f3f 100644 --- a/ImmichFrame.WebApi/Helpers/Config/ServerSettingsV1.cs +++ b/ImmichFrame.WebApi/Helpers/Config/ServerSettingsV1.cs @@ -26,6 +26,7 @@ public class ServerSettingsV1 : IConfigSettable public List Webcalendars { get; set; } = new List(); public int RefreshAlbumPeopleInterval { get; set; } = 12; public string? WeatherApiKey { get; set; } = string.Empty; + public string? WeatherApiKeyFile { get; set; } = null; public string? UnitSystem { get; set; } = "imperial"; public string? WeatherLatLong { get; set; } = "40.7128,74.0060"; public string Language { get; set; } = "en"; @@ -103,6 +104,7 @@ class GeneralSettingsV1Adapter(ServerSettingsV1 _delegate) : IGeneralSettings public List Webcalendars => _delegate.Webcalendars; public int RefreshAlbumPeopleInterval => _delegate.RefreshAlbumPeopleInterval; public string? WeatherApiKey => _delegate.WeatherApiKey; + public string? WeatherApiKeyFile => _delegate.WeatherApiKeyFile; public string? WeatherLatLong => _delegate.WeatherLatLong; public string? UnitSystem => _delegate.UnitSystem; public string? Webhook => _delegate.Webhook; @@ -136,6 +138,17 @@ class GeneralSettingsV1Adapter(ServerSettingsV1 _delegate) : IGeneralSettings public string Layout => _delegate.Layout; public string Language => _delegate.Language; - public void Validate() { } + public void Validate() + { + if (!string.IsNullOrWhiteSpace(_delegate.WeatherApiKeyFile)) + { + if (!string.IsNullOrWhiteSpace(_delegate.WeatherApiKey)) + { + throw new Exception("Cannot specify both WeatherApiKey and WeatherApiKeyFile. Please provide only one."); + } + + _delegate.WeatherApiKey = File.ReadAllText(_delegate.WeatherApiKeyFile).Trim(); + } + } } } diff --git a/ImmichFrame.WebApi/Models/ServerSettings.cs b/ImmichFrame.WebApi/Models/ServerSettings.cs index 74d0fb8e..6e9a134b 100644 --- a/ImmichFrame.WebApi/Models/ServerSettings.cs +++ b/ImmichFrame.WebApi/Models/ServerSettings.cs @@ -68,12 +68,24 @@ public class GeneralSettings : IGeneralSettings, IConfigSettable public List Webcalendars { get; set; } = new(); public int RefreshAlbumPeopleInterval { get; set; } = 12; public string? WeatherApiKey { get; set; } = string.Empty; + public string? WeatherApiKeyFile { get; set; } = null; public string? UnitSystem { get; set; } = "imperial"; public string? WeatherLatLong { get; set; } = "40.7128,74.0060"; public string? Webhook { get; set; } public string? AuthenticationSecret { get; set; } - public void Validate() { } + public void Validate() + { + if (!string.IsNullOrWhiteSpace(WeatherApiKeyFile)) + { + if (!string.IsNullOrWhiteSpace(WeatherApiKey)) + { + throw new Exception("Cannot specify both WeatherApiKey and WeatherApiKeyFile. Please provide only one."); + } + + WeatherApiKey = File.ReadAllText(WeatherApiKeyFile).Trim(); + } + } } public class ServerAccountSettings : IAccountSettings, IConfigSettable diff --git a/Install_Web.md b/Install_Web.md index 5cf3f94c..6d28ef07 100644 --- a/Install_Web.md +++ b/Install_Web.md @@ -75,6 +75,7 @@ services: # Style: "none" # BaseFontSize: "17px" # WeatherApiKey: "" + # WeatherApiKeyFile: "/path/to/weather-api.key" # ShowWeatherDescription: "true" # WeatherIconUrl: "https://openweathermap.org/img/wn/{IconId}.png" # UnitSystem: "imperial" diff --git a/docker/Settings.example.json b/docker/Settings.example.json index a86a4d00..c63d399a 100644 --- a/docker/Settings.example.json +++ b/docker/Settings.example.json @@ -10,6 +10,7 @@ "PhotoDateFormat": "MM/dd/yyyy", "ImageLocationFormat": "City,State,Country", "WeatherApiKey": "", + "WeatherApiKeyFile": "/path/to/weather-api.key", "UnitSystem": "imperial", "WeatherLatLong": "40.730610,-73.935242", "Webhook": null, diff --git a/docker/Settings.example.yml b/docker/Settings.example.yml index 173b31a5..b73bf635 100644 --- a/docker/Settings.example.yml +++ b/docker/Settings.example.yml @@ -7,7 +7,9 @@ General: RefreshAlbumPeopleInterval: 12 PhotoDateFormat: MM/dd/yyyy ImageLocationFormat: 'City,State,Country' + # Exactly one of WeatherApiKey or WeatherApiKeyFile may be set. WeatherApiKey: '' + # WeatherApiKeyFile: "/path/to/weather-api.key" UnitSystem: imperial WeatherLatLong: '40.730610,-73.935242' Webhook: null diff --git a/docker/example.env b/docker/example.env index 51d80ed5..bf3304a7 100644 --- a/docker/example.env +++ b/docker/example.env @@ -42,6 +42,7 @@ ApiKey=KEY # Style=none # BaseFontSize=17px # WeatherApiKey= +# WeatherApiKeyFile=/path/to/weather-api.key # ShowWeatherDescription=true # WeatherIconUrl=https://openweathermap.org/img/wn/{IconId}.png # UnitSystem=imperial diff --git a/docs/docs/getting-started/configuration.md b/docs/docs/getting-started/configuration.md index 7378c7d1..7be90c25 100644 --- a/docs/docs/getting-started/configuration.md +++ b/docs/docs/getting-started/configuration.md @@ -48,8 +48,10 @@ General: # Date format. See https://date-fns.org/v4.1.0/docs/format for more information. PhotoDateFormat: 'MM/dd/yyyy' # string ImageLocationFormat: 'City,State,Country' - # Get an API key from OpenWeatherMap: https://openweathermap.org/appid + # Get an API key from OpenWeatherMap: https://openweathermap.org/appid. + # Exactly one of WeatherApiKey or WeatherApiKeyFile may be set. WeatherApiKey: '' # string + # WeatherApiKeyFile: '/path/to/weather-api.key' # string # Imperial or metric system (Fahrenheit or Celsius) UnitSystem: 'imperial' # 'imperial' | 'metric' # Set the weather location with lat/lon. @@ -160,7 +162,7 @@ For tags, use the full hierarchical path (the `value` field) as it appears in Im - `"Travel/Europe"` - matches a tag "Europe" under parent "Travel" ### Weather -Weather is enabled by entering an API key. Get yours free from [OpenWeatherMap][openweathermap-url] +Weather is enabled by entering an API key. Get yours free from [OpenWeatherMap][openweathermap-url]. Set either `WeatherApiKey` or `WeatherApiKeyFile`. ### Calendar If you are using Google Calendar, more information can be found [here](https://support.google.com/calendar/answer/37648?hl=en#zippy=%2Cget-your-calendar-view-only). diff --git a/docs/docs/getting-started/configurationV1.md b/docs/docs/getting-started/configurationV1.md index bf9ca932..3ae91a8d 100644 --- a/docs/docs/getting-started/configurationV1.md +++ b/docs/docs/getting-started/configurationV1.md @@ -30,6 +30,7 @@ sidebar_position: 4 | Image | Interval | int | 45 | Image interval in seconds. How long a image is displayed in the frame. | | Image | TransitionDuration | float | 2 | Duration in seconds. | | [Weather](#weather) | WeatherApiKey | string | | Get an API key from [OpenWeatherMap][openweathermap-url]. | +| [Weather](#weather) | WeatherApiKeyFile | string | | Path to a file containing an API key from [OpenWeatherMap][openweathermap-url]. Cannot be used together with `WeatherApiKey`. | | [Weather](#weather) | UnitSystem | imperial \| metric | imperial | Imperial or metric system. (Fahrenheit or degrees) | | [Weather](#weather) | Language | string | en | 2 digit ISO code, sets the language of the weather description. | | [Weather](#weather) | ShowWeatherDescription | boolean | true | Displays the description of the current weather. | @@ -62,7 +63,7 @@ If this is enabled, the web api required the `Authorization`-Header with `Bearer You can get the UUIDs from the URL of the album/person. For this URL: `https://demo.immich.app/albums/85c85b29-c95d-4a8b-90f7-c87da1d518ba` this is the UUID: `85c85b29-c95d-4a8b-90f7-c87da1d518ba` ### Weather -Weather is enabled by entering an API key. Get yours free from [OpenWeatherMap][openweathermap-url] +Weather is enabled by entering an API key. Get yours free from [OpenWeatherMap][openweathermap-url]. Set either `WeatherApiKey` or `WeatherApiKeyFile`. ### Calendar If you are using Google Calendar, more information can be found [here](https://support.google.com/calendar/answer/37648?hl=en#zippy=%2Cget-your-calendar-view-only). From 98efa6c52956ed710b89aba601d5803e8e921788 Mon Sep 17 00:00:00 2001 From: Gabriel Volpe Date: Thu, 13 Aug 2026 14:23:47 +0200 Subject: [PATCH 2/2] address review comments --- .../Helpers/Config/ConfigLoaderTest.cs | 26 +++++++++++++++++++ Install_Web.md | 5 +++- docker/Settings.example.json | 2 +- docker/Settings.example.yml | 5 ++-- docs/docs/getting-started/configuration.md | 7 ++--- 5 files changed, 38 insertions(+), 7 deletions(-) diff --git a/ImmichFrame.WebApi.Tests/Helpers/Config/ConfigLoaderTest.cs b/ImmichFrame.WebApi.Tests/Helpers/Config/ConfigLoaderTest.cs index a974103d..32f69697 100644 --- a/ImmichFrame.WebApi.Tests/Helpers/Config/ConfigLoaderTest.cs +++ b/ImmichFrame.WebApi.Tests/Helpers/Config/ConfigLoaderTest.cs @@ -131,6 +131,32 @@ public void ServerSettingsV1AdapterValidate_ReadsWeatherApiKeyFile() } } + [Test] + public void ServerSettingsV1AdapterValidate_ThrowsWhenWeatherApiKeyAndWeatherApiKeyFileAreBothSet() + { + var apiKeyFile = Path.GetTempFileName(); + File.WriteAllText(apiKeyFile, "weather-api-key\n"); + + try + { + var settings = new ServerSettingsV1 + { + WeatherApiKey = "weather-api-key", + WeatherApiKeyFile = apiKeyFile, + }; + + var adapter = new ServerSettingsV1Adapter(settings); + + var exception = Assert.Throws(() => adapter.Validate()); + + exception!.Message.Should().Contain("Cannot specify both WeatherApiKey and WeatherApiKeyFile"); + } + finally + { + File.Delete(apiKeyFile); + } + } + private void VerifyConfig(IServerSettings serverSettings, bool usePrefix, bool expectNullApiKeyFile) { VerifyProperties(serverSettings.GeneralSettings); diff --git a/Install_Web.md b/Install_Web.md index 6d28ef07..ae6c6262 100644 --- a/Install_Web.md +++ b/Install_Web.md @@ -34,6 +34,9 @@ services: restart: on-failure ports: - "8080:8080" + # To use WeatherApiKeyFile below, mount the host file at the configured container path. + # volumes: + # - /path/on/host/weather-api.key:/run/secrets/weather-api.key:ro environment: TZ: "Europe/Berlin" ImmichServerUrl: "URL" @@ -75,7 +78,7 @@ services: # Style: "none" # BaseFontSize: "17px" # WeatherApiKey: "" - # WeatherApiKeyFile: "/path/to/weather-api.key" + # WeatherApiKeyFile: "/run/secrets/weather-api.key" # ShowWeatherDescription: "true" # WeatherIconUrl: "https://openweathermap.org/img/wn/{IconId}.png" # UnitSystem: "imperial" diff --git a/docker/Settings.example.json b/docker/Settings.example.json index c63d399a..42d9c186 100644 --- a/docker/Settings.example.json +++ b/docker/Settings.example.json @@ -10,7 +10,7 @@ "PhotoDateFormat": "MM/dd/yyyy", "ImageLocationFormat": "City,State,Country", "WeatherApiKey": "", - "WeatherApiKeyFile": "/path/to/weather-api.key", + "WeatherApiKeyFile": null, "UnitSystem": "imperial", "WeatherLatLong": "40.730610,-73.935242", "Webhook": null, diff --git a/docker/Settings.example.yml b/docker/Settings.example.yml index b73bf635..36331429 100644 --- a/docker/Settings.example.yml +++ b/docker/Settings.example.yml @@ -7,9 +7,10 @@ General: RefreshAlbumPeopleInterval: 12 PhotoDateFormat: MM/dd/yyyy ImageLocationFormat: 'City,State,Country' - # Exactly one of WeatherApiKey or WeatherApiKeyFile may be set. + # At most one of WeatherApiKey or WeatherApiKeyFile may be set. WeatherApiKey: '' - # WeatherApiKeyFile: "/path/to/weather-api.key" + # Optionally set WeatherApiKeyFile to a file path instead of WeatherApiKey. + # WeatherApiKeyFile: "/run/secrets/weather-api.key" UnitSystem: imperial WeatherLatLong: '40.730610,-73.935242' Webhook: null diff --git a/docs/docs/getting-started/configuration.md b/docs/docs/getting-started/configuration.md index 7be90c25..3b48cbf9 100644 --- a/docs/docs/getting-started/configuration.md +++ b/docs/docs/getting-started/configuration.md @@ -49,9 +49,10 @@ General: PhotoDateFormat: 'MM/dd/yyyy' # string ImageLocationFormat: 'City,State,Country' # Get an API key from OpenWeatherMap: https://openweathermap.org/appid. - # Exactly one of WeatherApiKey or WeatherApiKeyFile may be set. + # At most one of WeatherApiKey or WeatherApiKeyFile may be set. WeatherApiKey: '' # string - # WeatherApiKeyFile: '/path/to/weather-api.key' # string + # Optionally set WeatherApiKeyFile to a file path instead of WeatherApiKey. + # WeatherApiKeyFile: '/run/secrets/weather-api.key' # string # Imperial or metric system (Fahrenheit or Celsius) UnitSystem: 'imperial' # 'imperial' | 'metric' # Set the weather location with lat/lon. @@ -162,7 +163,7 @@ For tags, use the full hierarchical path (the `value` field) as it appears in Im - `"Travel/Europe"` - matches a tag "Europe" under parent "Travel" ### Weather -Weather is enabled by entering an API key. Get yours free from [OpenWeatherMap][openweathermap-url]. Set either `WeatherApiKey` or `WeatherApiKeyFile`. +Weather is enabled by entering an API key. Get yours free from [OpenWeatherMap][openweathermap-url]. Set either `WeatherApiKey` or `WeatherApiKeyFile`, but not both. Leave both empty to disable weather. ### Calendar If you are using Google Calendar, more information can be found [here](https://support.google.com/calendar/answer/37648?hl=en#zippy=%2Cget-your-calendar-view-only).