📄 src/Api/ClipGenerationJob.cs
using System;
using System.ComponentModel.DataAnnotations;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Quartz;
using Slopper.Domain;

namespace Slopper.Api;

[DisallowConcurrentExecution]
public sealed class ClipGenerationJob(
    ILogger<ClipGenerationJob> logger,
    IOptionsMonitor<ClipGenerationJobOptions> options,
    ClipGenerator clipGenerator,
    TimeProvider timeProvider
) : IJob
{
    public static JobKey Key { get; } = new(nameof(ClipGenerationJob));

    public async Task Execute(IJobExecutionContext context)
    {
        logger.LogDebug("Running clip generation job");
        _ = await clipGenerator.Generate(context.CancellationToken);

        await context.Scheduler.ScheduleJob(
            TriggerBuilder
                .Create()
                .ForJob(Key)
                .StartAt(timeProvider.GetUtcNow() + options.CurrentValue.Interval)
                .Build(),
            context.CancellationToken
        );
    }
}

public static class ClipGenerationJobExtensions
{
    extension(IServiceCollectionQuartzConfigurator quartz)
    {
        public IServiceCollectionQuartzConfigurator AddClipGenerationJob() =>
            quartz.AddJob<ClipGenerationJob>(ClipGenerationJob.Key, options => options.StoreDurably());

        public IServiceCollectionQuartzConfigurator AddClipGenerationJobTrigger() =>
            quartz.AddTrigger(options => options.ForJob(ClipGenerationJob.Key).StartNow());
    }

    extension(IServiceCollection services)
    {
        public IServiceCollection AddClipGenerationJobOptions()
        {
            services.AddOptions<ClipGenerationJobOptions>().BindConfiguration("ClipGenerationJob").ValidateOnStart();
            services.AddTransient<IValidateOptions<ClipGenerationJobOptions>, ClipGenerationJobOptionsValidator>();

            return services;
        }
    }
}

public sealed class ClipGenerationJobOptions
{
    [Required]
    public required TimeSpan Interval { get; set; }
}

[OptionsValidator]
public sealed partial class ClipGenerationJobOptionsValidator : IValidateOptions<ClipGenerationJobOptions>;