fix: exclude live tracks under soft-deleted releases from home stats cut/mix figures
This commit is contained in:
@@ -159,16 +159,17 @@ public class TrackRepository : Repository<DeepDrftContext, TrackEntity>
|
|||||||
.ToDictionaryAsync(x => x.ReleaseId, x => x.Count, ct);
|
.ToDictionaryAsync(x => x.ReleaseId, x => x.Count, ct);
|
||||||
|
|
||||||
// Aggregate figures for the public home hero stat row, assembled in as few round-trips as is clean.
|
// Aggregate figures for the public home hero stat row, assembled in as few round-trips as is clean.
|
||||||
// All counts go through Query / the release set's !IsDeleted filter so soft-deleted rows never count.
|
// All counts go through Query (!t.IsDeleted) plus an explicit !t.Release.IsDeleted guard so tracks
|
||||||
// Mix runtime sums DurationSeconds with a null-coalesce to 0 so not-yet-backfilled rows contribute
|
// under a directly-deleted release are also excluded. Mix runtime sums DurationSeconds with a
|
||||||
// zero rather than throwing or skewing the total. The cut release-type breakdown is grouped here so
|
// null-coalesce to 0 so not-yet-backfilled rows contribute zero rather than throwing or skewing the
|
||||||
// a zero-count type is simply absent from the result (no present-with-zero row).
|
// total. The cut release-type breakdown is grouped here so a zero-count type is simply absent from
|
||||||
|
// the result (no present-with-zero row).
|
||||||
public async Task<HomeStatsDto> GetHomeStatsAsync(CancellationToken ct = default)
|
public async Task<HomeStatsDto> GetHomeStatsAsync(CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
var releases = _context.Set<ReleaseEntity>().Where(r => !r.IsDeleted);
|
var releases = _context.Set<ReleaseEntity>().Where(r => !r.IsDeleted);
|
||||||
|
|
||||||
var cutTrackCount = await Query
|
var cutTrackCount = await Query
|
||||||
.CountAsync(t => t.Release != null && t.Release.Medium == ReleaseMedium.Cut, ct);
|
.CountAsync(t => t.Release != null && !t.Release.IsDeleted && t.Release.Medium == ReleaseMedium.Cut, ct);
|
||||||
|
|
||||||
var cutReleaseTypeCounts = await releases
|
var cutReleaseTypeCounts = await releases
|
||||||
.Where(r => r.Medium == ReleaseMedium.Cut)
|
.Where(r => r.Medium == ReleaseMedium.Cut)
|
||||||
@@ -180,7 +181,7 @@ public class TrackRepository : Repository<DeepDrftContext, TrackEntity>
|
|||||||
.CountAsync(r => r.Medium == ReleaseMedium.Mix, ct);
|
.CountAsync(r => r.Medium == ReleaseMedium.Mix, ct);
|
||||||
|
|
||||||
var mixRuntimeSeconds = await Query
|
var mixRuntimeSeconds = await Query
|
||||||
.Where(t => t.Release != null && t.Release.Medium == ReleaseMedium.Mix)
|
.Where(t => t.Release != null && !t.Release.IsDeleted && t.Release.Medium == ReleaseMedium.Mix)
|
||||||
.SumAsync(t => t.DurationSeconds ?? 0d, ct);
|
.SumAsync(t => t.DurationSeconds ?? 0d, ct);
|
||||||
|
|
||||||
return new HomeStatsDto
|
return new HomeStatsDto
|
||||||
|
|||||||
@@ -149,4 +149,34 @@ public class HomeStatsQueryTests
|
|||||||
Assert.That(stats.CutReleaseTypeCounts.Single(c => c.ReleaseType == ReleaseType.Album).Count, Is.EqualTo(1));
|
Assert.That(stats.CutReleaseTypeCounts.Single(c => c.ReleaseType == ReleaseType.Album).Count, Is.EqualTo(1));
|
||||||
Assert.That(stats.MixRuntimeSeconds, Is.EqualTo(0d), "the only mix track is soft-deleted");
|
Assert.That(stats.MixRuntimeSeconds, Is.EqualTo(0d), "the only mix track is soft-deleted");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A live track under a directly-deleted release must be excluded from the track-based figures.
|
||||||
|
// SoftDeleteReleaseAsync does not cascade to child tracks, so without the !t.Release.IsDeleted
|
||||||
|
// guard the track-count and runtime figures are internally inconsistent with the release-level ones.
|
||||||
|
[Test]
|
||||||
|
public async Task GetHomeStatsAsync_ExcludesLiveTracksUnderSoftDeletedRelease()
|
||||||
|
{
|
||||||
|
var liveCut = Release("Live Cut", ReleaseMedium.Cut, ReleaseType.Album);
|
||||||
|
var deletedCut = Release("Dead Cut", ReleaseMedium.Cut, ReleaseType.Album);
|
||||||
|
deletedCut.IsDeleted = true;
|
||||||
|
var deletedMix = Release("Dead Mix", ReleaseMedium.Mix);
|
||||||
|
deletedMix.IsDeleted = true;
|
||||||
|
|
||||||
|
// Both tracks are themselves live — only their parent release is soft-deleted.
|
||||||
|
var liveTrackUnderDeletedCut = Track(deletedCut);
|
||||||
|
var liveTrackUnderDeletedMix = Track(deletedMix, duration: 900d);
|
||||||
|
|
||||||
|
await SeedAsync(
|
||||||
|
new[] { liveCut, deletedCut, deletedMix },
|
||||||
|
new[] { Track(liveCut), liveTrackUnderDeletedCut, liveTrackUnderDeletedMix });
|
||||||
|
|
||||||
|
var stats = await CreateRepository().GetHomeStatsAsync();
|
||||||
|
|
||||||
|
Assert.That(stats.CutTrackCount, Is.EqualTo(1),
|
||||||
|
"live track under deleted Cut release must not inflate CutTrackCount");
|
||||||
|
Assert.That(stats.MixReleaseCount, Is.EqualTo(0),
|
||||||
|
"deleted Mix release must not count");
|
||||||
|
Assert.That(stats.MixRuntimeSeconds, Is.EqualTo(0d),
|
||||||
|
"live track under deleted Mix release must not inflate MixRuntimeSeconds");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user