Design Prot Merge #1

Merged
NotS1mp merged 217 commits from design-prot into master 2026-09-21 15:48:16 +00:00
54 changed files with 68 additions and 66 deletions
Showing only changes of commit 4251d292e1 - Show all commits
@@ -20,7 +20,7 @@ namespace Musify.Application.Albums
var album = await database.Albums var album = await database.Albums
.AsNoTracking() .AsNoTracking()
.SingleOrDefaultAsync(a => a.Id == request.AlbumId, cancellationToken); .SingleOrDefaultAsync(a => a.Id == request.AlbumId, cancellationToken);
if (album is null) if (album == null)
{ {
logger.LogInformation("Album {AlbumId} not found", request.AlbumId); logger.LogInformation("Album {AlbumId} not found", request.AlbumId);
return Error.NotFound(); return Error.NotFound();
@@ -35,7 +35,7 @@ namespace Musify.Application.Albums
var track = await database.Tracks var track = await database.Tracks
.AsNoTracking() .AsNoTracking()
.SingleOrDefaultAsync(t => t.Id == request.TrackId, cancellationToken); .SingleOrDefaultAsync(t => t.Id == request.TrackId, cancellationToken);
if (track is null) if (track == null)
{ {
logger.LogInformation("Track {TrackId} not found", request.TrackId); logger.LogInformation("Track {TrackId} not found", request.TrackId);
return Error.NotFound(description: $"Track {request.TrackId} not found"); return Error.NotFound(description: $"Track {request.TrackId} not found");
@@ -18,7 +18,7 @@ namespace Musify.Application.Albums
{ {
var album = await database.Albums var album = await database.Albums
.SingleOrDefaultAsync(a => a.Id == request.AlbumId, cancellationToken); .SingleOrDefaultAsync(a => a.Id == request.AlbumId, cancellationToken);
if (album is null) if (album == null)
{ {
logger.LogDebug("Album {AlbumId} not found", request.AlbumId); logger.LogDebug("Album {AlbumId} not found", request.AlbumId);
return Error.NotFound(); return Error.NotFound();
@@ -29,7 +29,7 @@ namespace Musify.Application.Albums
}) })
.SingleOrDefaultAsync(cancellationToken); .SingleOrDefaultAsync(cancellationToken);
if (album is null) if (album == null)
return Error.NotFound(); return Error.NotFound();
return AlbumApplicationResponse.FromEntity(album.Album, album.TrackCount, album.CoverTrackIds); return AlbumApplicationResponse.FromEntity(album.Album, album.TrackCount, album.CoverTrackIds);
@@ -25,7 +25,7 @@ namespace Musify.Application.Albums
.Select(a => a.Pictures) .Select(a => a.Pictures)
.SingleOrDefaultAsync(cancellationToken); .SingleOrDefaultAsync(cancellationToken);
if (pictures is null) if (pictures == null)
return Error.NotFound(); return Error.NotFound();
var routes = albumConfiguration.Routes; var routes = albumConfiguration.Routes;
@@ -19,7 +19,7 @@ namespace Musify.Application.Albums
var album = await database.Albums var album = await database.Albums
.AsNoTracking() .AsNoTracking()
.SingleOrDefaultAsync(a => a.Id == request.AlbumId, cancellationToken); .SingleOrDefaultAsync(a => a.Id == request.AlbumId, cancellationToken);
if (album is null) if (album == null)
{ {
logger.LogInformation("Album {AlbumId} not found", request.AlbumId); logger.LogInformation("Album {AlbumId} not found", request.AlbumId);
return Error.NotFound(); return Error.NotFound();
@@ -37,7 +37,7 @@ namespace Musify.Application.Albums
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
var link = albumTracks.SingleOrDefault(albumTrack => albumTrack.TrackId == request.TrackId); var link = albumTracks.SingleOrDefault(albumTrack => albumTrack.TrackId == request.TrackId);
if (link is null) if (link == null)
{ {
logger.LogInformation("Track {TrackId} not in album {AlbumId}", request.TrackId, request.AlbumId); logger.LogInformation("Track {TrackId} not in album {AlbumId}", request.TrackId, request.AlbumId);
return Error.NotFound(description: "Track is not in the album."); return Error.NotFound(description: "Track is not in the album.");
@@ -36,7 +36,7 @@ namespace Musify.Application.Albums
{ {
var album = await database.Albums var album = await database.Albums
.SingleOrDefaultAsync(a => a.Id == request.AlbumId, cancellationToken); .SingleOrDefaultAsync(a => a.Id == request.AlbumId, cancellationToken);
if (album is null) if (album == null)
{ {
logger.LogInformation("Album {AlbumId} not found", request.AlbumId); logger.LogInformation("Album {AlbumId} not found", request.AlbumId);
return Error.NotFound(); return Error.NotFound();
@@ -30,7 +30,7 @@ namespace Musify.Application.Likes
.SingleOrDefaultAsync(like => like.UserId == request.UserId && like.TrackId == request.TrackId, cancellationToken); .SingleOrDefaultAsync(like => like.UserId == request.UserId && like.TrackId == request.TrackId, cancellationToken);
bool nowLiked; bool nowLiked;
if (existingLike is not null) if (existingLike != null)
{ {
database.TrackLikes.Remove(existingLike); database.TrackLikes.Remove(existingLike);
nowLiked = false; nowLiked = false;
@@ -19,7 +19,7 @@ namespace Musify.Application.Mixes
.Include(mix => mix.Items) .Include(mix => mix.Items)
.SingleOrDefaultAsync(mix => mix.Id == request.MixId && mix.UserId == request.UserId, cancellationToken); .SingleOrDefaultAsync(mix => mix.Id == request.MixId && mix.UserId == request.UserId, cancellationToken);
if (mix is null) if (mix == null)
return Error.NotFound(description: $"Mix {request.MixId} was not found"); return Error.NotFound(description: $"Mix {request.MixId} was not found");
var orderedItems = mix.Items.OrderBy(item => item.Position).ToList(); var orderedItems = mix.Items.OrderBy(item => item.Position).ToList();
@@ -19,7 +19,7 @@ namespace Musify.Application.PlayLists
{ {
var playList = await database.PlayLists var playList = await database.PlayLists
.SingleOrDefaultAsync(pl => pl.Id == request.PlayListId, cancellationToken); .SingleOrDefaultAsync(pl => pl.Id == request.PlayListId, cancellationToken);
if (playList is null) if (playList == null)
{ {
logger.LogInformation("Playlist {PlayListId} not found", request.PlayListId); logger.LogInformation("Playlist {PlayListId} not found", request.PlayListId);
return Error.NotFound(); return Error.NotFound();
@@ -20,7 +20,7 @@ namespace Musify.Application.PlayLists
public async ValueTask<ErrorOr<Success>> Handle(DeletePlayListCommand request, CancellationToken cancellationToken) public async ValueTask<ErrorOr<Success>> Handle(DeletePlayListCommand request, CancellationToken cancellationToken)
{ {
var playList = await database.PlayLists.SingleOrDefaultAsync(p => p.Id == request.PlayListId, cancellationToken); var playList = await database.PlayLists.SingleOrDefaultAsync(p => p.Id == request.PlayListId, cancellationToken);
if (playList is null) if (playList == null)
{ {
logger.LogDebug("Playlist {PlayListId} not found", request.PlayListId); logger.LogDebug("Playlist {PlayListId} not found", request.PlayListId);
return Error.NotFound(); return Error.NotFound();
@@ -29,7 +29,7 @@ namespace Musify.Application.PlayLists
}) })
.SingleOrDefaultAsync(cancellationToken); .SingleOrDefaultAsync(cancellationToken);
if (entry is null) if (entry == null)
return Error.NotFound(); return Error.NotFound();
if (entry.PlayList.Visibility == PlaylistVisibility.Private if (entry.PlayList.Visibility == PlaylistVisibility.Private
@@ -25,7 +25,7 @@ namespace Musify.Application.PlayLists
.Select(p => p.Pictures) .Select(p => p.Pictures)
.SingleOrDefaultAsync(cancellationToken); .SingleOrDefaultAsync(cancellationToken);
if (pictures is null) if (pictures == null)
return Error.NotFound(); return Error.NotFound();
var routes = playListConfiguration.Routes; var routes = playListConfiguration.Routes;
@@ -18,7 +18,7 @@ namespace Musify.Application.PlayLists
{ {
var playList = await database.PlayLists var playList = await database.PlayLists
.SingleOrDefaultAsync(pl => pl.Id == request.PlayListId, cancellationToken); .SingleOrDefaultAsync(pl => pl.Id == request.PlayListId, cancellationToken);
if (playList is null) if (playList == null)
{ {
logger.LogInformation("Playlist {PlayListId} not found", request.PlayListId); logger.LogInformation("Playlist {PlayListId} not found", request.PlayListId);
return Error.NotFound(); return Error.NotFound();
@@ -32,7 +32,7 @@ namespace Musify.Application.PlayLists
var link = await database.PlayListHasTracks var link = await database.PlayListHasTracks
.SingleOrDefaultAsync(plt => plt.PlayListId == request.PlayListId && plt.TrackId == request.TrackId, cancellationToken); .SingleOrDefaultAsync(plt => plt.PlayListId == request.PlayListId && plt.TrackId == request.TrackId, cancellationToken);
if (link is null) if (link == null)
{ {
logger.LogInformation("Track {TrackId} not in playlist {PlayListId}", request.TrackId, request.PlayListId); logger.LogInformation("Track {TrackId} not in playlist {PlayListId}", request.TrackId, request.PlayListId);
return Error.NotFound(description: "Track is not in the playlist."); return Error.NotFound(description: "Track is not in the playlist.");
@@ -35,7 +35,7 @@ namespace Musify.Application.PlayLists
public async ValueTask<ErrorOr<PlayListApplicationResponse>> Handle(UpdatePlayListCommand request, CancellationToken cancellationToken) public async ValueTask<ErrorOr<PlayListApplicationResponse>> Handle(UpdatePlayListCommand request, CancellationToken cancellationToken)
{ {
var playListEntity = await database.PlayLists.SingleOrDefaultAsync(pl => pl.Id == request.PlayListId, cancellationToken); var playListEntity = await database.PlayLists.SingleOrDefaultAsync(pl => pl.Id == request.PlayListId, cancellationToken);
if (playListEntity is null) if (playListEntity == null)
{ {
logger.LogInformation("Playlist {PlayListId} not found", request.PlayListId); logger.LogInformation("Playlist {PlayListId} not found", request.PlayListId);
return Error.NotFound(); return Error.NotFound();
@@ -32,7 +32,7 @@ public sealed class TrackStreamIssuer(
// Anonymous listeners have no User row to satisfy ListeningHistory's foreign key against, and // Anonymous listeners have no User row to satisfy ListeningHistory's foreign key against, and
// their plays aren't "history" anyone can look back on anyway, so there's nothing to record. // their plays aren't "history" anyone can look back on anyway, so there's nothing to record.
if (userId is not null) if (userId != null)
{ {
var newListeningHistory = new ListeningHistory var newListeningHistory = new ListeningHistory
{ {
@@ -48,7 +48,7 @@ public sealed class TrackStreamIssuer(
private long? AnonymousFragmentBytes(long? userId) private long? AnonymousFragmentBytes(long? userId)
{ {
if (userId is not null || playbackConfiguration.AnonymousFragmentSeconds <= 0) if (userId != null || playbackConfiguration.AnonymousFragmentSeconds <= 0)
return null; return null;
return (long)playbackConfiguration.AnonymousFragmentSeconds * playbackConfiguration.EstimatedAudioBytesPerSecond; return (long)playbackConfiguration.AnonymousFragmentSeconds * playbackConfiguration.EstimatedAudioBytesPerSecond;
@@ -42,7 +42,7 @@ public sealed class UploadIntentValidator(
var intent = await database.UploadIntents var intent = await database.UploadIntents
.FirstOrDefaultAsync(i => i.Id == intentId, cancellationToken); .FirstOrDefaultAsync(i => i.Id == intentId, cancellationToken);
if (intent is null || intent.UserId != userId) if (intent == null || intent.UserId != userId)
return Error.NotFound(description: "Upload intent not found or not accessible."); return Error.NotFound(description: "Upload intent not found or not accessible.");
if (intent.IsConsumed) if (intent.IsConsumed)
@@ -52,7 +52,7 @@ public sealed class UploadIntentValidator(
return Error.Validation(description: "Upload intent has expired."); return Error.Validation(description: "Upload intent has expired.");
var metadata = await storageService.HeadObjectAsync(intent.Bucket, intent.Key, cancellationToken); var metadata = await storageService.HeadObjectAsync(intent.Bucket, intent.Key, cancellationToken);
if (metadata is null) if (metadata == null)
return Error.NotFound(description: "Uploaded object not found in storage. Upload the file first."); return Error.NotFound(description: "Uploaded object not found in storage. Upload the file first.");
if (metadata.ContentLength > config.MaxUploadBytes) if (metadata.ContentLength > config.MaxUploadBytes)
@@ -35,7 +35,7 @@ namespace Musify.Application.Tracks
{ {
var user = await database.Users var user = await database.Users
.SingleOrDefaultAsync(u => u.Id == request.UserId, cancellationToken); .SingleOrDefaultAsync(u => u.Id == request.UserId, cancellationToken);
if (user is null) if (user == null)
{ {
logger.LogWarning("User {UserId} not found", request.UserId); logger.LogWarning("User {UserId} not found", request.UserId);
return Error.NotFound(description: $"User {request.UserId} not found"); return Error.NotFound(description: $"User {request.UserId} not found");
@@ -23,7 +23,7 @@ namespace Musify.Application.Tracks
public async ValueTask<ErrorOr<Success>> Handle(DeleteTrackCommand request, CancellationToken cancellationToken) public async ValueTask<ErrorOr<Success>> Handle(DeleteTrackCommand request, CancellationToken cancellationToken)
{ {
var track = await database.Tracks.SingleOrDefaultAsync(t => t.Id == request.TrackId, cancellationToken); var track = await database.Tracks.SingleOrDefaultAsync(t => t.Id == request.TrackId, cancellationToken);
if (track is null) if (track == null)
{ {
logger.LogInformation("Track {TrackId} not found", request.TrackId); logger.LogInformation("Track {TrackId} not found", request.TrackId);
return Error.NotFound(); return Error.NotFound();
@@ -21,7 +21,7 @@ namespace Musify.Application.Tracks
.Select(t => new { Track = t, ListensCount = t.ListeningHistories.Count }) .Select(t => new { Track = t, ListensCount = t.ListeningHistories.Count })
.SingleOrDefaultAsync(cancellationToken); .SingleOrDefaultAsync(cancellationToken);
if (entity is null) if (entity == null)
return Error.NotFound(); return Error.NotFound();
return TrackApplicationResponse.FromEntity(entity.Track, entity.ListensCount); return TrackApplicationResponse.FromEntity(entity.Track, entity.ListensCount);
@@ -23,7 +23,7 @@ namespace Musify.Application.Tracks
{ {
public async ValueTask<ErrorOr<TrackStreamResponse>> Handle(GetTrackStreamQuery request, CancellationToken cancellationToken) public async ValueTask<ErrorOr<TrackStreamResponse>> Handle(GetTrackStreamQuery request, CancellationToken cancellationToken)
{ {
if (request.UserId is null && !playbackConfiguration.AllowAnonymousListening) if (request.UserId == null && !playbackConfiguration.AllowAnonymousListening)
return Error.Unauthorized(description: "Sign in to stream music, or ask an administrator to enable anonymous listening."); return Error.Unauthorized(description: "Sign in to stream music, or ask an administrator to enable anonymous listening.");
var track = await database.Tracks.AsNoTracking() var track = await database.Tracks.AsNoTracking()
@@ -20,7 +20,7 @@ namespace Musify.Application.Users
.Select(u => UserApplicationResponse.FromEntity(u)) .Select(u => UserApplicationResponse.FromEntity(u))
.SingleOrDefaultAsync(cancellationToken); .SingleOrDefaultAsync(cancellationToken);
if (user is null) if (user == null)
return Error.NotFound(); return Error.NotFound();
return user; return user;
@@ -38,7 +38,7 @@ namespace Musify.Application.Users
}) })
.SingleOrDefaultAsync(cancellationToken); .SingleOrDefaultAsync(cancellationToken);
if (user is null) if (user == null)
return Error.NotFound(); return Error.NotFound();
var followersCount = await database.UserFollows var followersCount = await database.UserFollows
@@ -23,7 +23,7 @@ namespace Musify.Application.Users
var user = await database.Users var user = await database.Users
.SingleOrDefaultAsync(u => u.Id == request.Id, cancellationToken); .SingleOrDefaultAsync(u => u.Id == request.Id, cancellationToken);
if (user is null) if (user == null)
{ {
await database.Users.AddAsync(new User await database.Users.AddAsync(new User
{ {
@@ -18,7 +18,7 @@ namespace Musify.Application.Users
var follow = await database.UserFollows var follow = await database.UserFollows
.SingleOrDefaultAsync(f => f.FollowerId == request.FollowerId && f.FollowedId == request.FollowedId, cancellationToken); .SingleOrDefaultAsync(f => f.FollowerId == request.FollowerId && f.FollowedId == request.FollowedId, cancellationToken);
if (follow is null) if (follow == null)
return new Success(); return new Success();
database.UserFollows.Remove(follow); database.UserFollows.Remove(follow);
@@ -19,7 +19,7 @@ namespace Musify.Infrastructure.MassTransit.Activities.Albums
var album = await database.Albums var album = await database.Albums
.SingleOrDefaultAsync(a => a.Id == executeContext.Arguments.AlbumId, executeContext.CancellationToken); .SingleOrDefaultAsync(a => a.Id == executeContext.Arguments.AlbumId, executeContext.CancellationToken);
if (album is not null) if (album != null)
{ {
album.LifeCycleStatus = LifeCycleStatus.Failed; album.LifeCycleStatus = LifeCycleStatus.Failed;
await database.SaveChangesAsync(executeContext.CancellationToken); await database.SaveChangesAsync(executeContext.CancellationToken);
@@ -93,7 +93,7 @@ namespace Musify.Infrastructure.MassTransit.Activities.Audio
try try
{ {
var track = await database.Tracks.SingleOrDefaultAsync(t => t.Id == compensateContext.Log.TrackId, compensateContext.CancellationToken); var track = await database.Tracks.SingleOrDefaultAsync(t => t.Id == compensateContext.Log.TrackId, compensateContext.CancellationToken);
if (track is null) if (track == null)
throw new InvalidOperationException($"Track with id {compensateContext.Log.TrackId} not found"); throw new InvalidOperationException($"Track with id {compensateContext.Log.TrackId} not found");
track.Audio.FolderName = compensateContext.Log.PreviousAudioFolderName; track.Audio.FolderName = compensateContext.Log.PreviousAudioFolderName;
@@ -31,14 +31,14 @@ namespace Musify.Infrastructure.MassTransit.Activities.Pictures
[executeContext.Arguments.AlbumId], [executeContext.Arguments.AlbumId],
cancellationToken: executeContext.CancellationToken); cancellationToken: executeContext.CancellationToken);
if (album is null) if (album == null)
{ {
logger.LogWarning("Album {AlbumId} not found", logger.LogWarning("Album {AlbumId} not found",
executeContext.Arguments.AlbumId); executeContext.Arguments.AlbumId);
throw new InvalidOperationException($"Album with id {executeContext.Arguments.AlbumId} not found"); throw new InvalidOperationException($"Album with id {executeContext.Arguments.AlbumId} not found");
} }
if (album.Pictures is null) if (album.Pictures == null)
{ {
logger.LogWarning("Album {AlbumId} has no pending picture upload", logger.LogWarning("Album {AlbumId} has no pending picture upload",
executeContext.Arguments.AlbumId); executeContext.Arguments.AlbumId);
@@ -85,7 +85,7 @@ namespace Musify.Infrastructure.MassTransit.Activities.Pictures
[compensateContext.Log.AlbumId], [compensateContext.Log.AlbumId],
cancellationToken: compensateContext.CancellationToken); cancellationToken: compensateContext.CancellationToken);
if (album is null) if (album == null)
{ {
return compensateContext.Compensated(); return compensateContext.Compensated();
} }
@@ -31,14 +31,14 @@ namespace Musify.Infrastructure.MassTransit.Activities.Pictures
[executeContext.Arguments.PlayListId], [executeContext.Arguments.PlayListId],
cancellationToken: executeContext.CancellationToken); cancellationToken: executeContext.CancellationToken);
if (playList is null) if (playList == null)
{ {
logger.LogWarning("Playlist {PlayListId} not found", logger.LogWarning("Playlist {PlayListId} not found",
executeContext.Arguments.PlayListId); executeContext.Arguments.PlayListId);
throw new InvalidOperationException($"PlayList with id {executeContext.Arguments.PlayListId} not found"); throw new InvalidOperationException($"PlayList with id {executeContext.Arguments.PlayListId} not found");
} }
if (playList.Pictures is null) if (playList.Pictures == null)
{ {
logger.LogWarning("Playlist {PlayListId} has no pending picture upload", logger.LogWarning("Playlist {PlayListId} has no pending picture upload",
executeContext.Arguments.PlayListId); executeContext.Arguments.PlayListId);
@@ -85,7 +85,7 @@ namespace Musify.Infrastructure.MassTransit.Activities.Pictures
[compensateContext.Log.PlayListId], [compensateContext.Log.PlayListId],
cancellationToken: compensateContext.CancellationToken); cancellationToken: compensateContext.CancellationToken);
if (playList is null) if (playList == null)
{ {
return compensateContext.Compensated(); return compensateContext.Compensated();
} }
@@ -31,7 +31,7 @@ namespace Musify.Infrastructure.MassTransit.Activities.Pictures
[executeContext.Arguments.TrackId], [executeContext.Arguments.TrackId],
cancellationToken: executeContext.CancellationToken); cancellationToken: executeContext.CancellationToken);
if (track is null) if (track == null)
{ {
logger.LogWarning("Track {TrackId} not found", logger.LogWarning("Track {TrackId} not found",
executeContext.Arguments.TrackId); executeContext.Arguments.TrackId);
@@ -79,7 +79,7 @@ namespace Musify.Infrastructure.MassTransit.Activities.Pictures
[compensateContext.Log.TrackId], [compensateContext.Log.TrackId],
cancellationToken: compensateContext.CancellationToken); cancellationToken: compensateContext.CancellationToken);
if (track is null) if (track == null)
return compensateContext.Compensated(); return compensateContext.Compensated();
track.Pictures.OriginalName = compensateContext.Log.PreviousOriginalPictureKey; track.Pictures.OriginalName = compensateContext.Log.PreviousOriginalPictureKey;
@@ -18,7 +18,7 @@ namespace Musify.Infrastructure.MassTransit.Activities.PlayLists
var playList = await database.PlayLists var playList = await database.PlayLists
.SingleOrDefaultAsync(p => p.Id == executeContext.Arguments.PlayListId, executeContext.CancellationToken); .SingleOrDefaultAsync(p => p.Id == executeContext.Arguments.PlayListId, executeContext.CancellationToken);
if (playList is null) if (playList == null)
{ {
logger.LogInformation( logger.LogInformation(
"PlayList {PlayListId} not found while deleting from DB, skipping", "PlayList {PlayListId} not found while deleting from DB, skipping",
@@ -19,7 +19,7 @@ namespace Musify.Infrastructure.MassTransit.Activities.PlayLists
var playList = await database.PlayLists var playList = await database.PlayLists
.SingleOrDefaultAsync(p => p.Id == executeContext.Arguments.PlayListId, executeContext.CancellationToken); .SingleOrDefaultAsync(p => p.Id == executeContext.Arguments.PlayListId, executeContext.CancellationToken);
if (playList is not null) if (playList != null)
{ {
playList.LifeCycleStatus = LifeCycleStatus.Failed; playList.LifeCycleStatus = LifeCycleStatus.Failed;
await database.SaveChangesAsync(executeContext.CancellationToken); await database.SaveChangesAsync(executeContext.CancellationToken);
@@ -19,7 +19,7 @@ namespace Musify.Infrastructure.MassTransit.Activities.PlayLists
var playList = await database.PlayLists var playList = await database.PlayLists
.SingleOrDefaultAsync(p => p.Id == executeContext.Arguments.PlayListId, executeContext.CancellationToken); .SingleOrDefaultAsync(p => p.Id == executeContext.Arguments.PlayListId, executeContext.CancellationToken);
if (playList is null) if (playList == null)
{ {
logger.LogInformation( logger.LogInformation(
"PlayList {PlayListId} not found while marking as removing, skipping", "PlayList {PlayListId} not found while marking as removing, skipping",
@@ -18,7 +18,7 @@ namespace Musify.Infrastructure.MassTransit.Activities.Tracks
var track = await database.Tracks var track = await database.Tracks
.SingleOrDefaultAsync(t => t.Id == executeContext.Arguments.TrackId, executeContext.CancellationToken); .SingleOrDefaultAsync(t => t.Id == executeContext.Arguments.TrackId, executeContext.CancellationToken);
if (track is null) if (track == null)
{ {
logger.LogInformation( logger.LogInformation(
"Track {TrackId} not found while deleting from DB, skipping", "Track {TrackId} not found while deleting from DB, skipping",
@@ -19,7 +19,7 @@ namespace Musify.Infrastructure.MassTransit.Activities.Tracks
var track = await database.Tracks var track = await database.Tracks
.SingleOrDefaultAsync(t => t.Id == executeContext.Arguments.TrackId, executeContext.CancellationToken); .SingleOrDefaultAsync(t => t.Id == executeContext.Arguments.TrackId, executeContext.CancellationToken);
if (track is not null) if (track != null)
{ {
track.LifeCycleStatus = LifeCycleStatus.Failed; track.LifeCycleStatus = LifeCycleStatus.Failed;
await database.SaveChangesAsync(executeContext.CancellationToken); await database.SaveChangesAsync(executeContext.CancellationToken);
@@ -19,7 +19,7 @@ namespace Musify.Infrastructure.MassTransit.Activities.Tracks
var track = await database.Tracks var track = await database.Tracks
.SingleOrDefaultAsync(t => t.Id == executeContext.Arguments.TrackId, executeContext.CancellationToken); .SingleOrDefaultAsync(t => t.Id == executeContext.Arguments.TrackId, executeContext.CancellationToken);
if (track is null) if (track == null)
{ {
logger.LogInformation( logger.LogInformation(
"Track {TrackId} not found while marking as removing, skipping", "Track {TrackId} not found while marking as removing, skipping",
@@ -37,7 +37,7 @@ namespace Musify.Infrastructure.MassTransit.RoutingSlip.Builders
EndpointHelper.BuildExecuteActivityUri(MarkPlayListAsRemovingActivity.ExecuteEndpointName), EndpointHelper.BuildExecuteActivityUri(MarkPlayListAsRemovingActivity.ExecuteEndpointName),
new MarkPlayListAsRemovingArguments(playListId)); new MarkPlayListAsRemovingArguments(playListId));
if (playList?.Pictures is not null) if (playList?.Pictures != null)
{ {
var bucket = storageConfiguration.Bucket; var bucket = storageConfiguration.Bucket;
var routes = playListConfiguration.Routes; var routes = playListConfiguration.Routes;
@@ -37,7 +37,7 @@ namespace Musify.Infrastructure.MassTransit.RoutingSlip.Builders
EndpointHelper.BuildExecuteActivityUri(MarkTrackAsRemovingActivity.ExecuteEndpointName), EndpointHelper.BuildExecuteActivityUri(MarkTrackAsRemovingActivity.ExecuteEndpointName),
new MarkTrackAsRemovingArguments(trackId)); new MarkTrackAsRemovingArguments(trackId));
if (track is not null) if (track != null)
{ {
var bucket = storageConfiguration.Bucket; var bucket = storageConfiguration.Bucket;
@@ -21,7 +21,7 @@ namespace Musify.Infrastructure.Persistence
private static void ApplyTimestamps(DbContext? context) private static void ApplyTimestamps(DbContext? context)
{ {
if (context is null) if (context == null)
return; return;
// SavingChanges(Async) fires before EF's own automatic change detection for this // SavingChanges(Async) fires before EF's own automatic change detection for this
@@ -16,7 +16,7 @@ namespace Musify.Infrastructure.Services
if (duration <= TimeSpan.Zero) if (duration <= TimeSpan.Zero)
return await factory(); return await factory();
if (cache.TryGetValue(key, out TValue? cached) && cached is not null) if (cache.TryGetValue(key, out TValue? cached) && cached != null)
return cached; return cached;
var pending = (Lazy<Task<ErrorOr<TValue>>>)inFlight.GetOrAdd( var pending = (Lazy<Task<ErrorOr<TValue>>>)inFlight.GetOrAdd(
@@ -276,7 +276,7 @@ namespace Musify.Infrastructure.Services
{ {
listResponse = await amazonS3.ListObjectsV2Async(listRequest, cancellationToken); listResponse = await amazonS3.ListObjectsV2Async(listRequest, cancellationToken);
if (listResponse.S3Objects is null or { Count: 0 } ) if (listResponse.S3Objects == null|| listResponse.S3Objects.Count == 0)
break; break;
var deleteRequest = new DeleteObjectsRequest var deleteRequest = new DeleteObjectsRequest
@@ -289,9 +289,6 @@ namespace Musify.Infrastructure.Services
var deleteResponse = await amazonS3.DeleteObjectsAsync(deleteRequest, cancellationToken); var deleteResponse = await amazonS3.DeleteObjectsAsync(deleteRequest, cancellationToken);
// Some S3-compatible backends (SeaweedFS included) omit the <Error> list from the
// XML response entirely when nothing failed, which the SDK deserializes as null
// rather than an empty list — real AWS S3 always returns a (possibly empty) list.
var deleteErrors = deleteResponse.DeleteErrors ?? []; var deleteErrors = deleteResponse.DeleteErrors ?? [];
if (deleteErrors.Count > 0) if (deleteErrors.Count > 0)
{ {
@@ -29,9 +29,9 @@ namespace Musify.Infrastructure.Services
var now = DateTimeOffset.UtcNow; var now = DateTimeOffset.UtcNow;
var claims = new Dictionary<string, object> { ["prefix"] = keyPrefix }; var claims = new Dictionary<string, object> { ["prefix"] = keyPrefix };
if (userId is not null) if (userId != null)
claims["sub"] = userId.Value.ToString(); claims["sub"] = userId.Value.ToString();
if (maxBytes is not null) if (maxBytes != null)
claims["maxBytes"] = maxBytes.Value.ToString(); claims["maxBytes"] = maxBytes.Value.ToString();
var descriptor = new SecurityTokenDescriptor var descriptor = new SecurityTokenDescriptor
+3 -2
View File
@@ -29,8 +29,9 @@
<PackageVersion Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="10.0.12" /> <PackageVersion Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="10.0.12" />
<PackageVersion Include="Microsoft.Extensions.Options.DataAnnotations" Version="10.0.12" /> <PackageVersion Include="Microsoft.Extensions.Options.DataAnnotations" Version="10.0.12" />
<PackageVersion Include="Microsoft.IdentityModel.JsonWebTokens" Version="8.23.0" /> <PackageVersion Include="Microsoft.IdentityModel.JsonWebTokens" Version="8.23.0" />
<PackageVersion Include="Microsoft.OpenApi" Version="3.10.2" /> <PackageVersion Include="Microsoft.OpenApi" Version="2.12.2" />
<PackageVersion Include="MimeMapping" Version="4.0.0" /> <PackageVersion Include="MimeMapping" Version="4.0.0" />
<PackageVersion Include="Newtonsoft.Json" Version="13.0.4" />
<PackageVersion Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.3" /> <PackageVersion Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.3" />
<PackageVersion Include="OpenTelemetry" Version="1.19.0" /> <PackageVersion Include="OpenTelemetry" Version="1.19.0" />
<PackageVersion Include="OpenTelemetry.Api" Version="1.19.0" /> <PackageVersion Include="OpenTelemetry.Api" Version="1.19.0" />
@@ -43,7 +44,7 @@
<PackageVersion Include="OpenTelemetry.Instrumentation.Process" Version="1.15.1-beta.1" /> <PackageVersion Include="OpenTelemetry.Instrumentation.Process" Version="1.15.1-beta.1" />
<PackageVersion Include="OpenTelemetry.Instrumentation.Runtime" Version="1.19.0" /> <PackageVersion Include="OpenTelemetry.Instrumentation.Runtime" Version="1.19.0" />
<PackageVersion Include="Scalar.AspNetCore" Version="2.17.6" /> <PackageVersion Include="Scalar.AspNetCore" Version="2.17.6" />
<PackageVersion Include="SixLabors.ImageSharp" Version="4.1.2" /> <PackageVersion Include="SixLabors.ImageSharp" Version="2.1.13" />
<PackageVersion Include="X.PagedList" Version="10.6.0" /> <PackageVersion Include="X.PagedList" Version="10.6.0" />
<PackageVersion Include="X.PagedList.EF" Version="10.6.0" /> <PackageVersion Include="X.PagedList.EF" Version="10.6.0" />
<PackageVersion Include="Yarp.ReverseProxy" Version="2.3.0" /> <PackageVersion Include="Yarp.ReverseProxy" Version="2.3.0" />
+1
View File
@@ -19,6 +19,7 @@
<PackageReference Include="Microsoft.EntityFrameworkCore" /> <PackageReference Include="Microsoft.EntityFrameworkCore" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" />
<PackageReference Include="Microsoft.OpenApi" /> <PackageReference Include="Microsoft.OpenApi" />
<PackageReference Include="Newtonsoft.Json" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" /> <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" />
<PackageReference Include="OpenTelemetry" /> <PackageReference Include="OpenTelemetry" />
<PackageReference Include="OpenTelemetry.Api" /> <PackageReference Include="OpenTelemetry.Api" />
@@ -125,7 +125,7 @@ public sealed class JwtBearerEventsHandler(
public override Task Challenge(JwtBearerChallengeContext context) public override Task Challenge(JwtBearerChallengeContext context)
{ {
if (context.AuthenticateFailure is not null) if (context.AuthenticateFailure != null)
logger.LogWarning("JWT challenge: {Error} - {Description}", context.Error, context.ErrorDescription); logger.LogWarning("JWT challenge: {Error} - {Description}", context.Error, context.ErrorDescription);
return Task.CompletedTask; return Task.CompletedTask;
} }
@@ -8,7 +8,7 @@ public sealed class ValidationFilter<T>(IValidator<T> validator) : IEndpointFilt
{ {
var argument = context.Arguments.OfType<T>().FirstOrDefault(); var argument = context.Arguments.OfType<T>().FirstOrDefault();
if (argument is null) if (argument == null)
return await next(context); return await next(context);
var result = await validator.ValidateAsync(argument, context.HttpContext.RequestAborted); var result = await validator.ValidateAsync(argument, context.HttpContext.RequestAborted);
@@ -10,10 +10,10 @@ public sealed class UpdatePlayListRequestValidator : AbstractValidator<UpdatePla
RuleFor(x => x.NewName) RuleFor(x => x.NewName)
.NotEmpty() .NotEmpty()
.MaximumLength(50) .MaximumLength(50)
.When(x => x.NewName is not null); .When(x => x.NewName != null);
RuleFor(x => x.NewDescription) RuleFor(x => x.NewDescription)
.MaximumLength(256) .MaximumLength(256)
.When(x => x.NewDescription is not null); .When(x => x.NewDescription != null);
} }
} }
@@ -15,6 +15,7 @@
<PackageReference Include="Microsoft.Extensions.Hosting" /> <PackageReference Include="Microsoft.Extensions.Hosting" />
<PackageReference Include="Microsoft.Extensions.Options.DataAnnotations" /> <PackageReference Include="Microsoft.Extensions.Options.DataAnnotations" />
<PackageReference Include="MimeMapping" /> <PackageReference Include="MimeMapping" />
<PackageReference Include="Newtonsoft.Json" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" /> <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" />
<PackageReference Include="SixLabors.ImageSharp" /> <PackageReference Include="SixLabors.ImageSharp" />
</ItemGroup> </ItemGroup>
@@ -11,6 +11,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" /> <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" />
<PackageReference Include="Microsoft.NET.Test.Sdk" /> <PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="Newtonsoft.Json" />
<PackageReference Include="NSubstitute" /> <PackageReference Include="NSubstitute" />
<PackageReference Include="Testcontainers.PostgreSql" /> <PackageReference Include="Testcontainers.PostgreSql" />
<PackageReference Include="xunit.v3" /> <PackageReference Include="xunit.v3" />
@@ -47,7 +47,7 @@ namespace Musify.Application.Tests.Albums
var stored = await Database.Albums.FindAsync([result.Value.Id], TestContext.Current.CancellationToken); var stored = await Database.Albums.FindAsync([result.Value.Id], TestContext.Current.CancellationToken);
Assert.NotNull(stored); Assert.NotNull(stored);
Assert.Equal("MY ALBUM", stored.NormalizedTitle); Assert.Equal("MY ALBUM", stored.NormalizedTitle);
Assert.Equal("cover.webp", stored.Pictures.OriginalName); Assert.Equal("cover.webp", stored.Pictures?.OriginalName);
await eventBus.Received(1).PublishAsync(Arg.Any<Musify.Application.Events.CreateAlbumResourcesEvent>(), Arg.Any<CancellationToken>()); await eventBus.Received(1).PublishAsync(Arg.Any<Musify.Application.Events.CreateAlbumResourcesEvent>(), Arg.Any<CancellationToken>());
} }
@@ -62,7 +62,7 @@ namespace Musify.Application.Tests.Albums
Assert.False(result.IsError); Assert.False(result.IsError);
var stored = await Database.Albums.FindAsync([album.Id], TestContext.Current.CancellationToken); var stored = await Database.Albums.FindAsync([album.Id], TestContext.Current.CancellationToken);
Assert.NotNull(stored); Assert.NotNull(stored);
Assert.Equal("new-cover.webp", stored.Pictures.OriginalName); Assert.Equal("new-cover.webp", stored.Pictures?.OriginalName);
await eventBus.Received(1).PublishAsync(Arg.Any<Musify.Application.Events.UpdateAlbumPictureSourceEvent>(), Arg.Any<CancellationToken>()); await eventBus.Received(1).PublishAsync(Arg.Any<Musify.Application.Events.UpdateAlbumPictureSourceEvent>(), Arg.Any<CancellationToken>());
} }
@@ -56,7 +56,7 @@ namespace Musify.Application.Tests.PlayLists
Assert.False(result.IsError); Assert.False(result.IsError);
var stored = await Database.PlayLists.FindAsync([result.Value.Id], TestContext.Current.CancellationToken); var stored = await Database.PlayLists.FindAsync([result.Value.Id], TestContext.Current.CancellationToken);
Assert.NotNull(stored); Assert.NotNull(stored);
Assert.Equal("cover.webp", stored.Pictures.OriginalName); Assert.Equal("cover.webp", stored.Pictures?.OriginalName);
await eventBus.Received(1).PublishAsync(Arg.Any<Musify.Application.Events.CreatePlayListResourcesEvent>(), Arg.Any<CancellationToken>()); await eventBus.Received(1).PublishAsync(Arg.Any<Musify.Application.Events.CreatePlayListResourcesEvent>(), Arg.Any<CancellationToken>());
} }
@@ -14,6 +14,7 @@
solution (and Musify.Infrastructure itself) is built against. --> solution (and Musify.Infrastructure itself) is built against. -->
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" />
<PackageReference Include="Microsoft.NET.Test.Sdk" /> <PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="Newtonsoft.Json" />
<PackageReference Include="NSubstitute" /> <PackageReference Include="NSubstitute" />
<PackageReference Include="Testcontainers" /> <PackageReference Include="Testcontainers" />
<PackageReference Include="Testcontainers.PostgreSql" /> <PackageReference Include="Testcontainers.PostgreSql" />
@@ -36,7 +36,7 @@ namespace Musify.StreamingGateway.Tests.Authentication
DateTime? expires = null, DateTime? notBefore = null, long? maxBytes = null) DateTime? expires = null, DateTime? notBefore = null, long? maxBytes = null)
{ {
var claims = new Dictionary<string, object> { ["prefix"] = prefix }; var claims = new Dictionary<string, object> { ["prefix"] = prefix };
if (maxBytes is not null) if (maxBytes != null)
claims["maxBytes"] = maxBytes.Value.ToString(); claims["maxBytes"] = maxBytes.Value.ToString();
var descriptor = new SecurityTokenDescriptor var descriptor = new SecurityTokenDescriptor
@@ -39,7 +39,7 @@ namespace Musify.StreamingGateway.Tests.Middleware
private string IssueValidToken(string prefix = Prefix, long? maxBytes = null) private string IssueValidToken(string prefix = Prefix, long? maxBytes = null)
{ {
var claims = new Dictionary<string, object> { ["prefix"] = prefix }; var claims = new Dictionary<string, object> { ["prefix"] = prefix };
if (maxBytes is not null) if (maxBytes != null)
claims["maxBytes"] = maxBytes.Value.ToString(); claims["maxBytes"] = maxBytes.Value.ToString();
return new JsonWebTokenHandler().CreateToken(new SecurityTokenDescriptor return new JsonWebTokenHandler().CreateToken(new SecurityTokenDescriptor