Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 50 additions & 17 deletions ImmichFrame.Core/Services/IcalCalendarService.cs
Original file line number Diff line number Diff line change
@@ -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<IcalCalendarService> _logger;
private readonly IHttpClientFactory _httpClientFactory;
private readonly ApiCache _appointmentCache = new(TimeSpan.FromMinutes(15));

public IcalCalendarService(IGeneralSettings serverSettings)
public IcalCalendarService(IGeneralSettings serverSettings, ILogger<IcalCalendarService> logger, IHttpClientFactory httpClientFactory)
{
_logger = logger;
_serverSettings = serverSettings;
_httpClientFactory = httpClientFactory;
}

public async Task<List<IAppointment>> GetAppointments()
Expand All @@ -19,7 +26,26 @@ public async Task<List<IAppointment>> GetAppointments()
{
var appointments = new List<IAppointment>();

var icals = await GetCalendars(_serverSettings.Webcalendars);
List<(string? auth, string url)> cals = _serverSettings.Webcalendars.Select<string, (string? auth, string url)?>(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)
{
Expand All @@ -32,29 +58,36 @@ public async Task<List<IAppointment>> GetAppointments()
});
}

public async Task<List<string>> GetCalendars(IEnumerable<string> calendars)
public async Task<List<string>> GetCalendars(IEnumerable<(string? auth, string url)> calendars)
{
var icals = new List<string>();
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;
}

}
2 changes: 1 addition & 1 deletion docker/example.env
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 8 additions & 1 deletion docs/docs/getting-started/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down
Loading