📄 src/Api/ClipGenerationJob.cs
using System;
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");
        try
        {
            _ = await clipGenerator.Generate(context.CancellationToken);
        }
        catch (Exception ex) when (!context.CancellationToken.IsCancellationRequested)
        {
            throw new JobExecutionException(ex, refireImmediately: true);
        }

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

public static class ClipGenerationJobExtensions
{
    extension(IServiceCollectionQuartzConfigurator quartz)
    {
        public IServiceCollectionQuartzConfigurator AddClipGenerationJob(bool startNow)
        {
            quartz.AddJob<ClipGenerationJob>(ClipGenerationJob.Key, options => options.StoreDurably());
            if (startNow)
            {
                quartz.AddTrigger(options => options.ForJob(ClipGenerationJob.Key).StartNow());
            }
            return quartz;
        }
    }

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

            return services;
        }
    }
}

public sealed class ClipGenerationJobOptions
{
    public TimeSpan? Interval { get; set; }
}