fix: replace-audio duration write now unconditional via SetDuration
UpdateDuration's null guard matched zero rows for tracks that already had a duration (all normally-uploaded tracks). Add SetDurationAsync/SetDuration/ITrackService.SetDuration with no null guard; fail on zero rows. ReplaceAudioAsync now calls SetDuration.
This commit is contained in:
@@ -217,7 +217,7 @@ public class UnifiedTrackService
|
|||||||
// authoritative metadata update for the replace. A failure here is surfaced (unlike the
|
// authoritative metadata update for the replace. A failure here is surfaced (unlike the
|
||||||
// best-effort waveform regen above) because a stale DurationSeconds silently corrupts
|
// best-effort waveform regen above) because a stale DurationSeconds silently corrupts
|
||||||
// derived aggregates (e.g. MixRuntimeSeconds on the home stats endpoint).
|
// derived aggregates (e.g. MixRuntimeSeconds on the home stats endpoint).
|
||||||
var durationWrite = await _sqlTrackService.UpdateDuration(trackId, newAudio.Duration, ct);
|
var durationWrite = await _sqlTrackService.SetDuration(trackId, newAudio.Duration, ct);
|
||||||
if (!durationWrite.Success)
|
if (!durationWrite.Success)
|
||||||
{
|
{
|
||||||
var error = durationWrite.Messages.FirstOrDefault()?.Message ?? "Unknown error";
|
var error = durationWrite.Messages.FirstOrDefault()?.Message ?? "Unknown error";
|
||||||
|
|||||||
@@ -47,6 +47,14 @@ public interface ITrackService
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
Task<ResultContainer<int>> UpdateDuration(long id, double durationSeconds, CancellationToken cancellationToken = default);
|
Task<ResultContainer<int>> UpdateDuration(long id, double durationSeconds, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Unconditionally overwrite the SQL duration for one track. Unlike <see cref="UpdateDuration"/>,
|
||||||
|
/// this carries no null guard — it is for the replace-audio path where the track already has a
|
||||||
|
/// non-null duration that must be overwritten with the new audio's value. Returns a fail Result
|
||||||
|
/// when zero rows are affected (track removed between lookup and write).
|
||||||
|
/// </summary>
|
||||||
|
Task<ResultContainer<int>> SetDuration(long id, double durationSeconds, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Resolve the release matching <paramref name="title"/> + <paramref name="artist"/>, creating
|
/// Resolve the release matching <paramref name="title"/> + <paramref name="artist"/>, creating
|
||||||
/// one from <paramref name="releaseData"/> when none exists. Backs the upload flow's FK
|
/// one from <paramref name="releaseData"/> when none exists. Backs the upload flow's FK
|
||||||
|
|||||||
@@ -210,6 +210,18 @@ public class TrackRepository : Repository<DeepDrftContext, TrackEntity>
|
|||||||
.SetProperty(t => t.DurationSeconds, durationSeconds)
|
.SetProperty(t => t.DurationSeconds, durationSeconds)
|
||||||
.SetProperty(t => t.UpdatedAt, DateTime.UtcNow), ct);
|
.SetProperty(t => t.UpdatedAt, DateTime.UtcNow), ct);
|
||||||
|
|
||||||
|
// Unconditional duration overwrite for one track (no load round-trip), used by the replace-audio
|
||||||
|
// path. Unlike UpdateDurationAsync, there is no null guard — replace always overwrites the
|
||||||
|
// existing value because a normally-uploaded track already has a non-null DurationSeconds and the
|
||||||
|
// null-guarded backfill query would match zero rows and silently leave it stale. Returns the count
|
||||||
|
// of rows affected; zero means the track was removed between the GetById lookup and this write.
|
||||||
|
public async Task<int> SetDurationAsync(long id, double durationSeconds, CancellationToken ct = default)
|
||||||
|
=> await Query
|
||||||
|
.Where(t => t.Id == id)
|
||||||
|
.ExecuteUpdateAsync(s => s
|
||||||
|
.SetProperty(t => t.DurationSeconds, durationSeconds)
|
||||||
|
.SetProperty(t => t.UpdatedAt, DateTime.UtcNow), ct);
|
||||||
|
|
||||||
// Resolve an existing release by its natural key (title + artist). Returns null when no match,
|
// Resolve an existing release by its natural key (title + artist). Returns null when no match,
|
||||||
// signalling the manager to create one. Soft-deleted releases never match.
|
// signalling the manager to create one. Soft-deleted releases never match.
|
||||||
public async Task<ReleaseEntity?> GetReleaseByTitleAndArtistAsync(
|
public async Task<ReleaseEntity?> GetReleaseByTitleAndArtistAsync(
|
||||||
|
|||||||
@@ -276,6 +276,21 @@ public class TrackManager
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<ResultContainer<int>> SetDuration(long id, double durationSeconds, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var affected = await Repository.SetDurationAsync(id, durationSeconds, cancellationToken);
|
||||||
|
if (affected == 0)
|
||||||
|
return ResultContainer<int>.CreateFailResult($"Duration write matched no rows for track {id}.");
|
||||||
|
return ResultContainer<int>.CreatePassResult(affected);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
return ResultContainer<int>.CreateFailResult(e.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<ResultContainer<TrackDto>> Create(TrackDto newTrack)
|
public async Task<ResultContainer<TrackDto>> Create(TrackDto newTrack)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
|||||||
Reference in New Issue
Block a user