diff --git a/backend/.editorconfig b/backend/.editorconfig
index bcc34a2..e78ac4b 100644
--- a/backend/.editorconfig
+++ b/backend/.editorconfig
@@ -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
diff --git a/backend/CLAUDE.md b/backend/CLAUDE.md
index a8545e7..5a29132 100644
--- a/backend/CLAUDE.md
+++ b/backend/CLAUDE.md
@@ -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`).
diff --git a/backend/Core/Musify.Application/Albums/AddTracksToAlbum.cs b/backend/Core/Musify.Application/Albums/AddTracksToAlbum.cs
index 39a0679..455660e 100644
--- a/backend/Core/Musify.Application/Albums/AddTracksToAlbum.cs
+++ b/backend/Core/Musify.Application/Albums/AddTracksToAlbum.cs
@@ -7,71 +7,72 @@ using Musify.Application.Shared;
using Musify.Domain.Entities;
using Musify.Domain.ValueObjects;
-namespace Musify.Application.Albums;
-
-/// Appends your own tracks to an album in the given order, skipping the ones it already has.
-public record AddTracksToAlbumCommand(long UserId, Guid AlbumId, IReadOnlyList TrackIds)
- : ICommand>;
-
-public class AddTracksToAlbumCommandHandler(
- IDatabase database,
- ILogger logger)
- : ICommandHandler>
+namespace Musify.Application.Albums
{
- public async ValueTask> Handle(AddTracksToAlbumCommand request, CancellationToken cancellationToken)
+ /// Appends your own tracks to an album in the given order, skipping the ones it already has.
+ public record AddTracksToAlbumCommand(long UserId, Guid AlbumId, IReadOnlyList TrackIds)
+ : ICommand>;
+
+ public class AddTracksToAlbumCommandHandler(
+ IDatabase database,
+ ILogger logger)
+ : ICommandHandler>
{
- var album = await database.Albums.FindOwnedAsync(request.AlbumId, request.UserId, cancellationToken);
- if (album.IsError)
- return album.Errors;
-
- var requestedIds = request.TrackIds.Distinct().ToList();
-
- var tracks = await database.Tracks
- .AsNoTracking()
- .Where(track => requestedIds.Contains(track.Id) && track.LifeCycleStatus == LifeCycleStatus.Active)
- .Select(track => new { track.Id, track.OwnerUserId })
- .ToListAsync(cancellationToken);
-
- var missing = requestedIds.FirstOrDefault(id => tracks.All(track => track.Id != id));
- if (missing != Guid.Empty)
- return AppErrors.NotFound("Track", missing);
-
- var foreign = tracks.FirstOrDefault(track => track.OwnerUserId != request.UserId);
- if (foreign != null)
- return AppErrors.Forbidden("Track", foreign.Id);
-
- var alreadyInAlbum = await database.AlbumHasTracks
- .AsNoTracking()
- .Where(link => link.AlbumId == request.AlbumId && requestedIds.Contains(link.TrackId))
- .Select(link => link.TrackId)
- .ToListAsync(cancellationToken);
-
- var toAdd = requestedIds.Where(id => !alreadyInAlbum.Contains(id)).ToList();
- if (toAdd.Count == 0)
- return Result.Success;
-
- // Album track numbers are user-visible, so they start at 1.
- var nextTrackNumber = (await database.AlbumHasTracks
- .Where(link => link.AlbumId == request.AlbumId)
- .MaxAsync(link => (int?)link.TrackNumber, cancellationToken) ?? 0) + 1;
-
- foreach (var trackId in toAdd)
+ public async ValueTask> Handle(AddTracksToAlbumCommand request, CancellationToken cancellationToken)
{
- await database.AlbumHasTracks.AddAsync(new AlbumHasTrack
+ var album = await database.Albums.FindOwnedAsync(request.AlbumId, request.UserId, cancellationToken);
+ if (album.IsError)
+ return album.Errors;
+
+ var requestedIds = request.TrackIds.Distinct().ToList();
+
+ var tracks = await database.Tracks
+ .AsNoTracking()
+ .Where(track => requestedIds.Contains(track.Id) && track.LifeCycleStatus == LifeCycleStatus.Active)
+ .Select(track => new { track.Id, track.OwnerUserId })
+ .ToListAsync(cancellationToken);
+
+ var missing = requestedIds.FirstOrDefault(id => tracks.All(track => track.Id != id));
+ if (missing != Guid.Empty)
+ return AppErrors.NotFound("Track", missing);
+
+ var foreign = tracks.FirstOrDefault(track => track.OwnerUserId != request.UserId);
+ if (foreign != null)
+ return AppErrors.Forbidden("Track", foreign.Id);
+
+ var alreadyInAlbum = await database.AlbumHasTracks
+ .AsNoTracking()
+ .Where(link => link.AlbumId == request.AlbumId && requestedIds.Contains(link.TrackId))
+ .Select(link => link.TrackId)
+ .ToListAsync(cancellationToken);
+
+ var toAdd = requestedIds.Where(id => !alreadyInAlbum.Contains(id)).ToList();
+ if (toAdd.Count == 0)
+ return Result.Success;
+
+ // Album track numbers are user-visible, so they start at 1.
+ var nextTrackNumber = (await database.AlbumHasTracks
+ .Where(link => link.AlbumId == request.AlbumId)
+ .MaxAsync(link => (int?)link.TrackNumber, cancellationToken) ?? 0) + 1;
+
+ foreach (var trackId in toAdd)
{
- AlbumId = request.AlbumId,
- TrackId = trackId,
- TrackNumber = nextTrackNumber++
- }, cancellationToken);
+ await database.AlbumHasTracks.AddAsync(new AlbumHasTrack
+ {
+ AlbumId = request.AlbumId,
+ TrackId = trackId,
+ TrackNumber = nextTrackNumber++
+ }, cancellationToken);
+ }
+
+ var saved = await database.TrySaveChangesAsync(
+ AppErrors.Conflict("Album.ConcurrentChange", "The album changed while adding tracks. Try again."), cancellationToken);
+ if (saved.IsError)
+ return saved;
+
+ logger.LogInformation("Added {Count} tracks to album {AlbumId}", toAdd.Count, request.AlbumId);
+
+ return Result.Success;
}
-
- var saved = await database.TrySaveChangesAsync(
- AppErrors.Conflict("Album.ConcurrentChange", "The album changed while adding tracks. Try again."), cancellationToken);
- if (saved.IsError)
- return saved;
-
- logger.LogInformation("Added {Count} tracks to album {AlbumId}", toAdd.Count, request.AlbumId);
-
- return Result.Success;
}
}
diff --git a/backend/Core/Musify.Application/Albums/CreateAlbum.cs b/backend/Core/Musify.Application/Albums/CreateAlbum.cs
index de98485..8be75e6 100644
--- a/backend/Core/Musify.Application/Albums/CreateAlbum.cs
+++ b/backend/Core/Musify.Application/Albums/CreateAlbum.cs
@@ -12,61 +12,62 @@ using Musify.Application.Shared;
using Musify.Domain.Entities;
using Musify.Domain.ValueObjects;
-namespace Musify.Application.Albums;
-
-public record CreateAlbumCommand(
- long UserId,
- string Title,
- string? Description,
- int? ReleaseYear,
- Guid PictureIntentId)
- : ICommand>;
-
-public class CreateAlbumCommandHandler(
- IEventBus eventBus,
- IDatabase database,
- UploadIntentValidator uploadIntentValidator,
- ILogger logger,
- AlbumConfiguration albumConfiguration)
- : ICommandHandler>
+namespace Musify.Application.Albums
{
- public async ValueTask> Handle(CreateAlbumCommand request, CancellationToken cancellationToken)
+ public record CreateAlbumCommand(
+ long UserId,
+ string Title,
+ string? Description,
+ int? ReleaseYear,
+ Guid PictureIntentId)
+ : ICommand>;
+
+ public class CreateAlbumCommandHandler(
+ IEventBus eventBus,
+ IDatabase database,
+ UploadIntentValidator uploadIntentValidator,
+ ILogger logger,
+ AlbumConfiguration albumConfiguration)
+ : ICommandHandler>
{
- var userExists = await database.Users
- .AsNoTracking()
- .AnyAsync(user => user.Id == request.UserId, cancellationToken);
- if (!userExists)
- return AppErrors.NotFound("User", request.UserId);
-
- var prepared = await PictureSourceChange.PrepareAsync(
- uploadIntentValidator, request.PictureIntentId, request.UserId, UploadIntentPurpose.AlbumPicture, albumConfiguration, cancellationToken);
- if (prepared.IsError)
- return prepared.Errors;
-
- var picture = prepared.Value;
- var title = request.Title.Trim();
-
- var album = new Album
+ public async ValueTask> Handle(CreateAlbumCommand request, CancellationToken cancellationToken)
{
- OwnerUserId = request.UserId,
- Title = title,
- NormalizedTitle = TextNormalizer.Normalize(title),
- Description = request.Description,
- ReleaseYear = request.ReleaseYear,
- Pictures = picture.Pictures
- };
+ var userExists = await database.Users
+ .AsNoTracking()
+ .AnyAsync(user => user.Id == request.UserId, cancellationToken);
+ if (!userExists)
+ return AppErrors.NotFound("User", request.UserId);
- await database.Albums.AddAsync(album, cancellationToken);
+ var prepared = await PictureSourceChange.PrepareAsync(
+ uploadIntentValidator, request.PictureIntentId, request.UserId, UploadIntentPurpose.AlbumPicture, albumConfiguration, cancellationToken);
+ if (prepared.IsError)
+ return prepared.Errors;
- await eventBus.PublishAsync(
- new CreateAlbumResourcesEvent(
- album.Id, picture.Intent.Id, picture.Intent.Bucket, picture.Intent.Key, picture.FinalKey, picture.Sizes),
- cancellationToken);
+ var picture = prepared.Value;
+ var title = request.Title.Trim();
- await database.SaveChangesAsync(cancellationToken);
+ var album = new Album
+ {
+ OwnerUserId = request.UserId,
+ Title = title,
+ NormalizedTitle = TextNormalizer.Normalize(title),
+ Description = request.Description,
+ ReleaseYear = request.ReleaseYear,
+ Pictures = picture.Pictures
+ };
- logger.LogInformation("Created album {AlbumId} for user {UserId}", album.Id, request.UserId);
+ await database.Albums.AddAsync(album, cancellationToken);
- return AlbumApplicationResponse.FromEntity(album, trackCount: 0);
+ await eventBus.PublishAsync(
+ new CreateAlbumResourcesEvent(
+ album.Id, picture.Intent.Id, picture.Intent.Bucket, picture.Intent.Key, picture.FinalKey, picture.Sizes),
+ cancellationToken);
+
+ await database.SaveChangesAsync(cancellationToken);
+
+ logger.LogInformation("Created album {AlbumId} for user {UserId}", album.Id, request.UserId);
+
+ return AlbumApplicationResponse.FromEntity(album, trackCount: 0);
+ }
}
}
diff --git a/backend/Core/Musify.Application/Albums/DeleteAlbum.cs b/backend/Core/Musify.Application/Albums/DeleteAlbum.cs
index bb4a96e..137aeb5 100644
--- a/backend/Core/Musify.Application/Albums/DeleteAlbum.cs
+++ b/backend/Core/Musify.Application/Albums/DeleteAlbum.cs
@@ -6,31 +6,32 @@ using Musify.Application.Events;
using Musify.Application.Shared;
using Musify.Domain.ValueObjects;
-namespace Musify.Application.Albums;
-
-public record DeleteAlbumCommand(long UserId, Guid AlbumId)
- : ICommand>;
-
-public class DeleteAlbumCommandHandler(
- IDatabase database,
- IEventBus eventBus,
- ILogger logger)
- : ICommandHandler>
+namespace Musify.Application.Albums
{
- public async ValueTask> Handle(DeleteAlbumCommand request, CancellationToken cancellationToken)
+ public record DeleteAlbumCommand(long UserId, Guid AlbumId)
+ : ICommand>;
+
+ public class DeleteAlbumCommandHandler(
+ IDatabase database,
+ IEventBus eventBus,
+ ILogger logger)
+ : ICommandHandler>
{
- var found = await database.Albums.FindOwnedAsync(request.AlbumId, request.UserId, cancellationToken);
- if (found.IsError)
- return found.Errors;
+ public async ValueTask> Handle(DeleteAlbumCommand request, CancellationToken cancellationToken)
+ {
+ var found = await database.Albums.FindOwnedAsync(request.AlbumId, request.UserId, cancellationToken);
+ if (found.IsError)
+ return found.Errors;
- var album = found.Value;
- album.LifeCycleStatus = LifeCycleStatus.Removing;
+ var album = found.Value;
+ album.LifeCycleStatus = LifeCycleStatus.Removing;
- await eventBus.PublishAsync(new DeleteAlbumEvent(album.Id, request.UserId), cancellationToken);
- await database.SaveChangesAsync(cancellationToken);
+ await eventBus.PublishAsync(new DeleteAlbumEvent(album.Id, request.UserId), cancellationToken);
+ await database.SaveChangesAsync(cancellationToken);
- logger.LogInformation("Marked album {AlbumId} as removing", request.AlbumId);
+ logger.LogInformation("Marked album {AlbumId} as removing", request.AlbumId);
- return Result.Success;
+ return Result.Success;
+ }
}
}
diff --git a/backend/Core/Musify.Application/Albums/GetAlbumById.cs b/backend/Core/Musify.Application/Albums/GetAlbumById.cs
index 20856b6..5461a17 100644
--- a/backend/Core/Musify.Application/Albums/GetAlbumById.cs
+++ b/backend/Core/Musify.Application/Albums/GetAlbumById.cs
@@ -5,25 +5,26 @@ using Musify.Application.Albums.Responses;
using Musify.Application.Contracts;
using Musify.Application.Shared;
-namespace Musify.Application.Albums;
-
-public record GetAlbumByIdQuery(Guid Id)
- : IQuery>;
-
-public class GetAlbumByIdQueryHandler(IDatabase database)
- : IQueryHandler>
+namespace Musify.Application.Albums
{
- public async ValueTask> Handle(GetAlbumByIdQuery request, CancellationToken cancellationToken)
- {
- var album = await database.Albums
- .AsNoTracking()
- .Active()
- .Where(a => a.Id == request.Id)
- .SelectResponse()
- .SingleOrDefaultAsync(cancellationToken);
+ public record GetAlbumByIdQuery(Guid Id)
+ : IQuery>;
- return album == null
- ? AppErrors.NotFound("Album", request.Id)
- : album;
+ public class GetAlbumByIdQueryHandler(IDatabase database)
+ : IQueryHandler>
+ {
+ public async ValueTask> Handle(GetAlbumByIdQuery request, CancellationToken cancellationToken)
+ {
+ var album = await database.Albums
+ .AsNoTracking()
+ .Active()
+ .Where(a => a.Id == request.Id)
+ .SelectResponse()
+ .SingleOrDefaultAsync(cancellationToken);
+
+ return album == null
+ ? AppErrors.NotFound("Album", request.Id)
+ : album;
+ }
}
}
diff --git a/backend/Core/Musify.Application/Albums/GetAlbumTracks.cs b/backend/Core/Musify.Application/Albums/GetAlbumTracks.cs
index ba9955a..7da6847 100644
--- a/backend/Core/Musify.Application/Albums/GetAlbumTracks.cs
+++ b/backend/Core/Musify.Application/Albums/GetAlbumTracks.cs
@@ -6,33 +6,34 @@ using Musify.Application.Contracts;
using Musify.Application.Shared;
using Musify.Application.Tracks.Responses;
-namespace Musify.Application.Albums;
-
-public record GetAlbumTracksQuery(
- Guid AlbumId,
- int PageNumber,
- int PageSize)
- : IQuery>>;
-
-public class GetAlbumTracksQueryHandler(IDatabase database)
- : IQueryHandler>>
+namespace Musify.Application.Albums
{
- public async ValueTask>> Handle(GetAlbumTracksQuery request, CancellationToken cancellationToken)
- {
- var albumExists = await database.Albums
- .AsNoTracking()
- .Active()
- .AnyAsync(album => album.Id == request.AlbumId, cancellationToken);
- if (!albumExists)
- return AppErrors.NotFound("Album", request.AlbumId);
+ public record GetAlbumTracksQuery(
+ Guid AlbumId,
+ int PageNumber,
+ int PageSize)
+ : IQuery>>;
- return await database.AlbumHasTracks
- .AsNoTracking()
- .Where(albumTrack => albumTrack.AlbumId == request.AlbumId && albumTrack.Track.LifeCycleStatus == Domain.ValueObjects.LifeCycleStatus.Active)
- .OrderBy(albumTrack => albumTrack.TrackNumber)
- .ThenBy(albumTrack => albumTrack.Id)
- .Select(albumTrack => albumTrack.Track)
- .SelectResponse()
- .ToPaginatedAsync(new PageRequest(request.PageNumber, request.PageSize), cancellationToken);
+ public class GetAlbumTracksQueryHandler(IDatabase database)
+ : IQueryHandler>>
+ {
+ public async ValueTask>> Handle(GetAlbumTracksQuery request, CancellationToken cancellationToken)
+ {
+ var albumExists = await database.Albums
+ .AsNoTracking()
+ .Active()
+ .AnyAsync(album => album.Id == request.AlbumId, cancellationToken);
+ if (!albumExists)
+ return AppErrors.NotFound("Album", request.AlbumId);
+
+ return await database.AlbumHasTracks
+ .AsNoTracking()
+ .Where(albumTrack => albumTrack.AlbumId == request.AlbumId && albumTrack.Track.LifeCycleStatus == Domain.ValueObjects.LifeCycleStatus.Active)
+ .OrderBy(albumTrack => albumTrack.TrackNumber)
+ .ThenBy(albumTrack => albumTrack.Id)
+ .Select(albumTrack => albumTrack.Track)
+ .SelectResponse()
+ .ToPaginatedAsync(new PageRequest(request.PageNumber, request.PageSize), cancellationToken);
+ }
}
}
diff --git a/backend/Core/Musify.Application/Albums/GetAlbums.cs b/backend/Core/Musify.Application/Albums/GetAlbums.cs
index 0c7143e..2da6cbc 100644
--- a/backend/Core/Musify.Application/Albums/GetAlbums.cs
+++ b/backend/Core/Musify.Application/Albums/GetAlbums.cs
@@ -4,30 +4,31 @@ 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)
- : IQuery>;
-
-public class GetAlbumsQueryHandler(IDatabase database)
- : IQueryHandler>
+namespace Musify.Application.Albums
{
- public async ValueTask> Handle(GetAlbumsQuery request, CancellationToken cancellationToken)
+ public record GetAlbumsQuery(string? Title, int PageNumber, int PageSize)
+ : IQuery>;
+
+ public class GetAlbumsQueryHandler(IDatabase database)
+ : IQueryHandler>
{
- var albumsQuery = database.Albums
- .AsNoTracking()
- .Active();
-
- if (!string.IsNullOrWhiteSpace(request.Title))
+ public async ValueTask> Handle(GetAlbumsQuery request, CancellationToken cancellationToken)
{
- var normalizedTitle = TextNormalizer.Normalize(request.Title);
- albumsQuery = albumsQuery.Where(album => album.NormalizedTitle.Contains(normalizedTitle));
- }
+ var albumsQuery = database.Albums
+ .AsNoTracking()
+ .Active();
- return await albumsQuery
- .OrderByDescending(album => album.CreatedAt)
- .ThenBy(album => album.Id)
- .SelectResponse()
- .ToPaginatedAsync(new PageRequest(request.PageNumber, request.PageSize), cancellationToken);
+ if (!string.IsNullOrWhiteSpace(request.Title))
+ {
+ var normalizedTitle = TextNormalizer.Normalize(request.Title);
+ albumsQuery = albumsQuery.Where(album => album.NormalizedTitle.Contains(normalizedTitle));
+ }
+
+ return await albumsQuery
+ .OrderByDescending(album => album.CreatedAt)
+ .ThenBy(album => album.Id)
+ .SelectResponse()
+ .ToPaginatedAsync(new PageRequest(request.PageNumber, request.PageSize), cancellationToken);
+ }
}
}
diff --git a/backend/Core/Musify.Application/Albums/GetAlbumsByUserId.cs b/backend/Core/Musify.Application/Albums/GetAlbumsByUserId.cs
index dfe03d2..53fe0d7 100644
--- a/backend/Core/Musify.Application/Albums/GetAlbumsByUserId.cs
+++ b/backend/Core/Musify.Application/Albums/GetAlbumsByUserId.cs
@@ -5,29 +5,30 @@ 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)
- : IQuery>>;
-
-public class GetAlbumsByUserIdQueryHandler(IDatabase database)
- : IQueryHandler>>
+namespace Musify.Application.Albums
{
- public async ValueTask>> Handle(GetAlbumsByUserIdQuery request, CancellationToken cancellationToken)
- {
- var userExists = await database.Users
- .AsNoTracking()
- .AnyAsync(user => user.Id == request.UserId, cancellationToken);
- if (!userExists)
- return AppErrors.NotFound("User", request.UserId);
+ public record GetAlbumsByUserIdQuery(long UserId, int PageNumber, int PageSize)
+ : IQuery>>;
- return await database.Albums
- .AsNoTracking()
- .Active()
- .Where(album => album.OwnerUserId == request.UserId)
- .OrderByDescending(album => album.CreatedAt)
- .ThenBy(album => album.Id)
- .SelectResponse()
- .ToPaginatedAsync(new PageRequest(request.PageNumber, request.PageSize), cancellationToken);
+ public class GetAlbumsByUserIdQueryHandler(IDatabase database)
+ : IQueryHandler>>
+ {
+ public async ValueTask>> Handle(GetAlbumsByUserIdQuery request, CancellationToken cancellationToken)
+ {
+ var userExists = await database.Users
+ .AsNoTracking()
+ .AnyAsync(user => user.Id == request.UserId, cancellationToken);
+ if (!userExists)
+ return AppErrors.NotFound("User", request.UserId);
+
+ return await database.Albums
+ .AsNoTracking()
+ .Active()
+ .Where(album => album.OwnerUserId == request.UserId)
+ .OrderByDescending(album => album.CreatedAt)
+ .ThenBy(album => album.Id)
+ .SelectResponse()
+ .ToPaginatedAsync(new PageRequest(request.PageNumber, request.PageSize), cancellationToken);
+ }
}
}
diff --git a/backend/Core/Musify.Application/Albums/GetRecentlyListenedAlbums.cs b/backend/Core/Musify.Application/Albums/GetRecentlyListenedAlbums.cs
index c038271..2f87e1a 100644
--- a/backend/Core/Musify.Application/Albums/GetRecentlyListenedAlbums.cs
+++ b/backend/Core/Musify.Application/Albums/GetRecentlyListenedAlbums.cs
@@ -3,49 +3,50 @@ using Microsoft.EntityFrameworkCore;
using Musify.Application.Albums.Responses;
using Musify.Application.Contracts;
-namespace Musify.Application.Albums;
-
-public record GetRecentlyListenedAlbumsQuery(long UserId, int Limit)
- : IQuery>;
-
-public class GetRecentlyListenedAlbumsQueryHandler(IDatabase database)
- : IQueryHandler>
+namespace Musify.Application.Albums
{
- public async ValueTask> Handle(GetRecentlyListenedAlbumsQuery request, CancellationToken cancellationToken)
+ public record GetRecentlyListenedAlbumsQuery(long UserId, int Limit)
+ : IQuery>;
+
+ public class GetRecentlyListenedAlbumsQueryHandler(IDatabase database)
+ : IQueryHandler>
{
- var listens = database.ListeningHistories
- .AsNoTracking()
- .Where(history => history.UserId == request.UserId && history.IsCounted);
+ public async ValueTask> Handle(GetRecentlyListenedAlbumsQuery request, CancellationToken cancellationToken)
+ {
+ var listens = database.ListeningHistories
+ .AsNoTracking()
+ .Where(history => history.UserId == request.UserId && history.IsCounted);
- var ranked = await database.AlbumHasTracks
- .AsNoTracking()
- .Where(albumTrack => albumTrack.Album.LifeCycleStatus == Domain.ValueObjects.LifeCycleStatus.Active)
- .Join(
- listens,
- albumTrack => albumTrack.TrackId,
- history => history.TrackId,
- (albumTrack, history) => new { albumTrack.AlbumId, history.ListenedAt })
- .GroupBy(entry => entry.AlbumId)
- .Select(group => new { AlbumId = group.Key, LastListenedAt = group.Max(entry => entry.ListenedAt) })
- .OrderByDescending(entry => entry.LastListenedAt)
- .ThenBy(entry => entry.AlbumId)
- .Take(request.Limit)
- .ToListAsync(cancellationToken);
+ var ranked = await database.AlbumHasTracks
+ .AsNoTracking()
+ .Where(albumTrack => albumTrack.Album.LifeCycleStatus == Domain.ValueObjects.LifeCycleStatus.Active)
+ .Join(
+ listens,
+ albumTrack => albumTrack.TrackId,
+ history => history.TrackId,
+ (albumTrack, history) => new { albumTrack.AlbumId, history.ListenedAt })
+ .GroupBy(entry => entry.AlbumId)
+ .Select(group => new { AlbumId = group.Key, LastListenedAt = group.Max(entry => entry.ListenedAt) })
+ .OrderByDescending(entry => entry.LastListenedAt)
+ .ThenBy(entry => entry.AlbumId)
+ .Take(request.Limit)
+ .ToListAsync(cancellationToken);
- if (ranked.Count == 0)
- return [];
+ if (ranked.Count == 0)
+ return [];
- var albumIds = ranked.Select(entry => entry.AlbumId).ToList();
+ var albumIds = ranked.Select(entry => entry.AlbumId).ToList();
- var albumsById = await database.Albums
- .AsNoTracking()
- .Where(album => albumIds.Contains(album.Id))
- .SelectResponse()
- .ToDictionaryAsync(album => album.Id, cancellationToken);
+ var albumsById = await database.Albums
+ .AsNoTracking()
+ .Where(album => albumIds.Contains(album.Id))
+ .SelectResponse()
+ .ToDictionaryAsync(album => album.Id, cancellationToken);
- return ranked
- .Where(entry => albumsById.ContainsKey(entry.AlbumId))
- .Select(entry => albumsById[entry.AlbumId])
- .ToList();
+ return ranked
+ .Where(entry => albumsById.ContainsKey(entry.AlbumId))
+ .Select(entry => albumsById[entry.AlbumId])
+ .ToList();
+ }
}
}
diff --git a/backend/Core/Musify.Application/Albums/RemoveTrackFromAlbum.cs b/backend/Core/Musify.Application/Albums/RemoveTrackFromAlbum.cs
index 2edb9c9..04849fd 100644
--- a/backend/Core/Musify.Application/Albums/RemoveTrackFromAlbum.cs
+++ b/backend/Core/Musify.Application/Albums/RemoveTrackFromAlbum.cs
@@ -5,41 +5,42 @@ 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)
- : ICommand>;
-
-public class RemoveTrackFromAlbumCommandHandler(
- IDatabase database,
- ILogger logger)
- : ICommandHandler>
+namespace Musify.Application.Albums
{
- public async ValueTask> Handle(RemoveTrackFromAlbumCommand request, CancellationToken cancellationToken)
+ public record RemoveTrackFromAlbumCommand(long UserId, Guid AlbumId, Guid TrackId)
+ : ICommand>;
+
+ public class RemoveTrackFromAlbumCommandHandler(
+ IDatabase database,
+ ILogger logger)
+ : ICommandHandler>
{
- var album = await database.Albums.FindOwnedAsync(request.AlbumId, request.UserId, cancellationToken);
- if (album.IsError)
- return album.Errors;
+ public async ValueTask> Handle(RemoveTrackFromAlbumCommand request, CancellationToken cancellationToken)
+ {
+ var album = await database.Albums.FindOwnedAsync(request.AlbumId, request.UserId, cancellationToken);
+ if (album.IsError)
+ return album.Errors;
- var albumTracks = await database.AlbumHasTracks
- .Where(albumTrack => albumTrack.AlbumId == request.AlbumId)
- .OrderBy(albumTrack => albumTrack.TrackNumber)
- .ToListAsync(cancellationToken);
+ var albumTracks = await database.AlbumHasTracks
+ .Where(albumTrack => albumTrack.AlbumId == request.AlbumId)
+ .OrderBy(albumTrack => albumTrack.TrackNumber)
+ .ToListAsync(cancellationToken);
- var link = albumTracks.SingleOrDefault(albumTrack => albumTrack.TrackId == request.TrackId);
- if (link == null)
- return Error.NotFound("Album.TrackNotFound", "Track is not in the album.");
+ var link = albumTracks.SingleOrDefault(albumTrack => albumTrack.TrackId == request.TrackId);
+ if (link == null)
+ return Error.NotFound("Album.TrackNotFound", "Track is not in the album.");
- database.AlbumHasTracks.Remove(link);
+ database.AlbumHasTracks.Remove(link);
- var trackNumber = 1;
- foreach (var remaining in albumTracks.Where(albumTrack => albumTrack.Id != link.Id))
- remaining.TrackNumber = trackNumber++;
+ var trackNumber = 1;
+ foreach (var remaining in albumTracks.Where(albumTrack => albumTrack.Id != link.Id))
+ remaining.TrackNumber = trackNumber++;
- await database.SaveChangesAsync(cancellationToken);
+ await database.SaveChangesAsync(cancellationToken);
- logger.LogInformation("Removed track {TrackId} from album {AlbumId}", request.TrackId, request.AlbumId);
+ logger.LogInformation("Removed track {TrackId} from album {AlbumId}", request.TrackId, request.AlbumId);
- return Result.Success;
+ return Result.Success;
+ }
}
}
diff --git a/backend/Core/Musify.Application/Albums/RequestAlbumPictureUpload.cs b/backend/Core/Musify.Application/Albums/RequestAlbumPictureUpload.cs
index 14fdd71..68c0dfb 100644
--- a/backend/Core/Musify.Application/Albums/RequestAlbumPictureUpload.cs
+++ b/backend/Core/Musify.Application/Albums/RequestAlbumPictureUpload.cs
@@ -5,20 +5,21 @@ using Musify.Application.Pictures.Responses;
using Musify.Application.Services;
using Musify.Domain.ValueObjects;
-namespace Musify.Application.Albums;
-
-public record RequestAlbumPictureUploadCommand(
- long UserId,
- string FileType,
- string ContentType,
- long? ExpectedSizeBytes = null)
- : ICommand>;
-
-public class RequestAlbumPictureUploadCommandHandler(UploadIntentIssuer issuer, AlbumConfiguration albumConfiguration)
- : ICommandHandler>
+namespace Musify.Application.Albums
{
- public async ValueTask> Handle(RequestAlbumPictureUploadCommand request, CancellationToken cancellationToken) =>
- await issuer.IssuePictureAsync(
- request.UserId, UploadIntentPurpose.AlbumPicture, albumConfiguration,
- request.FileType, request.ContentType, request.ExpectedSizeBytes, cancellationToken);
+ public record RequestAlbumPictureUploadCommand(
+ long UserId,
+ string FileType,
+ string ContentType,
+ long? ExpectedSizeBytes = null)
+ : ICommand>;
+
+ public class RequestAlbumPictureUploadCommandHandler(UploadIntentIssuer issuer, AlbumConfiguration albumConfiguration)
+ : ICommandHandler>
+ {
+ public async ValueTask> Handle(RequestAlbumPictureUploadCommand request, CancellationToken cancellationToken) =>
+ await issuer.IssuePictureAsync(
+ request.UserId, UploadIntentPurpose.AlbumPicture, albumConfiguration,
+ request.FileType, request.ContentType, request.ExpectedSizeBytes, cancellationToken);
+ }
}
diff --git a/backend/Core/Musify.Application/Albums/Responses/AlbumApplicationResponse.cs b/backend/Core/Musify.Application/Albums/Responses/AlbumApplicationResponse.cs
index 5a97f89..77ce78e 100644
--- a/backend/Core/Musify.Application/Albums/Responses/AlbumApplicationResponse.cs
+++ b/backend/Core/Musify.Application/Albums/Responses/AlbumApplicationResponse.cs
@@ -1,40 +1,41 @@
using Musify.Domain.Entities;
-namespace Musify.Application.Albums.Responses;
-
-public record AlbumApplicationResponse(
- Guid Id,
- string Title,
- string? Description,
- int? ReleaseYear,
- long OwnerUserId,
- int TrackCount,
- string? SmallImageKeyName,
- string? MediumImageKeyName,
- string? LargeImageKeyName,
- DateTime CreatedAt,
- DateTime UpdatedAt,
- IReadOnlyList CoverTrackIds)
+namespace Musify.Application.Albums.Responses
{
- public const int CoverTrackCount = AlbumProjections.CoverTrackCount;
-
- public static AlbumApplicationResponse FromEntity(
- Album album,
- int trackCount,
- IReadOnlyList? coverTrackIds = null)
+ public record AlbumApplicationResponse(
+ Guid Id,
+ string Title,
+ string? Description,
+ int? ReleaseYear,
+ long OwnerUserId,
+ int TrackCount,
+ string? SmallImageKeyName,
+ string? MediumImageKeyName,
+ string? LargeImageKeyName,
+ DateTime CreatedAt,
+ DateTime UpdatedAt,
+ IReadOnlyList CoverTrackIds)
{
- return new AlbumApplicationResponse(
- album.Id,
- album.Title,
- album.Description,
- album.ReleaseYear,
- album.OwnerUserId,
- trackCount,
- album.Pictures?.SmallName,
- album.Pictures?.MediumName,
- album.Pictures?.LargeName,
- album.CreatedAt,
- album.UpdatedAt,
- coverTrackIds ?? []);
+ public const int CoverTrackCount = AlbumProjections.CoverTrackCount;
+
+ public static AlbumApplicationResponse FromEntity(
+ Album album,
+ int trackCount,
+ IReadOnlyList? coverTrackIds = null)
+ {
+ return new AlbumApplicationResponse(
+ album.Id,
+ album.Title,
+ album.Description,
+ album.ReleaseYear,
+ album.OwnerUserId,
+ trackCount,
+ album.Pictures?.SmallName,
+ album.Pictures?.MediumName,
+ album.Pictures?.LargeName,
+ album.CreatedAt,
+ album.UpdatedAt,
+ coverTrackIds ?? []);
+ }
}
}
diff --git a/backend/Core/Musify.Application/Albums/Responses/AlbumProjections.cs b/backend/Core/Musify.Application/Albums/Responses/AlbumProjections.cs
index 196d1d2..cf4b63b 100644
--- a/backend/Core/Musify.Application/Albums/Responses/AlbumProjections.cs
+++ b/backend/Core/Musify.Application/Albums/Responses/AlbumProjections.cs
@@ -1,31 +1,32 @@
using Musify.Domain.Entities;
using Musify.Domain.ValueObjects;
-namespace Musify.Application.Albums.Responses;
-
-public static class AlbumProjections
+namespace Musify.Application.Albums.Responses
{
- public const int CoverTrackCount = 4;
+ public static class AlbumProjections
+ {
+ public const int CoverTrackCount = 4;
- public static IQueryable Active(this IQueryable query) =>
- query.Where(album => album.LifeCycleStatus == LifeCycleStatus.Active);
+ public static IQueryable Active(this IQueryable query) =>
+ query.Where(album => album.LifeCycleStatus == LifeCycleStatus.Active);
- public static IQueryable SelectResponse(this IQueryable query) =>
- query.Select(album => new AlbumApplicationResponse(
- album.Id,
- album.Title,
- album.Description,
- album.ReleaseYear,
- album.OwnerUserId,
- album.AlbumTracks.Count,
- album.Pictures!.SmallName,
- album.Pictures!.MediumName,
- album.Pictures!.LargeName,
- album.CreatedAt,
- album.UpdatedAt,
- album.AlbumTracks
- .OrderBy(albumTrack => albumTrack.TrackNumber)
- .Take(CoverTrackCount)
- .Select(albumTrack => albumTrack.TrackId)
- .ToList()));
+ public static IQueryable SelectResponse(this IQueryable query) =>
+ query.Select(album => new AlbumApplicationResponse(
+ album.Id,
+ album.Title,
+ album.Description,
+ album.ReleaseYear,
+ album.OwnerUserId,
+ album.AlbumTracks.Count,
+ album.Pictures!.SmallName,
+ album.Pictures!.MediumName,
+ album.Pictures!.LargeName,
+ album.CreatedAt,
+ album.UpdatedAt,
+ album.AlbumTracks
+ .OrderBy(albumTrack => albumTrack.TrackNumber)
+ .Take(CoverTrackCount)
+ .Select(albumTrack => albumTrack.TrackId)
+ .ToList()));
+ }
}
diff --git a/backend/Core/Musify.Application/Albums/UpdateAlbum.cs b/backend/Core/Musify.Application/Albums/UpdateAlbum.cs
index b4d798d..dc627dc 100644
--- a/backend/Core/Musify.Application/Albums/UpdateAlbum.cs
+++ b/backend/Core/Musify.Application/Albums/UpdateAlbum.cs
@@ -11,63 +11,64 @@ using Musify.Application.Services;
using Musify.Application.Shared;
using Musify.Domain.ValueObjects;
-namespace Musify.Application.Albums;
-
-public record UpdateAlbumCommand(
- long UserId,
- Guid AlbumId,
- string Title,
- string? Description,
- int? ReleaseYear,
- Guid? NewPictureIntentId)
- : ICommand>;
-
-public class UpdateAlbumCommandHandler(
- IEventBus eventBus,
- IDatabase database,
- UploadIntentValidator uploadIntentValidator,
- ILogger logger,
- AlbumConfiguration albumConfiguration)
- : ICommandHandler>
+namespace Musify.Application.Albums
{
- public async ValueTask> Handle(UpdateAlbumCommand request, CancellationToken cancellationToken)
+ public record UpdateAlbumCommand(
+ long UserId,
+ Guid AlbumId,
+ string Title,
+ string? Description,
+ int? ReleaseYear,
+ Guid? NewPictureIntentId)
+ : ICommand>;
+
+ public class UpdateAlbumCommandHandler(
+ IEventBus eventBus,
+ IDatabase database,
+ UploadIntentValidator uploadIntentValidator,
+ ILogger logger,
+ AlbumConfiguration albumConfiguration)
+ : ICommandHandler>
{
- var found = await database.Albums.FindOwnedAsync(request.AlbumId, request.UserId, cancellationToken);
- if (found.IsError)
- return found.Errors;
-
- var album = found.Value;
- var title = request.Title.Trim();
-
- album.Title = title;
- album.NormalizedTitle = TextNormalizer.Normalize(title);
- album.Description = request.Description;
- album.ReleaseYear = request.ReleaseYear;
-
- if (request.NewPictureIntentId is { } intentId)
+ public async ValueTask> Handle(UpdateAlbumCommand request, CancellationToken cancellationToken)
{
- var prepared = await PictureSourceChange.PrepareAsync(
- uploadIntentValidator, intentId, request.UserId, UploadIntentPurpose.AlbumPicture, albumConfiguration, cancellationToken);
- if (prepared.IsError)
- return prepared.Errors;
+ var found = await database.Albums.FindOwnedAsync(request.AlbumId, request.UserId, cancellationToken);
+ if (found.IsError)
+ return found.Errors;
- var picture = prepared.Value;
- album.Pictures = picture.Pictures;
+ var album = found.Value;
+ var title = request.Title.Trim();
- await eventBus.PublishAsync(
- new UpdateAlbumPictureSourceEvent(
- album.Id, picture.Intent.Id, picture.Intent.Bucket, picture.Intent.Key, picture.FinalKey, picture.Sizes),
- cancellationToken);
+ album.Title = title;
+ album.NormalizedTitle = TextNormalizer.Normalize(title);
+ album.Description = request.Description;
+ album.ReleaseYear = request.ReleaseYear;
+
+ if (request.NewPictureIntentId is { } intentId)
+ {
+ var prepared = await PictureSourceChange.PrepareAsync(
+ uploadIntentValidator, intentId, request.UserId, UploadIntentPurpose.AlbumPicture, albumConfiguration, cancellationToken);
+ if (prepared.IsError)
+ return prepared.Errors;
+
+ var picture = prepared.Value;
+ album.Pictures = picture.Pictures;
+
+ await eventBus.PublishAsync(
+ new UpdateAlbumPictureSourceEvent(
+ album.Id, picture.Intent.Id, picture.Intent.Bucket, picture.Intent.Key, picture.FinalKey, picture.Sizes),
+ cancellationToken);
+ }
+
+ await database.SaveChangesAsync(cancellationToken);
+
+ logger.LogInformation("Updated album {AlbumId}", album.Id);
+
+ return await database.Albums
+ .AsNoTracking()
+ .Where(a => a.Id == album.Id)
+ .SelectResponse()
+ .SingleAsync(cancellationToken);
}
-
- await database.SaveChangesAsync(cancellationToken);
-
- logger.LogInformation("Updated album {AlbumId}", album.Id);
-
- return await database.Albums
- .AsNoTracking()
- .Where(a => a.Id == album.Id)
- .SelectResponse()
- .SingleAsync(cancellationToken);
}
}
diff --git a/backend/Core/Musify.Application/ApplicationDependencyInjection.cs b/backend/Core/Musify.Application/ApplicationDependencyInjection.cs
index 8e22f82..a96334a 100644
--- a/backend/Core/Musify.Application/ApplicationDependencyInjection.cs
+++ b/backend/Core/Musify.Application/ApplicationDependencyInjection.cs
@@ -3,26 +3,27 @@ using Microsoft.Extensions.DependencyInjection;
using Musify.Application.Configuration;
using Musify.Application.Services;
-namespace Musify.Application;
-
-public static class ApplicationDependencyInjection
+namespace Musify.Application
{
- public static IServiceCollection AddApplication(this IServiceCollection services, IConfiguration configuration)
+ public static class ApplicationDependencyInjection
{
- services.AddMediator();
- services.AddScoped();
- services.AddScoped();
- services.AddScoped();
+ public static IServiceCollection AddApplication(this IServiceCollection services, IConfiguration configuration)
+ {
+ services.AddMediator();
+ services.AddScoped();
+ services.AddScoped();
+ services.AddScoped();
- services.AddValidatedOptions(configuration);
- services.AddValidatedOptions(configuration);
- services.AddValidatedOptions(configuration);
- services.AddValidatedOptions(configuration);
- services.AddValidatedOptions(configuration);
- services.AddValidatedOptions(configuration);
- services.AddValidatedOptions(configuration);
- services.AddValidatedOptions(configuration);
+ services.AddValidatedOptions(configuration);
+ services.AddValidatedOptions(configuration);
+ services.AddValidatedOptions(configuration);
+ services.AddValidatedOptions(configuration);
+ services.AddValidatedOptions(configuration);
+ services.AddValidatedOptions(configuration);
+ services.AddValidatedOptions(configuration);
+ services.AddValidatedOptions(configuration);
- return services;
+ return services;
+ }
}
}
diff --git a/backend/Core/Musify.Application/Configuration/AlbumConfiguration.cs b/backend/Core/Musify.Application/Configuration/AlbumConfiguration.cs
index c139a4d..88db57b 100644
--- a/backend/Core/Musify.Application/Configuration/AlbumConfiguration.cs
+++ b/backend/Core/Musify.Application/Configuration/AlbumConfiguration.cs
@@ -1,14 +1,15 @@
using System.ComponentModel.DataAnnotations;
-namespace Musify.Application.Configuration;
-
-public class AlbumConfiguration : IConfigurationOptions, IPictureOwnerConfiguration
+namespace Musify.Application.Configuration
{
- public static string SectionName => "Album";
+ public class AlbumConfiguration : IConfigurationOptions, IPictureOwnerConfiguration
+ {
+ public static string SectionName => "Album";
- [Required]
- public PictureRoutes Routes { get; set; } = new() { ParentFolder = "Albums" };
+ [Required]
+ public PictureRoutes Routes { get; set; } = new() { ParentFolder = "Albums" };
- [Required]
- public PictureSizes PicturesSizes { get; set; } = new();
+ [Required]
+ public PictureSizes PicturesSizes { get; set; } = new();
+ }
}
diff --git a/backend/Core/Musify.Application/Configuration/ApplicationStorageConfiguration.cs b/backend/Core/Musify.Application/Configuration/ApplicationStorageConfiguration.cs
index f6fedc4..199cc5e 100644
--- a/backend/Core/Musify.Application/Configuration/ApplicationStorageConfiguration.cs
+++ b/backend/Core/Musify.Application/Configuration/ApplicationStorageConfiguration.cs
@@ -1,12 +1,13 @@
using System.ComponentModel.DataAnnotations;
-namespace Musify.Application.Configuration;
-
-public class ApplicationStorageConfiguration : IConfigurationOptions
+namespace Musify.Application.Configuration
{
- public static string SectionName => "ApplicationStorage";
+ public class ApplicationStorageConfiguration : IConfigurationOptions
+ {
+ public static string SectionName => "ApplicationStorage";
- [Required]
- [StringLength(128, MinimumLength = 1)]
- public required string Bucket { get; set; }
+ [Required]
+ [StringLength(128, MinimumLength = 1)]
+ public required string Bucket { get; set; }
+ }
}
diff --git a/backend/Core/Musify.Application/Configuration/ConfigurationDependencyInjection.cs b/backend/Core/Musify.Application/Configuration/ConfigurationDependencyInjection.cs
index aa7fa1a..ff58e2f 100644
--- a/backend/Core/Musify.Application/Configuration/ConfigurationDependencyInjection.cs
+++ b/backend/Core/Musify.Application/Configuration/ConfigurationDependencyInjection.cs
@@ -2,28 +2,29 @@ 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 IServiceCollection AddValidatedOptions(this IServiceCollection services, IConfiguration configuration)
- where T : class, IConfigurationOptions =>
- services.AddValidatedOptions(configuration, T.SectionName);
-
- public static IServiceCollection AddValidatedOptions(
- this IServiceCollection services,
- IConfiguration configuration,
- string sectionName)
- where T : class
+ public static class ConfigurationDependencyInjection
{
- services
- .AddOptionsWithValidateOnStart()
- .Bind(configuration.GetRequiredSection(sectionName))
- .ValidateDataAnnotations();
+ public static IServiceCollection AddValidatedOptions(this IServiceCollection services, IConfiguration configuration)
+ where T : class, IConfigurationOptions =>
+ services.AddValidatedOptions(configuration, T.SectionName);
- services.AddSingleton(serviceProvider =>
- serviceProvider.GetRequiredService>().Value);
+ public static IServiceCollection AddValidatedOptions(
+ this IServiceCollection services,
+ IConfiguration configuration,
+ string sectionName)
+ where T : class
+ {
+ services
+ .AddOptionsWithValidateOnStart()
+ .Bind(configuration.GetRequiredSection(sectionName))
+ .ValidateDataAnnotations();
- return services;
+ services.AddSingleton(serviceProvider =>
+ serviceProvider.GetRequiredService>().Value);
+
+ return services;
+ }
}
}
diff --git a/backend/Core/Musify.Application/Configuration/GetPlaybackPublicConfig.cs b/backend/Core/Musify.Application/Configuration/GetPlaybackPublicConfig.cs
index e731ae2..8b96121 100644
--- a/backend/Core/Musify.Application/Configuration/GetPlaybackPublicConfig.cs
+++ b/backend/Core/Musify.Application/Configuration/GetPlaybackPublicConfig.cs
@@ -1,16 +1,17 @@
using Mediator;
using Musify.Application.Configuration.Responses;
-namespace Musify.Application.Configuration;
-
-
-public record GetPlaybackPublicConfigQuery : IQuery;
-
-public class GetPlaybackPublicConfigQueryHandler(PlaybackConfiguration playbackConfiguration)
- : IQueryHandler
+namespace Musify.Application.Configuration
{
- public ValueTask Handle(GetPlaybackPublicConfigQuery query, CancellationToken cancellationToken) =>
- ValueTask.FromResult(new PlaybackPublicConfigResponse(
- playbackConfiguration.AllowAnonymousListening,
- playbackConfiguration.AnonymousFragmentSeconds));
+
+ public record GetPlaybackPublicConfigQuery : IQuery;
+
+ public class GetPlaybackPublicConfigQueryHandler(PlaybackConfiguration playbackConfiguration)
+ : IQueryHandler
+ {
+ public ValueTask Handle(GetPlaybackPublicConfigQuery query, CancellationToken cancellationToken) =>
+ ValueTask.FromResult(new PlaybackPublicConfigResponse(
+ playbackConfiguration.AllowAnonymousListening,
+ playbackConfiguration.AnonymousFragmentSeconds));
+ }
}
diff --git a/backend/Core/Musify.Application/Configuration/IConfigurationOptions.cs b/backend/Core/Musify.Application/Configuration/IConfigurationOptions.cs
index 3864320..8bd3ccf 100644
--- a/backend/Core/Musify.Application/Configuration/IConfigurationOptions.cs
+++ b/backend/Core/Musify.Application/Configuration/IConfigurationOptions.cs
@@ -1,6 +1,7 @@
-namespace Musify.Application.Configuration;
-
-public interface IConfigurationOptions
+namespace Musify.Application.Configuration
{
- public static abstract string SectionName { get; }
+ public interface IConfigurationOptions
+ {
+ public static abstract string SectionName { get; }
+ }
}
diff --git a/backend/Core/Musify.Application/Configuration/MixConfiguration.cs b/backend/Core/Musify.Application/Configuration/MixConfiguration.cs
index bb16860..1a71d6e 100644
--- a/backend/Core/Musify.Application/Configuration/MixConfiguration.cs
+++ b/backend/Core/Musify.Application/Configuration/MixConfiguration.cs
@@ -1,11 +1,12 @@
using System.ComponentModel.DataAnnotations;
-namespace Musify.Application.Configuration;
-
-public class MixConfiguration : IConfigurationOptions
+namespace Musify.Application.Configuration
{
- public static string SectionName => "Mix";
+ public class MixConfiguration : IConfigurationOptions
+ {
+ public static string SectionName => "Mix";
- [Range(4, 100)]
- public int ItemsPerMix { get; set; } = 12;
+ [Range(4, 100)]
+ public int ItemsPerMix { get; set; } = 12;
+ }
}
diff --git a/backend/Core/Musify.Application/Configuration/Pictures.cs b/backend/Core/Musify.Application/Configuration/Pictures.cs
index 5e71035..08235a1 100644
--- a/backend/Core/Musify.Application/Configuration/Pictures.cs
+++ b/backend/Core/Musify.Application/Configuration/Pictures.cs
@@ -2,90 +2,91 @@ using System.ComponentModel.DataAnnotations;
using System.Globalization;
using Musify.Application.Shared;
-namespace Musify.Application.Configuration;
-
-public enum PictureSize
+namespace Musify.Application.Configuration
{
- Small,
- Medium,
- Large
-}
-
-public class PictureRoutes
-{
- [Required]
- public string UploadsFolder { get; set; } = "uploads";
-
- [Required]
- public string ParentFolder { get; set; } = string.Empty;
-
- [Required]
- public string OriginalPicturesFolder { get; set; } = "OriginalPictures";
-
- [Required]
- public string SmallPicturesFolder { get; set; } = "SmallPictures";
-
- [Required]
- public string MediumPicturesFolder { get; set; } = "MediumPictures";
-
- [Required]
- public string LargePicturesFolder { get; set; } = "LargePictures";
-
- public string FolderPath(PictureSize size) => StorageKey.Combine(ParentFolder, size switch
+ public enum PictureSize
{
- PictureSize.Small => SmallPicturesFolder,
- PictureSize.Medium => MediumPicturesFolder,
- PictureSize.Large => LargePicturesFolder,
- _ => throw new ArgumentOutOfRangeException(nameof(size), size, null)
- });
+ Small,
+ Medium,
+ Large
+ }
- public string BuildPicturePath(PictureSize size, string pictureName) => StorageKey.Combine(FolderPath(size), pictureName);
+ public class PictureRoutes
+ {
+ [Required]
+ public string UploadsFolder { get; set; } = "uploads";
- public string BuildOriginalPicturePath(long userId, string pictureName) =>
- StorageKey.Combine(UploadsFolder, userId.ToString(CultureInfo.InvariantCulture), ParentFolder, OriginalPicturesFolder, pictureName);
+ [Required]
+ public string ParentFolder { get; set; } = string.Empty;
- public string BuildTempPath(string tempRootPrefix, long userId, string objectName) =>
- StorageKey.Combine(tempRootPrefix, userId.ToString(CultureInfo.InvariantCulture), ParentFolder, objectName);
-}
-
-public class PictureSizes
-{
- [Range(1, 1024)]
- public int SmallWidth { get; set; } = 128;
-
- [Range(1, 1024)]
- public int SmallHeight { get; set; } = 128;
-
- [Range(1, 1024)]
- public int MediumWidth { get; set; } = 256;
-
- [Range(1, 1024)]
- public int MediumHeight { get; set; } = 256;
-
- [Range(1, 1024)]
- public int LargeWidth { get; set; } = 512;
-
- [Range(1, 1024)]
- public int LargeHeight { get; set; } = 512;
-
- public ImageSizes ToImageSizes(PictureRoutes routes) => new(
- 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 PictureRoutes Routes { get; }
-
- public PictureSizes PicturesSizes { get; }
-}
-
-public static class PictureSizeParser
-{
- /// Parses the size query value; anything unknown means .
- public static PictureSize Parse(string? value) =>
- Enum.TryParse(value, ignoreCase: true, out var size) && Enum.IsDefined(size)
- ? size
- : PictureSize.Medium;
+ [Required]
+ public string OriginalPicturesFolder { get; set; } = "OriginalPictures";
+
+ [Required]
+ public string SmallPicturesFolder { get; set; } = "SmallPictures";
+
+ [Required]
+ public string MediumPicturesFolder { get; set; } = "MediumPictures";
+
+ [Required]
+ public string LargePicturesFolder { get; set; } = "LargePictures";
+
+ public string FolderPath(PictureSize size) => StorageKey.Combine(ParentFolder, size switch
+ {
+ PictureSize.Small => SmallPicturesFolder,
+ PictureSize.Medium => MediumPicturesFolder,
+ PictureSize.Large => LargePicturesFolder,
+ _ => throw new ArgumentOutOfRangeException(nameof(size), size, null)
+ });
+
+ public string BuildPicturePath(PictureSize size, string pictureName) => StorageKey.Combine(FolderPath(size), pictureName);
+
+ public string BuildOriginalPicturePath(long userId, string pictureName) =>
+ StorageKey.Combine(UploadsFolder, userId.ToString(CultureInfo.InvariantCulture), ParentFolder, OriginalPicturesFolder, pictureName);
+
+ public string BuildTempPath(string tempRootPrefix, long userId, string objectName) =>
+ StorageKey.Combine(tempRootPrefix, userId.ToString(CultureInfo.InvariantCulture), ParentFolder, objectName);
+ }
+
+ public class PictureSizes
+ {
+ [Range(1, 1024)]
+ public int SmallWidth { get; set; } = 128;
+
+ [Range(1, 1024)]
+ public int SmallHeight { get; set; } = 128;
+
+ [Range(1, 1024)]
+ public int MediumWidth { get; set; } = 256;
+
+ [Range(1, 1024)]
+ public int MediumHeight { get; set; } = 256;
+
+ [Range(1, 1024)]
+ public int LargeWidth { get; set; } = 512;
+
+ [Range(1, 1024)]
+ public int LargeHeight { get; set; } = 512;
+
+ public ImageSizes ToImageSizes(PictureRoutes routes) => new(
+ 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 PictureRoutes Routes { get; }
+
+ public PictureSizes PicturesSizes { get; }
+ }
+
+ public static class PictureSizeParser
+ {
+ /// Parses the size query value; anything unknown means .
+ public static PictureSize Parse(string? value) =>
+ Enum.TryParse(value, ignoreCase: true, out var size) && Enum.IsDefined(size)
+ ? size
+ : PictureSize.Medium;
+ }
}
diff --git a/backend/Core/Musify.Application/Configuration/PlayListConfiguration.cs b/backend/Core/Musify.Application/Configuration/PlayListConfiguration.cs
index 10385a9..7550d1e 100644
--- a/backend/Core/Musify.Application/Configuration/PlayListConfiguration.cs
+++ b/backend/Core/Musify.Application/Configuration/PlayListConfiguration.cs
@@ -1,14 +1,15 @@
using System.ComponentModel.DataAnnotations;
-namespace Musify.Application.Configuration;
-
-public class PlayListConfiguration : IConfigurationOptions, IPictureOwnerConfiguration
+namespace Musify.Application.Configuration
{
- public static string SectionName => "PlayList";
+ public class PlayListConfiguration : IConfigurationOptions, IPictureOwnerConfiguration
+ {
+ public static string SectionName => "PlayList";
- [Required]
- public PictureRoutes Routes { get; set; } = new() { ParentFolder = "PlayLists" };
+ [Required]
+ public PictureRoutes Routes { get; set; } = new() { ParentFolder = "PlayLists" };
- [Required]
- public PictureSizes PicturesSizes { get; set; } = new();
+ [Required]
+ public PictureSizes PicturesSizes { get; set; } = new();
+ }
}
diff --git a/backend/Core/Musify.Application/Configuration/PlaybackConfiguration.cs b/backend/Core/Musify.Application/Configuration/PlaybackConfiguration.cs
index 2fac12b..bf0f0c2 100644
--- a/backend/Core/Musify.Application/Configuration/PlaybackConfiguration.cs
+++ b/backend/Core/Musify.Application/Configuration/PlaybackConfiguration.cs
@@ -1,17 +1,18 @@
using System.ComponentModel.DataAnnotations;
-namespace Musify.Application.Configuration;
-
-
-public sealed class PlaybackConfiguration : IConfigurationOptions
+namespace Musify.Application.Configuration
{
- public static string SectionName => "Playback";
- public bool AllowAnonymousListening { get; set; }
+ public sealed class PlaybackConfiguration : IConfigurationOptions
+ {
+ public static string SectionName => "Playback";
- [Range(0, 3600)]
- public int AnonymousFragmentSeconds { get; set; }
+ public bool AllowAnonymousListening { get; set; }
- [Range(1, int.MaxValue)]
- public int EstimatedAudioBytesPerSecond { get; set; } = 16_000;
+ [Range(0, 3600)]
+ public int AnonymousFragmentSeconds { get; set; }
+
+ [Range(1, int.MaxValue)]
+ public int EstimatedAudioBytesPerSecond { get; set; } = 16_000;
+ }
}
diff --git a/backend/Core/Musify.Application/Configuration/Responses/PlaybackPublicConfigResponse.cs b/backend/Core/Musify.Application/Configuration/Responses/PlaybackPublicConfigResponse.cs
index c1083a3..90a92b9 100644
--- a/backend/Core/Musify.Application/Configuration/Responses/PlaybackPublicConfigResponse.cs
+++ b/backend/Core/Musify.Application/Configuration/Responses/PlaybackPublicConfigResponse.cs
@@ -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);
+}
diff --git a/backend/Core/Musify.Application/Configuration/StreamGatewayConfiguration.cs b/backend/Core/Musify.Application/Configuration/StreamGatewayConfiguration.cs
index 3c9ecb8..488536d 100644
--- a/backend/Core/Musify.Application/Configuration/StreamGatewayConfiguration.cs
+++ b/backend/Core/Musify.Application/Configuration/StreamGatewayConfiguration.cs
@@ -1,15 +1,16 @@
using System.ComponentModel.DataAnnotations;
-namespace Musify.Application.Configuration;
-
-public class StreamGatewayConfiguration : IConfigurationOptions
+namespace Musify.Application.Configuration
{
- public static string SectionName => "StreamGateway";
+ public class StreamGatewayConfiguration : IConfigurationOptions
+ {
+ public static string SectionName => "StreamGateway";
- [Url]
- [Required]
- public required string PublicBaseUrl { get; set; }
+ [Url]
+ [Required]
+ public required string PublicBaseUrl { get; set; }
- [Required]
- public string AudioFileName { get; set; } = "audio.m4a";
+ [Required]
+ public string AudioFileName { get; set; } = "audio.m4a";
+ }
}
diff --git a/backend/Core/Musify.Application/Configuration/TrackConfiguration.cs b/backend/Core/Musify.Application/Configuration/TrackConfiguration.cs
index 80b8df3..b569b1f 100644
--- a/backend/Core/Musify.Application/Configuration/TrackConfiguration.cs
+++ b/backend/Core/Musify.Application/Configuration/TrackConfiguration.cs
@@ -2,55 +2,56 @@ 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 static string SectionName => "Track";
-
- [Required]
- public TrackRoutes Routes { get; set; } = new();
-
- [Required]
- public PictureSizes PicturesSizes { get; set; } = new();
-
- PictureRoutes IPictureOwnerConfiguration.Routes => Routes;
-}
-
-public class TrackRoutes : PictureRoutes
-{
- public TrackRoutes()
+ public class TrackConfiguration : IConfigurationOptions, IPictureOwnerConfiguration
{
- ParentFolder = "Tracks";
+ public static string SectionName => "Track";
+
+ [Required]
+ public TrackRoutes Routes { get; set; } = new();
+
+ [Required]
+ public PictureSizes PicturesSizes { get; set; } = new();
+
+ PictureRoutes IPictureOwnerConfiguration.Routes => Routes;
}
- [Required]
- public string PresetOriginalPicture { get; set; } = "PresetOriginalPicture.webp";
+ public class TrackRoutes : PictureRoutes
+ {
+ public TrackRoutes()
+ {
+ ParentFolder = "Tracks";
+ }
- [Required]
- public string PresetSmallPicture { get; set; } = "PresetSmallPicture.webp";
+ [Required]
+ public string PresetOriginalPicture { get; set; } = "PresetOriginalPicture.webp";
- [Required]
- public string PresetMediumPicture { get; set; } = "PresetMediumPicture.webp";
+ [Required]
+ public string PresetSmallPicture { get; set; } = "PresetSmallPicture.webp";
- [Required]
- public string PresetLargePicture { get; set; } = "PresetLargePicture.webp";
+ [Required]
+ public string PresetMediumPicture { get; set; } = "PresetMediumPicture.webp";
- [Required]
- public string OriginalAudiosFolder { get; set; } = "OriginalAudios";
+ [Required]
+ public string PresetLargePicture { get; set; } = "PresetLargePicture.webp";
- [Required]
- public string ProcessedAudiosFolder { get; set; } = "ProcessedAudios";
+ [Required]
+ public string OriginalAudiosFolder { get; set; } = "OriginalAudios";
- public string PresetSmallPicturePath => StorageKey.Combine(ParentFolder, PresetSmallPicture);
+ [Required]
+ public string ProcessedAudiosFolder { get; set; } = "ProcessedAudios";
- public string PresetMediumPicturePath => StorageKey.Combine(ParentFolder, PresetMediumPicture);
+ public string PresetSmallPicturePath => StorageKey.Combine(ParentFolder, PresetSmallPicture);
- public string PresetLargePicturePath => StorageKey.Combine(ParentFolder, PresetLargePicture);
+ public string PresetMediumPicturePath => StorageKey.Combine(ParentFolder, PresetMediumPicture);
- public string BuildOriginalAudioPath(long userId, string audioName) =>
- StorageKey.Combine(UploadsFolder, userId.ToString(CultureInfo.InvariantCulture), ParentFolder, OriginalAudiosFolder, audioName);
+ public string PresetLargePicturePath => StorageKey.Combine(ParentFolder, PresetLargePicture);
- public string BuildProcessedAudioPath(string folderName) =>
- StorageKey.Combine(ParentFolder, ProcessedAudiosFolder, folderName);
+ public string BuildOriginalAudioPath(long userId, string audioName) =>
+ StorageKey.Combine(UploadsFolder, userId.ToString(CultureInfo.InvariantCulture), ParentFolder, OriginalAudiosFolder, audioName);
+
+ public string BuildProcessedAudioPath(string folderName) =>
+ StorageKey.Combine(ParentFolder, ProcessedAudiosFolder, folderName);
+ }
}
diff --git a/backend/Core/Musify.Application/Configuration/UploadIntentConfiguration.cs b/backend/Core/Musify.Application/Configuration/UploadIntentConfiguration.cs
index 3b72185..eb68c90 100644
--- a/backend/Core/Musify.Application/Configuration/UploadIntentConfiguration.cs
+++ b/backend/Core/Musify.Application/Configuration/UploadIntentConfiguration.cs
@@ -1,41 +1,42 @@
using System.ComponentModel.DataAnnotations;
-namespace Musify.Application.Configuration;
-
-public class UploadIntentConfiguration : IConfigurationOptions
+namespace Musify.Application.Configuration
{
- public static string SectionName => "UploadIntent";
+ public class UploadIntentConfiguration : IConfigurationOptions
+ {
+ public static string SectionName => "UploadIntent";
- [Range(10, 3600)]
- public int UploadUrlExpiresInSeconds { get; set; } = 120;
+ [Range(10, 3600)]
+ public int UploadUrlExpiresInSeconds { get; set; } = 120;
- [Range(1, 100)]
- public int MaxActiveUploadIntentsPerUser { get; set; } = 5;
+ [Range(1, 100)]
+ public int MaxActiveUploadIntentsPerUser { get; set; } = 5;
- [Range(1, long.MaxValue)]
- public long MaxActiveUploadBytesPerUser { get; set; } = 200L * 1024 * 1024;
+ [Range(1, long.MaxValue)]
+ public long MaxActiveUploadBytesPerUser { get; set; } = 200L * 1024 * 1024;
- [Range(1, long.MaxValue)]
- public long MaxUploadBytes { get; set; } = 100L * 1024 * 1024;
+ [Range(1, long.MaxValue)]
+ public long MaxUploadBytes { get; set; } = 100L * 1024 * 1024;
- [Range(1, long.MaxValue)]
- public long DefaultExpectedPictureSizeBytes { get; set; } = 10L * 1024 * 1024;
+ [Range(1, long.MaxValue)]
+ public long DefaultExpectedPictureSizeBytes { get; set; } = 10L * 1024 * 1024;
- [Range(1, long.MaxValue)]
- public long DefaultExpectedAudioSizeBytes { get; set; } = 100L * 1024 * 1024;
+ [Range(1, long.MaxValue)]
+ public long DefaultExpectedAudioSizeBytes { get; set; } = 100L * 1024 * 1024;
- [Range(30, 86400)]
- public int ExpirationJobIntervalSeconds { get; set; } = 300;
+ [Range(30, 86400)]
+ public int ExpirationJobIntervalSeconds { get; set; } = 300;
- [Range(0, 365)]
- public int ExpiredIntentsRetentionDays { get; set; } = 7;
+ [Range(0, 365)]
+ public int ExpiredIntentsRetentionDays { get; set; } = 7;
- [Required, MinLength(1)]
- public string TempRootPrefix { get; set; } = "temp";
+ [Required, MinLength(1)]
+ public string TempRootPrefix { get; set; } = "temp";
- [Range(1, 30)]
- public int TempUploadsRetentionDays { get; set; } = 3;
+ [Range(1, 30)]
+ public int TempUploadsRetentionDays { get; set; } = 3;
- [Range(60, 86400)]
- public int TempCleanupJobIntervalSeconds { get; set; } = 3600;
+ [Range(60, 86400)]
+ public int TempCleanupJobIntervalSeconds { get; set; } = 3600;
+ }
}
diff --git a/backend/Core/Musify.Application/Contracts/AudioTranscodeResult.cs b/backend/Core/Musify.Application/Contracts/AudioTranscodeResult.cs
index 85e45e4..43902f1 100644
--- a/backend/Core/Musify.Application/Contracts/AudioTranscodeResult.cs
+++ b/backend/Core/Musify.Application/Contracts/AudioTranscodeResult.cs
@@ -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);
+}
diff --git a/backend/Core/Musify.Application/Contracts/IAudioTranscoderService.cs b/backend/Core/Musify.Application/Contracts/IAudioTranscoderService.cs
index 29f8b81..e4f56a0 100644
--- a/backend/Core/Musify.Application/Contracts/IAudioTranscoderService.cs
+++ b/backend/Core/Musify.Application/Contracts/IAudioTranscoderService.cs
@@ -1,6 +1,7 @@
-namespace Musify.Application.Contracts;
-
-public interface IAudioTranscoderService
+namespace Musify.Application.Contracts
{
- public Task TranscodeToAudioFileAsync(Stream audioStream, string destinationPath, CancellationToken cancellationToken);
+ public interface IAudioTranscoderService
+ {
+ public Task TranscodeToAudioFileAsync(Stream audioStream, string destinationPath, CancellationToken cancellationToken);
+ }
}
diff --git a/backend/Core/Musify.Application/Contracts/IDatabase.cs b/backend/Core/Musify.Application/Contracts/IDatabase.cs
index 89e0964..840a894 100644
--- a/backend/Core/Musify.Application/Contracts/IDatabase.cs
+++ b/backend/Core/Musify.Application/Contracts/IDatabase.cs
@@ -2,41 +2,42 @@ using ErrorOr;
using Microsoft.EntityFrameworkCore;
using Musify.Domain.Entities;
-namespace Musify.Application.Contracts;
-
-public interface IDatabase
+namespace Musify.Application.Contracts
{
- public DbSet Users { get; }
+ public interface IDatabase
+ {
+ public DbSet Users { get; }
- public DbSet PlayLists { get; }
+ public DbSet PlayLists { get; }
- public DbSet Albums { get; }
+ public DbSet Albums { get; }
- public DbSet AlbumHasTracks { get; }
+ public DbSet AlbumHasTracks { get; }
- public DbSet