feat: normalize release-cardinal fields out of track into a Release entity (Phase 8 §8.0)
This commit is contained in:
@@ -77,12 +77,13 @@ public class TrackController : ControllerBase
|
||||
}
|
||||
|
||||
// GET api/track/albums (unauthenticated)
|
||||
// Distinct non-null albums with track counts and cover keys. Public browse data, same posture as
|
||||
// GET api/track/page. Literal segment, declared before the parameterized "{trackId}" route.
|
||||
// All releases with per-release track counts. Public browse data, same posture as GET
|
||||
// api/track/page. Literal segment, declared before the parameterized "{trackId}" route.
|
||||
// Route name kept as "albums" for client/proxy compatibility; the payload is List<ReleaseDto>.
|
||||
[HttpGet("albums")]
|
||||
public async Task<ActionResult> GetAlbums(CancellationToken ct = default)
|
||||
{
|
||||
var result = await _sqlTrackService.GetDistinctAlbums(ct);
|
||||
var result = await _sqlTrackService.GetReleases(ct);
|
||||
if (!result.Success || result.Value is null)
|
||||
{
|
||||
var error = result.Messages.FirstOrDefault()?.Message ?? "Unknown error";
|
||||
@@ -367,23 +368,33 @@ public class TrackController : ControllerBase
|
||||
return BadRequest("trackNumber must be a positive integer when provided.");
|
||||
|
||||
var track = lookup.Value;
|
||||
|
||||
// Track-cardinal fields update the track row directly.
|
||||
track.TrackName = request.TrackName;
|
||||
track.Artist = request.Artist;
|
||||
track.Album = request.Album;
|
||||
track.Genre = request.Genre;
|
||||
track.ReleaseDate = request.ReleaseDate;
|
||||
|
||||
// Only update ImagePath when the request explicitly provides a value (null = no change, "" = clear).
|
||||
if (request.ImagePath is not null)
|
||||
track.ImagePath = string.IsNullOrEmpty(request.ImagePath) ? null : request.ImagePath;
|
||||
|
||||
// ReleaseType / TrackNumber are non-null on the entity; null in the request means "no change".
|
||||
if (request.ReleaseType is not null)
|
||||
track.ReleaseType = request.ReleaseType.Value;
|
||||
|
||||
if (request.TrackNumber is > 0)
|
||||
track.TrackNumber = request.TrackNumber.Value;
|
||||
|
||||
// Release-cardinal fields update the linked release (handled in TrackManager.Update, which
|
||||
// persists track.Release when the track carries a resolved ReleaseId). The loaded track has
|
||||
// its Release populated via the Include; mutate it in place so the edited values flow through.
|
||||
// A loose track (no release) cannot take release-cardinal edits — there is no release row to
|
||||
// write to — so these fields are simply not persisted in that case.
|
||||
if (track.Release is { } release)
|
||||
{
|
||||
release.Artist = request.Artist;
|
||||
release.Title = request.Album ?? string.Empty;
|
||||
release.Genre = request.Genre;
|
||||
release.ReleaseDate = request.ReleaseDate;
|
||||
|
||||
// ImagePath is tri-state: null = no change, "" = clear, value = set.
|
||||
if (request.ImagePath is not null)
|
||||
release.ImagePath = string.IsNullOrEmpty(request.ImagePath) ? null : request.ImagePath;
|
||||
|
||||
// ReleaseType is non-null on the release; null in the request means "no change".
|
||||
if (request.ReleaseType is not null)
|
||||
release.ReleaseType = request.ReleaseType.Value;
|
||||
}
|
||||
|
||||
var update = await _sqlTrackService.Update(track);
|
||||
if (!update.Success)
|
||||
{
|
||||
|
||||
@@ -65,11 +65,43 @@ public class UnifiedTrackService
|
||||
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));
|
||||
// Resolve the release FK before persisting the track. An upload with an album lands on the
|
||||
// shared release (created on first sighting); an upload without one stays a loose track with
|
||||
// a null ReleaseId. Release-cardinal metadata (artist/genre/releaseDate/type/uploader) rides
|
||||
// on the release, not the track.
|
||||
long? releaseId = null;
|
||||
if (!string.IsNullOrWhiteSpace(album))
|
||||
{
|
||||
var releaseData = new ReleaseDto
|
||||
{
|
||||
Title = album,
|
||||
Artist = artist,
|
||||
Genre = genre,
|
||||
ReleaseDate = releaseDate,
|
||||
ReleaseType = releaseType,
|
||||
CreatedByUserId = createdByUserId,
|
||||
};
|
||||
|
||||
var releaseResult = await _sqlTrackService.FindOrCreateRelease(album, artist, releaseData, ct);
|
||||
if (!releaseResult.Success || releaseResult.Value is null)
|
||||
{
|
||||
var error = releaseResult.Messages.FirstOrDefault()?.Message ?? "Unknown error";
|
||||
_logger.LogError(
|
||||
"Track persisted to vault but release resolution failed. Orphaned entry: {EntryKey}. Error: {Error}",
|
||||
unpersisted.EntryKey, error);
|
||||
return ResultContainer<TrackDto>.CreateFailResult($"Track was uploaded but could not be saved: {error}");
|
||||
}
|
||||
|
||||
releaseId = releaseResult.Value.Id;
|
||||
}
|
||||
|
||||
var trackDto = TrackConverter.Convert(unpersisted);
|
||||
trackDto.ReleaseId = releaseId;
|
||||
trackDto.Release = null; // FK already resolved; Create must not re-resolve a detached graph.
|
||||
|
||||
var saveResult = await _sqlTrackService.Create(trackDto);
|
||||
if (!saveResult.Success || saveResult.Value is null)
|
||||
{
|
||||
// Vault write succeeded, SQL persist failed — audio is orphaned in the tracks vault
|
||||
|
||||
Reference in New Issue
Block a user