Wire NowPlayingStats to live aggregates: add SQL track duration column, stats endpoint, and duration backfill
This commit is contained in:
@@ -193,6 +193,54 @@ public class UnifiedTrackService
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// One-time backfill: for every non-deleted track whose SQL duration is still null, read the
|
||||
/// processor-extracted runtime from the vault audio (by EntryKey) and write it to SQL. The migration
|
||||
/// cannot read the vault, so this runs at runtime after deploy. Idempotent — a re-run only touches
|
||||
/// rows still missing a duration. Returns (updated, skipped) counts. A per-track vault miss or SQL
|
||||
/// failure is logged and skipped, never aborting the batch.
|
||||
/// </summary>
|
||||
public async Task<ResultContainer<(int Updated, int Skipped)>> BackfillDurationsAsync(CancellationToken ct)
|
||||
{
|
||||
var missing = await _sqlTrackService.GetTracksMissingDuration(ct);
|
||||
if (!missing.Success || missing.Value is null)
|
||||
{
|
||||
var error = missing.Messages.FirstOrDefault()?.Message ?? "Unknown error";
|
||||
_logger.LogError("BackfillDurationsAsync: failed to load tracks missing duration: {Error}", error);
|
||||
return ResultContainer<(int, int)>.CreateFailResult($"Could not load tracks: {error}");
|
||||
}
|
||||
|
||||
var updated = 0;
|
||||
var skipped = 0;
|
||||
foreach (var track in missing.Value)
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
|
||||
var audio = await _contentTrackContentService.GetAudioBinaryAsync(track.EntryKey);
|
||||
if (audio is null)
|
||||
{
|
||||
_logger.LogWarning("BackfillDurationsAsync: no vault audio for {EntryKey} (track {Id}); skipping.",
|
||||
track.EntryKey, track.Id);
|
||||
skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
var write = await _sqlTrackService.UpdateDuration(track.Id, audio.Duration, ct);
|
||||
if (!write.Success)
|
||||
{
|
||||
var error = write.Messages.FirstOrDefault()?.Message ?? "Unknown error";
|
||||
_logger.LogWarning("BackfillDurationsAsync: SQL update failed for track {Id}: {Error}", track.Id, error);
|
||||
skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
updated++;
|
||||
}
|
||||
|
||||
_logger.LogInformation("BackfillDurationsAsync complete: {Updated} updated, {Skipped} skipped.", updated, skipped);
|
||||
return ResultContainer<(int, int)>.CreatePassResult((updated, skipped));
|
||||
}
|
||||
|
||||
/// <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
|
||||
|
||||
Reference in New Issue
Block a user