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

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