Design Prot Merge #1

Merged
NotS1mp merged 217 commits from design-prot into master 2026-09-21 15:48:16 +00:00
2 changed files with 78 additions and 0 deletions
Showing only changes of commit 54484d9fd4 - Show all commits
@@ -2,6 +2,7 @@ using Mediator;
using Musify.Application.Tracks; using Musify.Application.Tracks;
using Musify.Application.Tracks.Responses; using Musify.Application.Tracks.Responses;
using Musify.Application.Shared; using Musify.Application.Shared;
using Musify.Application.Contracts;
using Musify.Api.Authentication; using Musify.Api.Authentication;
using Musify.Api.DataTransferObjects.Tracks; using Musify.Api.DataTransferObjects.Tracks;
using Musify.Api.Extensions; using Musify.Api.Extensions;
@@ -27,6 +28,12 @@ public static class TrackEndpoints
.Produces<TrackApplicationResponse>() .Produces<TrackApplicationResponse>()
.Produces(StatusCodes.Status404NotFound); .Produces(StatusCodes.Status404NotFound);
group.MapGet("/{id}/cover", GetTrackCover)
.WithName("GetTrackCover")
.WithSummary("Get A Track Cover Image.")
.Produces(StatusCodes.Status200OK)
.Produces(StatusCodes.Status404NotFound);
group.MapGet("/{id}/stream", GetTrackStream) group.MapGet("/{id}/stream", GetTrackStream)
.WithName("GetTrackStream") .WithName("GetTrackStream")
.WithSummary("Get A Streaming Manifest URL And Ticket For A Track.") .WithSummary("Get A Streaming Manifest URL And Ticket For A Track.")
@@ -94,6 +101,24 @@ public static class TrackEndpoints
return result.ToHttpResult(); return result.ToHttpResult();
} }
private static async Task<IResult> GetTrackCover(
IMediator mediator,
IStorageService storageService,
HttpResponse response,
Guid id,
CancellationToken cancellationToken,
string size = "medium")
{
var result = await mediator.Send(new GetTrackCoverQuery(id, size), cancellationToken);
if (result.IsError)
return Results.NotFound();
var location = result.Value;
var stream = await storageService.GetFileAsync(location.Bucket, location.Key, cancellationToken);
response.Headers.CacheControl = "public, max-age=31536000, immutable";
return Results.Stream(stream, location.ContentType);
}
private static async Task<IResult> GetTrackStream( private static async Task<IResult> GetTrackStream(
IMediator mediator, IMediator mediator,
CurrentUser currentUser, CurrentUser currentUser,
@@ -0,0 +1,53 @@
using ErrorOr;
using Mediator;
using Microsoft.EntityFrameworkCore;
using Musify.Application.Configuration;
using Musify.Application.Contracts;
namespace Musify.Application.Tracks
{
public record GetTrackCoverQuery(Guid TrackId, string Size)
: IQuery<ErrorOr<TrackCoverLocation>>;
public record TrackCoverLocation(string Bucket, string Key, string ContentType);
public class GetTrackCoverQueryHandler(
IDatabase database,
ApplicationStorageConfiguration storageConfiguration,
TrackConfiguration trackConfiguration)
: IQueryHandler<GetTrackCoverQuery, ErrorOr<TrackCoverLocation>>
{
public async ValueTask<ErrorOr<TrackCoverLocation>> Handle(GetTrackCoverQuery request, CancellationToken cancellationToken)
{
var track = await database.Tracks.AsNoTracking()
.Where(t => t.Id == request.TrackId)
.Select(t => new { t.SmallPictureName, t.MediumPictureName, t.LargePictureName })
.SingleOrDefaultAsync(cancellationToken);
if (track == null)
return Error.NotFound();
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))
};
if (string.IsNullOrEmpty(name) || key == null)
return Error.NotFound();
var contentType = name.EndsWith(".webp", StringComparison.OrdinalIgnoreCase)
? "image/webp"
: name.EndsWith(".png", StringComparison.OrdinalIgnoreCase)
? "image/png"
: "image/jpeg";
return new TrackCoverLocation(storageConfiguration.Bucket, key, contentType);
}
private static string? BuildKey(Func<string, string> builder, string? name)
=> string.IsNullOrEmpty(name)? null: builder(name);
}
}