# Melodee: Codebase Analysis ## Project Structure Overview Melodee follows a layered architecture with clear separation of concerns across multiple projects. The solution structure reflects .NET best practices with distinct assemblies for web, data access, business logic, and testing. **Estimated Project Structure**: ``` melodee/ ├── src/ │ ├── Melodee.Web/ # Blazor Server UI and API controllers │ ├── Melodee.Core/ # Business logic and domain models │ ├── Melodee.Data/ # EF Core, repositories, migrations │ ├── Melodee.Providers/ # Metadata provider implementations │ └── Melodee.Jobs/ # Quartz.NET background jobs ├── tests/ │ ├── Melodee.Tests.Unit/ # xUnit unit tests │ ├── Melodee.Tests.Integration/# Integration tests │ ├── Melodee.Tests.Blazor/ # bUnit component tests │ └── Melodee.Tests.Performance/# NBomber load tests ├── docker/ │ ├── Dockerfile │ ├── docker-compose.yml │ └── entrypoint.sh ├── docs/ │ ├── api/ # API documentation │ └── architecture/ # Architecture diagrams ├── .github/ │ └── workflows/ # CI/CD pipelines ├── biome.json # Biome linter configuration ├── .gitignore ├── LICENSE └── README.md ``` ## Core Projects ### Melodee.Web **Responsibilities**: - Blazor Server UI components and pages - API controllers (Native REST, OpenSubsonic, Jellyfin) - Authentication and authorization - SignalR hubs (Party Mode) - Middleware (rate limiting, error handling) - Startup configuration **Key Files**: **Program.cs**: ```csharp var builder = WebApplication.CreateBuilder(args); // Add services builder.Services.AddRazorPages(); builder.Services.AddServerSideBlazor(); builder.Services.AddControllers(); // Database builder.Services.AddDbContext(options => options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection"))); // Authentication builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = true, ValidateAudience = true, ValidateLifetime = true, ValidateIssuerSigningKey = true, ValidIssuer = builder.Configuration["Jwt:Issuer"], ValidAudience = builder.Configuration["Jwt:Audience"], IssuerSigningKey = new SymmetricSecurityKey( Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"])) }; }) .AddGoogle(options => { options.ClientId = builder.Configuration["Google:ClientId"]; options.ClientSecret = builder.Configuration["Google:ClientSecret"]; }); // Authorization builder.Services.AddAuthorization(options => { options.AddPolicy("AdminOnly", policy => policy.RequireRole("admin")); }); // SignalR builder.Services.AddSignalR(); // Background jobs builder.Services.AddQuartz(q => { q.UseMicrosoftDependencyInjectionJobFactory(); JobConfiguration.ConfigureJobs(q); }); builder.Services.AddQuartzHostedService(q => q.WaitForJobsToComplete = true); // Repositories and services builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); // Metadata providers builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); // Scrobblers builder.Services.AddScoped(); // Logging builder.Host.UseSerilog((context, config) => { config.ReadFrom.Configuration(context.Configuration); }); // Health checks builder.Services.AddHealthChecks() .AddDbContextCheck() .AddCheck("melodee"); var app = builder.Build(); // Middleware pipeline if (app.Environment.IsDevelopment()) { app.UseDeveloperExceptionPage(); } else { app.UseExceptionHandler("/Error"); app.UseHsts(); } app.UseStaticFiles(); app.UseRouting(); app.UseAuthentication(); app.UseAuthorization(); app.UseEndpoints(endpoints => { endpoints.MapBlazorHub(); endpoints.MapFallbackToPage("/_Host"); endpoints.MapControllers(); endpoints.MapHub("/hubs/party"); endpoints.MapHealthChecks("/health"); }); app.Run(); ``` **Controllers/AlbumsController.cs**: ```csharp [ApiController] [Route("api/v1/albums")] [Authorize] [RateLimit(MaxRequests = 30, Window = TimeSpan.FromSeconds(30))] public class AlbumsController : ControllerBase { private readonly ILibraryService _libraryService; private readonly ILogger _logger; public AlbumsController(ILibraryService libraryService, ILogger logger) { _libraryService = libraryService; _logger = logger; } [HttpGet] public async Task>> GetAlbums( [FromQuery] int page = 1, [FromQuery] int pageSize = 20, [FromQuery] string sort = "title", [FromQuery] string order = "asc", [FromQuery] string genre = null) { var result = await _libraryService.GetAlbumsAsync(page, pageSize, sort, order, genre); return Ok(result); } [HttpGet("{id}")] public async Task> GetAlbum(int id) { var album = await _libraryService.GetAlbumAsync(id); if (album == null) return NotFound(new { error = "NotFound", message = $"Album with ID {id} not found" }); return Ok(album); } [HttpGet("{id}/cover")] [AllowAnonymous] public async Task GetCoverArt(int id, [FromQuery] int? size = null) { var album = await _libraryService.GetAlbumAsync(id); if (album == null) return NotFound(); var imagePath = size.HasValue ? await _imageService.GetResizedCoverArtAsync(album.CoverArtPath, size.Value) : album.CoverArtPath; var imageBytes = await System.IO.File.ReadAllBytesAsync(imagePath); return File(imageBytes, "image/jpeg"); } } ``` **Pages/Albums.razor**: ```razor @page "/albums" @inject ILibraryService LibraryService @inject NavigationManager Navigation Albums - Melodee

Albums

@code { private List albums = new(); private List genres = new(); private string searchQuery; private string selectedGenre; private RadzenDataGrid grid; protected override async Task OnInitializedAsync() { await LoadGenres(); await LoadAlbums(); } private async Task LoadGenres() { genres = await LibraryService.GetGenresAsync(); } private async Task LoadAlbums() { albums = await LibraryService.SearchAlbumsAsync(searchQuery, selectedGenre); } private void ViewAlbum(int id) { Navigation.NavigateTo($"/albums/{id}"); } private async Task PlayAlbum(int id) { await PlayerService.PlayAlbumAsync(id); } } ``` **Hubs/PartyModeHub.cs**: ```csharp public class PartyModeHub : Hub { private readonly ILogger _logger; public PartyModeHub(ILogger logger) { _logger = logger; } public async Task JoinParty(string partyId) { await Groups.AddToGroupAsync(Context.ConnectionId, partyId); _logger.LogInformation("User {ConnectionId} joined party {PartyId}", Context.ConnectionId, partyId); } public async Task LeaveParty(string partyId) { await Groups.RemoveFromGroupAsync(Context.ConnectionId, partyId); _logger.LogInformation("User {ConnectionId} left party {PartyId}", Context.ConnectionId, partyId); } public async Task BroadcastPlaybackState(string partyId, PlaybackState state) { await Clients.Group(partyId).SendAsync("UpdatePlayback", state); _logger.LogDebug("Broadcast playback state to party {PartyId}: {State}", partyId, state); } public async Task BroadcastQueueUpdate(string partyId, List queue) { await Clients.Group(partyId).SendAsync("UpdateQueue", queue); } } ``` ### Melodee.Core **Responsibilities**: - Domain models and entities - Business logic services - DTOs and view models - Interfaces for repositories and services - Domain exceptions **Key Files**: **Models/Album.cs**: ```csharp public class Album { public int Id { get; set; } public string Title { get; set; } public string SortTitle { get; set; } public int ArtistId { get; set; } public Artist Artist { get; set; } public DateTime? ReleaseDate { get; set; } public string ReleaseType { get; set; } public string Country { get; set; } public string Label { get; set; } public string Barcode { get; set; } public string CoverArtPath { get; set; } public Dictionary ExternalIds { get; set; } public List Genres { get; set; } public int TrackCount { get; set; } public int Duration { get; set; } public DateTime CreatedAt { get; set; } public DateTime UpdatedAt { get; set; } public List Tracks { get; set; } public List PlaylistAlbums { get; set; } } ``` **Services/LibraryService.cs**: ```csharp public class LibraryService : ILibraryService { private readonly IUnitOfWork _unitOfWork; private readonly IMetadataAggregator _metadataAggregator; private readonly ILogger _logger; public LibraryService( IUnitOfWork unitOfWork, IMetadataAggregator metadataAggregator, ILogger logger) { _unitOfWork = unitOfWork; _metadataAggregator = metadataAggregator; _logger = logger; } public async Task GetAlbumAsync(int id) { var album = await _unitOfWork.Albums.GetByIdAsync(id); if (album == null) return null; return MapToDto(album); } public async Task> GetAlbumsAsync( int page, int pageSize, string sort, string order, string genre) { var albums = await _unitOfWork.Albums.GetPagedAsync(page, pageSize, sort, order, genre); return new PagedResult { Items = albums.Items.Select(MapToDto).ToList(), TotalCount = albums.TotalCount, Page = page, PageSize = pageSize }; } public async Task EnrichAlbumMetadataAsync(int albumId) { var album = await _unitOfWork.Albums.GetByIdAsync(albumId); if (album == null) throw new NotFoundException($"Album {albumId} not found"); _logger.LogInformation("Enriching metadata for album {AlbumId}: {Artist} - {Title}", albumId, album.Artist.Name, album.Title); var metadata = await _metadataAggregator.AggregateAlbumMetadataAsync( album.Artist.Name, album.Title); if (metadata != null) { album.ReleaseDate = metadata.ReleaseDate ?? album.ReleaseDate; album.Genres = metadata.Genres ?? album.Genres; album.ExternalIds = metadata.ExternalIds ?? album.ExternalIds; if (!string.IsNullOrEmpty(metadata.CoverArtUrl)) { album.CoverArtPath = await DownloadCoverArtAsync(metadata.CoverArtUrl, albumId); } album.UpdatedAt = DateTime.UtcNow; await _unitOfWork.SaveChangesAsync(); _logger.LogInformation("Metadata enriched for album {AlbumId}", albumId); } } private AlbumDto MapToDto(Album album) { return new AlbumDto { Id = album.Id, Title = album.Title, ArtistId = album.ArtistId, ArtistName = album.Artist.Name, ReleaseDate = album.ReleaseDate, Genre = album.Genres?.FirstOrDefault(), TrackCount = album.TrackCount, Duration = album.Duration, CoverArtUrl = $"/api/v1/albums/{album.Id}/cover", ExternalIds = album.ExternalIds }; } } ``` **Services/MetadataAggregator.cs**: ```csharp public class MetadataAggregator : IMetadataAggregator { private readonly List _providers; private readonly ILogger _logger; public MetadataAggregator( IEnumerable providers, ILogger logger) { _providers = providers.OrderByDescending(p => p.Priority).ToList(); _logger = logger; } public async Task AggregateAlbumMetadataAsync(string artist, string album) { var results = new List(); foreach (var provider in _providers) { if (!provider.IsEnabled) continue; try { _logger.LogDebug("Querying {Provider} for {Artist} - {Album}", provider.Name, artist, album); var metadata = await provider.GetAlbumMetadataAsync(artist, album); if (metadata != null) { metadata.Source = provider.Name; results.Add(metadata); _logger.LogDebug("{Provider} returned metadata for {Artist} - {Album}", provider.Name, artist, album); } } catch (Exception ex) { _logger.LogWarning(ex, "{Provider} failed for {Artist} - {Album}", provider.Name, artist, album); } } if (!results.Any()) { _logger.LogWarning("No metadata found for {Artist} - {Album}", artist, album); return null; } return MergeMetadata(results); } private AlbumMetadata MergeMetadata(List results) { var merged = new AlbumMetadata(); // Title: prefer highest-priority provider merged.Title = results.FirstOrDefault(r => !string.IsNullOrEmpty(r.Title))?.Title; // Artist: prefer highest-priority provider merged.Artist = results.FirstOrDefault(r => !string.IsNullOrEmpty(r.Artist))?.Artist; // Release date: prefer most specific date merged.ReleaseDate = results .Where(r => r.ReleaseDate.HasValue) .OrderByDescending(r => r.ReleaseDate.Value) .FirstOrDefault()?.ReleaseDate; // Genres: combine from all providers, deduplicate merged.Genres = results .SelectMany(r => r.Genres ?? new List()) .Distinct() .ToList(); // Cover art: prefer highest resolution merged.CoverArtUrl = results .Where(r => !string.IsNullOrEmpty(r.CoverArtUrl)) .OrderByDescending(r => EstimateImageResolution(r.CoverArtUrl)) .FirstOrDefault()?.CoverArtUrl; // External IDs: combine from all providers merged.ExternalIds = results .SelectMany(r => r.ExternalIds ?? new Dictionary()) .GroupBy(kvp => kvp.Key) .ToDictionary(g => g.Key, g => g.First().Value); _logger.LogInformation("Merged metadata from {Count} providers: {Providers}", results.Count, string.Join(", ", results.Select(r => r.Source))); return merged; } private int EstimateImageResolution(string url) { // Extract resolution from URL patterns var match = Regex.Match(url, @"(\d+)x(\d+)"); if (match.Success) { return int.Parse(match.Groups[1].Value) * int.Parse(match.Groups[2].Value); } // Default resolution estimates if (url.Contains("large") || url.Contains("xl")) return 600 * 600; if (url.Contains("medium")) return 300 * 300; if (url.Contains("small") || url.Contains("thumb")) return 150 * 150; return 300 * 300; // Default } } ``` ### Melodee.Data **Responsibilities**: - EF Core DbContext - Entity configurations - Migrations - Repository implementations - Database seeding **Key Files**: **MelodeeDbContext.cs**: ```csharp public class MelodeeDbContext : DbContext { public MelodeeDbContext(DbContextOptions options) : base(options) { } public DbSet Albums { get; set; } public DbSet Artists { get; set; } public DbSet Tracks { get; set; } public DbSet Users { get; set; } public DbSet UserSettings { get; set; } public DbSet Playlists { get; set; } public DbSet PlaylistTracks { get; set; } public DbSet Scrobbles { get; set; } public DbSet Genres { get; set; } public DbSet MetadataProviders { get; set; } public DbSet MetadataCache { get; set; } public DbSet Jobs { get; set; } public DbSet HealthChecks { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); // Apply configurations modelBuilder.ApplyConfiguration(new AlbumConfiguration()); modelBuilder.ApplyConfiguration(new ArtistConfiguration()); modelBuilder.ApplyConfiguration(new TrackConfiguration()); modelBuilder.ApplyConfiguration(new UserConfiguration()); modelBuilder.ApplyConfiguration(new PlaylistConfiguration()); modelBuilder.ApplyConfiguration(new ScrobbleConfiguration()); } } ``` **Configurations/AlbumConfiguration.cs**: ```csharp public class AlbumConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { builder.ToTable("albums"); builder.HasKey(a => a.Id); builder.Property(a => a.Title) .IsRequired() .HasMaxLength(500); builder.Property(a => a.SortTitle) .HasMaxLength(500); builder.Property(a => a.ExternalIds) .HasColumnType("jsonb"); builder.Property(a => a.Genres) .HasColumnType("jsonb"); builder.HasOne(a => a.Artist) .WithMany(a => a.Albums) .HasForeignKey(a => a.ArtistId) .OnDelete(DeleteBehavior.Cascade); builder.HasMany(a => a.Tracks) .WithOne(t => t.Album) .HasForeignKey(t => t.AlbumId) .OnDelete(DeleteBehavior.Cascade); builder.HasIndex(a => a.Title); builder.HasIndex(a => a.ArtistId); builder.HasIndex(a => a.ReleaseDate); builder.HasIndex(a => a.ExternalIds) .HasMethod("gin"); builder.HasIndex(a => a.Genres) .HasMethod("gin"); } } ``` **Repositories/AlbumRepository.cs**: ```csharp public class AlbumRepository : IAlbumRepository { private readonly MelodeeDbContext _context; public AlbumRepository(MelodeeDbContext context) { _context = context; } public async Task GetByIdAsync(int id) { return await _context.Albums .Include(a => a.Artist) .Include(a => a.Tracks) .FirstOrDefaultAsync(a => a.Id == id); } public async Task> GetPagedAsync( int page, int pageSize, string sort, string order, string genre) { var query = _context.Albums .Include(a => a.Artist) .AsQueryable(); if (!string.IsNullOrEmpty(genre)) { query = query.Where(a => a.Genres.Contains(genre)); } // Apply sorting query = sort.ToLower() switch { "title" => order == "desc" ? query.OrderByDescending(a => a.Title) : query.OrderBy(a => a.Title), "artist" => order == "desc" ? query.OrderByDescending(a => a.Artist.Name) : query.OrderBy(a => a.Artist.Name), "releasedate" => order == "desc" ? query.OrderByDescending(a => a.ReleaseDate) : query.OrderBy(a => a.ReleaseDate), _ => query.OrderBy(a => a.Title) }; var totalCount = await query.CountAsync(); var items = await query .Skip((page - 1) * pageSize) .Take(pageSize) .ToListAsync(); return new PagedResult { Items = items, TotalCount = totalCount, Page = page, PageSize = pageSize }; } public async Task> SearchAsync(string query) { return await _context.Albums .Include(a => a.Artist) .Where(a => EF.Functions.ILike(a.Title, $"%{query}%") || EF.Functions.ILike(a.Artist.Name, $"%{query}%")) .OrderBy(a => a.Title) .ToListAsync(); } public async Task CreateAsync(Album album) { _context.Albums.Add(album); await _context.SaveChangesAsync(); return album; } public async Task UpdateAsync(Album album) { album.UpdatedAt = DateTime.UtcNow; _context.Albums.Update(album); await _context.SaveChangesAsync(); } public async Task DeleteAsync(int id) { var album = await _context.Albums.FindAsync(id); if (album != null) { _context.Albums.Remove(album); await _context.SaveChangesAsync(); } } } ``` ### Melodee.Providers **Responsibilities**: - Metadata provider implementations - Provider-specific API clients - Response parsing and mapping - Rate limiting and caching **Key Files**: **MusicBrainzProvider.cs**: ```csharp public class MusicBrainzProvider : IMetadataProvider { private readonly MusicBrainzCache _cache; private readonly HttpClient _httpClient; private readonly ILogger _logger; private readonly SemaphoreSlim _rateLimiter = new SemaphoreSlim(1, 1); public string Name => "MusicBrainz"; public int Priority => 100; public bool IsEnabled => true; public async Task GetAlbumMetadataAsync(string artist, string album) { // Try local cache first var cached = await _cache.SearchReleasesAsync(artist, album); if (cached.Any()) { _logger.LogDebug("MusicBrainz cache hit for {Artist} - {Album}", artist, album); return MapToMetadata(cached.First()); } // Fallback to API await _rateLimiter.WaitAsync(); try { await Task.Delay(1000); // Rate limit: 1 request per second var query = $"artist:{artist} AND release:{album}"; var encodedQuery = Uri.EscapeDataString(query); var response = await _httpClient.GetAsync( $"https://musicbrainz.org/ws/2/release?query={encodedQuery}&fmt=json&limit=1" ); if (!response.IsSuccessStatusCode) { _logger.LogWarning("MusicBrainz API returned {StatusCode}", response.StatusCode); return null; } var data = await response.Content.ReadFromJsonAsync(); if (data.Releases.Any()) { var release = data.Releases.First(); // Cache for future lookups await _cache.InsertReleaseAsync(release); return MapToMetadata(release); } return null; } finally { _rateLimiter.Release(); } } private AlbumMetadata MapToMetadata(MbRelease release) { return new AlbumMetadata { Title = release.Title, Artist = release.ArtistCredit, ReleaseDate = ParseDate(release.ReleaseDate), Country = release.Country, Barcode = release.Barcode, ExternalIds = new Dictionary { ["musicbrainz"] = release.Id, ["musicbrainz_release_group"] = release.ReleaseGroupId } }; } } ``` **SpotifyProvider.cs**: ```csharp public class SpotifyProvider : IMetadataProvider { private readonly SpotifyAuthService _authService; private readonly HttpClient _httpClient; private readonly IMemoryCache _cache; private readonly ILogger _logger; public string Name => "Spotify"; public int Priority => 80; public bool IsEnabled { get; private set; } public SpotifyProvider( SpotifyAuthService authService, HttpClient httpClient, IMemoryCache cache, IConfiguration configuration, ILogger logger) { _authService = authService; _httpClient = httpClient; _cache = cache; _logger = logger; IsEnabled = !string.IsNullOrEmpty(configuration["Spotify:ClientId"]); } public async Task GetAlbumMetadataAsync(string artist, string album) { if (!IsEnabled) return null; var cacheKey = $"spotify:{artist}:{album}"; if (_cache.TryGetValue(cacheKey, out AlbumMetadata cached)) { _logger.LogDebug("Spotify cache hit for {Artist} - {Album}", artist, album); return cached; } var token = await _authService.GetAccessTokenAsync(); var query = $"artist:{artist} album:{album}"; var encodedQuery = Uri.EscapeDataString(query); var request = new HttpRequestMessage( HttpMethod.Get, $"https://api.spotify.com/v1/search?q={encodedQuery}&type=album&limit=1" ); request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token); var response = await _httpClient.SendAsync(request); if (!response.IsSuccessStatusCode) { _logger.LogWarning("Spotify API returned {StatusCode}", response.StatusCode); return null; } var data = await response.Content.ReadFromJsonAsync(); if (data.Albums.Items.Any()) { var spotifyAlbum = data.Albums.Items.First(); var metadata = MapToMetadata(spotifyAlbum); _cache.Set(cacheKey, metadata, TimeSpan.FromHours(1)); return metadata; } return null; } private AlbumMetadata MapToMetadata(SpotifyAlbum album) { return new AlbumMetadata { Title = album.Name, Artist = album.Artists.First().Name, ReleaseDate = DateTime.Parse(album.ReleaseDate), Genres = album.Genres, CoverArtUrl = album.Images.FirstOrDefault()?.Url, ExternalIds = new Dictionary { ["spotify"] = album.Id, ["spotify_uri"] = album.Uri }, Popularity = album.Popularity }; } } ``` ### Melodee.Jobs **Responsibilities**: - Quartz.NET job implementations - Job scheduling configuration - Background task orchestration **Key Files**: **LibraryScanJob.cs**: ```csharp public class LibraryScanJob : IJob { private readonly ILibraryService _libraryService; private readonly IFileScanner _fileScanner; private readonly ILogger _logger; public async Task Execute(IJobExecutionContext context) { _logger.LogInformation("Starting library scan"); var libraryPath = context.MergedJobDataMap.GetString("LibraryPath"); var changes = await _fileScanner.ScanAsync(libraryPath); _logger.LogInformation("Found {NewFiles} new files, {ChangedFiles} changed files, {DeletedFiles} deleted files", changes.NewFiles.Count, changes.ChangedFiles.Count, changes.DeletedFiles.Count); foreach (var file in changes.NewFiles) { await _libraryService.ImportFileAsync(file); } foreach (var file in changes.ChangedFiles) { await _libraryService.UpdateFileAsync(file); } foreach (var file in changes.DeletedFiles) { await _libraryService.RemoveFileAsync(file); } _logger.LogInformation("Library scan completed"); // Trigger metadata enrichment job if (changes.NewFiles.Any()) { await context.Scheduler.TriggerJob(new JobKey("MetadataEnrichment")); } } } ``` **JobConfiguration.cs**: ```csharp public static class JobConfiguration { public static void ConfigureJobs(IServiceCollectionQuartzConfigurator quartz) { // Library scan every 6 hours quartz.AddJob(opts => opts.WithIdentity("LibraryScan")); quartz.AddTrigger(opts => opts .ForJob("LibraryScan") .WithIdentity("LibraryScanTrigger") .WithCronSchedule("0 0 */6 * * ?")); // Metadata enrichment (triggered by library scan) quartz.AddJob(opts => opts.WithIdentity("MetadataEnrichment")); // MusicBrainz cache update monthly quartz.AddJob(opts => opts.WithIdentity("MbCacheUpdate")); quartz.AddTrigger(opts => opts .ForJob("MbCacheUpdate") .WithIdentity("MbCacheUpdateTrigger") .WithCronSchedule("0 0 0 1 * ?")); // First day of month at midnight // Scrobble submission every 5 minutes quartz.AddJob(opts => opts.WithIdentity("ScrobbleSubmission")); quartz.AddTrigger(opts => opts .ForJob("ScrobbleSubmission") .WithIdentity("ScrobbleSubmissionTrigger") .WithSimpleSchedule(x => x.WithIntervalInMinutes(5).RepeatForever())); // Statistics calculation daily at 3 AM quartz.AddJob(opts => opts.WithIdentity("StatisticsCalculation")); quartz.AddTrigger(opts => opts .ForJob("StatisticsCalculation") .WithIdentity("StatisticsCalculationTrigger") .WithCronSchedule("0 0 3 * * ?")); // Database maintenance weekly quartz.AddJob(opts => opts.WithIdentity("DatabaseMaintenance")); quartz.AddTrigger(opts => opts .ForJob("DatabaseMaintenance") .WithIdentity("DatabaseMaintenanceTrigger") .WithCronSchedule("0 0 2 ? * SUN")); // Sundays at 2 AM } } ``` ## Testing Strategy ### Unit Tests (xUnit) **Melodee.Tests.Unit/Services/LibraryServiceTests.cs**: ```csharp public class LibraryServiceTests { private readonly Mock _unitOfWorkMock; private readonly Mock _metadataAggregatorMock; private readonly Mock> _loggerMock; private readonly LibraryService _service; public LibraryServiceTests() { _unitOfWorkMock = new Mock(); _metadataAggregatorMock = new Mock(); _loggerMock = new Mock>(); _service = new LibraryService( _unitOfWorkMock.Object, _metadataAggregatorMock.Object, _loggerMock.Object ); } [Fact] public async Task GetAlbumAsync_ExistingAlbum_ReturnsDto() { // Arrange var album = new Album { Id = 1, Title = "OK Computer", Artist = new Artist { Id = 1, Name = "Radiohead" } }; _unitOfWorkMock.Setup(u => u.Albums.GetByIdAsync(1)) .ReturnsAsync(album); // Act var result = await _service.GetAlbumAsync(1); // Assert Assert.NotNull(result); Assert.Equal("OK Computer", result.Title); Assert.Equal("Radiohead", result.ArtistName); } [Fact] public async Task GetAlbumAsync_NonExistentAlbum_ReturnsNull() { // Arrange _unitOfWorkMock.Setup(u => u.Albums.GetByIdAsync(999)) .ReturnsAsync((Album)null); // Act var result = await _service.GetAlbumAsync(999); // Assert Assert.Null(result); } } ``` ### Component Tests (bUnit) **Melodee.Tests.Blazor/Pages/AlbumsTests.cs**: ```csharp public class AlbumsTests : TestContext { [Fact] public void Albums_RendersAlbumGrid() { // Arrange var libraryServiceMock = new Mock(); libraryServiceMock.Setup(s => s.SearchAlbumsAsync(null, null)) .ReturnsAsync(new List { new AlbumDto { Id = 1, Title = "OK Computer", ArtistName = "Radiohead" } }); Services.AddSingleton(libraryServiceMock.Object); // Act var cut = RenderComponent(); // Assert cut.Find("h1").TextContent.Should().Be("Albums"); cut.FindAll("tr").Count.Should().BeGreaterThan(0); } } ``` ### Performance Tests (NBomber) **Melodee.Tests.Performance/ApiLoadTests.cs**: ```csharp public class ApiLoadTests { [Fact] public void AlbumsEndpoint_CanHandle100ConcurrentUsers() { var httpClient = new HttpClient { BaseAddress = new Uri("http://localhost:5000") }; var scenario = Scenario.Create("albums_load_test", async context => { var response = await httpClient.GetAsync("/api/v1/albums?page=1&pageSize=20"); return response.IsSuccessStatusCode ? Response.Ok() : Response.Fail(); }) .WithLoadSimulations( Simulation.InjectPerSec(rate: 100, during: TimeSpan.FromSeconds(30)) ); var stats = NBomberRunner .RegisterScenarios(scenario) .Run(); var albumsStats = stats.ScenarioStats[0]; Assert.True(albumsStats.Ok.Request.RPS > 90); // At least 90 RPS Assert.True(albumsStats.Ok.Latency.Percent99 < 500); // P99 < 500ms } } ``` ## Code Quality ### Biome Configuration **biome.json**: ```json { "$schema": "https://biomejs.dev/schemas/1.0.0/schema.json", "organizeImports": { "enabled": true }, "linter": { "enabled": true, "rules": { "recommended": true, "complexity": { "noExtraBooleanCast": "error", "noMultipleSpacesInRegularExpressionLiterals": "error", "noUselessCatch": "error", "noWith": "error" }, "correctness": { "noConstAssign": "error", "noConstantCondition": "error", "noEmptyPattern": "error", "noGlobalObjectCalls": "error", "noInnerDeclarations": "error", "noInvalidConstructorSuper": "error", "noNewSymbol": "error", "noUnreachable": "error", "noUnreachableSuper": "error", "noUnsafeFinally": "error", "noUnsafeOptionalChaining": "error", "noUnusedLabels": "error", "noUnusedVariables": "error", "useValidForDirection": "error" }, "style": { "noArguments": "error", "noVar": "error", "useConst": "error" }, "suspicious": { "noAsyncPromiseExecutor": "error", "noCatchAssign": "error", "noClassAssign": "error", "noCompareNegZero": "error", "noDebugger": "error", "noDoubleEquals": "error", "noDuplicateCase": "error", "noDuplicateClassMembers": "error", "noDuplicateObjectKeys": "error", "noDuplicateParameters": "error", "noEmptyBlockStatements": "error", "noExplicitAny": "warn", "noExtraNonNullAssertion": "error", "noFallthroughSwitchClause": "error", "noFunctionAssign": "error", "noGlobalAssign": "error", "noImportAssign": "error", "noMisleadingCharacterClass": "error", "noPrototypeBuiltins": "error", "noRedeclare": "error", "noShadowRestrictedNames": "error", "noUnsafeNegation": "error", "useGetterReturn": "error", "useValidTypeof": "error" } } }, "formatter": { "enabled": true, "formatWithErrors": false, "indentStyle": "space", "indentWidth": 2, "lineWidth": 100 } } ``` ## Conclusion Melodee's codebase demonstrates clean architecture with clear separation of concerns across multiple projects. The layered structure (Web, Core, Data, Providers, Jobs) enables maintainability and testability. Key strengths: - **Layered architecture**: Clear boundaries between UI, business logic, and data access - **Dependency injection**: Loose coupling and testability - **Repository pattern**: Abstraction over data access - **Comprehensive testing**: Unit, integration, component, and performance tests - **Code quality**: Biome linting for frontend code Key challenges: - **100+ migrations**: Complex schema evolution requires careful management - **Multiple projects**: Increased build complexity and dependency management - **Blazor Server**: Server-side rendering limits scalability The codebase positions Melodee as a well-architected music server with modern .NET practices and comprehensive testing.