diff --git a/Musify.WebApi.slnx b/Musify.WebApi.slnx
new file mode 100644
index 0000000..c239259
--- /dev/null
+++ b/Musify.WebApi.slnx
@@ -0,0 +1,3 @@
+
+
+
diff --git a/WebApi/AppSettings.Development.json b/WebApi/AppSettings.Development.json
new file mode 100644
index 0000000..16ecf29
--- /dev/null
+++ b/WebApi/AppSettings.Development.json
@@ -0,0 +1,17 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ },
+ "ConnectionStrings": {
+ "Default": ""
+ },
+ "Storage": {
+ "BucketName": "",
+ "Routes": {
+ "Uploads": ""
+ }
+ }
+}
\ No newline at end of file
diff --git a/WebApi/AppSettings.json b/WebApi/AppSettings.json
new file mode 100644
index 0000000..721a2b5
--- /dev/null
+++ b/WebApi/AppSettings.json
@@ -0,0 +1,94 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ },
+ "AllowedHosts": "*",
+ "ConnectionStrings": {
+ "Default": ""
+ },
+ "PlayListConfiguration": {
+ "Routes": {
+ "ParentFolders": "PlayLists",
+ "OriginalPicturesFolder": "OriginalPictures",
+ "SmallPicturesFolder": "SmallPictures",
+ "MediumPicturesFolder": "MediumPictures",
+ "LargePicturesFolder": "LargePictures",
+ "PresetOriginalPicture": "PresetOriginalPicture.webp",
+ "PresetSmallPicture": "PresetSmallPicture.webp",
+ "PresetMediumPicture": "PresetMediumPicture.webp",
+ "PresetLargePicture": "PresetLargePicture.webp"
+ },
+ "PicturesSizes": {
+ "SmallPictureWidth": 128,
+ "SmallPictureHeight": 128,
+ "MediumPictureWidth": 256,
+ "MediumPictureHeight": 256,
+ "LargePictureWidth": 512,
+ "LargePictureHeight": 512
+ }
+ },
+ "Keycloak": {
+ "Address": "1",
+ "Realm": "1",
+ "Username": "1",
+ "Password": "1"
+ },
+ "Database": {
+ "ConnectionString": "Host=localhost;Port=59000;Database=musify_db;Username=postgres;Password=postgres"
+ },
+ "ApplicationStorage": {
+ "BucketName": "webapi-storage"
+ },
+ "InfrastructureStorage": {
+ "Address": "http://localhost:8333",
+ "AccessKey": "admin_access_key",
+ "SecretAccessKey": "admin_secret_key",
+ "ForcePathStyle": true,
+ "UseHttp": true
+ },
+ "Messaging": {
+ "Address": "rabbitmq://127.0.0.1:5672",
+ "Username": "guest",
+ "Password": "guest"
+ },
+ "OpenTelemetry": {
+ "OtlpEndpoint": "http://localhost:4317"
+ },
+ "TrackConfiguration": {
+ "Routes": {
+ "ParentFolder": "Tracks",
+ "OriginalPicturesFolder": "OriginalPictures",
+ "SmallPicturesFolder": "SmallPictures",
+ "MediumPicturesFolder": "MediumPictures",
+ "LargePicturesFolder": "LargePictures",
+ "PresetOriginalPicture": "PresetOriginalPicture.webp",
+ "PresetSmallPicture": "PresetSmallPicture.webp",
+ "PresetMediumPicture": "PresetMediumPicture.webp",
+ "PresetLargePicture": "PresetLargePicture.webp",
+ "OriginalAudiosFolder": "OriginalAudios",
+ "ProcessedAudioFolder": "ProcessedAudios"
+ },
+ "PicturesSizes": {
+ "SmallPictureWidth": 128,
+ "SmallPictureHeight": 128,
+ "MediumPictureWidth": 256,
+ "MediumPictureHeight": 256,
+ "LargePictureWidth": 512,
+ "LargePictureHeight": 512
+ }
+ },
+ "AudioTranscoder": {
+ "Routes": {
+ "WorkingDirectory": "D:\\tempsFilesDev"
+ },
+ "TranscodingTimeout": "00:05:00"
+ },
+ "Workers": {
+ "Routes": {
+ "TemporaryFilesDirectory": ""
+ }
+ }
+}
\ No newline at end of file
diff --git a/WebApi/DataTransferObjects/PlayLists/CreatePlayListRequest.cs b/WebApi/DataTransferObjects/PlayLists/CreatePlayListRequest.cs
new file mode 100644
index 0000000..f6a5e62
--- /dev/null
+++ b/WebApi/DataTransferObjects/PlayLists/CreatePlayListRequest.cs
@@ -0,0 +1,6 @@
+namespace WebApi.DataTransferObjects.PlayLists;
+
+public record CreatePlayListRequest(
+ string Name,
+ string Description,
+ Guid? PictureIntentId);
diff --git a/WebApi/DataTransferObjects/PlayLists/RequestPlayListPictureUploadRequest.cs b/WebApi/DataTransferObjects/PlayLists/RequestPlayListPictureUploadRequest.cs
new file mode 100644
index 0000000..4998aa1
--- /dev/null
+++ b/WebApi/DataTransferObjects/PlayLists/RequestPlayListPictureUploadRequest.cs
@@ -0,0 +1,6 @@
+namespace WebApi.DataTransferObjects.PlayLists;
+
+public record RequestPlayListPictureUploadRequest(
+ string FileType,
+ string ContentType,
+ long? ExpectedSizeBytes = null);
diff --git a/WebApi/DataTransferObjects/PlayLists/UpdatePlayListRequest.cs b/WebApi/DataTransferObjects/PlayLists/UpdatePlayListRequest.cs
new file mode 100644
index 0000000..083a51d
--- /dev/null
+++ b/WebApi/DataTransferObjects/PlayLists/UpdatePlayListRequest.cs
@@ -0,0 +1,6 @@
+namespace WebApi.DataTransferObjects.PlayLists;
+
+public record UpdatePlayListRequest(
+ string? NewName,
+ string? NewDescription,
+ Guid? NewPictureIntentId);
diff --git a/WebApi/DataTransferObjects/Tracks/CreateTrackRequest.cs b/WebApi/DataTransferObjects/Tracks/CreateTrackRequest.cs
new file mode 100644
index 0000000..cedac41
--- /dev/null
+++ b/WebApi/DataTransferObjects/Tracks/CreateTrackRequest.cs
@@ -0,0 +1,6 @@
+namespace WebApi.DataTransferObjects.Tracks;
+
+public record CreateTrackRequest(
+ string Title,
+ Guid PictureIntentId,
+ Guid AudioIntentId);
diff --git a/WebApi/DataTransferObjects/Tracks/RequestTrackUploadUrlsRequest.cs b/WebApi/DataTransferObjects/Tracks/RequestTrackUploadUrlsRequest.cs
new file mode 100644
index 0000000..64144a6
--- /dev/null
+++ b/WebApi/DataTransferObjects/Tracks/RequestTrackUploadUrlsRequest.cs
@@ -0,0 +1,9 @@
+namespace WebApi.DataTransferObjects.Tracks;
+
+public record RequestTrackUploadUrlsRequest(
+ string PictureFileType,
+ string PictureContentType,
+ string AudioFileType,
+ string AudioContentType,
+ long? ExpectedPictureSizeBytes = null,
+ long? ExpectedAudioSizeBytes = null);
diff --git a/WebApi/DataTransferObjects/Users/CreateUserRequest.cs b/WebApi/DataTransferObjects/Users/CreateUserRequest.cs
new file mode 100644
index 0000000..5937975
--- /dev/null
+++ b/WebApi/DataTransferObjects/Users/CreateUserRequest.cs
@@ -0,0 +1,7 @@
+namespace WebApi.DataTransferObjects.Users;
+
+public record CreateUserRequest(
+ Guid Id,
+ string Name,
+ string? FirstName,
+ string? SecondName);
diff --git a/WebApi/Endpoints/PlayListEndpoints.cs b/WebApi/Endpoints/PlayListEndpoints.cs
new file mode 100644
index 0000000..b58e981
--- /dev/null
+++ b/WebApi/Endpoints/PlayListEndpoints.cs
@@ -0,0 +1,130 @@
+using Mediator;
+using Musify.Application.PlayLists.Commands;
+using Musify.Application.PlayLists.Queries;
+using WebApi.DataTransferObjects.PlayLists;
+using WebApi.Extensions;
+
+namespace WebApi.Endpoints;
+
+public static class PlayListEndpoints
+{
+ public static IEndpointRouteBuilder MapPlayListEndpoints(this IEndpointRouteBuilder app)
+ {
+ var group = app.MapGroup("/api/playlists")
+ .WithTags("PlayLists");
+
+ group.MapGet("/", GetPlayLists)
+ .WithName("GetPlayLists")
+ .WithSummary("Get paginated playlists.");
+
+ group.MapGet("/{id:guid}", GetPlayListById)
+ .WithName("GetPlayListById")
+ .WithSummary("Get a playlist by ID.");
+
+ group.MapGet("/users/{userId:guid}", GetPlayListsByUserId)
+ .WithName("GetPlayListsByUserId")
+ .WithSummary("Get paginated playlists for a user.");
+
+ group.MapPost("/users/{userId:guid}", CreatePlayList)
+ .WithName("CreatePlayList")
+ .WithSummary("Create a new playlist.");
+
+ group.MapPut("/{playlistId:guid}/users/{userId:guid}", UpdatePlayList)
+ .WithName("UpdatePlayList")
+ .WithSummary("Update an existing playlist.");
+
+ group.MapDelete("/{playlistId:guid}/users/{userId:guid}", DeletePlayList)
+ .WithName("DeletePlayList")
+ .WithSummary("Delete a playlist.");
+
+ group.MapPost("/upload-picture/users/{userId:guid}", RequestPlayListPictureUpload)
+ .WithName("RequestPlayListPictureUpload")
+ .WithSummary("Request a pre-signed URL to upload a playlist picture.");
+
+ return app;
+ }
+
+ private static async Task GetPlayLists(
+ IMediator mediator,
+ int pageNumber = 1,
+ int pageSize = 10,
+ CancellationToken ct = default)
+ {
+ var result = await mediator.Send(new GetPlayListsQuery(pageNumber, pageSize), ct);
+ return result.ToHttpResult();
+ }
+
+ private static async Task GetPlayListById(
+ IMediator mediator,
+ Guid id,
+ CancellationToken ct)
+ {
+ var result = await mediator.Send(new GetPlayListByIdQuery(id), ct);
+ return result.ToHttpResult();
+ }
+
+ private static async Task GetPlayListsByUserId(
+ IMediator mediator,
+ Guid userId,
+ string? name,
+ int pageNumber = 1,
+ int pageSize = 10,
+ CancellationToken ct = default)
+ {
+ var result = await mediator.Send(new GetPlayListsByUserIdQuery(userId, name, pageNumber, pageSize), ct);
+ return result.ToHttpResult();
+ }
+
+ private static async Task CreatePlayList(
+ IMediator mediator,
+ Guid userId,
+ CreatePlayListRequest request,
+ CancellationToken ct)
+ {
+ var result = await mediator.Send(
+ new CreatePlayListCommand(userId, request.Name, request.Description, request.PictureIntentId),
+ ct);
+
+ if (!result.IsSuccess)
+ return result.ToHttpResult();
+
+ return Results.Created($"/api/playlists/{result.Value.Id}", result.Value);
+ }
+
+ private static async Task UpdatePlayList(
+ IMediator mediator,
+ Guid userId,
+ Guid playlistId,
+ UpdatePlayListRequest request,
+ CancellationToken ct)
+ {
+ var result = await mediator.Send(
+ new UpdatePlayListCommand(userId, playlistId, request.NewName, request.NewDescription, request.NewPictureIntentId),
+ ct);
+
+ return result.ToHttpResult();
+ }
+
+ private static async Task DeletePlayList(
+ IMediator mediator,
+ Guid userId,
+ Guid playlistId,
+ CancellationToken ct)
+ {
+ var result = await mediator.Send(new DeletePlayListCommand(userId, playlistId), ct);
+ return result.ToHttpResult();
+ }
+
+ private static async Task RequestPlayListPictureUpload(
+ IMediator mediator,
+ Guid userId,
+ RequestPlayListPictureUploadRequest request,
+ CancellationToken ct)
+ {
+ var result = await mediator.Send(
+ new RequestPlayListPictureUploadCommand(userId, request.FileType, request.ContentType, request.ExpectedSizeBytes),
+ ct);
+
+ return result.ToHttpResult();
+ }
+}
diff --git a/WebApi/Endpoints/TrackEndpoints.cs b/WebApi/Endpoints/TrackEndpoints.cs
new file mode 100644
index 0000000..d576341
--- /dev/null
+++ b/WebApi/Endpoints/TrackEndpoints.cs
@@ -0,0 +1,119 @@
+using Mediator;
+using Musify.Application.Tracks.Commands;
+using Musify.Application.Tracks.Queries;
+using WebApi.DataTransferObjects.Tracks;
+using WebApi.Extensions;
+
+namespace WebApi.Endpoints;
+
+public static class TrackEndpoints
+{
+ public static IEndpointRouteBuilder MapTrackEndpoints(this IEndpointRouteBuilder app)
+ {
+ var group = app.MapGroup("/api/tracks")
+ .WithTags("Tracks");
+
+ group.MapGet("/", GetTracks)
+ .WithName("GetTracks")
+ .WithSummary("Get paginated tracks.");
+
+ group.MapGet("/{id:guid}", GetTrackById)
+ .WithName("GetTrackById")
+ .WithSummary("Get a track by ID.");
+
+ group.MapGet("/users/{userId:guid}", GetTracksByUserId)
+ .WithName("GetTracksByUserId")
+ .WithSummary("Get paginated tracks for a user.");
+
+ group.MapPost("/users/{userId:guid}", CreateTrack)
+ .WithName("CreateTrack")
+ .WithSummary("Create a new track.");
+
+ group.MapDelete("/{trackId:guid}/users/{userId:guid}", DeleteTrack)
+ .WithName("DeleteTrack")
+ .WithSummary("Delete a track.");
+
+ group.MapPost("/upload-urls/users/{userId:guid}", RequestTrackUploadUrls)
+ .WithName("RequestTrackUploadUrls")
+ .WithSummary("Request pre-signed URLs to upload track picture and audio.");
+
+ return app;
+ }
+
+ private static async Task GetTracks(
+ IMediator mediator,
+ int pageNumber = 1,
+ int pageSize = 10,
+ CancellationToken ct = default)
+ {
+ var result = await mediator.Send(new GetTracksQuery(pageNumber, pageSize), ct);
+ return result.ToHttpResult();
+ }
+
+ private static async Task GetTrackById(
+ IMediator mediator,
+ Guid id,
+ CancellationToken ct)
+ {
+ var result = await mediator.Send(new GetTrackByIdQuery(id), ct);
+ return result.ToHttpResult();
+ }
+
+ private static async Task GetTracksByUserId(
+ IMediator mediator,
+ Guid userId,
+ string? name,
+ int pageNumber = 1,
+ int pageSize = 10,
+ CancellationToken ct = default)
+ {
+ var result = await mediator.Send(new GetTracksByUserIdQuery(userId, name, pageNumber, pageSize), ct);
+ return result.ToHttpResult();
+ }
+
+ private static async Task CreateTrack(
+ IMediator mediator,
+ Guid userId,
+ CreateTrackRequest request,
+ CancellationToken ct)
+ {
+ var result = await mediator.Send(
+ new CreateTrackCommand(userId, request.Title, request.PictureIntentId, request.AudioIntentId),
+ ct);
+
+ if (!result.IsSuccess)
+ return result.ToHttpResult();
+
+ return Results.Created($"/api/tracks/{result.Value.Id}", result.Value);
+ }
+
+ private static async Task DeleteTrack(
+ IMediator mediator,
+ Guid userId,
+ Guid trackId,
+ CancellationToken ct)
+ {
+ var result = await mediator.Send(new DeleteTrackCommand(userId, trackId), ct);
+ return result.ToHttpResult();
+ }
+
+ private static async Task RequestTrackUploadUrls(
+ IMediator mediator,
+ Guid userId,
+ RequestTrackUploadUrlsRequest request,
+ CancellationToken ct)
+ {
+ var result = await mediator.Send(
+ new RequestTrackUploadUrlsCommand(
+ userId,
+ request.PictureFileType,
+ request.PictureContentType,
+ request.AudioFileType,
+ request.AudioContentType,
+ request.ExpectedPictureSizeBytes,
+ request.ExpectedAudioSizeBytes),
+ ct);
+
+ return result.ToHttpResult();
+ }
+}
diff --git a/WebApi/Endpoints/UserEndpoints.cs b/WebApi/Endpoints/UserEndpoints.cs
new file mode 100644
index 0000000..099d961
--- /dev/null
+++ b/WebApi/Endpoints/UserEndpoints.cs
@@ -0,0 +1,78 @@
+using Mediator;
+using Musify.Application.Users.Commands;
+using Musify.Application.Users.Queries;
+using WebApi.DataTransferObjects.Users;
+using WebApi.Extensions;
+
+namespace WebApi.Endpoints;
+
+public static class UserEndpoints
+{
+ public static IEndpointRouteBuilder MapUserEndpoints(this IEndpointRouteBuilder app)
+ {
+ var group = app.MapGroup("/api/users")
+ .WithTags("Users");
+
+ group.MapGet("/", GetUsers)
+ .WithName("GetUsers")
+ .WithSummary("Get paginated users.");
+
+ group.MapGet("/{id:guid}", GetUserById)
+ .WithName("GetUserById")
+ .WithSummary("Get a user by ID.");
+
+ group.MapGet("/keycloak/{id:guid}", GetUserByKeycloak)
+ .WithName("GetUserByKeycloak")
+ .WithSummary("Get a user by Keycloak ID.");
+
+ group.MapPost("/", CreateUser)
+ .WithName("CreateUser")
+ .WithSummary("Create a new user.");
+
+ return app;
+ }
+
+ private static async Task GetUsers(
+ IMediator mediator,
+ int pageNumber = 1,
+ int pageSize = 10,
+ string? usernameSearch = null,
+ CancellationToken ct = default)
+ {
+ var result = await mediator.Send(new GetUsersQuery(pageNumber, pageSize, usernameSearch), ct);
+ return result.ToHttpResult();
+ }
+
+ private static async Task GetUserById(
+ IMediator mediator,
+ Guid id,
+ CancellationToken ct)
+ {
+ var result = await mediator.Send(new GetUserByIdQuery(id), ct);
+ return result.ToHttpResult();
+ }
+
+ private static async Task GetUserByKeycloak(
+ IMediator mediator,
+ Guid id,
+ CancellationToken ct)
+ {
+ var result = await mediator.Send(new GetUserByKeycloakQuery(id), ct);
+ return result.ToHttpResult();
+ }
+
+ private static async Task CreateUser(
+ IMediator mediator,
+ CreateUserRequest request,
+ CancellationToken ct)
+ {
+ var result = await mediator.Send(
+ new CreateUserCommand(request.Id, request.Name, request.FirstName, request.SecondName),
+ ct);
+
+ if (!result.IsSuccess)
+ return result.ToHttpResult();
+
+ return Results.Created($"/api/users/{result.Value.Id}", result.Value);
+ }
+}
diff --git a/WebApi/Extensions/ResultHttpExtensions.cs b/WebApi/Extensions/ResultHttpExtensions.cs
new file mode 100644
index 0000000..7b8a939
--- /dev/null
+++ b/WebApi/Extensions/ResultHttpExtensions.cs
@@ -0,0 +1,35 @@
+using Ardalis.Result;
+
+namespace WebApi.Extensions;
+
+public static class ResultHttpExtensions
+{
+ public static IResult ToHttpResult(this Result result) =>
+ result.Status switch
+ {
+ ResultStatus.Ok or ResultStatus.NoContent => Results.NoContent(),
+ ResultStatus.NotFound => Results.NotFound(result.Errors.FirstOrDefault()),
+ ResultStatus.Invalid => Results.ValidationProblem(ToValidationDictionary(result.ValidationErrors)),
+ ResultStatus.Unauthorized => Results.Unauthorized(),
+ ResultStatus.Forbidden => Results.StatusCode(StatusCodes.Status403Forbidden),
+ ResultStatus.Conflict => Results.Conflict(result.Errors.FirstOrDefault()),
+ _ => Results.Problem(string.Join(", ", result.Errors))
+ };
+
+ public static IResult ToHttpResult(this Result result) =>
+ result.Status switch
+ {
+ ResultStatus.Ok => Results.Ok(result.Value),
+ ResultStatus.NotFound => Results.NotFound(result.Errors.FirstOrDefault()),
+ ResultStatus.Invalid => Results.ValidationProblem(ToValidationDictionary(result.ValidationErrors)),
+ ResultStatus.Unauthorized => Results.Unauthorized(),
+ ResultStatus.Forbidden => Results.StatusCode(StatusCodes.Status403Forbidden),
+ ResultStatus.Conflict => Results.Conflict(result.Errors.FirstOrDefault()),
+ _ => Results.Problem(string.Join(", ", result.Errors))
+ };
+
+ private static Dictionary ToValidationDictionary(IEnumerable errors) =>
+ errors
+ .GroupBy(e => e.Identifier)
+ .ToDictionary(g => g.Key, g => g.Select(e => e.ErrorMessage).ToArray());
+}
diff --git a/WebApi/Extensions/ServiceCollectionExtensions.cs b/WebApi/Extensions/ServiceCollectionExtensions.cs
new file mode 100644
index 0000000..e4fd603
--- /dev/null
+++ b/WebApi/Extensions/ServiceCollectionExtensions.cs
@@ -0,0 +1,50 @@
+using Mediator;
+using Musify.Application;
+using Musify.Infrastructure.MassTransit;
+using Musify.Infrastructure.Persistence;
+using Musify.Infrastructure.Services;
+using OpenTelemetry.Logs;
+using OpenTelemetry.Metrics;
+using OpenTelemetry.Trace;
+using WebApi.Options;
+
+namespace WebApi.Extensions;
+
+public static class ServiceCollectionExtensions
+{
+ public static IServiceCollection AddApplicationServices(this IServiceCollection services, IConfiguration configuration)
+ {
+ services.AddApplicationConfigurations(configuration);
+ services.AddDatabase(configuration);
+ services.AddKeycloakService(configuration);
+ services.AddStorageService(configuration);
+ services.AddMassTransitClient(configuration);
+ services.AddMediator();
+ services.AddOpenApi();
+
+ return services;
+ }
+
+ public static IServiceCollection AddObservability(this IServiceCollection services, IConfiguration configuration)
+ {
+ var otlpEndpoint = new Uri(configuration
+ .GetRequiredSection(OpenTelemetryOptions.SectionName)
+ .GetRequiredSection(nameof(OpenTelemetryOptions.OtlpEndpoint))
+ .Value!);
+
+ services.AddOpenTelemetry()
+ .WithTracing(tracing => tracing
+ .AddAspNetCoreInstrumentation()
+ .AddHttpClientInstrumentation()
+ .AddOtlpExporter(o => o.Endpoint = otlpEndpoint))
+ .WithMetrics(metrics => metrics
+ .AddAspNetCoreInstrumentation()
+ .AddHttpClientInstrumentation()
+ .AddOtlpExporter(o => o.Endpoint = otlpEndpoint));
+
+ services.Configure(logging =>
+ logging.AddOtlpExporter(o => o.Endpoint = otlpEndpoint));
+
+ return services;
+ }
+}
diff --git a/WebApi/Options/OpenTelemetryOptions.cs b/WebApi/Options/OpenTelemetryOptions.cs
new file mode 100644
index 0000000..938e30a
--- /dev/null
+++ b/WebApi/Options/OpenTelemetryOptions.cs
@@ -0,0 +1,11 @@
+using System.ComponentModel.DataAnnotations;
+
+namespace WebApi.Options;
+
+public sealed class OpenTelemetryOptions
+{
+ public const string SectionName = "OpenTelemetry";
+
+ [Required]
+ public string OtlpEndpoint { get; init; } = string.Empty;
+}
diff --git a/WebApi/Program.cs b/WebApi/Program.cs
new file mode 100644
index 0000000..d52c69b
--- /dev/null
+++ b/WebApi/Program.cs
@@ -0,0 +1,24 @@
+using Scalar.AspNetCore;
+using WebApi.Endpoints;
+using WebApi.Extensions;
+
+var builder = WebApplication.CreateBuilder();
+
+builder.Services
+ .AddApplicationServices(builder.Configuration)
+ .AddObservability(builder.Configuration);
+
+var app = builder.Build();
+
+if (app.Environment.IsDevelopment())
+{
+ app.MapOpenApi();
+ app.MapScalarApiReference();
+}
+
+app
+ .MapPlayListEndpoints()
+ .MapTrackEndpoints()
+ .MapUserEndpoints();
+
+app.Run();
diff --git a/WebApi/Properties/launchSettings.json b/WebApi/Properties/launchSettings.json
new file mode 100644
index 0000000..a96b644
--- /dev/null
+++ b/WebApi/Properties/launchSettings.json
@@ -0,0 +1,14 @@
+{
+ "$schema": "https://json.schemastore.org/launchsettings.json",
+ "profiles": {
+ "http": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": false,
+ "applicationUrl": "http://localhost:5111",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ }
+ }
+}
diff --git a/WebApi/WebApi.csproj b/WebApi/WebApi.csproj
new file mode 100644
index 0000000..dc08b91
--- /dev/null
+++ b/WebApi/WebApi.csproj
@@ -0,0 +1,44 @@
+
+
+
+ net10.0
+ enable
+ enable
+ 1c3fa41c-6f07-433b-af79-1f67fad338fd
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ..\..\MusifyBackend\Infrastructure\bin\Debug\net10.0\Application.dll
+
+
+ ..\..\MusifyBackend\Infrastructure\bin\Debug\net10.0\Domain.dll
+
+
+ ..\..\MusifyBackend\Infrastructure\bin\Debug\net10.0\Infrastructure.dll
+
+
+
+
diff --git a/WebApi/WebApi.http b/WebApi/WebApi.http
new file mode 100644
index 0000000..213749e
--- /dev/null
+++ b/WebApi/WebApi.http
@@ -0,0 +1,6 @@
+@WebApi_HostAddress = http://localhost:5111
+
+GET {{WebApi_HostAddress}}/weatherforecast/
+Accept: application/json
+
+###