147 lines
6.2 KiB
C#
147 lines
6.2 KiB
C#
using DeepDrftContent;
|
|
using DeepDrftContent.Constants;
|
|
using DeepDrftContent.Processors;
|
|
using DeepDrftData;
|
|
using DeepDrftModels.DTOs;
|
|
using DeepDrftModels.Enums;
|
|
using NetBlocks.Models;
|
|
using FileDb = DeepDrftContent.FileDatabase.Services.FileDatabase;
|
|
|
|
namespace DeepDrftAPI.Services;
|
|
|
|
/// <summary>
|
|
/// Host-internal orchestrator that makes DeepDrftAPI the single authority over both the
|
|
/// vault (FileDatabase) and SQL metadata (DeepDrftData). Owns the two-database write/delete
|
|
/// flow so the controller stays a thin HTTP boundary and no caller coordinates the two stores.
|
|
/// </summary>
|
|
public class UnifiedTrackService
|
|
{
|
|
internal const string TrackNotFoundMessage = "Track not found.";
|
|
private readonly TrackContentService _contentTrackContentService;
|
|
private readonly ITrackService _sqlTrackService;
|
|
private readonly FileDb _fileDatabase;
|
|
private readonly WaveformProfileService _waveformProfileService;
|
|
private readonly ILogger<UnifiedTrackService> _logger;
|
|
|
|
public UnifiedTrackService(
|
|
TrackContentService contentTrackContentService,
|
|
ITrackService sqlTrackService,
|
|
FileDb fileDatabase,
|
|
WaveformProfileService waveformProfileService,
|
|
ILogger<UnifiedTrackService> logger)
|
|
{
|
|
_contentTrackContentService = contentTrackContentService;
|
|
_sqlTrackService = sqlTrackService;
|
|
_fileDatabase = fileDatabase;
|
|
_waveformProfileService = waveformProfileService;
|
|
_logger = logger;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Process a WAV into the vault, then persist its metadata to SQL. On success the returned
|
|
/// DTO carries the SQL-assigned Id. If the vault write succeeds but the SQL persist fails,
|
|
/// the audio is orphaned under EntryKey — logged loudly so it is recoverable manually.
|
|
/// </summary>
|
|
public async Task<ResultContainer<TrackDto>> UploadAsync(
|
|
string tempFilePath,
|
|
string trackName,
|
|
string artist,
|
|
string? album,
|
|
string? genre,
|
|
DateOnly? releaseDate,
|
|
long createdByUserId,
|
|
string? originalFileName,
|
|
ReleaseType releaseType,
|
|
int trackNumber,
|
|
CancellationToken ct)
|
|
{
|
|
var unpersisted = await _contentTrackContentService.AddTrackFromWavAsync(
|
|
tempFilePath, trackName, artist, album, genre, releaseDate, originalFileName: originalFileName);
|
|
|
|
if (unpersisted is null)
|
|
{
|
|
_logger.LogWarning("UploadAsync: content TrackContentService returned null for {TrackName}", trackName);
|
|
return ResultContainer<TrackDto>.CreateFailResult("Failed to process and store WAV.");
|
|
}
|
|
|
|
unpersisted.CreatedByUserId = createdByUserId;
|
|
unpersisted.ReleaseType = releaseType;
|
|
unpersisted.TrackNumber = trackNumber;
|
|
|
|
var saveResult = await _sqlTrackService.Create(TrackConverter.Convert(unpersisted));
|
|
if (!saveResult.Success || saveResult.Value is null)
|
|
{
|
|
// Vault write succeeded, SQL persist failed — audio is orphaned in the tracks vault
|
|
// under EntryKey. Log loudly (include EntryKey) so it is recoverable manually.
|
|
var error = saveResult.Messages.FirstOrDefault()?.Message ?? "Unknown error";
|
|
_logger.LogError(
|
|
"Track persisted to vault but SQL save failed. Orphaned entry: {EntryKey}. Error: {Error}",
|
|
unpersisted.EntryKey, error);
|
|
return ResultContainer<TrackDto>.CreateFailResult($"Track was uploaded but could not be saved: {error}");
|
|
}
|
|
|
|
// Best-effort waveform profile: both stores succeeded, so the upload is a success
|
|
// regardless of the profile outcome. A missing profile renders as a flat seekbar on the
|
|
// frontend, so a failure here is logged and swallowed — never fails the upload.
|
|
await TryStoreWaveformProfileAsync(tempFilePath, unpersisted.EntryKey, ct);
|
|
|
|
return saveResult;
|
|
}
|
|
|
|
private async Task TryStoreWaveformProfileAsync(string tempFilePath, string entryKey, CancellationToken ct)
|
|
{
|
|
try
|
|
{
|
|
var wavBytes = await File.ReadAllBytesAsync(tempFilePath, ct);
|
|
await _waveformProfileService.ComputeAndStoreAsync(wavBytes, entryKey);
|
|
}
|
|
catch (Exception ex) when (ex is not OperationCanceledException)
|
|
{
|
|
_logger.LogError(ex, "Waveform profile step failed for {EntryKey}; upload unaffected.", entryKey);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Delete a track's SQL row, then its vault entry. SQL is the source of truth: a SQL delete
|
|
/// failure fails the operation (and leaves the vault untouched), but a subsequent vault delete
|
|
/// failure is logged as an orphan and swallowed — it is a maintenance concern, not user-facing.
|
|
/// </summary>
|
|
public async Task<Result> DeleteAsync(long id, CancellationToken ct)
|
|
{
|
|
var lookup = await _sqlTrackService.GetById(id);
|
|
if (!lookup.Success)
|
|
{
|
|
var error = lookup.Messages.FirstOrDefault()?.Message ?? "unknown error";
|
|
_logger.LogError("DeleteAsync: GetById failed for track {TrackId}: {Error}", id, error);
|
|
return Result.CreateFailResult("Failed to load track.");
|
|
}
|
|
|
|
if (lookup.Value is null)
|
|
{
|
|
return Result.CreateFailResult(TrackNotFoundMessage);
|
|
}
|
|
|
|
var entryKey = lookup.Value.EntryKey;
|
|
|
|
var sqlDelete = await _sqlTrackService.Delete(id);
|
|
if (!sqlDelete.Success)
|
|
{
|
|
var error = sqlDelete.Messages.FirstOrDefault()?.Message ?? "unknown error";
|
|
_logger.LogError("DeleteAsync: SQL delete failed for track {TrackId}: {Error}", id, error);
|
|
return Result.CreateFailResult("Failed to delete track.");
|
|
}
|
|
|
|
// Tri-state per FileDatabase's error-swallow contract: null = vault missing/error,
|
|
// false = entry not present, true = removed. Anything but a clean removal is an orphan.
|
|
var removed = await _fileDatabase.RemoveResourceAsync(VaultConstants.Tracks, entryKey);
|
|
if (removed is not true)
|
|
{
|
|
_logger.LogWarning(
|
|
"Vault delete did not remove entry after SQL delete. {TrackId} {EntryKey} outcome={Outcome}",
|
|
id, entryKey, removed);
|
|
}
|
|
|
|
return Result.CreatePassResult();
|
|
}
|
|
}
|