diff --git a/ImmichFrame.Core/Services/IcalCalendarService.cs b/ImmichFrame.Core/Services/IcalCalendarService.cs index 27ae77eb..b17a9317 100644 --- a/ImmichFrame.Core/Services/IcalCalendarService.cs +++ b/ImmichFrame.Core/Services/IcalCalendarService.cs @@ -1,16 +1,23 @@ +using System.Net.Http.Headers; +using System.Text; using Ical.Net; using ImmichFrame.Core.Helpers; using ImmichFrame.Core.Interfaces; using ImmichFrame.WebApi.Helpers; +using Microsoft.Extensions.Logging; public class IcalCalendarService : ICalendarService { private readonly IGeneralSettings _serverSettings; - private readonly IApiCache _appointmentCache = new ApiCache(TimeSpan.FromMinutes(15)); + private readonly ILogger _logger; + private readonly IHttpClientFactory _httpClientFactory; + private readonly ApiCache _appointmentCache = new(TimeSpan.FromMinutes(15)); - public IcalCalendarService(IGeneralSettings serverSettings) + public IcalCalendarService(IGeneralSettings serverSettings, ILogger logger, IHttpClientFactory httpClientFactory) { + _logger = logger; _serverSettings = serverSettings; + _httpClientFactory = httpClientFactory; } public async Task> GetAppointments() @@ -19,7 +26,26 @@ public async Task> GetAppointments() { var appointments = new List(); - var icals = await GetCalendars(_serverSettings.Webcalendars); + List<(string? auth, string url)> cals = _serverSettings.Webcalendars.Select(x => + { + try + { + var uri = new Uri(x.Replace("webcal://", "https://")); + if (!string.IsNullOrEmpty(uri.UserInfo)) + { + var url = uri.GetComponents(UriComponents.AbsoluteUri & ~UriComponents.UserInfo, UriFormat.UriEscaped); + return (Uri.UnescapeDataString(uri.UserInfo), url); + } + return (null, x); + } + catch (UriFormatException) + { + _logger.LogError($"Invalid calendar URL: '{x}'"); + return null; + } + }).Where(x => x != null).Select(x => x!.Value).ToList(); + + var icals = await GetCalendars(cals); foreach (var ical in icals) { @@ -32,29 +58,36 @@ public async Task> GetAppointments() }); } - public async Task> GetCalendars(IEnumerable calendars) + public async Task> GetCalendars(IEnumerable<(string? auth, string url)> calendars) { var icals = new List(); + var client = _httpClientFactory.CreateClient(); - foreach (var webcal in calendars) + foreach (var calendar in calendars) { - string httpUrl = webcal.Replace("webcal://", "https://"); + _logger.LogDebug($"Loading calendar: {(calendar.auth != null ? "[authenticated]" : "no auth")} - {calendar.url}"); + + string httpUrl = calendar.url.Replace("webcal://", "https://"); - using (HttpClient client = new HttpClient()) + using var request = new HttpRequestMessage(HttpMethod.Get, httpUrl); + + if (!string.IsNullOrEmpty(calendar.auth)) { - HttpResponseMessage response = await client.GetAsync(httpUrl); - if (response.IsSuccessStatusCode) - { - icals.Add(await response.Content.ReadAsStringAsync()); - } - else - { - throw new Exception("Failed to load calendar data"); - } + var byteArray = Encoding.UTF8.GetBytes(calendar.auth); + request.Headers.Authorization = new AuthenticationHeaderValue("Basic", Convert.ToBase64String(byteArray)); + } + + using var response = await client.SendAsync(request); + if (response.IsSuccessStatusCode) + { + icals.Add(await response.Content.ReadAsStringAsync()); + } + else + { + _logger.LogError($"Failed to load calendar data from '{httpUrl}' (Status: {response.StatusCode})"); } } return icals; } - } \ No newline at end of file diff --git a/docker/example.env b/docker/example.env index 75e875fd..8e43f8df 100644 --- a/docker/example.env +++ b/docker/example.env @@ -22,7 +22,7 @@ ApiKey=KEY # Albums=ALBUM1,ALBUM2 # ExcludedAlbums=ALBUM3,ALBUM4 # People=PERSON1,PERSON2 -# Webcalendars=https://calendar.mycalendar.com/basic.ics,webcal://calendar.mycalendar.com/basic.ics +# Webcalendars=https://calendar.google.com/calendar/ical/XXXXXX/public/basic.ics,https://user:pass@calendar.immichframe.dev/dav/calendars/basic.ics # RefreshAlbumPeopleInterval=12 # ShowClock=true # ClockFormat=hh:mm diff --git a/docs/docs/getting-started/configuration.md b/docs/docs/getting-started/configuration.md index cf4e3295..9b6870b0 100644 --- a/docs/docs/getting-started/configuration.md +++ b/docs/docs/getting-started/configuration.md @@ -38,7 +38,9 @@ General: DownloadImages: false # boolean # if images are downloaded, re-download if age (in days) is more than this RenewImagesDuration: 30 # int - # A list of webcalendar URIs in the .ics format. e.g. https://calendar.google.com/calendar/ical/XXXXXX/public/basic.ics + # A list of webcalendar URIs in the .ics format. Supports basic auth via standard URL format. + # e.g. https://calendar.google.com/calendar/ical/XXXXXX/public/basic.ics + # e.g. https://user:pass@calendar.immichframe.dev/dav/calendars/basic.ics Webcalendars: # string[] - UUID # Interval in hours. Determines how often images are pulled from a person in immich. @@ -159,6 +161,11 @@ Weather is enabled by entering an API key. Get yours free from [OpenWeatherMap][ ### 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). +Calendar supports basic authentication using the standard URL userinfo format: +Example: +No Auth: `https://calendar.google.com/calendar/ical/XXXXXX/public/basic.ics` +With Auth: `https://username:password@calendar.immichframe.dev/dav/calendars/basic.ics` + ### Misc #### Webhook A webhook to notify an external service is available. This is only enabled when the `Webhook`-Setting is set in your configuration. Your configured Webhook will be notified via `HTTP POST`-request.