Add track publication, album types, optional track pictures and batch upload URLs
Tracks
- Track.IsPublished (migration marks existing tracks as published). New
tracks are created unpublished.
- PUT /tracks/{id}/published and PUT /tracks/published change the flag;
publishing requires completed audio processing (the batch endpoint
reports skipped tracks).
- An unpublished track is only visible to its owner. A WherePublished /
VisibleTo filter is applied to search, genres, listening history, last
listened, likes, mixes, album and playlist contents and counts, recently
listened albums, public user tracks, track details and stream tickets.
- isPublished is part of the track responses, GET /tracks/users/{userId}
accepts a published filter and status-counts reports published and
unpublished totals.
- CreateTrackRequest.pictureIntentId is optional: without it the track
uses the album cover or the preset picture and no picture processing
runs. CreateTrackResourcesEvent carries nullable picture fields, so
queues must be drained before deploying.
- POST /tracks/upload-urls accepts an audios list and one optional picture
shared by all of them, and returns an intent per file. The upload intent
limits are raised so a batch fits.
Albums
- AlbumType (Album, EP, Single) on create, update and responses.
- CreateAlbumRequest.trackIds creates the album and links the tracks in a
single save.
This commit is contained in:
@@ -7,14 +7,17 @@ using Musify.Domain.ValueObjects;
|
||||
|
||||
namespace Musify.Application.Tracks
|
||||
{
|
||||
public record AudioUploadSpec(string FileType, string ContentType, long? ExpectedSizeBytes = null);
|
||||
|
||||
public record RequestTrackUploadUrlsCommand(
|
||||
long UserId,
|
||||
string PictureFileType,
|
||||
string PictureContentType,
|
||||
string AudioFileType,
|
||||
string AudioContentType,
|
||||
string? PictureFileType,
|
||||
string? PictureContentType,
|
||||
string? AudioFileType = null,
|
||||
string? AudioContentType = null,
|
||||
long? ExpectedPictureSizeBytes = null,
|
||||
long? ExpectedAudioSizeBytes = null)
|
||||
long? ExpectedAudioSizeBytes = null,
|
||||
IReadOnlyList<AudioUploadSpec>? Audios = null)
|
||||
: ICommand<ErrorOr<TrackUploadUrlsResponse>>;
|
||||
|
||||
public class RequestTrackUploadUrlsCommandHandler(
|
||||
@@ -24,35 +27,75 @@ namespace Musify.Application.Tracks
|
||||
UploadIntentConfiguration uploadIntentConfiguration)
|
||||
: ICommandHandler<RequestTrackUploadUrlsCommand, ErrorOr<TrackUploadUrlsResponse>>
|
||||
{
|
||||
private const int PictureKeyGroup = 1;
|
||||
|
||||
public async ValueTask<ErrorOr<TrackUploadUrlsResponse>> Handle(RequestTrackUploadUrlsCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var audios = ResolveAudios(request);
|
||||
if (audios.Count == 0)
|
||||
return Error.Validation(description: "At least one audio file is required.");
|
||||
|
||||
var hasPicture = request.PictureFileType != null && request.PictureContentType != null;
|
||||
var routes = trackConfiguration.Routes;
|
||||
var issued = await issuer.IssueAsync(
|
||||
request.UserId,
|
||||
[
|
||||
new UploadRequest(UploadIntentPurpose.TrackPicture, routes, request.PictureFileType, request.PictureContentType, request.ExpectedPictureSizeBytes),
|
||||
new UploadRequest(UploadIntentPurpose.TrackAudio, routes, request.AudioFileType, request.AudioContentType, request.ExpectedAudioSizeBytes)
|
||||
],
|
||||
cancellationToken);
|
||||
|
||||
var uploads = new List<UploadRequest>();
|
||||
if (hasPicture)
|
||||
{
|
||||
for (var index = 0; index < audios.Count; index++)
|
||||
{
|
||||
uploads.Add(new UploadRequest(
|
||||
UploadIntentPurpose.TrackPicture, routes, request.PictureFileType!, request.PictureContentType!,
|
||||
request.ExpectedPictureSizeBytes, PictureKeyGroup));
|
||||
}
|
||||
}
|
||||
|
||||
uploads.AddRange(audios.Select(audio =>
|
||||
new UploadRequest(UploadIntentPurpose.TrackAudio, routes, audio.FileType, audio.ContentType, audio.ExpectedSizeBytes)));
|
||||
|
||||
var issued = await issuer.IssueAsync(request.UserId, uploads, cancellationToken);
|
||||
if (issued.IsError)
|
||||
return issued.Errors;
|
||||
|
||||
var picture = issued.Value[0];
|
||||
var audio = issued.Value[1];
|
||||
var pictures = hasPicture ? issued.Value.Take(audios.Count).ToList() : [];
|
||||
var audioUploads = issued.Value.Skip(pictures.Count).ToList();
|
||||
|
||||
var items = audioUploads
|
||||
.Select((audio, index) => new TrackAudioUpload(
|
||||
audio.IntentId,
|
||||
hasPicture ? pictures[index].IntentId : null,
|
||||
audio.Key,
|
||||
audio.ObjectName,
|
||||
audio.ContentType,
|
||||
audio.UploadUrl))
|
||||
.ToList();
|
||||
|
||||
var firstPicture = pictures.Count > 0 ? pictures[0] : null;
|
||||
var firstAudio = audioUploads[0];
|
||||
|
||||
return new TrackUploadUrlsResponse(
|
||||
picture.IntentId,
|
||||
audio.IntentId,
|
||||
firstPicture?.IntentId,
|
||||
firstAudio.IntentId,
|
||||
storageConfiguration.Bucket,
|
||||
picture.Key,
|
||||
picture.ObjectName,
|
||||
picture.ContentType,
|
||||
picture.UploadUrl,
|
||||
audio.Key,
|
||||
audio.ObjectName,
|
||||
audio.ContentType,
|
||||
audio.UploadUrl,
|
||||
uploadIntentConfiguration.UploadUrlExpiresInSeconds);
|
||||
firstPicture?.Key,
|
||||
firstPicture?.ObjectName,
|
||||
firstPicture?.ContentType,
|
||||
firstPicture?.UploadUrl,
|
||||
firstAudio.Key,
|
||||
firstAudio.ObjectName,
|
||||
firstAudio.ContentType,
|
||||
firstAudio.UploadUrl,
|
||||
uploadIntentConfiguration.UploadUrlExpiresInSeconds,
|
||||
items);
|
||||
}
|
||||
|
||||
private static IReadOnlyList<AudioUploadSpec> ResolveAudios(RequestTrackUploadUrlsCommand request)
|
||||
{
|
||||
if (request.Audios is { Count: > 0 } audios)
|
||||
return audios;
|
||||
|
||||
return request.AudioFileType != null && request.AudioContentType != null
|
||||
? [new AudioUploadSpec(request.AudioFileType, request.AudioContentType, request.ExpectedAudioSizeBytes)]
|
||||
: [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user