refactor: standardize project names to Musify.* and flatten layout
- Flatten nested per-repo folders into flat Musify.* projects at root - Rename assemblies/csproj: Domain->Musify.Domain, Application->Musify.Application, Infrastructure->Musify.Infrastructure, WebApi->Musify.Api, Worker->Musify.Worker, StreamingGateway->Musify.StreamingGateway - Rename WebApi.* namespaces to Musify.Api.* - Update all ProjectReference paths and the solution file - Consolidate stray per-repo dotfiles; add root .gitattributes - Move flutter client out to _archive Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
using Ardalis.Result;
|
||||
using Mediator;
|
||||
using Musify.Application.Tracks.Responses;
|
||||
|
||||
namespace Musify.Application.Tracks.Commands
|
||||
{
|
||||
public record CreateTrackCommand(
|
||||
long UserId,
|
||||
string Title,
|
||||
Guid PictureIntentId,
|
||||
Guid AudioIntentId)
|
||||
: ICommand<Result<TrackApplicationResponse>>;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
using Ardalis.Result;
|
||||
using Mediator;
|
||||
|
||||
namespace Musify.Application.Tracks.Commands
|
||||
{
|
||||
public record DeleteTrackCommand(
|
||||
long UserId,
|
||||
Guid TrackId)
|
||||
: ICommand<Result>;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
using Ardalis.Result;
|
||||
using Mediator;
|
||||
using Musify.Application.Tracks.Responses;
|
||||
|
||||
namespace Musify.Application.Tracks.Commands
|
||||
{
|
||||
public record RequestTrackUploadUrlsCommand(
|
||||
long UserId,
|
||||
string PictureFileType,
|
||||
string PictureContentType,
|
||||
string AudioFileType,
|
||||
string AudioContentType,
|
||||
long? ExpectedPictureSizeBytes = null,
|
||||
long? ExpectedAudioSizeBytes = null)
|
||||
: ICommand<Result<TrackUploadUrlsResponse>>;
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
using Ardalis.Result;
|
||||
using Mediator;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Musify.Application.Configuration;
|
||||
using Musify.Application.Shared;
|
||||
using Musify.Application.Events;
|
||||
using Musify.Application.Tracks.Commands;
|
||||
using Musify.Application.Tracks.Responses;
|
||||
using Musify.Application.Extensions;
|
||||
using Musify.Application.Services;
|
||||
using Musify.Domain.Entities;
|
||||
using Musify.Domain.ValueObjects;
|
||||
using Musify.Application.Contracts;
|
||||
|
||||
namespace Musify.Application.Tracks.Handlers
|
||||
{
|
||||
public class CreateTrackCommandHandler(
|
||||
IDatabase database,
|
||||
IEventBus eventBus,
|
||||
UploadIntentValidator uploadIntentValidator,
|
||||
ILogger<CreateTrackCommandHandler> logger,
|
||||
ApplicationStorageConfiguration storageConfiguration,
|
||||
TrackConfiguration trackConfiguration,
|
||||
UploadIntentConfiguration uploadIntentConfiguration)
|
||||
: ICommandHandler<CreateTrackCommand, Result<TrackApplicationResponse>>
|
||||
{
|
||||
public async ValueTask<Result<TrackApplicationResponse>> Handle(CreateTrackCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var userExists = await database.Users.AsNoTracking()
|
||||
.AnyAsync(u => u.Id == request.UserId, cancellationToken);
|
||||
if (!userExists)
|
||||
{
|
||||
logger.LogWarning("User {UserId} not found", request.UserId);
|
||||
return Result.NotFound($"User {request.UserId} not found");
|
||||
}
|
||||
|
||||
var pictureValidation = await uploadIntentValidator.ValidateAndLoadAsync(
|
||||
uploadIntentConfiguration,
|
||||
request.PictureIntentId, request.UserId, cancellationToken);
|
||||
if (!pictureValidation.IsSuccess)
|
||||
return pictureValidation.As<UploadIntent, TrackApplicationResponse>();
|
||||
|
||||
var audioValidation = await uploadIntentValidator.ValidateAndLoadAsync(
|
||||
uploadIntentConfiguration,
|
||||
request.AudioIntentId, request.UserId, cancellationToken);
|
||||
if (!audioValidation.IsSuccess)
|
||||
return audioValidation.As<UploadIntent, TrackApplicationResponse>();
|
||||
|
||||
var pictureIntent = pictureValidation.Value;
|
||||
var audioIntent = audioValidation.Value;
|
||||
|
||||
var finalPictureKey = trackConfiguration.Routes.BuildOriginalPicturePath(request.UserId, pictureIntent.ObjectName);
|
||||
var finalAudioKey = trackConfiguration.Routes.BuildOriginalAudioPath(request.UserId, audioIntent.ObjectName);
|
||||
var audioProcessedFolderKey = trackConfiguration.Routes.BuildProcessedAudioPath(Guid.NewGuid().ToString());
|
||||
|
||||
var trackEntity = new Track
|
||||
{
|
||||
Title = request.Title,
|
||||
NormalizedTitle = request.Title.ToUpperInvariant(),
|
||||
OriginalPictureName = pictureIntent.ObjectName,
|
||||
OriginalAudioName = audioIntent.ObjectName,
|
||||
SmallPictureName = trackConfiguration.Routes.PresetSmallPicture,
|
||||
MediumPictureName = trackConfiguration.Routes.PresetMediumPicture,
|
||||
LargePictureName = trackConfiguration.Routes.PresetLargePicture,
|
||||
PicturesProcessingStatus = ProcessingStatus.Pending,
|
||||
AudioTranscodeProcessingStatus = ProcessingStatus.Pending
|
||||
};
|
||||
|
||||
await database.Tracks.AddAsync(trackEntity, cancellationToken);
|
||||
|
||||
await database.UserHasTracks.AddAsync(new UserHasTrack
|
||||
{
|
||||
UserId = request.UserId,
|
||||
TrackId = trackEntity.Id
|
||||
}, cancellationToken);
|
||||
|
||||
try
|
||||
{
|
||||
await eventBus.PublishAsync(
|
||||
new CreateTrackResourcesEvent(
|
||||
trackEntity.Id,
|
||||
pictureIntent.Id,
|
||||
audioIntent.Id,
|
||||
storageConfiguration.Bucket,
|
||||
pictureIntent.Key,
|
||||
finalPictureKey,
|
||||
audioIntent.Key,
|
||||
finalAudioKey,
|
||||
audioProcessedFolderKey,
|
||||
new ImageSize(
|
||||
trackConfiguration.Routes.SmallPicturesPath,
|
||||
trackConfiguration.PicturesSizes.SmallPictureWidth,
|
||||
trackConfiguration.PicturesSizes.SmallPictureHeight),
|
||||
new ImageSize(
|
||||
trackConfiguration.Routes.MediumPicturesPath,
|
||||
trackConfiguration.PicturesSizes.MediumPictureWidth,
|
||||
trackConfiguration.PicturesSizes.MediumPictureHeight),
|
||||
new ImageSize(
|
||||
trackConfiguration.Routes.LargePicturesPath,
|
||||
trackConfiguration.PicturesSizes.LargePictureWidth,
|
||||
trackConfiguration.PicturesSizes.LargePictureHeight)),
|
||||
cancellationToken);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
logger.LogError(exception, "Failed to publish create track event for track {TrackId}", trackEntity.Id);
|
||||
return Result.Error($"Failed to create track for {request.Title}");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await database.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
logger.LogError(exception, "Failed to save track {Title}", request.Title);
|
||||
return Result.Error($"Failed to create track for {request.Title}");
|
||||
}
|
||||
|
||||
return Result.Created(TrackApplicationResponse.FromEntity(trackEntity));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
using Ardalis.Result;
|
||||
using Mediator;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Musify.Application.Tracks.Commands;
|
||||
using Musify.Domain.ValueObjects;
|
||||
using Musify.Application.Events;
|
||||
using Musify.Application.Contracts;
|
||||
|
||||
namespace Musify.Application.Tracks.Handlers
|
||||
{
|
||||
public class DeleteTrackCommandHandler(
|
||||
IDatabase database,
|
||||
IEventBus eventBus,
|
||||
ILogger<DeleteTrackCommandHandler> logger)
|
||||
: ICommandHandler<DeleteTrackCommand, Result>
|
||||
{
|
||||
public async ValueTask<Result> Handle(DeleteTrackCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var track = await database.Tracks.SingleOrDefaultAsync(t => t.Id == request.TrackId, cancellationToken);
|
||||
if (track is null)
|
||||
{
|
||||
logger.LogInformation("Track {TrackId} not found", request.TrackId);
|
||||
return Result.NotFound();
|
||||
}
|
||||
|
||||
var isOwner = await database.UserHasTracks
|
||||
.AsNoTracking()
|
||||
.AnyAsync(ut => ut.TrackId == request.TrackId && ut.UserId == request.UserId, cancellationToken);
|
||||
if (!isOwner)
|
||||
{
|
||||
logger.LogWarning("User {UserId} unauthorized to delete track {TrackId}", request.UserId, request.TrackId);
|
||||
return Result.Unauthorized();
|
||||
}
|
||||
|
||||
if (track.AudioTranscodeProcessingStatus == ProcessingStatus.Processing
|
||||
|| track.AudioTranscodeProcessingStatus == ProcessingStatus.Pending)
|
||||
{
|
||||
logger.LogWarning("Track {TrackId} is currently being processed and cannot be deleted", request.TrackId);
|
||||
return Result.Conflict("Track is currently being processed and cannot be deleted");
|
||||
}
|
||||
|
||||
track.LifeCycleStatus = LifeCycleStatus.Removing;
|
||||
|
||||
try
|
||||
{
|
||||
await eventBus.PublishAsync(
|
||||
new DeleteTrackEvent(track.Id, request.UserId),
|
||||
cancellationToken);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
logger.LogError(exception, "Failed to publish delete track event for track {TrackId}", request.TrackId);
|
||||
return Result.Error($"Failed to delete track {request.TrackId}");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await database.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
logger.LogError(exception, "Failed to mark track {TrackId} as processing before deletion", request.TrackId);
|
||||
return Result.Error($"Failed to delete track {request.TrackId}");
|
||||
}
|
||||
|
||||
return Result.NoContent();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
using Ardalis.Result;
|
||||
using Mediator;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Musify.Application.Contracts;
|
||||
using Musify.Application.Tracks.Queries;
|
||||
using Musify.Application.Tracks.Responses;
|
||||
|
||||
namespace Musify.Application.Tracks.Handlers
|
||||
{
|
||||
public class GetTrackByIdQueryHandler(IDatabase database)
|
||||
: IQueryHandler<GetTrackByIdQuery, Result<TrackApplicationResponse> >
|
||||
{
|
||||
public async ValueTask<Result<TrackApplicationResponse>> Handle(GetTrackByIdQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
var track = await database.Tracks.AsNoTracking()
|
||||
.Where(t => t.Id == request.TrackId)
|
||||
.Select(t => TrackApplicationResponse.FromEntity(t))
|
||||
.SingleOrDefaultAsync(cancellationToken);
|
||||
|
||||
return track is null
|
||||
? Result<TrackApplicationResponse>.NotFound()
|
||||
: Result.Success(track);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
using Ardalis.Result;
|
||||
using Mediator;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Musify.Application.Configuration;
|
||||
using Musify.Application.Contracts;
|
||||
using Musify.Application.Tracks.Queries;
|
||||
using Musify.Application.Tracks.Responses;
|
||||
using Musify.Domain.ValueObjects;
|
||||
|
||||
namespace Musify.Application.Tracks.Handlers
|
||||
{
|
||||
public class GetTrackStreamQueryHandler(
|
||||
IDatabase database,
|
||||
IStreamTicketService ticketService,
|
||||
TrackConfiguration trackConfiguration,
|
||||
StreamGatewayConfiguration streamGatewayConfiguration,
|
||||
ILogger<GetTrackStreamQueryHandler> logger)
|
||||
: IQueryHandler<GetTrackStreamQuery, Result<TrackStreamResponse>>
|
||||
{
|
||||
public async ValueTask<Result<TrackStreamResponse>> Handle(GetTrackStreamQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
var track = await database.Tracks.AsNoTracking()
|
||||
.Where(t => t.Id == request.TrackId)
|
||||
.Select(t => new { t.Id, t.AudioTranscodeProcessingStatus, t.AudioFolderName })
|
||||
.SingleOrDefaultAsync(cancellationToken);
|
||||
|
||||
if (track is null)
|
||||
return Result<TrackStreamResponse>.NotFound();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(track.AudioFolderName)
|
||||
|| track.AudioTranscodeProcessingStatus != ProcessingStatus.Completed)
|
||||
{
|
||||
logger.LogInformation("Stream requested for track {TrackId} but audio is not ready", request.TrackId);
|
||||
return Result<TrackStreamResponse>.Conflict("Track audio is not available for streaming yet.");
|
||||
}
|
||||
|
||||
var folderPath = trackConfiguration.Routes.BuildProcessedAudioPath(track.AudioFolderName);
|
||||
var keyPrefix = $"{folderPath}/";
|
||||
|
||||
var ticket = ticketService.IssueTicket(request.UserId, keyPrefix);
|
||||
|
||||
var manifestUrl = string.Join('/',
|
||||
streamGatewayConfiguration.PublicBaseUrl.TrimEnd('/'),
|
||||
"media",
|
||||
folderPath,
|
||||
streamGatewayConfiguration.AudioFileName);
|
||||
|
||||
logger.LogInformation("Issued stream ticket for track {TrackId} to user {UserId}", request.TrackId, request.UserId);
|
||||
|
||||
return Result.Success(new TrackStreamResponse(manifestUrl, ticket.Token, ticket.ExpiresInSeconds));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
using Ardalis.Result;
|
||||
using Mediator;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Musify.Application.Pagination;
|
||||
using Musify.Application.Tracks.Queries;
|
||||
using Musify.Application.Tracks.Responses;
|
||||
using X.PagedList.EF;
|
||||
using Musify.Application.Contracts;
|
||||
|
||||
namespace Musify.Application.Tracks.Handlers
|
||||
{
|
||||
public class GetTracksByUserIdQueryHandler(IDatabase database)
|
||||
: IQueryHandler<GetTracksByUserIdQuery, Result<PaginatedResponse<TrackApplicationResponse>> >
|
||||
{
|
||||
public async ValueTask<Result<PaginatedResponse<TrackApplicationResponse> >> Handle(GetTracksByUserIdQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
var pageNumber = request.PageNumber < 1 ? 1 : request.PageNumber;
|
||||
var pageSize = request.PageSize < 1 ? 10 : request.PageSize;
|
||||
|
||||
var tracksQuery = database.UserHasTracks
|
||||
.AsNoTracking()
|
||||
.Include(ut => ut.Track)
|
||||
.Where(p => p.UserId == request.UserId);
|
||||
|
||||
if (request.Name is not null)
|
||||
{
|
||||
var normalizedName = request.Name.Trim().ToUpperInvariant();
|
||||
tracksQuery = tracksQuery.Where(t => t.Track.NormalizedTitle.Contains(normalizedName));
|
||||
}
|
||||
|
||||
var totalCount = await tracksQuery.CountAsync(cancellationToken);
|
||||
|
||||
var pagedTracks = await tracksQuery
|
||||
.OrderBy(t => t.Track.CreatedAt)
|
||||
.Select(t => TrackApplicationResponse.FromEntity(t.Track))
|
||||
.ToPagedListAsync(pageNumber, pageSize, totalCount, cancellationToken);
|
||||
|
||||
return Result.Success(PaginatedResponse<TrackApplicationResponse>.FromPagedList(pagedTracks));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
using Ardalis.Result;
|
||||
using Mediator;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Musify.Application.Pagination;
|
||||
using Musify.Application.Tracks.Queries;
|
||||
using Musify.Application.Tracks.Responses;
|
||||
using X.PagedList.EF;
|
||||
using Musify.Application.Contracts;
|
||||
|
||||
namespace Musify.Application.Tracks.Handlers
|
||||
{
|
||||
public class GetTracksQueryHandler(IDatabase database)
|
||||
: IQueryHandler<GetTracksQuery, Result<PaginatedResponse<TrackApplicationResponse>> >
|
||||
{
|
||||
public async ValueTask<Result<PaginatedResponse<TrackApplicationResponse> >> Handle(GetTracksQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
var tracksQuery = database.UserHasTracks
|
||||
.AsNoTracking()
|
||||
.OrderBy(t => t.Id)
|
||||
.Include(ut => ut.Track)
|
||||
.Select(t => TrackApplicationResponse.FromEntity(t.Track));
|
||||
|
||||
var totalCount = await tracksQuery.CountAsync(cancellationToken);
|
||||
var pagedTracks = await tracksQuery.ToPagedListAsync(request.PageNumber, request.PageSize, totalCount, cancellationToken);
|
||||
|
||||
return Result.Success(PaginatedResponse<TrackApplicationResponse>.FromPagedList(pagedTracks));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
using Ardalis.Result;
|
||||
using Mediator;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Musify.Application.Configuration;
|
||||
using Musify.Application.Services;
|
||||
using Musify.Application.Tracks.Commands;
|
||||
using Musify.Application.Tracks.Responses;
|
||||
using Musify.Domain.Entities;
|
||||
using Musify.Domain.ValueObjects;
|
||||
using Musify.Application.Contracts;
|
||||
|
||||
namespace Musify.Application.Tracks.Handlers
|
||||
{
|
||||
public class RequestTrackUploadUrlsCommandHandler(
|
||||
IDatabase database,
|
||||
UploadIntentValidator uploadIntentValidator,
|
||||
IStorageService storageService,
|
||||
ILogger<RequestTrackUploadUrlsCommandHandler> logger,
|
||||
ApplicationStorageConfiguration storageConfiguration,
|
||||
TrackConfiguration trackConfiguration,
|
||||
UploadIntentConfiguration uploadIntentConfiguration)
|
||||
: ICommandHandler<RequestTrackUploadUrlsCommand, Result<TrackUploadUrlsResponse>>
|
||||
{
|
||||
public async ValueTask<Result<TrackUploadUrlsResponse>> Handle(RequestTrackUploadUrlsCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var userExists = await database.Users
|
||||
.AsNoTracking()
|
||||
.AnyAsync(u => u.Id == request.UserId, cancellationToken);
|
||||
if (!userExists)
|
||||
{
|
||||
logger.LogWarning("User {UserId} not found", request.UserId);
|
||||
return Result.NotFound($"User {request.UserId} not found");
|
||||
}
|
||||
|
||||
var effectivePictureSize = request.ExpectedPictureSizeBytes
|
||||
?? uploadIntentConfiguration.DefaultExpectedPictureSizeBytes;
|
||||
var effectiveAudioSize = request.ExpectedAudioSizeBytes
|
||||
?? uploadIntentConfiguration.DefaultExpectedAudioSizeBytes;
|
||||
|
||||
var pictureObjectName = $"{Guid.NewGuid()}.{request.PictureFileType.TrimStart('.').ToLowerInvariant()}";
|
||||
var audioObjectName = $"{Guid.NewGuid()}.{request.AudioFileType.TrimStart('.').ToLowerInvariant()}";
|
||||
|
||||
var tempPictureKey = trackConfiguration.Routes.BuildTempPicturePath(
|
||||
uploadIntentConfiguration.TemporalRootPrefix, request.UserId, pictureObjectName);
|
||||
var tempAudioKey = trackConfiguration.Routes.BuildTempAudioPath(
|
||||
uploadIntentConfiguration.TemporalRootPrefix, request.UserId, audioObjectName);
|
||||
|
||||
try
|
||||
{
|
||||
var expiresIn = TimeSpan.FromSeconds(uploadIntentConfiguration.UploadUrlExpiresInSeconds);
|
||||
|
||||
var pictureUploadUrl = await storageService.GetUploadUrlAsync(
|
||||
storageConfiguration.Bucket,
|
||||
tempPictureKey,
|
||||
request.PictureContentType,
|
||||
expiresIn,
|
||||
cancellationToken);
|
||||
|
||||
var audioUploadUrl = await storageService.GetUploadUrlAsync(
|
||||
storageConfiguration.Bucket,
|
||||
tempAudioKey,
|
||||
request.AudioContentType,
|
||||
expiresIn,
|
||||
cancellationToken);
|
||||
|
||||
var expiresAt = DateTime.UtcNow.AddSeconds(uploadIntentConfiguration.UploadUrlExpiresInSeconds);
|
||||
|
||||
await using var transaction = await database.BeginTransactionAsync(
|
||||
System.Data.IsolationLevel.Serializable, cancellationToken);
|
||||
|
||||
var quotaCheck = await uploadIntentValidator.CheckQuotaAsync(
|
||||
uploadIntentConfiguration,
|
||||
request.UserId, effectivePictureSize + effectiveAudioSize, 2, cancellationToken);
|
||||
if (!quotaCheck.IsSuccess)
|
||||
{
|
||||
logger.LogWarning("User {UserId} failed upload intent quota check", request.UserId);
|
||||
return quotaCheck;
|
||||
}
|
||||
|
||||
var pictureIntent = new UploadIntent
|
||||
{
|
||||
UserId = request.UserId,
|
||||
Bucket = storageConfiguration.Bucket,
|
||||
Key = tempPictureKey,
|
||||
ObjectName = pictureObjectName,
|
||||
ContentType = request.PictureContentType,
|
||||
ExpectedSizeBytes = effectivePictureSize,
|
||||
Purpose = UploadIntentPurpose.TrackPicture,
|
||||
ExpiresAt = expiresAt,
|
||||
};
|
||||
|
||||
var audioIntent = new UploadIntent
|
||||
{
|
||||
UserId = request.UserId,
|
||||
Bucket = storageConfiguration.Bucket,
|
||||
Key = tempAudioKey,
|
||||
ObjectName = audioObjectName,
|
||||
ContentType = request.AudioContentType,
|
||||
ExpectedSizeBytes = effectiveAudioSize,
|
||||
Purpose = UploadIntentPurpose.TrackAudio,
|
||||
ExpiresAt = expiresAt,
|
||||
};
|
||||
|
||||
await database.UploadIntents.AddAsync(pictureIntent, cancellationToken);
|
||||
await database.UploadIntents.AddAsync(audioIntent, cancellationToken);
|
||||
await database.SaveChangesAsync(cancellationToken);
|
||||
await transaction.CommitAsync(cancellationToken);
|
||||
|
||||
logger.LogInformation("Issued upload intents {PictureIntentId}/{AudioIntentId} for user {UserId} (TrackPicture/TrackAudio)",
|
||||
pictureIntent.Id, audioIntent.Id, request.UserId);
|
||||
|
||||
return Result.Success(new TrackUploadUrlsResponse(
|
||||
pictureIntent.Id,
|
||||
audioIntent.Id,
|
||||
storageConfiguration.Bucket,
|
||||
tempPictureKey,
|
||||
pictureObjectName,
|
||||
request.PictureContentType,
|
||||
pictureUploadUrl,
|
||||
tempAudioKey,
|
||||
audioObjectName,
|
||||
request.AudioContentType,
|
||||
audioUploadUrl,
|
||||
uploadIntentConfiguration.UploadUrlExpiresInSeconds));
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
logger.LogError(exception, "Failed to generate track upload URLs for user {UserId}", request.UserId);
|
||||
return Result.Error("Failed to generate upload URLs");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
using Ardalis.Result;
|
||||
using Mediator;
|
||||
using Musify.Application.Tracks.Responses;
|
||||
|
||||
namespace Musify.Application.Tracks.Queries
|
||||
{
|
||||
public record GetTrackByIdQuery(Guid TrackId)
|
||||
: IQuery<Result<TrackApplicationResponse>>;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
using Ardalis.Result;
|
||||
using Mediator;
|
||||
using Musify.Application.Tracks.Responses;
|
||||
|
||||
namespace Musify.Application.Tracks.Queries
|
||||
{
|
||||
public record GetTrackStreamQuery(Guid TrackId, long UserId)
|
||||
: IQuery<Result<TrackStreamResponse>>;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using Ardalis.Result;
|
||||
using Mediator;
|
||||
using Musify.Application.Pagination;
|
||||
using Musify.Application.Tracks.Responses;
|
||||
|
||||
namespace Musify.Application.Tracks.Queries
|
||||
{
|
||||
public record GetTracksByUserIdQuery(
|
||||
long UserId,
|
||||
string? Name = null,
|
||||
int PageNumber = 1,
|
||||
int PageSize = 10)
|
||||
: IQuery<Result<PaginatedResponse<TrackApplicationResponse> > >;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
using Ardalis.Result;
|
||||
using Mediator;
|
||||
using Musify.Application.Pagination;
|
||||
using Musify.Application.Tracks.Responses;
|
||||
|
||||
namespace Musify.Application.Tracks.Queries
|
||||
{
|
||||
public record GetTracksQuery(int PageNumber = 1, int PageSize = 10)
|
||||
: IQuery<Result<PaginatedResponse<TrackApplicationResponse> >>;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using Musify.Domain.Entities;
|
||||
|
||||
namespace Musify.Application.Tracks.Responses
|
||||
{
|
||||
public record TrackApplicationResponse(
|
||||
Guid Id,
|
||||
string Title,
|
||||
DateTime CreatedAt,
|
||||
DateTime UpdatedAt)
|
||||
{
|
||||
public static TrackApplicationResponse FromEntity(Track track)
|
||||
{
|
||||
return new TrackApplicationResponse(
|
||||
track.Id,
|
||||
track.Title,
|
||||
track.CreatedAt,
|
||||
track.UpdatedAt);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace Musify.Application.Tracks.Responses
|
||||
{
|
||||
public record TrackStreamResponse(
|
||||
string ManifestUrl,
|
||||
string Ticket,
|
||||
int ExpiresInSeconds);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
namespace Musify.Application.Tracks.Responses
|
||||
{
|
||||
public record TrackUploadUrlsResponse(
|
||||
Guid PictureIntentId,
|
||||
Guid AudioIntentId,
|
||||
string Bucket,
|
||||
string PictureKey,
|
||||
string PictureName,
|
||||
string PictureContentType,
|
||||
string PictureUploadUrl,
|
||||
string AudioKey,
|
||||
string AudioName,
|
||||
string AudioContentType,
|
||||
string AudioUploadUrl,
|
||||
int ExpiresInSeconds);
|
||||
}
|
||||
Reference in New Issue
Block a user