Change editor rule

This commit is contained in:
2026-09-24 13:24:43 +02:00
parent 16478678b5
commit 73e37dbecd
401 changed files with 14735 additions and 12995 deletions
+1 -1
View File
@@ -18,7 +18,7 @@ generated_code = true
[*.cs] [*.cs]
indent_size = 4 indent_size = 4
csharp_style_namespace_declarations = file_scoped:warning csharp_style_namespace_declarations = block_scoped:warning
csharp_using_directive_placement = outside_namespace:warning csharp_using_directive_placement = outside_namespace:warning
dotnet_sort_system_directives_first = true dotnet_sort_system_directives_first = true
dotnet_separate_import_directive_groups = false dotnet_separate_import_directive_groups = false
+1 -1
View File
@@ -19,7 +19,7 @@ dotnet format Musify.slnx --verify-no-changes
## Style ## Style
- File-scoped namespaces, usings outside the namespace and sorted (`.editorconfig` enforces it). - Block-scoped namespaces (`namespace X { }`; file-scoped `namespace X;` is not allowed), usings outside the namespace and sorted (`.editorconfig` enforces it).
- Private fields camelCase without underscore, constants and `static readonly` fields PascalCase, explicit accessibility. - Private fields camelCase without underscore, constants and `static readonly` fields PascalCase, explicit accessibility.
- `ArgumentNullException.ThrowIfNull(x)` without `nameof`. - `ArgumentNullException.ThrowIfNull(x)` without `nameof`.
- Use `PlayList` (not `Playlist`) in code; configuration POCOs end in `Configuration`; injected services are named after their type (`storageService`). - Use `PlayList` (not `Playlist`) in code; configuration POCOs end in `Configuration`; injected services are named after their type (`storageService`).
@@ -7,17 +7,17 @@ using Musify.Application.Shared;
using Musify.Domain.Entities; using Musify.Domain.Entities;
using Musify.Domain.ValueObjects; using Musify.Domain.ValueObjects;
namespace Musify.Application.Albums; namespace Musify.Application.Albums
{
/// <summary>Appends your own tracks to an album in the given order, skipping the ones it already has.</summary> /// <summary>Appends your own tracks to an album in the given order, skipping the ones it already has.</summary>
public record AddTracksToAlbumCommand(long UserId, Guid AlbumId, IReadOnlyList<Guid> TrackIds) public record AddTracksToAlbumCommand(long UserId, Guid AlbumId, IReadOnlyList<Guid> TrackIds)
: ICommand<ErrorOr<Success>>; : ICommand<ErrorOr<Success>>;
public class AddTracksToAlbumCommandHandler( public class AddTracksToAlbumCommandHandler(
IDatabase database, IDatabase database,
ILogger<AddTracksToAlbumCommandHandler> logger) ILogger<AddTracksToAlbumCommandHandler> logger)
: ICommandHandler<AddTracksToAlbumCommand, ErrorOr<Success>> : ICommandHandler<AddTracksToAlbumCommand, ErrorOr<Success>>
{ {
public async ValueTask<ErrorOr<Success>> Handle(AddTracksToAlbumCommand request, CancellationToken cancellationToken) public async ValueTask<ErrorOr<Success>> Handle(AddTracksToAlbumCommand request, CancellationToken cancellationToken)
{ {
var album = await database.Albums.FindOwnedAsync(request.AlbumId, request.UserId, cancellationToken); var album = await database.Albums.FindOwnedAsync(request.AlbumId, request.UserId, cancellationToken);
@@ -74,4 +74,5 @@ public class AddTracksToAlbumCommandHandler(
return Result.Success; return Result.Success;
} }
}
} }
@@ -12,9 +12,9 @@ using Musify.Application.Shared;
using Musify.Domain.Entities; using Musify.Domain.Entities;
using Musify.Domain.ValueObjects; using Musify.Domain.ValueObjects;
namespace Musify.Application.Albums; namespace Musify.Application.Albums
{
public record CreateAlbumCommand( public record CreateAlbumCommand(
long UserId, long UserId,
string Title, string Title,
string? Description, string? Description,
@@ -22,14 +22,14 @@ public record CreateAlbumCommand(
Guid PictureIntentId) Guid PictureIntentId)
: ICommand<ErrorOr<AlbumApplicationResponse>>; : ICommand<ErrorOr<AlbumApplicationResponse>>;
public class CreateAlbumCommandHandler( public class CreateAlbumCommandHandler(
IEventBus eventBus, IEventBus eventBus,
IDatabase database, IDatabase database,
UploadIntentValidator uploadIntentValidator, UploadIntentValidator uploadIntentValidator,
ILogger<CreateAlbumCommandHandler> logger, ILogger<CreateAlbumCommandHandler> logger,
AlbumConfiguration albumConfiguration) AlbumConfiguration albumConfiguration)
: ICommandHandler<CreateAlbumCommand, ErrorOr<AlbumApplicationResponse>> : ICommandHandler<CreateAlbumCommand, ErrorOr<AlbumApplicationResponse>>
{ {
public async ValueTask<ErrorOr<AlbumApplicationResponse>> Handle(CreateAlbumCommand request, CancellationToken cancellationToken) public async ValueTask<ErrorOr<AlbumApplicationResponse>> Handle(CreateAlbumCommand request, CancellationToken cancellationToken)
{ {
var userExists = await database.Users var userExists = await database.Users
@@ -69,4 +69,5 @@ public class CreateAlbumCommandHandler(
return AlbumApplicationResponse.FromEntity(album, trackCount: 0); return AlbumApplicationResponse.FromEntity(album, trackCount: 0);
} }
}
} }
@@ -6,17 +6,17 @@ using Musify.Application.Events;
using Musify.Application.Shared; using Musify.Application.Shared;
using Musify.Domain.ValueObjects; using Musify.Domain.ValueObjects;
namespace Musify.Application.Albums; namespace Musify.Application.Albums
{
public record DeleteAlbumCommand(long UserId, Guid AlbumId) public record DeleteAlbumCommand(long UserId, Guid AlbumId)
: ICommand<ErrorOr<Success>>; : ICommand<ErrorOr<Success>>;
public class DeleteAlbumCommandHandler( public class DeleteAlbumCommandHandler(
IDatabase database, IDatabase database,
IEventBus eventBus, IEventBus eventBus,
ILogger<DeleteAlbumCommandHandler> logger) ILogger<DeleteAlbumCommandHandler> logger)
: ICommandHandler<DeleteAlbumCommand, ErrorOr<Success>> : ICommandHandler<DeleteAlbumCommand, ErrorOr<Success>>
{ {
public async ValueTask<ErrorOr<Success>> Handle(DeleteAlbumCommand request, CancellationToken cancellationToken) public async ValueTask<ErrorOr<Success>> Handle(DeleteAlbumCommand request, CancellationToken cancellationToken)
{ {
var found = await database.Albums.FindOwnedAsync(request.AlbumId, request.UserId, cancellationToken); var found = await database.Albums.FindOwnedAsync(request.AlbumId, request.UserId, cancellationToken);
@@ -33,4 +33,5 @@ public class DeleteAlbumCommandHandler(
return Result.Success; return Result.Success;
} }
}
} }
@@ -5,14 +5,14 @@ using Musify.Application.Albums.Responses;
using Musify.Application.Contracts; using Musify.Application.Contracts;
using Musify.Application.Shared; using Musify.Application.Shared;
namespace Musify.Application.Albums; namespace Musify.Application.Albums
{
public record GetAlbumByIdQuery(Guid Id) public record GetAlbumByIdQuery(Guid Id)
: IQuery<ErrorOr<AlbumApplicationResponse>>; : IQuery<ErrorOr<AlbumApplicationResponse>>;
public class GetAlbumByIdQueryHandler(IDatabase database) public class GetAlbumByIdQueryHandler(IDatabase database)
: IQueryHandler<GetAlbumByIdQuery, ErrorOr<AlbumApplicationResponse>> : IQueryHandler<GetAlbumByIdQuery, ErrorOr<AlbumApplicationResponse>>
{ {
public async ValueTask<ErrorOr<AlbumApplicationResponse>> Handle(GetAlbumByIdQuery request, CancellationToken cancellationToken) public async ValueTask<ErrorOr<AlbumApplicationResponse>> Handle(GetAlbumByIdQuery request, CancellationToken cancellationToken)
{ {
var album = await database.Albums var album = await database.Albums
@@ -26,4 +26,5 @@ public class GetAlbumByIdQueryHandler(IDatabase database)
? AppErrors.NotFound("Album", request.Id) ? AppErrors.NotFound("Album", request.Id)
: album; : album;
} }
}
} }
@@ -6,17 +6,17 @@ using Musify.Application.Contracts;
using Musify.Application.Shared; using Musify.Application.Shared;
using Musify.Application.Tracks.Responses; using Musify.Application.Tracks.Responses;
namespace Musify.Application.Albums; namespace Musify.Application.Albums
{
public record GetAlbumTracksQuery( public record GetAlbumTracksQuery(
Guid AlbumId, Guid AlbumId,
int PageNumber, int PageNumber,
int PageSize) int PageSize)
: IQuery<ErrorOr<PaginatedResponse<TrackApplicationResponse>>>; : IQuery<ErrorOr<PaginatedResponse<TrackApplicationResponse>>>;
public class GetAlbumTracksQueryHandler(IDatabase database) public class GetAlbumTracksQueryHandler(IDatabase database)
: IQueryHandler<GetAlbumTracksQuery, ErrorOr<PaginatedResponse<TrackApplicationResponse>>> : IQueryHandler<GetAlbumTracksQuery, ErrorOr<PaginatedResponse<TrackApplicationResponse>>>
{ {
public async ValueTask<ErrorOr<PaginatedResponse<TrackApplicationResponse>>> Handle(GetAlbumTracksQuery request, CancellationToken cancellationToken) public async ValueTask<ErrorOr<PaginatedResponse<TrackApplicationResponse>>> Handle(GetAlbumTracksQuery request, CancellationToken cancellationToken)
{ {
var albumExists = await database.Albums var albumExists = await database.Albums
@@ -35,4 +35,5 @@ public class GetAlbumTracksQueryHandler(IDatabase database)
.SelectResponse() .SelectResponse()
.ToPaginatedAsync(new PageRequest(request.PageNumber, request.PageSize), cancellationToken); .ToPaginatedAsync(new PageRequest(request.PageNumber, request.PageSize), cancellationToken);
} }
}
} }
@@ -4,14 +4,14 @@ using Musify.Application.Albums.Responses;
using Musify.Application.Contracts; using Musify.Application.Contracts;
using Musify.Application.Shared; using Musify.Application.Shared;
namespace Musify.Application.Albums; namespace Musify.Application.Albums
{
public record GetAlbumsQuery(string? Title, int PageNumber, int PageSize) public record GetAlbumsQuery(string? Title, int PageNumber, int PageSize)
: IQuery<PaginatedResponse<AlbumApplicationResponse>>; : IQuery<PaginatedResponse<AlbumApplicationResponse>>;
public class GetAlbumsQueryHandler(IDatabase database) public class GetAlbumsQueryHandler(IDatabase database)
: IQueryHandler<GetAlbumsQuery, PaginatedResponse<AlbumApplicationResponse>> : IQueryHandler<GetAlbumsQuery, PaginatedResponse<AlbumApplicationResponse>>
{ {
public async ValueTask<PaginatedResponse<AlbumApplicationResponse>> Handle(GetAlbumsQuery request, CancellationToken cancellationToken) public async ValueTask<PaginatedResponse<AlbumApplicationResponse>> Handle(GetAlbumsQuery request, CancellationToken cancellationToken)
{ {
var albumsQuery = database.Albums var albumsQuery = database.Albums
@@ -30,4 +30,5 @@ public class GetAlbumsQueryHandler(IDatabase database)
.SelectResponse() .SelectResponse()
.ToPaginatedAsync(new PageRequest(request.PageNumber, request.PageSize), cancellationToken); .ToPaginatedAsync(new PageRequest(request.PageNumber, request.PageSize), cancellationToken);
} }
}
} }
@@ -5,14 +5,14 @@ using Musify.Application.Albums.Responses;
using Musify.Application.Contracts; using Musify.Application.Contracts;
using Musify.Application.Shared; using Musify.Application.Shared;
namespace Musify.Application.Albums; namespace Musify.Application.Albums
{
public record GetAlbumsByUserIdQuery(long UserId, int PageNumber, int PageSize) public record GetAlbumsByUserIdQuery(long UserId, int PageNumber, int PageSize)
: IQuery<ErrorOr<PaginatedResponse<AlbumApplicationResponse>>>; : IQuery<ErrorOr<PaginatedResponse<AlbumApplicationResponse>>>;
public class GetAlbumsByUserIdQueryHandler(IDatabase database) public class GetAlbumsByUserIdQueryHandler(IDatabase database)
: IQueryHandler<GetAlbumsByUserIdQuery, ErrorOr<PaginatedResponse<AlbumApplicationResponse>>> : IQueryHandler<GetAlbumsByUserIdQuery, ErrorOr<PaginatedResponse<AlbumApplicationResponse>>>
{ {
public async ValueTask<ErrorOr<PaginatedResponse<AlbumApplicationResponse>>> Handle(GetAlbumsByUserIdQuery request, CancellationToken cancellationToken) public async ValueTask<ErrorOr<PaginatedResponse<AlbumApplicationResponse>>> Handle(GetAlbumsByUserIdQuery request, CancellationToken cancellationToken)
{ {
var userExists = await database.Users var userExists = await database.Users
@@ -30,4 +30,5 @@ public class GetAlbumsByUserIdQueryHandler(IDatabase database)
.SelectResponse() .SelectResponse()
.ToPaginatedAsync(new PageRequest(request.PageNumber, request.PageSize), cancellationToken); .ToPaginatedAsync(new PageRequest(request.PageNumber, request.PageSize), cancellationToken);
} }
}
} }
@@ -3,14 +3,14 @@ using Microsoft.EntityFrameworkCore;
using Musify.Application.Albums.Responses; using Musify.Application.Albums.Responses;
using Musify.Application.Contracts; using Musify.Application.Contracts;
namespace Musify.Application.Albums; namespace Musify.Application.Albums
{
public record GetRecentlyListenedAlbumsQuery(long UserId, int Limit) public record GetRecentlyListenedAlbumsQuery(long UserId, int Limit)
: IQuery<IReadOnlyList<AlbumApplicationResponse>>; : IQuery<IReadOnlyList<AlbumApplicationResponse>>;
public class GetRecentlyListenedAlbumsQueryHandler(IDatabase database) public class GetRecentlyListenedAlbumsQueryHandler(IDatabase database)
: IQueryHandler<GetRecentlyListenedAlbumsQuery, IReadOnlyList<AlbumApplicationResponse>> : IQueryHandler<GetRecentlyListenedAlbumsQuery, IReadOnlyList<AlbumApplicationResponse>>
{ {
public async ValueTask<IReadOnlyList<AlbumApplicationResponse>> Handle(GetRecentlyListenedAlbumsQuery request, CancellationToken cancellationToken) public async ValueTask<IReadOnlyList<AlbumApplicationResponse>> Handle(GetRecentlyListenedAlbumsQuery request, CancellationToken cancellationToken)
{ {
var listens = database.ListeningHistories var listens = database.ListeningHistories
@@ -48,4 +48,5 @@ public class GetRecentlyListenedAlbumsQueryHandler(IDatabase database)
.Select(entry => albumsById[entry.AlbumId]) .Select(entry => albumsById[entry.AlbumId])
.ToList(); .ToList();
} }
}
} }
@@ -5,16 +5,16 @@ using Microsoft.Extensions.Logging;
using Musify.Application.Contracts; using Musify.Application.Contracts;
using Musify.Application.Shared; using Musify.Application.Shared;
namespace Musify.Application.Albums; namespace Musify.Application.Albums
{
public record RemoveTrackFromAlbumCommand(long UserId, Guid AlbumId, Guid TrackId) public record RemoveTrackFromAlbumCommand(long UserId, Guid AlbumId, Guid TrackId)
: ICommand<ErrorOr<Success>>; : ICommand<ErrorOr<Success>>;
public class RemoveTrackFromAlbumCommandHandler( public class RemoveTrackFromAlbumCommandHandler(
IDatabase database, IDatabase database,
ILogger<RemoveTrackFromAlbumCommandHandler> logger) ILogger<RemoveTrackFromAlbumCommandHandler> logger)
: ICommandHandler<RemoveTrackFromAlbumCommand, ErrorOr<Success>> : ICommandHandler<RemoveTrackFromAlbumCommand, ErrorOr<Success>>
{ {
public async ValueTask<ErrorOr<Success>> Handle(RemoveTrackFromAlbumCommand request, CancellationToken cancellationToken) public async ValueTask<ErrorOr<Success>> Handle(RemoveTrackFromAlbumCommand request, CancellationToken cancellationToken)
{ {
var album = await database.Albums.FindOwnedAsync(request.AlbumId, request.UserId, cancellationToken); var album = await database.Albums.FindOwnedAsync(request.AlbumId, request.UserId, cancellationToken);
@@ -42,4 +42,5 @@ public class RemoveTrackFromAlbumCommandHandler(
return Result.Success; return Result.Success;
} }
}
} }
@@ -5,20 +5,21 @@ using Musify.Application.Pictures.Responses;
using Musify.Application.Services; using Musify.Application.Services;
using Musify.Domain.ValueObjects; using Musify.Domain.ValueObjects;
namespace Musify.Application.Albums; namespace Musify.Application.Albums
{
public record RequestAlbumPictureUploadCommand( public record RequestAlbumPictureUploadCommand(
long UserId, long UserId,
string FileType, string FileType,
string ContentType, string ContentType,
long? ExpectedSizeBytes = null) long? ExpectedSizeBytes = null)
: ICommand<ErrorOr<PictureUploadResponse>>; : ICommand<ErrorOr<PictureUploadResponse>>;
public class RequestAlbumPictureUploadCommandHandler(UploadIntentIssuer issuer, AlbumConfiguration albumConfiguration) public class RequestAlbumPictureUploadCommandHandler(UploadIntentIssuer issuer, AlbumConfiguration albumConfiguration)
: ICommandHandler<RequestAlbumPictureUploadCommand, ErrorOr<PictureUploadResponse>> : ICommandHandler<RequestAlbumPictureUploadCommand, ErrorOr<PictureUploadResponse>>
{ {
public async ValueTask<ErrorOr<PictureUploadResponse>> Handle(RequestAlbumPictureUploadCommand request, CancellationToken cancellationToken) => public async ValueTask<ErrorOr<PictureUploadResponse>> Handle(RequestAlbumPictureUploadCommand request, CancellationToken cancellationToken) =>
await issuer.IssuePictureAsync( await issuer.IssuePictureAsync(
request.UserId, UploadIntentPurpose.AlbumPicture, albumConfiguration, request.UserId, UploadIntentPurpose.AlbumPicture, albumConfiguration,
request.FileType, request.ContentType, request.ExpectedSizeBytes, cancellationToken); request.FileType, request.ContentType, request.ExpectedSizeBytes, cancellationToken);
}
} }
@@ -1,8 +1,8 @@
using Musify.Domain.Entities; using Musify.Domain.Entities;
namespace Musify.Application.Albums.Responses; namespace Musify.Application.Albums.Responses
{
public record AlbumApplicationResponse( public record AlbumApplicationResponse(
Guid Id, Guid Id,
string Title, string Title,
string? Description, string? Description,
@@ -15,7 +15,7 @@ public record AlbumApplicationResponse(
DateTime CreatedAt, DateTime CreatedAt,
DateTime UpdatedAt, DateTime UpdatedAt,
IReadOnlyList<Guid> CoverTrackIds) IReadOnlyList<Guid> CoverTrackIds)
{ {
public const int CoverTrackCount = AlbumProjections.CoverTrackCount; public const int CoverTrackCount = AlbumProjections.CoverTrackCount;
public static AlbumApplicationResponse FromEntity( public static AlbumApplicationResponse FromEntity(
@@ -37,4 +37,5 @@ public record AlbumApplicationResponse(
album.UpdatedAt, album.UpdatedAt,
coverTrackIds ?? []); coverTrackIds ?? []);
} }
}
} }
@@ -1,10 +1,10 @@
using Musify.Domain.Entities; using Musify.Domain.Entities;
using Musify.Domain.ValueObjects; using Musify.Domain.ValueObjects;
namespace Musify.Application.Albums.Responses; namespace Musify.Application.Albums.Responses
public static class AlbumProjections
{ {
public static class AlbumProjections
{
public const int CoverTrackCount = 4; public const int CoverTrackCount = 4;
public static IQueryable<Album> Active(this IQueryable<Album> query) => public static IQueryable<Album> Active(this IQueryable<Album> query) =>
@@ -28,4 +28,5 @@ public static class AlbumProjections
.Take(CoverTrackCount) .Take(CoverTrackCount)
.Select(albumTrack => albumTrack.TrackId) .Select(albumTrack => albumTrack.TrackId)
.ToList())); .ToList()));
}
} }
@@ -11,9 +11,9 @@ using Musify.Application.Services;
using Musify.Application.Shared; using Musify.Application.Shared;
using Musify.Domain.ValueObjects; using Musify.Domain.ValueObjects;
namespace Musify.Application.Albums; namespace Musify.Application.Albums
{
public record UpdateAlbumCommand( public record UpdateAlbumCommand(
long UserId, long UserId,
Guid AlbumId, Guid AlbumId,
string Title, string Title,
@@ -22,14 +22,14 @@ public record UpdateAlbumCommand(
Guid? NewPictureIntentId) Guid? NewPictureIntentId)
: ICommand<ErrorOr<AlbumApplicationResponse>>; : ICommand<ErrorOr<AlbumApplicationResponse>>;
public class UpdateAlbumCommandHandler( public class UpdateAlbumCommandHandler(
IEventBus eventBus, IEventBus eventBus,
IDatabase database, IDatabase database,
UploadIntentValidator uploadIntentValidator, UploadIntentValidator uploadIntentValidator,
ILogger<UpdateAlbumCommandHandler> logger, ILogger<UpdateAlbumCommandHandler> logger,
AlbumConfiguration albumConfiguration) AlbumConfiguration albumConfiguration)
: ICommandHandler<UpdateAlbumCommand, ErrorOr<AlbumApplicationResponse>> : ICommandHandler<UpdateAlbumCommand, ErrorOr<AlbumApplicationResponse>>
{ {
public async ValueTask<ErrorOr<AlbumApplicationResponse>> Handle(UpdateAlbumCommand request, CancellationToken cancellationToken) public async ValueTask<ErrorOr<AlbumApplicationResponse>> Handle(UpdateAlbumCommand request, CancellationToken cancellationToken)
{ {
var found = await database.Albums.FindOwnedAsync(request.AlbumId, request.UserId, cancellationToken); var found = await database.Albums.FindOwnedAsync(request.AlbumId, request.UserId, cancellationToken);
@@ -70,4 +70,5 @@ public class UpdateAlbumCommandHandler(
.SelectResponse() .SelectResponse()
.SingleAsync(cancellationToken); .SingleAsync(cancellationToken);
} }
}
} }
@@ -3,10 +3,10 @@ using Microsoft.Extensions.DependencyInjection;
using Musify.Application.Configuration; using Musify.Application.Configuration;
using Musify.Application.Services; using Musify.Application.Services;
namespace Musify.Application; namespace Musify.Application
public static class ApplicationDependencyInjection
{ {
public static class ApplicationDependencyInjection
{
public static IServiceCollection AddApplication(this IServiceCollection services, IConfiguration configuration) public static IServiceCollection AddApplication(this IServiceCollection services, IConfiguration configuration)
{ {
services.AddMediator(); services.AddMediator();
@@ -25,4 +25,5 @@ public static class ApplicationDependencyInjection
return services; return services;
} }
}
} }
@@ -1,9 +1,9 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
namespace Musify.Application.Configuration; namespace Musify.Application.Configuration
public class AlbumConfiguration : IConfigurationOptions, IPictureOwnerConfiguration
{ {
public class AlbumConfiguration : IConfigurationOptions, IPictureOwnerConfiguration
{
public static string SectionName => "Album"; public static string SectionName => "Album";
[Required] [Required]
@@ -11,4 +11,5 @@ public class AlbumConfiguration : IConfigurationOptions, IPictureOwnerConfigurat
[Required] [Required]
public PictureSizes PicturesSizes { get; set; } = new(); public PictureSizes PicturesSizes { get; set; } = new();
}
} }
@@ -1,12 +1,13 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
namespace Musify.Application.Configuration; namespace Musify.Application.Configuration
public class ApplicationStorageConfiguration : IConfigurationOptions
{ {
public class ApplicationStorageConfiguration : IConfigurationOptions
{
public static string SectionName => "ApplicationStorage"; public static string SectionName => "ApplicationStorage";
[Required] [Required]
[StringLength(128, MinimumLength = 1)] [StringLength(128, MinimumLength = 1)]
public required string Bucket { get; set; } public required string Bucket { get; set; }
}
} }
@@ -2,10 +2,10 @@ using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
namespace Musify.Application.Configuration; namespace Musify.Application.Configuration
public static class ConfigurationDependencyInjection
{ {
public static class ConfigurationDependencyInjection
{
public static IServiceCollection AddValidatedOptions<T>(this IServiceCollection services, IConfiguration configuration) public static IServiceCollection AddValidatedOptions<T>(this IServiceCollection services, IConfiguration configuration)
where T : class, IConfigurationOptions => where T : class, IConfigurationOptions =>
services.AddValidatedOptions<T>(configuration, T.SectionName); services.AddValidatedOptions<T>(configuration, T.SectionName);
@@ -26,4 +26,5 @@ public static class ConfigurationDependencyInjection
return services; return services;
} }
}
} }
@@ -1,16 +1,17 @@
using Mediator; using Mediator;
using Musify.Application.Configuration.Responses; using Musify.Application.Configuration.Responses;
namespace Musify.Application.Configuration; namespace Musify.Application.Configuration
public record GetPlaybackPublicConfigQuery : IQuery<PlaybackPublicConfigResponse>;
public class GetPlaybackPublicConfigQueryHandler(PlaybackConfiguration playbackConfiguration)
: IQueryHandler<GetPlaybackPublicConfigQuery, PlaybackPublicConfigResponse>
{ {
public record GetPlaybackPublicConfigQuery : IQuery<PlaybackPublicConfigResponse>;
public class GetPlaybackPublicConfigQueryHandler(PlaybackConfiguration playbackConfiguration)
: IQueryHandler<GetPlaybackPublicConfigQuery, PlaybackPublicConfigResponse>
{
public ValueTask<PlaybackPublicConfigResponse> Handle(GetPlaybackPublicConfigQuery query, CancellationToken cancellationToken) => public ValueTask<PlaybackPublicConfigResponse> Handle(GetPlaybackPublicConfigQuery query, CancellationToken cancellationToken) =>
ValueTask.FromResult(new PlaybackPublicConfigResponse( ValueTask.FromResult(new PlaybackPublicConfigResponse(
playbackConfiguration.AllowAnonymousListening, playbackConfiguration.AllowAnonymousListening,
playbackConfiguration.AnonymousFragmentSeconds)); playbackConfiguration.AnonymousFragmentSeconds));
}
} }
@@ -1,6 +1,7 @@
namespace Musify.Application.Configuration; namespace Musify.Application.Configuration
public interface IConfigurationOptions
{ {
public interface IConfigurationOptions
{
public static abstract string SectionName { get; } public static abstract string SectionName { get; }
}
} }
@@ -1,11 +1,12 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
namespace Musify.Application.Configuration; namespace Musify.Application.Configuration
public class MixConfiguration : IConfigurationOptions
{ {
public class MixConfiguration : IConfigurationOptions
{
public static string SectionName => "Mix"; public static string SectionName => "Mix";
[Range(4, 100)] [Range(4, 100)]
public int ItemsPerMix { get; set; } = 12; public int ItemsPerMix { get; set; } = 12;
}
} }
@@ -2,17 +2,17 @@ using System.ComponentModel.DataAnnotations;
using System.Globalization; using System.Globalization;
using Musify.Application.Shared; using Musify.Application.Shared;
namespace Musify.Application.Configuration; namespace Musify.Application.Configuration
public enum PictureSize
{ {
public enum PictureSize
{
Small, Small,
Medium, Medium,
Large Large
} }
public class PictureRoutes public class PictureRoutes
{ {
[Required] [Required]
public string UploadsFolder { get; set; } = "uploads"; public string UploadsFolder { get; set; } = "uploads";
@@ -46,10 +46,10 @@ public class PictureRoutes
public string BuildTempPath(string tempRootPrefix, long userId, string objectName) => public string BuildTempPath(string tempRootPrefix, long userId, string objectName) =>
StorageKey.Combine(tempRootPrefix, userId.ToString(CultureInfo.InvariantCulture), ParentFolder, objectName); StorageKey.Combine(tempRootPrefix, userId.ToString(CultureInfo.InvariantCulture), ParentFolder, objectName);
} }
public class PictureSizes public class PictureSizes
{ {
[Range(1, 1024)] [Range(1, 1024)]
public int SmallWidth { get; set; } = 128; public int SmallWidth { get; set; } = 128;
@@ -72,20 +72,21 @@ public class PictureSizes
new ImageSize(routes.FolderPath(PictureSize.Small), SmallWidth, SmallHeight), new ImageSize(routes.FolderPath(PictureSize.Small), SmallWidth, SmallHeight),
new ImageSize(routes.FolderPath(PictureSize.Medium), MediumWidth, MediumHeight), new ImageSize(routes.FolderPath(PictureSize.Medium), MediumWidth, MediumHeight),
new ImageSize(routes.FolderPath(PictureSize.Large), LargeWidth, LargeHeight)); new ImageSize(routes.FolderPath(PictureSize.Large), LargeWidth, LargeHeight));
} }
public interface IPictureOwnerConfiguration public interface IPictureOwnerConfiguration
{ {
public PictureRoutes Routes { get; } public PictureRoutes Routes { get; }
public PictureSizes PicturesSizes { get; } public PictureSizes PicturesSizes { get; }
} }
public static class PictureSizeParser public static class PictureSizeParser
{ {
/// <summary>Parses the <c>size</c> query value; anything unknown means <see cref="PictureSize.Medium"/>.</summary> /// <summary>Parses the <c>size</c> query value; anything unknown means <see cref="PictureSize.Medium"/>.</summary>
public static PictureSize Parse(string? value) => public static PictureSize Parse(string? value) =>
Enum.TryParse<PictureSize>(value, ignoreCase: true, out var size) && Enum.IsDefined(size) Enum.TryParse<PictureSize>(value, ignoreCase: true, out var size) && Enum.IsDefined(size)
? size ? size
: PictureSize.Medium; : PictureSize.Medium;
}
} }
@@ -1,9 +1,9 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
namespace Musify.Application.Configuration; namespace Musify.Application.Configuration
public class PlayListConfiguration : IConfigurationOptions, IPictureOwnerConfiguration
{ {
public class PlayListConfiguration : IConfigurationOptions, IPictureOwnerConfiguration
{
public static string SectionName => "PlayList"; public static string SectionName => "PlayList";
[Required] [Required]
@@ -11,4 +11,5 @@ public class PlayListConfiguration : IConfigurationOptions, IPictureOwnerConfigu
[Required] [Required]
public PictureSizes PicturesSizes { get; set; } = new(); public PictureSizes PicturesSizes { get; set; } = new();
}
} }
@@ -1,10 +1,10 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
namespace Musify.Application.Configuration; namespace Musify.Application.Configuration
public sealed class PlaybackConfiguration : IConfigurationOptions
{ {
public sealed class PlaybackConfiguration : IConfigurationOptions
{
public static string SectionName => "Playback"; public static string SectionName => "Playback";
public bool AllowAnonymousListening { get; set; } public bool AllowAnonymousListening { get; set; }
@@ -14,4 +14,5 @@ public sealed class PlaybackConfiguration : IConfigurationOptions
[Range(1, int.MaxValue)] [Range(1, int.MaxValue)]
public int EstimatedAudioBytesPerSecond { get; set; } = 16_000; public int EstimatedAudioBytesPerSecond { get; set; } = 16_000;
}
} }
@@ -1,4 +1,5 @@
namespace Musify.Application.Configuration.Responses; namespace Musify.Application.Configuration.Responses
{
public record PlaybackPublicConfigResponse(bool AllowAnonymousListening, int AnonymousFragmentSeconds);
public record PlaybackPublicConfigResponse(bool AllowAnonymousListening, int AnonymousFragmentSeconds); }
@@ -1,9 +1,9 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
namespace Musify.Application.Configuration; namespace Musify.Application.Configuration
public class StreamGatewayConfiguration : IConfigurationOptions
{ {
public class StreamGatewayConfiguration : IConfigurationOptions
{
public static string SectionName => "StreamGateway"; public static string SectionName => "StreamGateway";
[Url] [Url]
@@ -12,4 +12,5 @@ public class StreamGatewayConfiguration : IConfigurationOptions
[Required] [Required]
public string AudioFileName { get; set; } = "audio.m4a"; public string AudioFileName { get; set; } = "audio.m4a";
}
} }
@@ -2,10 +2,10 @@ using System.ComponentModel.DataAnnotations;
using System.Globalization; using System.Globalization;
using Musify.Application.Shared; using Musify.Application.Shared;
namespace Musify.Application.Configuration; namespace Musify.Application.Configuration
public class TrackConfiguration : IConfigurationOptions, IPictureOwnerConfiguration
{ {
public class TrackConfiguration : IConfigurationOptions, IPictureOwnerConfiguration
{
public static string SectionName => "Track"; public static string SectionName => "Track";
[Required] [Required]
@@ -15,10 +15,10 @@ public class TrackConfiguration : IConfigurationOptions, IPictureOwnerConfigurat
public PictureSizes PicturesSizes { get; set; } = new(); public PictureSizes PicturesSizes { get; set; } = new();
PictureRoutes IPictureOwnerConfiguration.Routes => Routes; PictureRoutes IPictureOwnerConfiguration.Routes => Routes;
} }
public class TrackRoutes : PictureRoutes public class TrackRoutes : PictureRoutes
{ {
public TrackRoutes() public TrackRoutes()
{ {
ParentFolder = "Tracks"; ParentFolder = "Tracks";
@@ -53,4 +53,5 @@ public class TrackRoutes : PictureRoutes
public string BuildProcessedAudioPath(string folderName) => public string BuildProcessedAudioPath(string folderName) =>
StorageKey.Combine(ParentFolder, ProcessedAudiosFolder, folderName); StorageKey.Combine(ParentFolder, ProcessedAudiosFolder, folderName);
}
} }
@@ -1,9 +1,9 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
namespace Musify.Application.Configuration; namespace Musify.Application.Configuration
public class UploadIntentConfiguration : IConfigurationOptions
{ {
public class UploadIntentConfiguration : IConfigurationOptions
{
public static string SectionName => "UploadIntent"; public static string SectionName => "UploadIntent";
[Range(10, 3600)] [Range(10, 3600)]
@@ -38,4 +38,5 @@ public class UploadIntentConfiguration : IConfigurationOptions
[Range(60, 86400)] [Range(60, 86400)]
public int TempCleanupJobIntervalSeconds { get; set; } = 3600; public int TempCleanupJobIntervalSeconds { get; set; } = 3600;
}
} }
@@ -1,3 +1,4 @@
namespace Musify.Application.Contracts; namespace Musify.Application.Contracts
{
public record AudioTranscodeResult(int StatusCode, TimeSpan Duration); public record AudioTranscodeResult(int StatusCode, TimeSpan Duration);
}
@@ -1,6 +1,7 @@
namespace Musify.Application.Contracts; namespace Musify.Application.Contracts
public interface IAudioTranscoderService
{ {
public interface IAudioTranscoderService
{
public Task<AudioTranscodeResult> TranscodeToAudioFileAsync(Stream audioStream, string destinationPath, CancellationToken cancellationToken); public Task<AudioTranscodeResult> TranscodeToAudioFileAsync(Stream audioStream, string destinationPath, CancellationToken cancellationToken);
}
} }
@@ -2,10 +2,10 @@ using ErrorOr;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Musify.Domain.Entities; using Musify.Domain.Entities;
namespace Musify.Application.Contracts; namespace Musify.Application.Contracts
public interface IDatabase
{ {
public interface IDatabase
{
public DbSet<User> Users { get; } public DbSet<User> Users { get; }
public DbSet<PlayList> PlayLists { get; } public DbSet<PlayList> PlayLists { get; }
@@ -39,4 +39,5 @@ public interface IDatabase
public Task<int> SaveChangesAsync(CancellationToken cancellationToken); public Task<int> SaveChangesAsync(CancellationToken cancellationToken);
public Task<ErrorOr<Success>> TrySaveChangesAsync(Error onUniqueViolation, CancellationToken cancellationToken); public Task<ErrorOr<Success>> TrySaveChangesAsync(Error onUniqueViolation, CancellationToken cancellationToken);
}
} }
@@ -1,6 +1,7 @@
namespace Musify.Application.Contracts; namespace Musify.Application.Contracts
public interface IDatabaseTransaction : IAsyncDisposable
{ {
public interface IDatabaseTransaction : IAsyncDisposable
{
public Task CommitAsync(CancellationToken cancellationToken = default); public Task CommitAsync(CancellationToken cancellationToken = default);
}
} }
@@ -1,6 +1,7 @@
namespace Musify.Application.Contracts; namespace Musify.Application.Contracts
public interface IEventBus
{ {
public interface IEventBus
{
public Task PublishAsync<T>(T message, CancellationToken cancellationToken) where T : class; public Task PublishAsync<T>(T message, CancellationToken cancellationToken) where T : class;
}
} }
@@ -1,6 +1,7 @@
namespace Musify.Application.Contracts; namespace Musify.Application.Contracts
public interface IPictureService
{ {
public interface IPictureService
{
public Task<Stream> ResizePictureAsWebpAsync(Stream pictureStream, int width, int height, CancellationToken cancellationToken); public Task<Stream> ResizePictureAsWebpAsync(Stream pictureStream, int width, int height, CancellationToken cancellationToken);
}
} }
@@ -1,7 +1,7 @@
namespace Musify.Application.Contracts; namespace Musify.Application.Contracts
public interface IStorageService
{ {
public interface IStorageService
{
/// <summary>Opens an object for reading, or returns null when it does not exist.</summary> /// <summary>Opens an object for reading, or returns null when it does not exist.</summary>
public Task<Stream?> GetFileAsync(string bucket, string key, CancellationToken cancellationToken); public Task<Stream?> GetFileAsync(string bucket, string key, CancellationToken cancellationToken);
@@ -21,4 +21,5 @@ public interface IStorageService
CancellationToken cancellationToken); CancellationToken cancellationToken);
public IAsyncEnumerable<StorageObject> ListObjectsAsync(string bucket, string prefix, CancellationToken cancellationToken); public IAsyncEnumerable<StorageObject> ListObjectsAsync(string bucket, string prefix, CancellationToken cancellationToken);
}
} }
@@ -1,8 +1,9 @@
namespace Musify.Application.Contracts; namespace Musify.Application.Contracts
public interface IStreamTicketService
{ {
public interface IStreamTicketService
{
public StreamTicket IssueTicket(long? userId, string keyPrefix, long? maxBytes = null); public StreamTicket IssueTicket(long? userId, string keyPrefix, long? maxBytes = null);
} }
public record StreamTicket(string Token, int ExpiresInSeconds); public record StreamTicket(string Token, int ExpiresInSeconds);
}
@@ -1,3 +1,4 @@
namespace Musify.Application.Contracts; namespace Musify.Application.Contracts
{
public record ObjectMetaData(string ContentType, long ContentLength); public record ObjectMetaData(string ContentType, long ContentLength);
}
@@ -1,3 +1,4 @@
namespace Musify.Application.Contracts; namespace Musify.Application.Contracts
{
public record StorageObject(string Key, DateTime LastModifiedUtc); public record StorageObject(string Key, DateTime LastModifiedUtc);
}
@@ -1,3 +1,4 @@
namespace Musify.Application.Events; namespace Musify.Application.Events
{
public record AlbumPictureProcessed(Guid AlbumId); public record AlbumPictureProcessed(Guid AlbumId);
}
@@ -1,3 +1,4 @@
namespace Musify.Application.Events; namespace Musify.Application.Events
{
public record AlbumPictureProcessingFailed(Guid AlbumId); public record AlbumPictureProcessingFailed(Guid AlbumId);
}
@@ -1,6 +1,7 @@
namespace Musify.Application.Events; namespace Musify.Application.Events
{
public record AlbumProcessingFailed( public record AlbumProcessingFailed(
Guid AlbumId, Guid AlbumId,
string? Bucket, string? Bucket,
string? PictureKey); string? PictureKey);
}
@@ -1,11 +1,12 @@
using Musify.Application.Shared; using Musify.Application.Shared;
namespace Musify.Application.Events; namespace Musify.Application.Events
{
public record CreateAlbumResourcesEvent( public record CreateAlbumResourcesEvent(
Guid AlbumId, Guid AlbumId,
Guid PictureIntentId, Guid PictureIntentId,
string Bucket, string Bucket,
string PictureSourceKey, string PictureSourceKey,
string PictureDestinationKey, string PictureDestinationKey,
ImageSizes Sizes); ImageSizes Sizes);
}
@@ -1,11 +1,12 @@
using Musify.Application.Shared; using Musify.Application.Shared;
namespace Musify.Application.Events; namespace Musify.Application.Events
{
public record CreatePlayListResourcesEvent( public record CreatePlayListResourcesEvent(
Guid PlayListId, Guid PlayListId,
Guid PictureIntentId, Guid PictureIntentId,
string Bucket, string Bucket,
string PictureSourceKey, string PictureSourceKey,
string PictureDestinationKey, string PictureDestinationKey,
ImageSizes Sizes); ImageSizes Sizes);
}
@@ -1,8 +1,8 @@
using Musify.Application.Shared; using Musify.Application.Shared;
namespace Musify.Application.Events; namespace Musify.Application.Events
{
public record CreateTrackResourcesEvent( public record CreateTrackResourcesEvent(
Guid TrackId, Guid TrackId,
Guid PictureIntentId, Guid PictureIntentId,
Guid AudioIntentId, Guid AudioIntentId,
@@ -13,3 +13,4 @@ public record CreateTrackResourcesEvent(
string AudioDestinationKey, string AudioDestinationKey,
string AudioProcessedFolderKey, string AudioProcessedFolderKey,
ImageSizes Sizes); ImageSizes Sizes);
}
@@ -1,5 +1,6 @@
namespace Musify.Application.Events; namespace Musify.Application.Events
{
public record DeleteAlbumEvent( public record DeleteAlbumEvent(
Guid AlbumId, Guid AlbumId,
long UserId); long UserId);
}
@@ -1,5 +1,6 @@
namespace Musify.Application.Events; namespace Musify.Application.Events
{
public record DeletePlayListEvent( public record DeletePlayListEvent(
Guid PlayListId, Guid PlayListId,
long UserId); long UserId);
}
@@ -1,5 +1,6 @@
namespace Musify.Application.Events; namespace Musify.Application.Events
{
public record DeleteTrackEvent( public record DeleteTrackEvent(
Guid TrackId, Guid TrackId,
long UserId); long UserId);
}
@@ -1,3 +1,4 @@
namespace Musify.Application.Events; namespace Musify.Application.Events
{
public record PlayListPictureProcessed(Guid PlayListId); public record PlayListPictureProcessed(Guid PlayListId);
}
@@ -1,3 +1,4 @@
namespace Musify.Application.Events; namespace Musify.Application.Events
{
public record PlayListPictureProcessingFailed(Guid PlayListId); public record PlayListPictureProcessingFailed(Guid PlayListId);
}
@@ -1,6 +1,7 @@
namespace Musify.Application.Events; namespace Musify.Application.Events
{
public record PlayListProcessingFailed( public record PlayListProcessingFailed(
Guid PlayListId, Guid PlayListId,
string? Bucket, string? Bucket,
string? PictureKey); string? PictureKey);
}
@@ -1,3 +1,4 @@
namespace Musify.Application.Events; namespace Musify.Application.Events
{
public record TrackAudioProcessed(Guid TrackId); public record TrackAudioProcessed(Guid TrackId);
}
@@ -1,3 +1,4 @@
namespace Musify.Application.Events; namespace Musify.Application.Events
{
public record TrackAudioProcessingFailed(Guid TrackId); public record TrackAudioProcessingFailed(Guid TrackId);
}
@@ -1,3 +1,4 @@
namespace Musify.Application.Events; namespace Musify.Application.Events
{
public record TrackPictureProcessed(Guid TrackId); public record TrackPictureProcessed(Guid TrackId);
}
@@ -1,3 +1,4 @@
namespace Musify.Application.Events; namespace Musify.Application.Events
{
public record TrackPictureProcessingFailed(Guid TrackId); public record TrackPictureProcessingFailed(Guid TrackId);
}
@@ -1,7 +1,8 @@
namespace Musify.Application.Events; namespace Musify.Application.Events
{
public record TrackProcessingFailed( public record TrackProcessingFailed(
Guid TrackId, Guid TrackId,
string? Bucket, string? Bucket,
string? PictureKey, string? PictureKey,
string? AudioKey); string? AudioKey);
}
@@ -1,9 +1,10 @@
using Musify.Application.Shared; using Musify.Application.Shared;
namespace Musify.Application.Events; namespace Musify.Application.Events
{
public record UpdateAlbumPictureEvent( public record UpdateAlbumPictureEvent(
Guid AlbumId, Guid AlbumId,
string Bucket, string Bucket,
string SourceKey, string SourceKey,
ImageSizes Sizes); ImageSizes Sizes);
}
@@ -1,11 +1,12 @@
using Musify.Application.Shared; using Musify.Application.Shared;
namespace Musify.Application.Events; namespace Musify.Application.Events
{
public record UpdateAlbumPictureSourceEvent( public record UpdateAlbumPictureSourceEvent(
Guid AlbumId, Guid AlbumId,
Guid PictureIntentId, Guid PictureIntentId,
string Bucket, string Bucket,
string PictureSourceKey, string PictureSourceKey,
string PictureDestinationKey, string PictureDestinationKey,
ImageSizes Sizes); ImageSizes Sizes);
}
@@ -1,9 +1,10 @@
using Musify.Application.Shared; using Musify.Application.Shared;
namespace Musify.Application.Events; namespace Musify.Application.Events
{
public record UpdatePlayListPictureEvent( public record UpdatePlayListPictureEvent(
Guid PlayListId, Guid PlayListId,
string Bucket, string Bucket,
string SourceKey, string SourceKey,
ImageSizes Sizes); ImageSizes Sizes);
}
@@ -1,11 +1,12 @@
using Musify.Application.Shared; using Musify.Application.Shared;
namespace Musify.Application.Events; namespace Musify.Application.Events
{
public record UpdatePlayListPictureSourceEvent( public record UpdatePlayListPictureSourceEvent(
Guid PlayListId, Guid PlayListId,
Guid PictureIntentId, Guid PictureIntentId,
string Bucket, string Bucket,
string PictureSourceKey, string PictureSourceKey,
string PictureDestinationKey, string PictureDestinationKey,
ImageSizes Sizes); ImageSizes Sizes);
}
@@ -1,8 +1,9 @@
namespace Musify.Application.Events; namespace Musify.Application.Events
{
public record UpdateTrackAudioEvent( public record UpdateTrackAudioEvent(
Guid TrackId, Guid TrackId,
string SourceBucket, string SourceBucket,
string SourceKey, string SourceKey,
string DestinationBucket, string DestinationBucket,
string DestinationFolderKey); string DestinationFolderKey);
}
@@ -1,9 +1,10 @@
using Musify.Application.Shared; using Musify.Application.Shared;
namespace Musify.Application.Events; namespace Musify.Application.Events
{
public record UpdateTrackPictureEvent( public record UpdateTrackPictureEvent(
Guid TrackId, Guid TrackId,
string Bucket, string Bucket,
string SourceKey, string SourceKey,
ImageSizes Sizes); ImageSizes Sizes);
}
@@ -2,13 +2,13 @@ using Mediator;
using Musify.Application.Genres.Responses; using Musify.Application.Genres.Responses;
using Musify.Domain.ValueObjects; using Musify.Domain.ValueObjects;
namespace Musify.Application.Genres; namespace Musify.Application.Genres
public record GetAvailableGenresQuery : IQuery<IReadOnlyList<AvailableGenreResponse>>;
public class GetAvailableGenresQueryHandler
: IQueryHandler<GetAvailableGenresQuery, IReadOnlyList<AvailableGenreResponse>>
{ {
public record GetAvailableGenresQuery : IQuery<IReadOnlyList<AvailableGenreResponse>>;
public class GetAvailableGenresQueryHandler
: IQueryHandler<GetAvailableGenresQuery, IReadOnlyList<AvailableGenreResponse>>
{
public ValueTask<IReadOnlyList<AvailableGenreResponse>> Handle(GetAvailableGenresQuery request, CancellationToken cancellationToken) public ValueTask<IReadOnlyList<AvailableGenreResponse>> Handle(GetAvailableGenresQuery request, CancellationToken cancellationToken)
{ {
var all = Enum.GetValues<Genre>(); var all = Enum.GetValues<Genre>();
@@ -21,4 +21,5 @@ public class GetAvailableGenresQueryHandler
return ValueTask.FromResult(result); return ValueTask.FromResult(result);
} }
}
} }
@@ -4,13 +4,13 @@ using Musify.Application.Contracts;
using Musify.Application.Genres.Responses; using Musify.Application.Genres.Responses;
using Musify.Domain.ValueObjects; using Musify.Domain.ValueObjects;
namespace Musify.Application.Genres; namespace Musify.Application.Genres
public record GetGenresQuery : IQuery<IReadOnlyList<GenreResponse>>;
public class GetGenresQueryHandler(IDatabase database)
: IQueryHandler<GetGenresQuery, IReadOnlyList<GenreResponse>>
{ {
public record GetGenresQuery : IQuery<IReadOnlyList<GenreResponse>>;
public class GetGenresQueryHandler(IDatabase database)
: IQueryHandler<GetGenresQuery, IReadOnlyList<GenreResponse>>
{
public async ValueTask<IReadOnlyList<GenreResponse>> Handle(GetGenresQuery request, CancellationToken cancellationToken) public async ValueTask<IReadOnlyList<GenreResponse>> Handle(GetGenresQuery request, CancellationToken cancellationToken)
{ {
var rows = await database.TrackTags var rows = await database.TrackTags
@@ -26,4 +26,5 @@ public class GetGenresQueryHandler(IDatabase database)
.Select(row => new GenreResponse(row.Genre, row.TrackCount)) .Select(row => new GenreResponse(row.Genre, row.TrackCount))
.ToList(); .ToList();
} }
}
} }
@@ -1,5 +1,6 @@
using Musify.Domain.ValueObjects; using Musify.Domain.ValueObjects;
namespace Musify.Application.Genres.Responses; namespace Musify.Application.Genres.Responses
{
public record AvailableGenreResponse(Genre Genre, IReadOnlyList<Genre> IncompatibleWith); public record AvailableGenreResponse(Genre Genre, IReadOnlyList<Genre> IncompatibleWith);
}
@@ -1,5 +1,6 @@
using Musify.Domain.ValueObjects; using Musify.Domain.ValueObjects;
namespace Musify.Application.Genres.Responses; namespace Musify.Application.Genres.Responses
{
public record GenreResponse(Genre Genre, int TrackCount); public record GenreResponse(Genre Genre, int TrackCount);
}
@@ -5,17 +5,17 @@ using Musify.Application.Shared;
using Musify.Application.Tracks.Responses; using Musify.Application.Tracks.Responses;
using Musify.Domain.ValueObjects; using Musify.Domain.ValueObjects;
namespace Musify.Application.Likes; namespace Musify.Application.Likes
{
public record GetLikedTracksQuery( public record GetLikedTracksQuery(
long UserId, long UserId,
int PageNumber, int PageNumber,
int PageSize) int PageSize)
: IQuery<PaginatedResponse<TrackApplicationResponse>>; : IQuery<PaginatedResponse<TrackApplicationResponse>>;
public class GetLikedTracksQueryHandler(IDatabase database) public class GetLikedTracksQueryHandler(IDatabase database)
: IQueryHandler<GetLikedTracksQuery, PaginatedResponse<TrackApplicationResponse>> : IQueryHandler<GetLikedTracksQuery, PaginatedResponse<TrackApplicationResponse>>
{ {
public async ValueTask<PaginatedResponse<TrackApplicationResponse>> Handle(GetLikedTracksQuery request, CancellationToken cancellationToken) public async ValueTask<PaginatedResponse<TrackApplicationResponse>> Handle(GetLikedTracksQuery request, CancellationToken cancellationToken)
{ {
return await database.TrackLikes return await database.TrackLikes
@@ -27,4 +27,5 @@ public class GetLikedTracksQueryHandler(IDatabase database)
.SelectResponse() .SelectResponse()
.ToPaginatedAsync(new PageRequest(request.PageNumber, request.PageSize), cancellationToken); .ToPaginatedAsync(new PageRequest(request.PageNumber, request.PageSize), cancellationToken);
} }
}
} }
@@ -7,16 +7,16 @@ using Musify.Application.Shared;
using Musify.Domain.Entities; using Musify.Domain.Entities;
using Musify.Domain.ValueObjects; using Musify.Domain.ValueObjects;
namespace Musify.Application.Likes; namespace Musify.Application.Likes
{
public record ToggleTrackLikeCommand(long UserId, Guid TrackId) public record ToggleTrackLikeCommand(long UserId, Guid TrackId)
: ICommand<ErrorOr<bool>>; : ICommand<ErrorOr<bool>>;
public class ToggleTrackLikeCommandHandler( public class ToggleTrackLikeCommandHandler(
IDatabase database, IDatabase database,
ILogger<ToggleTrackLikeCommandHandler> logger) ILogger<ToggleTrackLikeCommandHandler> logger)
: ICommandHandler<ToggleTrackLikeCommand, ErrorOr<bool>> : ICommandHandler<ToggleTrackLikeCommand, ErrorOr<bool>>
{ {
public async ValueTask<ErrorOr<bool>> Handle(ToggleTrackLikeCommand request, CancellationToken cancellationToken) public async ValueTask<ErrorOr<bool>> Handle(ToggleTrackLikeCommand request, CancellationToken cancellationToken)
{ {
var trackExists = await database.Tracks var trackExists = await database.Tracks
@@ -55,4 +55,5 @@ public class ToggleTrackLikeCommandHandler(
return nowLiked; return nowLiked;
} }
}
} }
@@ -6,17 +6,17 @@ using Musify.Application.Contracts;
using Musify.Domain.Entities; using Musify.Domain.Entities;
using Musify.Domain.ValueObjects; using Musify.Domain.ValueObjects;
namespace Musify.Application.Mixes; namespace Musify.Application.Mixes
{
public record GenerateMixesForUserCommand(long UserId) public record GenerateMixesForUserCommand(long UserId)
: ICommand; : ICommand;
public class GenerateMixesForUserCommandHandler( public class GenerateMixesForUserCommandHandler(
IDatabase database, IDatabase database,
MixConfiguration mixConfiguration, MixConfiguration mixConfiguration,
ILogger<GenerateMixesForUserCommandHandler> logger) ILogger<GenerateMixesForUserCommandHandler> logger)
: ICommandHandler<GenerateMixesForUserCommand> : ICommandHandler<GenerateMixesForUserCommand>
{ {
private sealed record MixDraft(MixKind Kind, IReadOnlyList<Guid> TrackIds); private sealed record MixDraft(MixKind Kind, IReadOnlyList<Guid> TrackIds);
public async ValueTask<Unit> Handle(GenerateMixesForUserCommand request, CancellationToken cancellationToken) public async ValueTask<Unit> Handle(GenerateMixesForUserCommand request, CancellationToken cancellationToken)
@@ -127,4 +127,5 @@ public class GenerateMixesForUserCommandHandler(
} }
} }
} }
}
} }
@@ -5,14 +5,14 @@ using Musify.Application.Contracts;
using Musify.Application.Mixes.Responses; using Musify.Application.Mixes.Responses;
using Musify.Application.Shared; using Musify.Application.Shared;
namespace Musify.Application.Mixes; namespace Musify.Application.Mixes
{
public record GetMixByIdQuery(long UserId, Guid MixId) public record GetMixByIdQuery(long UserId, Guid MixId)
: IQuery<ErrorOr<MixApplicationResponse>>; : IQuery<ErrorOr<MixApplicationResponse>>;
public class GetMixByIdQueryHandler(IDatabase database) public class GetMixByIdQueryHandler(IDatabase database)
: IQueryHandler<GetMixByIdQuery, ErrorOr<MixApplicationResponse>> : IQueryHandler<GetMixByIdQuery, ErrorOr<MixApplicationResponse>>
{ {
public async ValueTask<ErrorOr<MixApplicationResponse>> Handle(GetMixByIdQuery request, CancellationToken cancellationToken) public async ValueTask<ErrorOr<MixApplicationResponse>> Handle(GetMixByIdQuery request, CancellationToken cancellationToken)
{ {
var mix = await database.Mixes var mix = await database.Mixes
@@ -28,4 +28,5 @@ public class GetMixByIdQueryHandler(IDatabase database)
return new MixApplicationResponse(mix.Id, mix.Kind, items.Count, items); return new MixApplicationResponse(mix.Id, mix.Kind, items.Count, items);
} }
}
} }
@@ -3,14 +3,14 @@ using Microsoft.EntityFrameworkCore;
using Musify.Application.Contracts; using Musify.Application.Contracts;
using Musify.Application.Mixes.Responses; using Musify.Application.Mixes.Responses;
namespace Musify.Application.Mixes; namespace Musify.Application.Mixes
{
public record GetMixesByUserIdQuery(long UserId) public record GetMixesByUserIdQuery(long UserId)
: IQuery<IReadOnlyList<MixApplicationResponse>>; : IQuery<IReadOnlyList<MixApplicationResponse>>;
public class GetMixesByUserIdQueryHandler(IDatabase database) public class GetMixesByUserIdQueryHandler(IDatabase database)
: IQueryHandler<GetMixesByUserIdQuery, IReadOnlyList<MixApplicationResponse>> : IQueryHandler<GetMixesByUserIdQuery, IReadOnlyList<MixApplicationResponse>>
{ {
public async ValueTask<IReadOnlyList<MixApplicationResponse>> Handle(GetMixesByUserIdQuery request, CancellationToken cancellationToken) public async ValueTask<IReadOnlyList<MixApplicationResponse>> Handle(GetMixesByUserIdQuery request, CancellationToken cancellationToken)
{ {
var mixes = await database.Mixes var mixes = await database.Mixes
@@ -47,4 +47,5 @@ public class GetMixesByUserIdQueryHandler(IDatabase database)
return responses; return responses;
} }
}
} }
@@ -4,11 +4,11 @@ using Musify.Application.Mixes.Responses;
using Musify.Domain.Entities; using Musify.Domain.Entities;
using Musify.Domain.ValueObjects; using Musify.Domain.ValueObjects;
namespace Musify.Application.Mixes; namespace Musify.Application.Mixes
internal static class MixItemMapper
{ {
internal static class MixItemMapper
{
public static async Task<List<MixItemApplicationResponse>> ToResponsesAsync( public static async Task<List<MixItemApplicationResponse>> ToResponsesAsync(
IDatabase database, IDatabase database,
IReadOnlyList<MixItem> orderedItems, IReadOnlyList<MixItem> orderedItems,
@@ -37,4 +37,5 @@ internal static class MixItemMapper
}) })
.ToList(); .ToList();
} }
}
} }
@@ -1,12 +1,13 @@
using Musify.Domain.ValueObjects; using Musify.Domain.ValueObjects;
namespace Musify.Application.Mixes.Responses; namespace Musify.Application.Mixes.Responses
{
public record MixApplicationResponse( public record MixApplicationResponse(
Guid Id, Guid Id,
MixKind Kind, MixKind Kind,
int ItemCount, int ItemCount,
IReadOnlyList<MixItemApplicationResponse> Items) IReadOnlyList<MixItemApplicationResponse> Items)
{ {
public const int CoverItemCount = 4; public const int CoverItemCount = 4;
}
} }
@@ -1,8 +1,9 @@
namespace Musify.Application.Mixes.Responses; namespace Musify.Application.Mixes.Responses
{
public record MixItemApplicationResponse( public record MixItemApplicationResponse(
Guid TrackId, Guid TrackId,
string Title, string Title,
string? Artist, string? Artist,
double DurationSeconds, double DurationSeconds,
int ListensCount); int ListensCount);
}
@@ -7,28 +7,28 @@ using Musify.Application.Contracts;
using Musify.Application.Shared; using Musify.Application.Shared;
using Musify.Domain.ValueObjects; using Musify.Domain.ValueObjects;
namespace Musify.Application.Pictures; namespace Musify.Application.Pictures
public enum CoverOwner
{ {
public enum CoverOwner
{
Track, Track,
Album, Album,
PlayList PlayList
} }
public record GetCoverQuery(CoverOwner Owner, Guid Id, PictureSize Size = PictureSize.Medium) public record GetCoverQuery(CoverOwner Owner, Guid Id, PictureSize Size = PictureSize.Medium)
: IQuery<ErrorOr<CoverLocation>>; : IQuery<ErrorOr<CoverLocation>>;
public record CoverLocation(string Bucket, string Key, string ContentType); public record CoverLocation(string Bucket, string Key, string ContentType);
public class GetCoverQueryHandler( public class GetCoverQueryHandler(
IDatabase database, IDatabase database,
ApplicationStorageConfiguration storageConfiguration, ApplicationStorageConfiguration storageConfiguration,
TrackConfiguration trackConfiguration, TrackConfiguration trackConfiguration,
AlbumConfiguration albumConfiguration, AlbumConfiguration albumConfiguration,
PlayListConfiguration playListConfiguration) PlayListConfiguration playListConfiguration)
: IQueryHandler<GetCoverQuery, ErrorOr<CoverLocation>> : IQueryHandler<GetCoverQuery, ErrorOr<CoverLocation>>
{ {
private sealed record CoverSource(long OwnerUserId, string? Original, string? Small, string? Medium, string? Large); private sealed record CoverSource(long OwnerUserId, string? Original, string? Small, string? Medium, string? Large);
public async ValueTask<ErrorOr<CoverLocation>> Handle(GetCoverQuery request, CancellationToken cancellationToken) public async ValueTask<ErrorOr<CoverLocation>> Handle(GetCoverQuery request, CancellationToken cancellationToken)
@@ -72,4 +72,5 @@ public class GetCoverQueryHandler(
return new CoverLocation(storageConfiguration.Bucket, key, MimeUtility.GetMimeMapping(name)); return new CoverLocation(storageConfiguration.Bucket, key, MimeUtility.GetMimeMapping(name));
} }
}
} }
@@ -5,13 +5,13 @@ using Musify.Application.Shared;
using Musify.Domain.Entities; using Musify.Domain.Entities;
using Musify.Domain.ValueObjects; using Musify.Domain.ValueObjects;
namespace Musify.Application.Pictures; namespace Musify.Application.Pictures
/// <summary>What an album or playlist needs to switch to a freshly uploaded picture.</summary>
public sealed record PreparedPicture(UploadIntent Intent, EntityPictures Pictures, string FinalKey, ImageSizes Sizes);
public static class PictureSourceChange
{ {
/// <summary>What an album or playlist needs to switch to a freshly uploaded picture.</summary>
public sealed record PreparedPicture(UploadIntent Intent, EntityPictures Pictures, string FinalKey, ImageSizes Sizes);
public static class PictureSourceChange
{
public static async Task<ErrorOr<PreparedPicture>> PrepareAsync( public static async Task<ErrorOr<PreparedPicture>> PrepareAsync(
UploadIntentValidator uploadIntentValidator, UploadIntentValidator uploadIntentValidator,
Guid intentId, Guid intentId,
@@ -32,4 +32,5 @@ public static class PictureSourceChange
owner.Routes.BuildOriginalPicturePath(userId, intent.ObjectName), owner.Routes.BuildOriginalPicturePath(userId, intent.ObjectName),
owner.PicturesSizes.ToImageSizes(owner.Routes)); owner.PicturesSizes.ToImageSizes(owner.Routes));
} }
}
} }
@@ -1,6 +1,6 @@
namespace Musify.Application.Pictures.Responses; namespace Musify.Application.Pictures.Responses
{
public record PictureUploadResponse( public record PictureUploadResponse(
Guid IntentId, Guid IntentId,
string Bucket, string Bucket,
string Key, string Key,
@@ -8,3 +8,4 @@ public record PictureUploadResponse(
string ContentType, string ContentType,
int ExpiresInSeconds, int ExpiresInSeconds,
string UploadUrl); string UploadUrl);
}
@@ -7,17 +7,17 @@ using Musify.Application.Shared;
using Musify.Domain.Entities; using Musify.Domain.Entities;
using Musify.Domain.ValueObjects; using Musify.Domain.ValueObjects;
namespace Musify.Application.PlayLists; namespace Musify.Application.PlayLists
{
/// <summary>Appends tracks to a playlist in the given order, skipping the ones it already has.</summary> /// <summary>Appends tracks to a playlist in the given order, skipping the ones it already has.</summary>
public record AddTracksToPlayListCommand(long UserId, Guid PlayListId, IReadOnlyList<Guid> TrackIds) public record AddTracksToPlayListCommand(long UserId, Guid PlayListId, IReadOnlyList<Guid> TrackIds)
: ICommand<ErrorOr<Success>>; : ICommand<ErrorOr<Success>>;
public class AddTracksToPlayListCommandHandler( public class AddTracksToPlayListCommandHandler(
IDatabase database, IDatabase database,
ILogger<AddTracksToPlayListCommandHandler> logger) ILogger<AddTracksToPlayListCommandHandler> logger)
: ICommandHandler<AddTracksToPlayListCommand, ErrorOr<Success>> : ICommandHandler<AddTracksToPlayListCommand, ErrorOr<Success>>
{ {
public async ValueTask<ErrorOr<Success>> Handle(AddTracksToPlayListCommand request, CancellationToken cancellationToken) public async ValueTask<ErrorOr<Success>> Handle(AddTracksToPlayListCommand request, CancellationToken cancellationToken)
{ {
var playList = await database.PlayLists.FindOwnedAsync(request.PlayListId, request.UserId, cancellationToken); var playList = await database.PlayLists.FindOwnedAsync(request.PlayListId, request.UserId, cancellationToken);
@@ -70,4 +70,5 @@ public class AddTracksToPlayListCommandHandler(
return Result.Success; return Result.Success;
} }
}
} }
@@ -12,9 +12,9 @@ using Musify.Application.Shared;
using Musify.Domain.Entities; using Musify.Domain.Entities;
using Musify.Domain.ValueObjects; using Musify.Domain.ValueObjects;
namespace Musify.Application.PlayLists; namespace Musify.Application.PlayLists
{
public record CreatePlayListCommand( public record CreatePlayListCommand(
long UserId, long UserId,
string Name, string Name,
string? Description, string? Description,
@@ -22,14 +22,14 @@ public record CreatePlayListCommand(
PlayListVisibility Visibility = PlayListVisibility.Private) PlayListVisibility Visibility = PlayListVisibility.Private)
: ICommand<ErrorOr<PlayListApplicationResponse>>; : ICommand<ErrorOr<PlayListApplicationResponse>>;
public class CreatePlayListCommandHandler( public class CreatePlayListCommandHandler(
IEventBus eventBus, IEventBus eventBus,
IDatabase database, IDatabase database,
UploadIntentValidator uploadIntentValidator, UploadIntentValidator uploadIntentValidator,
ILogger<CreatePlayListCommandHandler> logger, ILogger<CreatePlayListCommandHandler> logger,
PlayListConfiguration playListConfiguration) PlayListConfiguration playListConfiguration)
: ICommandHandler<CreatePlayListCommand, ErrorOr<PlayListApplicationResponse>> : ICommandHandler<CreatePlayListCommand, ErrorOr<PlayListApplicationResponse>>
{ {
public async ValueTask<ErrorOr<PlayListApplicationResponse>> Handle(CreatePlayListCommand request, CancellationToken cancellationToken) public async ValueTask<ErrorOr<PlayListApplicationResponse>> Handle(CreatePlayListCommand request, CancellationToken cancellationToken)
{ {
var userExists = await database.Users var userExists = await database.Users
@@ -75,4 +75,5 @@ public class CreatePlayListCommandHandler(
return PlayListApplicationResponse.FromEntity(playList); return PlayListApplicationResponse.FromEntity(playList);
} }
}
} }
@@ -6,17 +6,17 @@ using Musify.Application.Events;
using Musify.Application.Shared; using Musify.Application.Shared;
using Musify.Domain.ValueObjects; using Musify.Domain.ValueObjects;
namespace Musify.Application.PlayLists; namespace Musify.Application.PlayLists
{
public record DeletePlayListCommand(long UserId, Guid PlayListId) public record DeletePlayListCommand(long UserId, Guid PlayListId)
: ICommand<ErrorOr<Success>>; : ICommand<ErrorOr<Success>>;
public class DeletePlayListCommandHandler( public class DeletePlayListCommandHandler(
IDatabase database, IDatabase database,
IEventBus eventBus, IEventBus eventBus,
ILogger<DeletePlayListCommandHandler> logger) ILogger<DeletePlayListCommandHandler> logger)
: ICommandHandler<DeletePlayListCommand, ErrorOr<Success>> : ICommandHandler<DeletePlayListCommand, ErrorOr<Success>>
{ {
public async ValueTask<ErrorOr<Success>> Handle(DeletePlayListCommand request, CancellationToken cancellationToken) public async ValueTask<ErrorOr<Success>> Handle(DeletePlayListCommand request, CancellationToken cancellationToken)
{ {
var found = await database.PlayLists.FindOwnedAsync(request.PlayListId, request.UserId, cancellationToken); var found = await database.PlayLists.FindOwnedAsync(request.PlayListId, request.UserId, cancellationToken);
@@ -33,4 +33,5 @@ public class DeletePlayListCommandHandler(
return Result.Success; return Result.Success;
} }
}
} }
@@ -5,14 +5,14 @@ using Musify.Application.Contracts;
using Musify.Application.PlayLists.Responses; using Musify.Application.PlayLists.Responses;
using Musify.Application.Shared; using Musify.Application.Shared;
namespace Musify.Application.PlayLists; namespace Musify.Application.PlayLists
{
public record GetPlayListByIdQuery(Guid Id, long? RequestingUserId = null) public record GetPlayListByIdQuery(Guid Id, long? RequestingUserId = null)
: IQuery<ErrorOr<PlayListApplicationResponse>>; : IQuery<ErrorOr<PlayListApplicationResponse>>;
public class GetPlayListByIdQueryHandler(IDatabase database) public class GetPlayListByIdQueryHandler(IDatabase database)
: IQueryHandler<GetPlayListByIdQuery, ErrorOr<PlayListApplicationResponse>> : IQueryHandler<GetPlayListByIdQuery, ErrorOr<PlayListApplicationResponse>>
{ {
public async ValueTask<ErrorOr<PlayListApplicationResponse>> Handle(GetPlayListByIdQuery request, CancellationToken cancellationToken) public async ValueTask<ErrorOr<PlayListApplicationResponse>> Handle(GetPlayListByIdQuery request, CancellationToken cancellationToken)
{ {
var playList = await database.PlayLists var playList = await database.PlayLists
@@ -27,4 +27,5 @@ public class GetPlayListByIdQueryHandler(IDatabase database)
? AppErrors.NotFound("PlayList", request.Id) ? AppErrors.NotFound("PlayList", request.Id)
: playList; : playList;
} }
}
} }
@@ -7,18 +7,18 @@ using Musify.Application.Shared;
using Musify.Application.Tracks.Responses; using Musify.Application.Tracks.Responses;
using Musify.Domain.ValueObjects; using Musify.Domain.ValueObjects;
namespace Musify.Application.PlayLists; namespace Musify.Application.PlayLists
{
public record GetPlayListTracksQuery( public record GetPlayListTracksQuery(
Guid PlayListId, Guid PlayListId,
int PageNumber, int PageNumber,
int PageSize, int PageSize,
long? ViewerId = null) long? ViewerId = null)
: IQuery<ErrorOr<PaginatedResponse<TrackApplicationResponse>>>; : IQuery<ErrorOr<PaginatedResponse<TrackApplicationResponse>>>;
public class GetPlayListTracksQueryHandler(IDatabase database) public class GetPlayListTracksQueryHandler(IDatabase database)
: IQueryHandler<GetPlayListTracksQuery, ErrorOr<PaginatedResponse<TrackApplicationResponse>>> : IQueryHandler<GetPlayListTracksQuery, ErrorOr<PaginatedResponse<TrackApplicationResponse>>>
{ {
public async ValueTask<ErrorOr<PaginatedResponse<TrackApplicationResponse>>> Handle(GetPlayListTracksQuery request, CancellationToken cancellationToken) public async ValueTask<ErrorOr<PaginatedResponse<TrackApplicationResponse>>> Handle(GetPlayListTracksQuery request, CancellationToken cancellationToken)
{ {
var playListVisible = await database.PlayLists var playListVisible = await database.PlayLists
@@ -38,4 +38,5 @@ public class GetPlayListTracksQueryHandler(IDatabase database)
.SelectResponse() .SelectResponse()
.ToPaginatedAsync(new PageRequest(request.PageNumber, request.PageSize), cancellationToken); .ToPaginatedAsync(new PageRequest(request.PageNumber, request.PageSize), cancellationToken);
} }
}
} }
@@ -4,9 +4,9 @@ using Musify.Application.Contracts;
using Musify.Application.PlayLists.Responses; using Musify.Application.PlayLists.Responses;
using Musify.Application.Shared; using Musify.Application.Shared;
namespace Musify.Application.PlayLists; namespace Musify.Application.PlayLists
{
public record GetPlayListsByUserIdQuery( public record GetPlayListsByUserIdQuery(
long UserId, long UserId,
string? Name, string? Name,
int PageNumber, int PageNumber,
@@ -14,9 +14,9 @@ public record GetPlayListsByUserIdQuery(
long? ViewerId = null) long? ViewerId = null)
: IQuery<PaginatedResponse<PlayListApplicationResponse>>; : IQuery<PaginatedResponse<PlayListApplicationResponse>>;
public class GetPlayListsByUserIdQueryHandler(IDatabase database) public class GetPlayListsByUserIdQueryHandler(IDatabase database)
: IQueryHandler<GetPlayListsByUserIdQuery, PaginatedResponse<PlayListApplicationResponse>> : IQueryHandler<GetPlayListsByUserIdQuery, PaginatedResponse<PlayListApplicationResponse>>
{ {
public async ValueTask<PaginatedResponse<PlayListApplicationResponse>> Handle(GetPlayListsByUserIdQuery request, CancellationToken cancellationToken) public async ValueTask<PaginatedResponse<PlayListApplicationResponse>> Handle(GetPlayListsByUserIdQuery request, CancellationToken cancellationToken)
{ {
var playListsQuery = database.PlayLists var playListsQuery = database.PlayLists
@@ -37,4 +37,5 @@ public class GetPlayListsByUserIdQueryHandler(IDatabase database)
.SelectResponse() .SelectResponse()
.ToPaginatedAsync(new PageRequest(request.PageNumber, request.PageSize), cancellationToken); .ToPaginatedAsync(new PageRequest(request.PageNumber, request.PageSize), cancellationToken);
} }
}
} }
@@ -5,16 +5,16 @@ using Microsoft.Extensions.Logging;
using Musify.Application.Contracts; using Musify.Application.Contracts;
using Musify.Application.Shared; using Musify.Application.Shared;
namespace Musify.Application.PlayLists; namespace Musify.Application.PlayLists
{
public record RemoveTrackFromPlayListCommand(long UserId, Guid PlayListId, Guid TrackId) public record RemoveTrackFromPlayListCommand(long UserId, Guid PlayListId, Guid TrackId)
: ICommand<ErrorOr<Success>>; : ICommand<ErrorOr<Success>>;
public class RemoveTrackFromPlayListCommandHandler( public class RemoveTrackFromPlayListCommandHandler(
IDatabase database, IDatabase database,
ILogger<RemoveTrackFromPlayListCommandHandler> logger) ILogger<RemoveTrackFromPlayListCommandHandler> logger)
: ICommandHandler<RemoveTrackFromPlayListCommand, ErrorOr<Success>> : ICommandHandler<RemoveTrackFromPlayListCommand, ErrorOr<Success>>
{ {
public async ValueTask<ErrorOr<Success>> Handle(RemoveTrackFromPlayListCommand request, CancellationToken cancellationToken) public async ValueTask<ErrorOr<Success>> Handle(RemoveTrackFromPlayListCommand request, CancellationToken cancellationToken)
{ {
var playList = await database.PlayLists.FindOwnedAsync(request.PlayListId, request.UserId, cancellationToken); var playList = await database.PlayLists.FindOwnedAsync(request.PlayListId, request.UserId, cancellationToken);
@@ -42,4 +42,5 @@ public class RemoveTrackFromPlayListCommandHandler(
return Result.Success; return Result.Success;
} }
}
} }
@@ -5,20 +5,21 @@ using Musify.Application.Pictures.Responses;
using Musify.Application.Services; using Musify.Application.Services;
using Musify.Domain.ValueObjects; using Musify.Domain.ValueObjects;
namespace Musify.Application.PlayLists; namespace Musify.Application.PlayLists
{
public record RequestPlayListPictureUploadCommand( public record RequestPlayListPictureUploadCommand(
long UserId, long UserId,
string FileType, string FileType,
string ContentType, string ContentType,
long? ExpectedSizeBytes = null) long? ExpectedSizeBytes = null)
: ICommand<ErrorOr<PictureUploadResponse>>; : ICommand<ErrorOr<PictureUploadResponse>>;
public class RequestPlayListPictureUploadCommandHandler(UploadIntentIssuer issuer, PlayListConfiguration playListConfiguration) public class RequestPlayListPictureUploadCommandHandler(UploadIntentIssuer issuer, PlayListConfiguration playListConfiguration)
: ICommandHandler<RequestPlayListPictureUploadCommand, ErrorOr<PictureUploadResponse>> : ICommandHandler<RequestPlayListPictureUploadCommand, ErrorOr<PictureUploadResponse>>
{ {
public async ValueTask<ErrorOr<PictureUploadResponse>> Handle(RequestPlayListPictureUploadCommand request, CancellationToken cancellationToken) => public async ValueTask<ErrorOr<PictureUploadResponse>> Handle(RequestPlayListPictureUploadCommand request, CancellationToken cancellationToken) =>
await issuer.IssuePictureAsync( await issuer.IssuePictureAsync(
request.UserId, UploadIntentPurpose.PlayListPicture, playListConfiguration, request.UserId, UploadIntentPurpose.PlayListPicture, playListConfiguration,
request.FileType, request.ContentType, request.ExpectedSizeBytes, cancellationToken); request.FileType, request.ContentType, request.ExpectedSizeBytes, cancellationToken);
}
} }
@@ -2,9 +2,9 @@ using Musify.Domain.Entities;
using Musify.Domain.ValueObjects; using Musify.Domain.ValueObjects;
namespace Musify.Application.PlayLists.Responses; namespace Musify.Application.PlayLists.Responses
{
public record PlayListApplicationResponse( public record PlayListApplicationResponse(
Guid Id, Guid Id,
string Name, string Name,
string? Description, string? Description,
@@ -18,7 +18,7 @@ public record PlayListApplicationResponse(
long OwnerUserId, long OwnerUserId,
int TrackCount, int TrackCount,
double DurationSeconds) double DurationSeconds)
{ {
public const int CoverTrackCount = PlayListProjections.CoverTrackCount; public const int CoverTrackCount = PlayListProjections.CoverTrackCount;
public static PlayListApplicationResponse FromEntity( public static PlayListApplicationResponse FromEntity(
@@ -42,4 +42,5 @@ public record PlayListApplicationResponse(
trackCount, trackCount,
durationSeconds); durationSeconds);
} }
}
} }
@@ -1,10 +1,10 @@
using Musify.Domain.Entities; using Musify.Domain.Entities;
using Musify.Domain.ValueObjects; using Musify.Domain.ValueObjects;
namespace Musify.Application.PlayLists.Responses; namespace Musify.Application.PlayLists.Responses
public static class PlayListProjections
{ {
public static class PlayListProjections
{
public const int CoverTrackCount = 4; public const int CoverTrackCount = 4;
public static IQueryable<PlayList> Active(this IQueryable<PlayList> query) => public static IQueryable<PlayList> Active(this IQueryable<PlayList> query) =>
@@ -34,4 +34,5 @@ public static class PlayListProjections
playList.PlayListTracks playList.PlayListTracks
.Where(playListTrack => playListTrack.Track.LifeCycleStatus == LifeCycleStatus.Active) .Where(playListTrack => playListTrack.Track.LifeCycleStatus == LifeCycleStatus.Active)
.Sum(playListTrack => playListTrack.Track.DurationSeconds))); .Sum(playListTrack => playListTrack.Track.DurationSeconds)));
}
} }
@@ -11,9 +11,9 @@ using Musify.Application.Services;
using Musify.Application.Shared; using Musify.Application.Shared;
using Musify.Domain.ValueObjects; using Musify.Domain.ValueObjects;
namespace Musify.Application.PlayLists; namespace Musify.Application.PlayLists
{
public record UpdatePlayListCommand( public record UpdatePlayListCommand(
long UserId, long UserId,
Guid PlayListId, Guid PlayListId,
string? NewName, string? NewName,
@@ -22,14 +22,14 @@ public record UpdatePlayListCommand(
PlayListVisibility? NewVisibility = null) PlayListVisibility? NewVisibility = null)
: ICommand<ErrorOr<PlayListApplicationResponse>>; : ICommand<ErrorOr<PlayListApplicationResponse>>;
public class UpdatePlayListCommandHandler( public class UpdatePlayListCommandHandler(
IEventBus eventBus, IEventBus eventBus,
IDatabase database, IDatabase database,
UploadIntentValidator uploadIntentValidator, UploadIntentValidator uploadIntentValidator,
ILogger<UpdatePlayListCommandHandler> logger, ILogger<UpdatePlayListCommandHandler> logger,
PlayListConfiguration playListConfiguration) PlayListConfiguration playListConfiguration)
: ICommandHandler<UpdatePlayListCommand, ErrorOr<PlayListApplicationResponse>> : ICommandHandler<UpdatePlayListCommand, ErrorOr<PlayListApplicationResponse>>
{ {
public async ValueTask<ErrorOr<PlayListApplicationResponse>> Handle(UpdatePlayListCommand request, CancellationToken cancellationToken) public async ValueTask<ErrorOr<PlayListApplicationResponse>> Handle(UpdatePlayListCommand request, CancellationToken cancellationToken)
{ {
var found = await database.PlayLists.FindOwnedAsync(request.PlayListId, request.UserId, cancellationToken); var found = await database.PlayLists.FindOwnedAsync(request.PlayListId, request.UserId, cancellationToken);
@@ -77,4 +77,5 @@ public class UpdatePlayListCommandHandler(
.SelectResponse() .SelectResponse()
.SingleAsync(cancellationToken); .SingleAsync(cancellationToken);
} }
}
} }
@@ -2,15 +2,15 @@ using Musify.Application.Configuration;
using Musify.Application.Contracts; using Musify.Application.Contracts;
using Musify.Application.Tracks.Responses; using Musify.Application.Tracks.Responses;
namespace Musify.Application.Services; namespace Musify.Application.Services
{
/// <summary>Builds the stream URL and signed ticket for a processed track. It never touches the database.</summary> /// <summary>Builds the stream URL and signed ticket for a processed track. It never touches the database.</summary>
public sealed class TrackStreamIssuer( public sealed class TrackStreamIssuer(
IStreamTicketService ticketService, IStreamTicketService ticketService,
TrackConfiguration trackConfiguration, TrackConfiguration trackConfiguration,
StreamGatewayConfiguration streamGatewayConfiguration, StreamGatewayConfiguration streamGatewayConfiguration,
PlaybackConfiguration playbackConfiguration) PlaybackConfiguration playbackConfiguration)
{ {
public TrackStreamResponse Issue(string audioFolderName, long? userId, Guid? listenId) public TrackStreamResponse Issue(string audioFolderName, long? userId, Guid? listenId)
{ {
var folderPath = trackConfiguration.Routes.BuildProcessedAudioPath(audioFolderName); var folderPath = trackConfiguration.Routes.BuildProcessedAudioPath(audioFolderName);
@@ -32,4 +32,5 @@ public sealed class TrackStreamIssuer(
return (long)playbackConfiguration.AnonymousFragmentSeconds * playbackConfiguration.EstimatedAudioBytesPerSecond; return (long)playbackConfiguration.AnonymousFragmentSeconds * playbackConfiguration.EstimatedAudioBytesPerSecond;
} }
}
} }
@@ -8,31 +8,31 @@ using Musify.Application.Shared;
using Musify.Domain.Entities; using Musify.Domain.Entities;
using Musify.Domain.ValueObjects; using Musify.Domain.ValueObjects;
namespace Musify.Application.Services; namespace Musify.Application.Services
{
public sealed record UploadRequest( public sealed record UploadRequest(
UploadIntentPurpose Purpose, UploadIntentPurpose Purpose,
PictureRoutes Routes, PictureRoutes Routes,
string FileType, string FileType,
string ContentType, string ContentType,
long? ExpectedSizeBytes); long? ExpectedSizeBytes);
public sealed record IssuedUpload( public sealed record IssuedUpload(
Guid IntentId, Guid IntentId,
string Key, string Key,
string ObjectName, string ObjectName,
string ContentType, string ContentType,
string UploadUrl); string UploadUrl);
/// <summary>Issues pre-signed upload URLs and the upload intents that track them, checking the user quota first.</summary> /// <summary>Issues pre-signed upload URLs and the upload intents that track them, checking the user quota first.</summary>
public sealed class UploadIntentIssuer( public sealed class UploadIntentIssuer(
IDatabase database, IDatabase database,
IStorageService storageService, IStorageService storageService,
UploadIntentValidator uploadIntentValidator, UploadIntentValidator uploadIntentValidator,
ApplicationStorageConfiguration storageConfiguration, ApplicationStorageConfiguration storageConfiguration,
UploadIntentConfiguration uploadIntentConfiguration, UploadIntentConfiguration uploadIntentConfiguration,
ILogger<UploadIntentIssuer> logger) ILogger<UploadIntentIssuer> logger)
{ {
public async Task<ErrorOr<IReadOnlyList<IssuedUpload>>> IssueAsync( public async Task<ErrorOr<IReadOnlyList<IssuedUpload>>> IssueAsync(
long userId, long userId,
IReadOnlyList<UploadRequest> requests, IReadOnlyList<UploadRequest> requests,
@@ -130,4 +130,5 @@ public sealed class UploadIntentIssuer(
purpose == UploadIntentPurpose.TrackAudio purpose == UploadIntentPurpose.TrackAudio
? uploadIntentConfiguration.DefaultExpectedAudioSizeBytes ? uploadIntentConfiguration.DefaultExpectedAudioSizeBytes
: uploadIntentConfiguration.DefaultExpectedPictureSizeBytes; : uploadIntentConfiguration.DefaultExpectedPictureSizeBytes;
}
} }
@@ -6,13 +6,13 @@ using Musify.Application.Shared;
using Musify.Domain.Entities; using Musify.Domain.Entities;
using Musify.Domain.ValueObjects; using Musify.Domain.ValueObjects;
namespace Musify.Application.Services; namespace Musify.Application.Services
{
public sealed class UploadIntentValidator( public sealed class UploadIntentValidator(
IDatabase database, IDatabase database,
IStorageService storageService, IStorageService storageService,
UploadIntentConfiguration config) UploadIntentConfiguration config)
{ {
public async Task<ErrorOr<Success>> CheckQuotaAsync( public async Task<ErrorOr<Success>> CheckQuotaAsync(
long userId, long userId,
long requiredBytes, long requiredBytes,
@@ -74,4 +74,5 @@ public sealed class UploadIntentValidator(
return intent; return intent;
} }
}
} }
@@ -1,9 +1,9 @@
using ErrorOr; using ErrorOr;
namespace Musify.Application.Shared; namespace Musify.Application.Shared
public static class AppErrors
{ {
public static class AppErrors
{
public static Error NotFound(string entity, object id) => public static Error NotFound(string entity, object id) =>
Error.NotFound($"{entity}.NotFound", $"{entity} {id} was not found."); Error.NotFound($"{entity}.NotFound", $"{entity} {id} was not found.");
@@ -12,4 +12,5 @@ public static class AppErrors
public static Error Conflict(string code, string description) => public static Error Conflict(string code, string description) =>
Error.Conflict(code, description); Error.Conflict(code, description);
}
} }
@@ -1,11 +1,12 @@
namespace Musify.Application.Shared; namespace Musify.Application.Shared
{
public record ImageSize( public record ImageSize(
string SavePath, string SavePath,
int Width, int Width,
int Height); int Height);
public record ImageSizes( public record ImageSizes(
ImageSize Small, ImageSize Small,
ImageSize Medium, ImageSize Medium,
ImageSize Large); ImageSize Large);
}
@@ -3,10 +3,10 @@ using Microsoft.EntityFrameworkCore;
using Musify.Domain.Abstractions; using Musify.Domain.Abstractions;
using Musify.Domain.ValueObjects; using Musify.Domain.ValueObjects;
namespace Musify.Application.Shared; namespace Musify.Application.Shared
public static class OwnershipExtensions
{ {
public static class OwnershipExtensions
{
/// <summary> /// <summary>
/// Loads an active, tracked entity owned by <paramref name="userId"/>: NotFound when it is missing /// Loads an active, tracked entity owned by <paramref name="userId"/>: NotFound when it is missing
/// or not active, Forbidden when someone else owns it. /// or not active, Forbidden when someone else owns it.
@@ -25,4 +25,5 @@ public static class OwnershipExtensions
? entity ? entity
: AppErrors.Forbidden(typeof(T).Name, id); : AppErrors.Forbidden(typeof(T).Name, id);
} }
}
} }
@@ -1,13 +1,13 @@
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
namespace Musify.Application.Shared; namespace Musify.Application.Shared
public record PageRequest(int PageNumber = 1, int PageSize = 20)
{ {
public record PageRequest(int PageNumber = 1, int PageSize = 20)
{
public const int MaxPageSize = 100; public const int MaxPageSize = 100;
} }
public record PaginatedResponse<T>( public record PaginatedResponse<T>(
IReadOnlyList<T> Items, IReadOnlyList<T> Items,
int PageNumber, int PageNumber,
int PageSize, int PageSize,
@@ -16,8 +16,8 @@ public record PaginatedResponse<T>(
bool HasNextPage, bool HasNextPage,
bool HasPreviousPage); bool HasPreviousPage);
public static class PaginationExtensions public static class PaginationExtensions
{ {
/// <summary>Counts and pages the same (already filtered, ordered and projected) query.</summary> /// <summary>Counts and pages the same (already filtered, ordered and projected) query.</summary>
public static async Task<PaginatedResponse<T>> ToPaginatedAsync<T>( public static async Task<PaginatedResponse<T>> ToPaginatedAsync<T>(
this IQueryable<T> query, PageRequest page, CancellationToken cancellationToken) this IQueryable<T> query, PageRequest page, CancellationToken cancellationToken)
@@ -42,4 +42,5 @@ public static class PaginationExtensions
HasNextPage: pageNumber < pageCount, HasNextPage: pageNumber < pageCount,
HasPreviousPage: pageNumber > 1 && pageCount > 0); HasPreviousPage: pageNumber > 1 && pageCount > 0);
} }
}
} }
@@ -1,11 +1,12 @@
namespace Musify.Application.Shared; namespace Musify.Application.Shared
public static class StorageKey
{ {
public static class StorageKey
{
public static string Combine(params string[] segments) public static string Combine(params string[] segments)
{ {
return string.Join('/', segments return string.Join('/', segments
.Where(static s => !string.IsNullOrWhiteSpace(s)) .Where(static s => !string.IsNullOrWhiteSpace(s))
.Select(static s => s.Trim().Trim('/', '\\'))); .Select(static s => s.Trim().Trim('/', '\\')));
} }
}
} }
@@ -1,6 +1,7 @@
namespace Musify.Application.Shared; namespace Musify.Application.Shared
public static class TextNormalizer
{ {
public static class TextNormalizer
{
public static string Normalize(string value) => value.Trim().ToUpperInvariant(); public static string Normalize(string value) => value.Trim().ToUpperInvariant();
}
} }
@@ -11,9 +11,9 @@ using Musify.Application.Tracks.Responses;
using Musify.Domain.Entities; using Musify.Domain.Entities;
using Musify.Domain.ValueObjects; using Musify.Domain.ValueObjects;
namespace Musify.Application.Tracks; namespace Musify.Application.Tracks
{
public record CreateTrackCommand( public record CreateTrackCommand(
long UserId, long UserId,
string Title, string Title,
Guid PictureIntentId, Guid PictureIntentId,
@@ -22,14 +22,14 @@ public record CreateTrackCommand(
bool IsExplicit = false) bool IsExplicit = false)
: ICommand<ErrorOr<TrackApplicationResponse>>; : ICommand<ErrorOr<TrackApplicationResponse>>;
public class CreateTrackCommandHandler( public class CreateTrackCommandHandler(
IDatabase database, IDatabase database,
IEventBus eventBus, IEventBus eventBus,
UploadIntentValidator uploadIntentValidator, UploadIntentValidator uploadIntentValidator,
ILogger<CreateTrackCommandHandler> logger, ILogger<CreateTrackCommandHandler> logger,
TrackConfiguration trackConfiguration) TrackConfiguration trackConfiguration)
: ICommandHandler<CreateTrackCommand, ErrorOr<TrackApplicationResponse>> : ICommandHandler<CreateTrackCommand, ErrorOr<TrackApplicationResponse>>
{ {
public async ValueTask<ErrorOr<TrackApplicationResponse>> Handle(CreateTrackCommand request, CancellationToken cancellationToken) public async ValueTask<ErrorOr<TrackApplicationResponse>> Handle(CreateTrackCommand request, CancellationToken cancellationToken)
{ {
var user = await database.Users var user = await database.Users
@@ -110,4 +110,5 @@ public class CreateTrackCommandHandler(
return TrackApplicationResponse.FromEntity(trackEntity, listensCount: 0); return TrackApplicationResponse.FromEntity(trackEntity, listensCount: 0);
} }
}
} }
@@ -3,15 +3,15 @@ using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Musify.Application.Contracts; using Musify.Application.Contracts;
namespace Musify.Application.Tracks; namespace Musify.Application.Tracks
{
public record DeleteStaleUncountedListensCommand : ICommand<int>;
public record DeleteStaleUncountedListensCommand : ICommand<int>; public class DeleteStaleUncountedListensCommandHandler(
public class DeleteStaleUncountedListensCommandHandler(
IDatabase database, IDatabase database,
ILogger<DeleteStaleUncountedListensCommandHandler> logger) ILogger<DeleteStaleUncountedListensCommandHandler> logger)
: ICommandHandler<DeleteStaleUncountedListensCommand, int> : ICommandHandler<DeleteStaleUncountedListensCommand, int>
{ {
public async ValueTask<int> Handle(DeleteStaleUncountedListensCommand request, CancellationToken cancellationToken) public async ValueTask<int> Handle(DeleteStaleUncountedListensCommand request, CancellationToken cancellationToken)
{ {
var cutoff = DateTime.UtcNow.AddDays(-ListeningRules.UncountedRetentionDays); var cutoff = DateTime.UtcNow.AddDays(-ListeningRules.UncountedRetentionDays);
@@ -29,4 +29,5 @@ public class DeleteStaleUncountedListensCommandHandler(
logger.LogInformation("Deleted {Count} uncounted listens older than {Days} days", stale.Count, ListeningRules.UncountedRetentionDays); logger.LogInformation("Deleted {Count} uncounted listens older than {Days} days", stale.Count, ListeningRules.UncountedRetentionDays);
return stale.Count; return stale.Count;
} }
}
} }
@@ -6,19 +6,19 @@ using Musify.Application.Events;
using Musify.Application.Shared; using Musify.Application.Shared;
using Musify.Domain.ValueObjects; using Musify.Domain.ValueObjects;
namespace Musify.Application.Tracks; namespace Musify.Application.Tracks
{
public record DeleteTrackCommand( public record DeleteTrackCommand(
long UserId, long UserId,
Guid TrackId) Guid TrackId)
: ICommand<ErrorOr<Success>>; : ICommand<ErrorOr<Success>>;
public class DeleteTrackCommandHandler( public class DeleteTrackCommandHandler(
IDatabase database, IDatabase database,
IEventBus eventBus, IEventBus eventBus,
ILogger<DeleteTrackCommandHandler> logger) ILogger<DeleteTrackCommandHandler> logger)
: ICommandHandler<DeleteTrackCommand, ErrorOr<Success>> : ICommandHandler<DeleteTrackCommand, ErrorOr<Success>>
{ {
public async ValueTask<ErrorOr<Success>> Handle(DeleteTrackCommand request, CancellationToken cancellationToken) public async ValueTask<ErrorOr<Success>> Handle(DeleteTrackCommand request, CancellationToken cancellationToken)
{ {
var found = await database.Tracks.FindOwnedAsync(request.TrackId, request.UserId, cancellationToken); var found = await database.Tracks.FindOwnedAsync(request.TrackId, request.UserId, cancellationToken);
@@ -38,4 +38,5 @@ public class DeleteTrackCommandHandler(
return Result.Success; return Result.Success;
} }
}
} }

Some files were not shown because too many files have changed in this diff Show More