diff --git a/CronTimer/CronEventArgs.cs b/CronTimer/CronEventArgs.cs
new file mode 100644
index 0000000..df87ec1
--- /dev/null
+++ b/CronTimer/CronEventArgs.cs
@@ -0,0 +1,6 @@
+using System;
+
+public class CronTimerEventArgs : EventArgs
+{
+    public DateTime At { get; set; }
+}
diff --git a/CronTimer/CronTimer.cs b/CronTimer/CronTimer.cs
new file mode 100644
index 0000000..00357a8
--- /dev/null
+++ b/CronTimer/CronTimer.cs
@@ -0,0 +1,78 @@
+using System;
+using System.Threading;
+using NCrontab;
+
+public class CronTimer
+{
+    public const string UTC = "Etc/UTC";
+
+    static readonly TimeSpan InfiniteTimeSpan = TimeSpan.FromMilliseconds(Timeout.Infinite); // net 3.5
+
+    readonly CrontabSchedule schedule;
+    readonly TimeZoneInfo tzi;
+    readonly string id;
+    readonly Timer t;
+
+    public string tz { get; }
+    public string Expression { get; }
+    public event EventHandler<CronTimerEventArgs> OnOccurence;
+
+    public DateTime Next { get; private set; }
+
+    public CronTimer(string expression, string tz = UTC, bool includingSeconds = false)
+    {
+        Expression = expression;
+        this.tz = tz;
+        id = TimeZoneConverter.TZConvert.IanaToWindows(tz);
+        tzi = TimeZoneInfo.FindSystemTimeZoneById(id);
+        schedule = CrontabSchedule.Parse(expression, new CrontabSchedule.ParseOptions { IncludingSeconds = includingSeconds });
+        Next = TimeZoneInfo.ConvertTimeFromUtc(DateTime.UtcNow, tzi);
+        OnOccurence += OnOccurenceScheduleNext;
+        t = new Timer(s =>
+        {
+            var ea = new CronTimerEventArgs
+            {
+                At = Next
+            };
+            OnOccurence(this, ea);
+        }, null, InfiniteTimeSpan, InfiniteTimeSpan);
+    }
+
+    void OnOccurenceScheduleNext(object sender, EventArgs e)
+    {
+        var delay = CalculateDelay();
+        //Console.WriteLine($"Next for [{tz} {expression}] in {delay}.");
+        t.Change(delay, InfiniteTimeSpan);
+    }
+
+    public void Start()
+    {
+        var delay = CalculateDelay();
+        //Console.WriteLine($"Next for [{tz} {expression}] in {delay}.");
+        t.Change(delay, InfiniteTimeSpan);
+    }
+
+    TimeSpan CalculateDelay()
+    {
+        var nowUtc = DateTime.UtcNow;
+        Next = schedule.GetNextOccurrence(Next);
+        TimeSpan delay;
+        if (tz != UTC)
+        {
+            var nextUtc = TimeZoneInfo.ConvertTimeToUtc(Next, tzi);
+            delay = nextUtc - nowUtc;
+        }
+        else
+        {
+            delay = Next - nowUtc;
+        }
+        //Console.WriteLine($"Now: {nowUtc} [utc] {now} [{tz}], Next: {next} [{tz}] {nextUtc} [utc], Delay: {delay}");
+        if (delay < TimeSpan.Zero) delay = TimeSpan.Zero;
+        return delay;
+    }
+
+    public void Stop()
+    {
+        t.Change(InfiniteTimeSpan, InfiniteTimeSpan);
+    }
+}
diff --git a/CronTimer/CronTimer.csproj b/CronTimer/CronTimer.csproj
new file mode 100644
index 0000000..f488cf3
--- /dev/null
+++ b/CronTimer/CronTimer.csproj
@@ -0,0 +1,49 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <LangVersion>latest</LangVersion>
+    <TargetFrameworks>net461;netstandard2.1</TargetFrameworks>
+    <PlatformTarget>AnyCPU</PlatformTarget>
+  </PropertyGroup>
+
+  <PropertyGroup>
+    <!-- AssemblyFileVersionAttribute -->
+    <FileVersion>2.0.1</FileVersion>
+    <!-- AssemblyInformationalVersionAttribute -->
+    <Version>$(FileVersion)</Version>
+    <!-- AssemblyVersionAttribute -->
+    <AssemblyVersion>2.0.0.0</AssemblyVersion>
+    <!-- Nuget -->
+    <PackageVersion>$(Version)</PackageVersion>
+    <PackageId>CronTimer</PackageId>
+    <Company>https://github.com/ramonsmits</Company>
+    <Authors>ramonsmits</Authors>
+    <Description>Simple .net Timer that is based on cron expressions with second accuracy to fire timer events to a very specific schedule.</Description>
+    <PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
+    <PackageReleaseNotes></PackageReleaseNotes>
+    <PackageProjectUrl>https://github.com/ramonsmits/CronTimer/tree/$(PackageVersion)</PackageProjectUrl>
+    <PackageLicenseExpression>MIT</PackageLicenseExpression>
+    <IncludeSymbols>True</IncludeSymbols>
+    <SymbolPackageFormat>snupkg</SymbolPackageFormat>
+    <IncludeSource>True</IncludeSource>
+    <RepositoryUrl>https://github.com/ramonsmits/CronTimer</RepositoryUrl>
+    <Copyright>Copyright 2022 (c) Ramon Smits</Copyright>
+    <PackageTags>cron timer</PackageTags>
+    <GeneratePackageOnBuild>true</GeneratePackageOnBuild>
+    <PublishRepositoryUrl>true</PublishRepositoryUrl>
+    <ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
+    <EmbedUntrackedSources>true</EmbedUntrackedSources>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <PackageReference Include="ncrontab" Version="3.3.1" />
+    <PackageReference Include="TimeZoneConverter" Version="6.0.1" />
+  </ItemGroup>
+
+  <!--<ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.1'">
+    <PackageReference Include="Microsoft.Extensions.Logging.Abstractions">
+      <Version>3.1.4</Version>
+    </PackageReference>
+  </ItemGroup>-->
+
+</Project>
diff --git a/CronTimer/CronTimer.sln b/CronTimer/CronTimer.sln
new file mode 100644
index 0000000..adc7e3f
--- /dev/null
+++ b/CronTimer/CronTimer.sln
@@ -0,0 +1,36 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 16
+VisualStudioVersion = 16.0.30717.126
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CronTimer", "CronTimer.csproj", "{FB64C227-8615-4AE1-94E3-F9F9DF192B72}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{CCDD0B34-653C-430C-9B17-5129618F8D7D}"
+	ProjectSection(SolutionItems) = preProject
+		..\README.md = ..\README.md
+	EndProjectSection
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Demo", "..\Demo\Demo.csproj", "{C2638357-1621-4422-8701-B55BFB37ACCF}"
+EndProject
+Global
+	GlobalSection(SolutionConfigurationPlatforms) = preSolution
+		Debug|Any CPU = Debug|Any CPU
+		Release|Any CPU = Release|Any CPU
+	EndGlobalSection
+	GlobalSection(ProjectConfigurationPlatforms) = postSolution
+		{FB64C227-8615-4AE1-94E3-F9F9DF192B72}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{FB64C227-8615-4AE1-94E3-F9F9DF192B72}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{FB64C227-8615-4AE1-94E3-F9F9DF192B72}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{FB64C227-8615-4AE1-94E3-F9F9DF192B72}.Release|Any CPU.Build.0 = Release|Any CPU
+		{C2638357-1621-4422-8701-B55BFB37ACCF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{C2638357-1621-4422-8701-B55BFB37ACCF}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{C2638357-1621-4422-8701-B55BFB37ACCF}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{C2638357-1621-4422-8701-B55BFB37ACCF}.Release|Any CPU.Build.0 = Release|Any CPU
+	EndGlobalSection
+	GlobalSection(SolutionProperties) = preSolution
+		HideSolutionNode = FALSE
+	EndGlobalSection
+	GlobalSection(ExtensibilityGlobals) = postSolution
+		SolutionGuid = {A639B164-6581-40DF-ADC4-479DAB467CFE}
+	EndGlobalSection
+EndGlobal
diff --git a/song_of_the_day/song_of_the_day.csproj b/song_of_the_day/song_of_the_day.csproj
index 246c091..443368d 100644
--- a/song_of_the_day/song_of_the_day.csproj
+++ b/song_of_the_day/song_of_the_day.csproj
@@ -21,4 +21,7 @@
   <ItemGroup>
     <OpenApiReference Include="swagger.json" SourceUrl="https://bbernhard.github.io/signal-cli-rest-api/src/docs/swagger.json" />
   </ItemGroup>
+  <ItemGroup>
+    <ProjectReference Include="..\CronTimer\CronTimer.csproj" />
+  </ItemGroup>
 </Project>
\ No newline at end of file