16784b37f2
Swap a track's audio by EntryKey (metadata/release/position preserved, waveform regenerated); hide per-track remove on a release's sole persisted track so it can only be replaced or release-deleted.
180 lines
7.4 KiB
C#
180 lines
7.4 KiB
C#
using DeepDrftContent.Constants;
|
|
using DeepDrftContent.FileDatabase.Services;
|
|
using DeepDrftContent.FileDatabase.Models;
|
|
using DeepDrftContent.Processors;
|
|
using DeepDrftModels.Entities;
|
|
|
|
namespace DeepDrftContent;
|
|
|
|
/// <summary>
|
|
/// Service for managing tracks in both SQL and FileDatabase
|
|
/// </summary>
|
|
public class TrackContentService
|
|
{
|
|
private readonly FileDatabase.Services.FileDatabase _fileDatabase;
|
|
private readonly AudioProcessorRouter _audioProcessorRouter;
|
|
|
|
public TrackContentService(FileDatabase.Services.FileDatabase fileDatabase, AudioProcessorRouter audioProcessorRouter)
|
|
{
|
|
_fileDatabase = fileDatabase;
|
|
_audioProcessorRouter = audioProcessorRouter;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adds a new track from a supported audio file (.wav, .mp3, .flac) to both databases. The
|
|
/// router selects the processor by extension; original bytes are stored for mp3/flac (no
|
|
/// transcoding), while EXTENSIBLE WAVs are normalized to standard PCM at storage time.
|
|
/// </summary>
|
|
/// <param name="audioFilePath">Path to the audio file</param>
|
|
/// <param name="trackName">Name of the track</param>
|
|
/// <param name="artist">Artist name</param>
|
|
/// <param name="album">Optional album name</param>
|
|
/// <param name="genre">Optional genre</param>
|
|
/// <param name="releaseDate">Optional release date</param>
|
|
/// <param name="originalFileName">Optional original browser filename captured at upload time</param>
|
|
/// <returns>The track entity with generated ID and media path</returns>
|
|
public async Task<TrackEntity?> AddTrackAsync(
|
|
string audioFilePath,
|
|
string trackName,
|
|
string artist,
|
|
string? album = null,
|
|
string? genre = null,
|
|
DateOnly? releaseDate = null,
|
|
string? originalFileName = null)
|
|
{
|
|
try
|
|
{
|
|
// Process the audio file (routed by extension)
|
|
var audioBinary = await _audioProcessorRouter.ProcessAudioFileAsync(audioFilePath);
|
|
if (audioBinary == null)
|
|
{
|
|
throw new InvalidOperationException("Failed to process audio file");
|
|
}
|
|
|
|
// Generate a unique track ID
|
|
var trackId = Guid.NewGuid().ToString();
|
|
|
|
// Ensure tracks vault exists
|
|
if (!_fileDatabase.HasVault(VaultConstants.Tracks))
|
|
{
|
|
await _fileDatabase.CreateVaultAsync(VaultConstants.Tracks, MediaVaultType.Audio);
|
|
}
|
|
|
|
// Store the audio in FileDatabase
|
|
var success = await _fileDatabase.RegisterResourceAsync(VaultConstants.Tracks, trackId, audioBinary);
|
|
if (!success)
|
|
{
|
|
throw new InvalidOperationException("Failed to store audio in FileDatabase");
|
|
}
|
|
|
|
// Create the track entity for SQL database. Post Phase 8 §8.0 the entity holds only
|
|
// track-cardinal fields; release-cardinal data (artist/album/genre/releaseDate) is
|
|
// resolved into a ReleaseEntity by the caller (UnifiedTrackService) and linked via FK.
|
|
var trackEntity = new TrackEntity
|
|
{
|
|
EntryKey = trackId, // FileDatabase entry ID
|
|
TrackName = trackName,
|
|
OriginalFileName = originalFileName
|
|
};
|
|
|
|
return trackEntity;
|
|
}
|
|
catch (Exception ex) when (ex is not OperationCanceledException)
|
|
{
|
|
Console.WriteLine($"TrackContentService.AddTrackAsync failed: {ex.Message}");
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Backward-compatible shim — delegates to <see cref="AddTrackAsync"/>. The router accepts WAV
|
|
/// alongside MP3 and FLAC, so this carries no WAV-specific logic of its own.
|
|
/// </summary>
|
|
public Task<TrackEntity?> AddTrackFromWavAsync(
|
|
string wavFilePath,
|
|
string trackName,
|
|
string artist,
|
|
string? album = null,
|
|
string? genre = null,
|
|
DateOnly? releaseDate = null,
|
|
string? originalFileName = null) =>
|
|
AddTrackAsync(wavFilePath, trackName, artist, album, genre, releaseDate, originalFileName);
|
|
|
|
/// <summary>
|
|
/// Swaps the audio bytes for an existing track in place: processes a new audio file and
|
|
/// re-registers it under the SAME <paramref name="entryKey"/> in the tracks vault. The track's
|
|
/// vault key — and therefore its SQL link, release membership, position, and metadata — is
|
|
/// untouched; only the binary changes. The prior entry is removed first so a replacement whose
|
|
/// extension differs from the original (e.g. .wav → .flac) does not strand the old file on disk
|
|
/// under its former filename. Returns the freshly stored <see cref="AudioBinary"/> on success
|
|
/// (so the caller can regenerate waveform data from the same bytes), or null on processing or
|
|
/// vault failure — matching the FileDatabase swallow-and-return-null contract.
|
|
/// </summary>
|
|
public async Task<AudioBinary?> ReplaceTrackAudioAsync(string entryKey, string audioFilePath)
|
|
{
|
|
try
|
|
{
|
|
var audioBinary = await _audioProcessorRouter.ProcessAudioFileAsync(audioFilePath);
|
|
if (audioBinary == null)
|
|
{
|
|
Console.WriteLine($"TrackContentService.ReplaceTrackAudioAsync: processing returned null for {entryKey}");
|
|
return null;
|
|
}
|
|
|
|
if (!_fileDatabase.HasVault(VaultConstants.Tracks))
|
|
{
|
|
await _fileDatabase.CreateVaultAsync(VaultConstants.Tracks, MediaVaultType.Audio);
|
|
}
|
|
|
|
// Drop the old entry first. The backing file is keyed by entryKey + its *stored*
|
|
// extension, so a register alone would leave a stale file when the new format differs.
|
|
// A null/false removal is non-fatal (the entry may already be absent); the register
|
|
// below is the authoritative write.
|
|
await _fileDatabase.RemoveResourceAsync(VaultConstants.Tracks, entryKey);
|
|
|
|
var success = await _fileDatabase.RegisterResourceAsync(VaultConstants.Tracks, entryKey, audioBinary);
|
|
if (!success)
|
|
{
|
|
Console.WriteLine($"TrackContentService.ReplaceTrackAudioAsync: vault write failed for {entryKey}");
|
|
return null;
|
|
}
|
|
|
|
return audioBinary;
|
|
}
|
|
catch (Exception ex) when (ex is not OperationCanceledException)
|
|
{
|
|
Console.WriteLine($"TrackContentService.ReplaceTrackAudioAsync failed: {ex.Message}");
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Retrieves audio binary from FileDatabase
|
|
/// </summary>
|
|
/// <param name="trackId">Track ID (EntryKey)</param>
|
|
/// <returns>Audio binary or null if not found</returns>
|
|
public async Task<AudioBinary?> GetAudioBinaryAsync(string trackId)
|
|
{
|
|
return await _fileDatabase.LoadResourceAsync<AudioBinary>(VaultConstants.Tracks, trackId);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Checks if FileDatabase is available and tracks vault exists
|
|
/// </summary>
|
|
public bool IsFileDatabaseReady()
|
|
{
|
|
return _fileDatabase.HasVault(VaultConstants.Tracks);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Initializes the tracks vault if it doesn't exist
|
|
/// </summary>
|
|
public async Task InitializeTracksVaultAsync()
|
|
{
|
|
if (!_fileDatabase.HasVault(VaultConstants.Tracks))
|
|
{
|
|
await _fileDatabase.CreateVaultAsync(VaultConstants.Tracks, MediaVaultType.Audio);
|
|
}
|
|
}
|
|
}
|