using Fragalytics.Web.Infrastructure; using Fragalytics.Web.Models; using Microsoft.Extensions.Options; namespace Fragalytics.Web.Services; public sealed class ClipMediaService { private readonly JsonAppStore _store; private readonly FragalyticsOptions _options; public ClipMediaService(JsonAppStore store, IOptions options) { _store = store; _options = options.Value; } public async Task AttachClipAssetAsync( int clipJobId, string fileName, string? contentType, Stream input, CancellationToken cancellationToken) { var extension = Path.GetExtension(fileName); if (!ClipMediaFormats.AllowedExtensions.Contains(extension)) { throw new InvalidOperationException($"Only {ClipMediaFormats.Mp4Extension} and {ClipMediaFormats.WebMExtension} clip uploads are supported."); } var clipsDirectory = Path.Combine(Path.GetFullPath(_options.DataDirectory), "clips"); Directory.CreateDirectory(clipsDirectory); var safeFileName = Path.GetFileName(fileName); var storedName = $"{DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()}_{clipJobId}{extension}"; var storedPath = Path.Combine(clipsDirectory, storedName); long fileSize; await using (var output = File.Create(storedPath)) { await input.CopyToAsync(output, cancellationToken); fileSize = output.Length; } return await _store.UpdateAsync(state => { var clipJob = state.ClipJobs.FirstOrDefault(item => item.Id == clipJobId) ?? throw new InvalidOperationException("Clip job not found."); if (clipJob.ClipAssetId is int existingAssetId) { var existingAsset = state.ClipAssets.FirstOrDefault(item => item.Id == existingAssetId); if (existingAsset is not null && File.Exists(existingAsset.StoredPath)) { File.Delete(existingAsset.StoredPath); } state.ClipAssets.RemoveAll(item => item.Id == existingAssetId); } var asset = new ClipAssetRecord { Id = state.NextClipAssetId++, ClipJobId = clipJobId, FileName = safeFileName, StoredPath = storedPath, ContentType = string.IsNullOrWhiteSpace(contentType) ? ClipMediaFormats.InferContentType(extension) : contentType, FileSizeBytes = fileSize }; state.ClipAssets.Add(asset); clipJob.ClipAssetId = asset.Id; clipJob.Status = ClipJobStatuses.Rendered; clipJob.ErrorMessage = null; clipJob.CompletedAtUtc = DateTime.UtcNow; return asset.Id; }, cancellationToken); } }