📄
src/Api/ApiEndpoints.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Routing; using Quartz; using Slopper.Domain; namespace Slopper.Api; public static class ApiEndpoints { extension(IEndpointRouteBuilder endpoints) { public IEndpointConventionBuilder MapApi() { var api = endpoints.MapGroup("/api"); var clips = api.MapGroup("/clips"); clips.MapGet("/", GetClips).WithDisplayName("Get Latest Clips"); clips.MapGet("/{id}/stream", GetClipStream).WithDisplayName("Get Video Stream for Clip"); var jobs = api.MapGroup("/jobs"); jobs.MapGet("/", GetJobStatus).WithDisplayName("Get Clip Job Status"); jobs.MapPut("/", StartJob) .WithDisplayName("Create New Clip") .WithSummary("Triggers a job generating a new clip."); api.MapYouTubeApi(); return api; } } private static IAsyncEnumerable<Clip> GetClips( [FromServices] IClipRepository clipRepository, CancellationToken cancellationToken, [FromQuery] Guid? after = null, [FromQuery, Range(0, 64)] int limit = 10 ) => clipRepository.GetLatest(after, limit, cancellationToken).Select(Clip.FromDomain); private static async Task<Results<PhysicalFileHttpResult, NotFound>> GetClipStream( [FromServices] IClipRepository clipRepository, [FromRoute] Guid id, CancellationToken cancellationToken ) { var clip = await clipRepository.Get(id, cancellationToken); if (clip is null) { return TypedResults.NotFound(); } return TypedResults.PhysicalFile( clip.Path, contentType: "video/mp4", fileDownloadName: $"{clip.Id}.mp4", lastModified: clip.CreatedAt, enableRangeProcessing: true ); } private static async Task<Ok<JobStatus>> GetJobStatus( [FromServices] ISchedulerFactory schedulerFactory, CancellationToken cancellationToken ) { var scheduler = await schedulerFactory.GetScheduler(cancellationToken); if (await IsClipGenerationJobRunning(scheduler, cancellationToken)) { return TypedResults.Ok(new JobStatus(true)); } var triggers = await scheduler.GetTriggersOfJob(ClipGenerationJob.Key, cancellationToken); var scheduledTimes = triggers .Select(t => t.GetNextFireTimeUtc()) .Where(n => n is not null) .OfType<DateTimeOffset>() .ToArray(); return TypedResults.Ok(new JobStatus(false, scheduledTimes is [] ? null : scheduledTimes.Min())); } private static async Task<Accepted> StartJob( [FromServices] ISchedulerFactory schedulerFactory, CancellationToken cancellationToken ) { var scheduler = await schedulerFactory.GetScheduler(cancellationToken); if (await IsClipGenerationJobRunning(scheduler, cancellationToken)) { return TypedResults.Accepted("/api/jobs"); } var triggers = await scheduler.GetTriggersOfJob(ClipGenerationJob.Key, cancellationToken); await scheduler.UnscheduleJobs([.. triggers.Select(t => t.Key)], cancellationToken); await scheduler.TriggerJob(ClipGenerationJob.Key, cancellationToken); return TypedResults.Accepted("/api/jobs"); } private static async Task<bool> IsClipGenerationJobRunning( IScheduler scheduler, CancellationToken cancellationToken ) { var currentlyExecutingJobs = await scheduler.GetCurrentlyExecutingJobs(cancellationToken); return currentlyExecutingJobs.Any(j => j.JobDetail.Key == ClipGenerationJob.Key); } } public sealed record Clip( Guid Id, TimeSpan Duration, DateTimeOffset CreatedAt, string? Caption, string[] Tags, ClipUpload[] Uploads ) { public static Clip FromDomain(Domain.Clip clip) => new( clip.Id, clip.Duration, clip.CreatedAt, clip.Caption, [.. clip.Tags.Select(t => t.Value)], [.. clip.Uploads.Select(u => new ClipUpload(u.CanonicalUrl.ToString(), u.Platform, u.PublishedAt))] ); } public sealed record ClipUpload(string CanonicalUrl, string Platform, DateTimeOffset PublishedAt);