Skip to content

Commit

Permalink
Ensure at least a 1 minute gap between runs to prevent certain calend…
Browse files Browse the repository at this point in the history
…ar UIs from rendering runs in a zipper pattern.
  • Loading branch information
Aldaviva committed Jan 6, 2025
1 parent 1a01fa2 commit a59cadd
Show file tree
Hide file tree
Showing 9 changed files with 469 additions and 345 deletions.
37 changes: 16 additions & 21 deletions GamesDoneQuickCalendarFactory/GamesDoneQuickCalendarFactory.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,21 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<RuntimeIdentifiers>win-x64;linux-x64</RuntimeIdentifiers>
<RuntimeIdentifiers>win-x64;win-arm64;linux-x64;linux-arm;linux-arm64</RuntimeIdentifiers>
<Version>2.7.5</Version>
<Product>Games Done Quick Calendar Factory</Product>
<AssemblyTitle>Games Done Quick Calendar Factory</AssemblyTitle>
<Company>Ben Hutchison</Company>
<Copyright2024 Ben Hutchison</Copyright>
<Copyright2025 Ben Hutchison</Copyright>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<RollForward>major</RollForward>
<LangVersion>latest</LangVersion>
<ApplicationIcon>gdq.ico</ApplicationIcon>
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
<ServerGarbageCollection>true</ServerGarbageCollection>
<PublishSingleFile>true</PublishSingleFile>
<SelfContained>false</SelfContained>
<DebugType>embedded</DebugType>
</PropertyGroup>

<ItemGroup>
Expand All @@ -27,11 +29,11 @@

<ItemGroup>
<PackageReference Include="Bom.Squad" Version="0.3.0" />
<PackageReference Include="Google.Apis.Calendar.v3" Version="1.68.0.3557" />
<PackageReference Include="Google.Apis.Calendar.v3" Version="1.68.0.3592" />
<PackageReference Include="Ical.Net" Version="4.3.1" />
<PackageReference Include="jaytwo.FluentUri" Version="0.1.4" />
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="8.0.1" />
<PackageReference Include="Microsoft.Extensions.Hosting.Systemd" Version="8.0.1" />
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting.Systemd" Version="9.0.0" />
<PackageReference Include="ThrottleDebounce" Version="2.0.0" />
<PackageReference Include="Unfucked" Version="0.0.0-beta3" />
<PackageReference Include="Unfucked.DateTime" Version="0.0.0-beta3" />
Expand All @@ -40,31 +42,24 @@
</ItemGroup>

<ItemGroup Condition="$(RuntimeIdentifier.StartsWith('win'))">
<None Update="Install service.ps1">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Install service.ps1" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>

<ItemGroup Condition="$(RuntimeIdentifier.StartsWith('linux'))">
<None Update="gamesdonequickcalendarfactory.service">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="gamesdonequickcalendarfactory.service" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>

<PropertyGroup Condition="$(RuntimeIdentifier.StartsWith('linux'))">
<AssemblyName>$(AssemblyName.ToLower())</AssemblyName>
</PropertyGroup>

<ItemGroup>
<InternalsVisibleTo Include="Tests" />
</ItemGroup>

<ItemGroup>
<Compile Update="Resources.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>Resources.resx</DependentUpon>
</Compile>
<EmbeddedResource Update="Resources.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
</EmbeddedResource>
<Compile Update="Resources.Designer.cs" DesignTime="True" AutoGen="True" DependentUpon="Resources.resx" />
<EmbeddedResource Update="Resources.resx" LastGenOutput="Resources.Designer.cs" Generator="ResXFileCodeGenerator" />
</ItemGroup>

</Project>
3 changes: 1 addition & 2 deletions GamesDoneQuickCalendarFactory/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,7 @@
});

webApp.MapGet("/", [OutputCache] async Task ([FromServices] ICalendarPoller calendarPoller, HttpResponse response) => {
CalendarResponse? mostRecentlyPolledCalendar = calendarPoller.mostRecentlyPolledCalendar;
if (mostRecentlyPolledCalendar != null) {
if (calendarPoller.mostRecentlyPolledCalendar is { } mostRecentlyPolledCalendar) {
ResponseHeaders responseHeaders = response.GetTypedHeaders();
responseHeaders.ContentType = icalendarContentType;
responseHeaders.ETag = new EntityTagHeaderValue(mostRecentlyPolledCalendar.etag);
Expand Down
65 changes: 34 additions & 31 deletions GamesDoneQuickCalendarFactory/Services/CalendarGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using Ical.Net;
using Ical.Net.CalendarComponents;
using Ical.Net.DataTypes;
using NodaTime;
using Unfucked;

namespace GamesDoneQuickCalendarFactory.Services;
Expand All @@ -14,44 +15,46 @@ public interface ICalendarGenerator {

public sealed class CalendarGenerator(IEventDownloader eventDownloader, ILogger<CalendarGenerator> logger): ICalendarGenerator {

private const int SCHEMA_VERSION = 2;
private const int SCHEMA_VERSION = 3;

private static readonly Duration MIN_RUN_GAP = Duration.FromMinutes(1);

public async Task<Calendar> generateCalendar() {
logger.LogTrace("Downloading schedule from Games Done Quick website");
Event? gdqEvent = await eventDownloader.downloadSchedule();
Calendar calendar = new() { Method = CalendarMethods.Publish };

if (gdqEvent != null) {
calendar.Events.AddRange(gdqEvent.runs
.Select((run, runIndex) => new CalendarEvent {
Uid = $"{SCHEMA_VERSION}/{gdqEvent.shortTitle}/{run.name}/{run.description}",
// UTC works better than trying to coerce the OffsetDateTime into a ZonedDateTime, because NodaTime will pick a zone like UTC-5 instead of America/New_York (which makes sense), but Vivaldi doesn't apply zones like UTC-5 correctly and render the times as if they were local time, leading to events starting 3 hours too early for subscribers in America/Los_Angeles. Alternatively, we could map offsets and dates to more well-known zones like America/New_York, or use the zone specified in the GdqEvent.timezone property except I don't know if Vivaldi handles US/Eastern
Start = run.start.ToIcalDateTimeUtc(),
Duration = run.duration.ToTimeSpan(),
IsAllDay = false, // needed because iCal.NET assumes all events that start at midnight are always all-day events, even if they have a duration that isn't 24 hours
Summary = run.name,
// having an Organizer makes Outlook show "this event has not been accepted"
Description =
$"{run.description}\nRun by {run.runners.Select(getName).JoinHumanized()}{(run.commentators.Any() ? $"\nCommentary by {run.commentators.Select(getName).JoinHumanized()}" : string.Empty)}{(run.hosts.Any() ? $"\nHosted by {run.hosts.Select(getName).JoinHumanized()}" : string.Empty)}",
// Location = TWITCH_STREAM_URL.ToString(),
Alarms = {
runIndex == 0 ? new Alarm {
Action = AlarmAction.Display,
Trigger = new Trigger(TimeSpan.FromDays(7)),
Description = $"{gdqEvent.longTitle} is coming up next week"
} : null,
runIndex == 0 ? new Alarm {
Action = AlarmAction.Display,
Trigger = new Trigger(TimeSpan.FromDays(1)),
Description = $"{gdqEvent.longTitle} is starting tomorrow"
} : null,
runIndex == 0 ? new Alarm {
Action = AlarmAction.Display,
Trigger = new Trigger(TimeSpan.FromMinutes(15)),
Description = $"{gdqEvent.longTitle} will be starting soon"
} : null
}
}));
calendar.Events.AddRange(gdqEvent.runs.Select((run, runIndex) => new CalendarEvent {
Uid = $"{SCHEMA_VERSION}/{gdqEvent.shortTitle}/{run.name}/{run.description}",
// UTC works better than trying to coerce the OffsetDateTime into a ZonedDateTime, because NodaTime will pick a zone like UTC-5 instead of America/New_York (which makes sense), but Vivaldi doesn't apply zones like UTC-5 correctly and render the times as if they were local time, leading to events starting 3 hours too early for subscribers in America/Los_Angeles. Alternatively, we could map offsets and dates to more well-known zones like America/New_York, or use the zone specified in the GdqEvent.timezone property except I don't know if Vivaldi handles US/Eastern
Start = run.start.ToIcalDateTimeUtc(),
// ensure at least 1 second gap between runs, to make calendars look nicer
Duration = ((Duration?[]) [run.duration, gdqEvent.runs.ElementAtOrDefault(runIndex + 1) is { } nextRun ? nextRun.start - run.start - MIN_RUN_GAP : null]).Compact().Min().ToTimeSpan(),
IsAllDay = false, // needed because iCal.NET assumes all events that start at midnight are always all-day events, even if they have a duration that isn't 24 hours
Summary = run.name,
// having an Organizer makes Outlook show "this event has not been accepted"
Description =
$"{run.description}\nRun by {run.runners.Select(getName).JoinHumanized()}{(run.commentators.Any() ? $"\nCommentary by {run.commentators.Select(getName).JoinHumanized()}" : string.Empty)}{(run.hosts.Any() ? $"\nHosted by {run.hosts.Select(getName).JoinHumanized()}" : string.Empty)}",
// Location = TWITCH_STREAM_URL.ToString(),
Alarms = {
runIndex == 0 ? new Alarm {
Action = AlarmAction.Display,
Trigger = new Trigger(TimeSpan.FromDays(7)),
Description = $"{gdqEvent.longTitle} is coming up next week"
} : null,
runIndex == 0 ? new Alarm {
Action = AlarmAction.Display,
Trigger = new Trigger(TimeSpan.FromDays(1)),
Description = $"{gdqEvent.longTitle} is starting tomorrow"
} : null,
runIndex == 0 ? new Alarm {
Action = AlarmAction.Display,
Trigger = new Trigger(TimeSpan.FromMinutes(15)),
Description = $"{gdqEvent.longTitle} will be starting soon"
} : null
}
}));
}

return calendar;
Expand Down
4 changes: 2 additions & 2 deletions GamesDoneQuickCalendarFactory/Services/GdqClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ public class GdqClient(HttpClient httpClient): IGdqClient {
EmptyToNullUriConverter.INSTANCE,
OffsetDateTimeConverter.INSTANCE,
PeriodConverter.INSTANCE,
new JsonStringEnumConverter(JsonNamingPolicy.SnakeCaseUpper)
new JsonStringEnumConverter()
}
};

Expand Down Expand Up @@ -86,7 +86,7 @@ public async Task<IEnumerable<GameRun>> getEventRuns(int eventId) {

private static Person getPerson(GdqPerson person) => new(person.id, person.name);

private async IAsyncEnumerable<T> downloadAllPages<T>(Uri firstPageUrl, ValueHolderStruct<int>? resultsCount = default, [EnumeratorCancellation] CancellationToken c = default) {
private async IAsyncEnumerable<T> downloadAllPages<T>(Uri firstPageUrl, ValueHolderStruct<int>? resultsCount = null, [EnumeratorCancellation] CancellationToken c = default) {
JsonObject? page;
for (Uri? nextPageToDownload = firstPageUrl; nextPageToDownload != null; nextPageToDownload = page?["next"]?.GetValue<Uri?>()) {
page = await httpClient.GetFromJsonAsync<JsonObject>(nextPageToDownload, JSON_SERIALIZER_OPTIONS, c);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ Description=Games Done Quick Calendar Factory

[Service]
Type=notify
ExecStart=/usr/local/bin/GamesDoneQuickCalendarFactory
ExecStart=/usr/local/bin/gamesdonequickcalendarfactory
WorkingDirectory=/usr/local/bin/
Restart=on-failure

Expand Down
Loading

0 comments on commit a59cadd

Please sign in to comment.