using DeepDrftContent; using DeepDrftContent.Constants; using DeepDrftContent.FileDatabase.Models; using DeepDrftContent.Processors; using DeepDrftData; using DeepDrftModels.Enums; using NetBlocks.Models; using FileDb = DeepDrftContent.FileDatabase.Services.FileDatabase; namespace DeepDrftAPI.Services; /// /// Host-internal orchestrator for the two release metadata write paths. Mirrors /// : it makes DeepDrftAPI the single authority over both the vault /// (FileDatabase) and SQL satellite rows, so the controller stays a thin HTTP boundary and no caller /// coordinates the two stores. /// public class UnifiedReleaseService { // High-res bucket count for Mix waveforms — 4x the player-bar default (512), feeding the // public-site MixWaveformVisualizer. private const int MixWaveformBucketCount = 2048; /// Error message returned when the Mix release has no linked track. public const string MixHasNoTrackMessage = "Mix release has no track."; /// Error message returned when the Mix track has no audio stored in the vault. public const string MixTrackNoAudioMessage = "No audio stored for the Mix track."; private readonly IReleaseService _releaseService; private readonly FileDb _fileDatabase; private readonly ImageProcessor _imageProcessor; private readonly TrackContentService _trackContentService; private readonly WaveformProfileService _waveformProfileService; private readonly ILogger _logger; public UnifiedReleaseService( IReleaseService releaseService, FileDb fileDatabase, ImageProcessor imageProcessor, TrackContentService trackContentService, WaveformProfileService waveformProfileService, ILogger logger) { _releaseService = releaseService; _fileDatabase = fileDatabase; _imageProcessor = imageProcessor; _trackContentService = trackContentService; _waveformProfileService = waveformProfileService; _logger = logger; } /// /// Process a hero image into the Images vault, then point the release's Session satellite at it. /// The medium check lives in : if the release /// is not a Session, the satellite is not written and the image is orphaned (logged, recoverable). /// public async Task SetHeroImageAsync(long releaseId, IFormFile imageFile, CancellationToken ct) { if (MimeTypeExtensions.GetExtension(imageFile.ContentType) == ".bin") { _logger.LogWarning( "SetHeroImage rejected: unsupported content type '{ContentType}' for release {ReleaseId}", imageFile.ContentType, releaseId); return Result.CreateFailResult($"Unsupported image content type: {imageFile.ContentType}"); } byte[] buffer; await using (var stream = imageFile.OpenReadStream()) using (var memory = new MemoryStream()) { await stream.CopyToAsync(memory, ct); buffer = memory.ToArray(); } var imageBinary = _imageProcessor.Process(buffer, imageFile.ContentType); if (imageBinary is null) { _logger.LogWarning("SetHeroImage: ImageProcessor rejected content type '{ContentType}'", imageFile.ContentType); return Result.CreateFailResult($"Unsupported image content type: {imageFile.ContentType}"); } var entryKey = Guid.NewGuid().ToString("N"); var stored = await _fileDatabase.RegisterResourceAsync(VaultConstants.Images, entryKey, imageBinary); if (!stored) { _logger.LogError("SetHeroImage: vault write failed for release {ReleaseId}, entryKey={EntryKey}", releaseId, entryKey); return Result.CreateFailResult("Failed to store hero image."); } var linked = await _releaseService.SetSessionHeroImageAsync(releaseId, entryKey, ct); if (!linked.Success) { // Vault write succeeded, SQL link failed — image is orphaned in the Images vault under // entryKey. Log loudly (include entryKey) so it is recoverable manually. var error = linked.Messages.FirstOrDefault()?.Message ?? "Unknown error"; _logger.LogError( "Hero image stored in vault but Session link failed. Orphaned entry: {EntryKey}. Release: {ReleaseId}. Error: {Error}", entryKey, releaseId, error); return linked; } return Result.CreatePassResult(); } /// /// Fetch the Mix's track audio from the vault, compute a high-res (2048-bucket) waveform datum, /// store it in the MixWaveforms vault under the track's EntryKey, then point the release's Mix /// satellite at that same key. The datum key equals the track's EntryKey — the Mix is single-track. /// public async Task TriggerMixWaveformAsync(long releaseId, CancellationToken ct) { var lookup = await _releaseService.GetByIdAsync(releaseId, ct); if (!lookup.Success) { var error = lookup.Messages.FirstOrDefault()?.Message ?? "Unknown error"; _logger.LogError("TriggerMixWaveform: release lookup failed for {ReleaseId}: {Error}", releaseId, error); return Result.CreateFailResult("Failed to load release."); } if (lookup.Value is null) return Result.CreateFailResult(ReleaseManager.ReleaseNotFoundMessage); // Pre-check medium here (before fetching audio) to avoid expensive waveform compute on a // non-Mix release. ReleaseManager.SetMixWaveformAsync enforces this too, so the double-check // is intentional — the orchestrator's guard is the cheap early-exit. if (lookup.Value.Medium != ReleaseMedium.Mix) return Result.CreateFailResult($"Release {releaseId} is not a Mix medium."); var keysResult = await _releaseService.GetTrackEntryKeysAsync(releaseId, ct); if (!keysResult.Success || keysResult.Value is null) { var error = keysResult.Messages.FirstOrDefault()?.Message ?? "Unknown error"; _logger.LogError("TriggerMixWaveform: entry-key lookup failed for release {ReleaseId}: {Error}", releaseId, error); return Result.CreateFailResult("Failed to load release tracks."); } var entryKey = keysResult.Value.FirstOrDefault(); if (entryKey is null) { _logger.LogWarning("TriggerMixWaveform: no track on Mix release {ReleaseId}", releaseId); return Result.CreateFailResult(MixHasNoTrackMessage); } var audio = await _trackContentService.GetAudioBinaryAsync(entryKey); if (audio is null) { _logger.LogWarning("TriggerMixWaveform: no audio in vault for {EntryKey} (release {ReleaseId})", entryKey, releaseId); return Result.CreateFailResult(MixTrackNoAudioMessage); } var computed = await _waveformProfileService.ComputeAndStoreAsync( audio.Buffer, entryKey, MixWaveformBucketCount, VaultConstants.MixWaveforms); if (!computed) { _logger.LogError("TriggerMixWaveform: waveform computation/storage failed for {EntryKey}", entryKey); return Result.CreateFailResult("Failed to compute the Mix waveform."); } var linked = await _releaseService.SetMixWaveformAsync(releaseId, entryKey, ct); if (!linked.Success) { var error = linked.Messages.FirstOrDefault()?.Message ?? "Unknown error"; _logger.LogError( "Mix waveform stored in vault but Mix link failed. Entry: {EntryKey}. Release: {ReleaseId}. Error: {Error}", entryKey, releaseId, error); return linked; } return Result.CreatePassResult(); } }