refactor(backend): group track pipeline state into owned types
Track mixed catalog fields (Title, Artist credit, Duration) with picture-processing and audio-transcode pipeline state as ten flat columns plus two computed [NotMapped] flags, making the entity read like a media-pipeline worksheet. Introduce TrackPictures and TrackAudio as EF owned types mapped onto the same existing columns (explicit column names, no schema change - verified via `dotnet ef migrations has-pending-model-changes`). Track now reads as Title/Artist/Duration/Pictures/Audio instead of fourteen loose properties.
This commit is contained in:
@@ -51,19 +51,19 @@ namespace Musify.Application.PlayLists
|
||||
|
||||
var track = provisionResult.Value.Track;
|
||||
|
||||
var failed = track.AudioTranscodeProcessingStatus == ProcessingStatus.Failed;
|
||||
var neverQueued = !track.DownloadRequested && !track.IsAudioProcessed;
|
||||
var failed = track.Audio.TranscodeStatus == ProcessingStatus.Failed;
|
||||
var neverQueued = !track.Audio.DownloadRequested && !track.Audio.IsProcessed;
|
||||
if (failed || neverQueued)
|
||||
{
|
||||
if (failed)
|
||||
{
|
||||
track.AudioTranscodeProcessingStatus = ProcessingStatus.Pending;
|
||||
track.PicturesProcessingStatus = ProcessingStatus.Pending;
|
||||
track.Audio.TranscodeStatus = ProcessingStatus.Pending;
|
||||
track.Pictures.ProcessingStatus = ProcessingStatus.Pending;
|
||||
track.LifeCycleStatus = LifeCycleStatus.Active;
|
||||
track.RetryCount += 1;
|
||||
track.LastRetryAt = DateTime.UtcNow;
|
||||
track.Audio.RetryCount += 1;
|
||||
track.Audio.LastRetryAt = DateTime.UtcNow;
|
||||
}
|
||||
track.DownloadRequested = true;
|
||||
track.Audio.DownloadRequested = true;
|
||||
|
||||
var thumbnailUrl = await ResolveThumbnailUrlAsync(request.VideoId, provisionResult.Value.Song, cancellationToken);
|
||||
if (thumbnailUrl.IsError)
|
||||
|
||||
@@ -50,15 +50,19 @@ public sealed class YouTubeTrackProvisioner(
|
||||
NormalizedTitle = title.ToUpperInvariant(),
|
||||
Source = TrackSource.YouTube,
|
||||
ExternalId = videoId,
|
||||
DownloadRequested = false,
|
||||
Duration = song.DurationSeconds,
|
||||
OriginalPictureName = null,
|
||||
OriginalAudioName = null,
|
||||
SmallPictureName = trackConfiguration.Routes.PresetSmallPicture,
|
||||
MediumPictureName = trackConfiguration.Routes.PresetMediumPicture,
|
||||
LargePictureName = trackConfiguration.Routes.PresetLargePicture,
|
||||
PicturesProcessingStatus = ProcessingStatus.Pending,
|
||||
AudioTranscodeProcessingStatus = ProcessingStatus.Pending
|
||||
Pictures = new TrackPictures
|
||||
{
|
||||
SmallName = trackConfiguration.Routes.PresetSmallPicture,
|
||||
MediumName = trackConfiguration.Routes.PresetMediumPicture,
|
||||
LargeName = trackConfiguration.Routes.PresetLargePicture,
|
||||
ProcessingStatus = ProcessingStatus.Pending
|
||||
},
|
||||
Audio = new TrackAudio
|
||||
{
|
||||
DownloadRequested = false,
|
||||
TranscodeStatus = ProcessingStatus.Pending
|
||||
}
|
||||
};
|
||||
|
||||
await database.Tracks.AddAsync(track, cancellationToken);
|
||||
|
||||
@@ -66,13 +66,19 @@ namespace Musify.Application.Tracks
|
||||
Title = request.Title,
|
||||
NormalizedTitle = request.Title.ToUpperInvariant(),
|
||||
OwnerUserId = user.Id,
|
||||
OriginalPictureName = pictureIntent.ObjectName,
|
||||
OriginalAudioName = audioIntent.ObjectName,
|
||||
SmallPictureName = trackConfiguration.Routes.PresetSmallPicture,
|
||||
MediumPictureName = trackConfiguration.Routes.PresetMediumPicture,
|
||||
LargePictureName = trackConfiguration.Routes.PresetLargePicture,
|
||||
PicturesProcessingStatus = ProcessingStatus.Pending,
|
||||
AudioTranscodeProcessingStatus = ProcessingStatus.Pending
|
||||
Pictures = new TrackPictures
|
||||
{
|
||||
OriginalName = pictureIntent.ObjectName,
|
||||
SmallName = trackConfiguration.Routes.PresetSmallPicture,
|
||||
MediumName = trackConfiguration.Routes.PresetMediumPicture,
|
||||
LargeName = trackConfiguration.Routes.PresetLargePicture,
|
||||
ProcessingStatus = ProcessingStatus.Pending
|
||||
},
|
||||
Audio = new TrackAudio
|
||||
{
|
||||
OriginalName = audioIntent.ObjectName,
|
||||
TranscodeStatus = ProcessingStatus.Pending
|
||||
}
|
||||
};
|
||||
|
||||
await database.Tracks.AddAsync(trackEntity, cancellationToken);
|
||||
|
||||
@@ -34,8 +34,8 @@ namespace Musify.Application.Tracks
|
||||
return Error.Unauthorized();
|
||||
}
|
||||
|
||||
if (track.AudioTranscodeProcessingStatus == ProcessingStatus.Processing
|
||||
|| track.AudioTranscodeProcessingStatus == ProcessingStatus.Pending)
|
||||
if (track.Audio.TranscodeStatus == ProcessingStatus.Processing
|
||||
|| track.Audio.TranscodeStatus == ProcessingStatus.Pending)
|
||||
{
|
||||
logger.LogWarning("Track {TrackId} is currently being processed and cannot be deleted", request.TrackId);
|
||||
return Error.Conflict(description: "Track is currently being processed and cannot be deleted");
|
||||
|
||||
@@ -21,7 +21,7 @@ namespace Musify.Application.Tracks
|
||||
{
|
||||
var track = await database.Tracks.AsNoTracking()
|
||||
.Where(t => t.Id == request.TrackId)
|
||||
.Select(t => new { t.SmallPictureName, t.MediumPictureName, t.LargePictureName })
|
||||
.Select(t => new { t.Pictures.SmallName, t.Pictures.MediumName, t.Pictures.LargeName })
|
||||
.SingleOrDefaultAsync(cancellationToken);
|
||||
|
||||
if (track == null)
|
||||
@@ -30,9 +30,9 @@ namespace Musify.Application.Tracks
|
||||
var routes = trackConfiguration.Routes;
|
||||
var (name, key) = request.Size.ToLowerInvariant() switch
|
||||
{
|
||||
"small" => (track.SmallPictureName, BuildKey(routes.BuildSmallPicturePath, track.SmallPictureName)),
|
||||
"large" => (track.LargePictureName, BuildKey(routes.BuildLargePicturePath, track.LargePictureName)),
|
||||
_ => (track.MediumPictureName, BuildKey(routes.BuildMediumPicturePath, track.MediumPictureName))
|
||||
"small" => (track.SmallName, BuildKey(routes.BuildSmallPicturePath, track.SmallName)),
|
||||
"large" => (track.LargeName, BuildKey(routes.BuildLargePicturePath, track.LargeName)),
|
||||
_ => (track.MediumName, BuildKey(routes.BuildMediumPicturePath, track.MediumName))
|
||||
};
|
||||
|
||||
if (string.IsNullOrEmpty(name) || key == null)
|
||||
|
||||
@@ -22,20 +22,20 @@ namespace Musify.Application.Tracks
|
||||
{
|
||||
var track = await database.Tracks.AsNoTracking()
|
||||
.Where(t => t.Id == request.TrackId)
|
||||
.Select(t => new { t.Id, t.AudioTranscodeProcessingStatus, t.AudioFolderName } )
|
||||
.Select(t => new { t.Id, t.Audio.TranscodeStatus, t.Audio.FolderName } )
|
||||
.SingleOrDefaultAsync(cancellationToken);
|
||||
|
||||
if (track == null)
|
||||
return Error.NotFound();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(track.AudioFolderName)
|
||||
|| track.AudioTranscodeProcessingStatus != ProcessingStatus.Completed)
|
||||
if (string.IsNullOrWhiteSpace(track.FolderName)
|
||||
|| track.TranscodeStatus != ProcessingStatus.Completed)
|
||||
{
|
||||
logger.LogInformation("Stream requested for track {TrackId} but audio is not ready", request.TrackId);
|
||||
return Error.Conflict(description: "Track audio is not available for streaming yet.");
|
||||
}
|
||||
|
||||
var response = await streamIssuer.IssueAsync(track.Id, track.AudioFolderName, request.UserId, cancellationToken);
|
||||
var response = await streamIssuer.IssueAsync(track.Id, track.FolderName, request.UserId, cancellationToken);
|
||||
|
||||
logger.LogInformation("Issued stream ticket for track {TrackId} to user {UserId}", request.TrackId, request.UserId);
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ namespace Musify.Application.Tracks.Responses
|
||||
FormatArtist(track),
|
||||
track.Source,
|
||||
track.ExternalId,
|
||||
track.AudioTranscodeProcessingStatus,
|
||||
track.Audio.TranscodeStatus,
|
||||
track.Duration,
|
||||
listensCount,
|
||||
track.OwnerUserId,
|
||||
|
||||
@@ -24,9 +24,9 @@ namespace Musify.Application.YouTube
|
||||
{
|
||||
var track = await provisioner.FindAsync(request.VideoId, cancellationToken);
|
||||
|
||||
if (track is not null && track.IsAudioProcessed)
|
||||
if (track is not null && track.Audio.IsProcessed)
|
||||
{
|
||||
var serverStream = await streamIssuer.IssueAsync(track.Id, track.AudioFolderName, request.UserId, cancellationToken);
|
||||
var serverStream = await streamIssuer.IssueAsync(track.Id, track.Audio.FolderName, request.UserId, cancellationToken);
|
||||
return new YouTubeStreamResponse(
|
||||
YouTubeStreamMode.Server,
|
||||
serverStream.ManifestUrl,
|
||||
|
||||
Reference in New Issue
Block a user