This document outlines the architecture for the Spotify Tools web interface, which provides playlist management functionality based on genres, clusters, and existing playlists.
- ASP.NET Core 8.0 - Web API + Blazor Server hosting
- Existing Services - Reuse
IAnalyticsService,ISyncService,IUnitOfWork - PostgreSQL - Existing database schema (no changes needed)
- Blazor Server - C# components with SignalR for reactivity
- Blazor.Virtualizer - Efficient rendering of large lists
- HTML/CSS - Bootstrap 5 for responsive layout
- JavaScript - Minimal (drag-drop library integration only)
Even though Blazor Server can directly inject services, we're implementing a full REST API layer for:
- Clean Architecture - Proper separation of concerns
- Future-Proofing - Easy migration to Blazor WASM or mobile apps
- Testability - API endpoints can be tested independently
- Standard Patterns - Familiar REST conventions for backend developers
- Swagger Documentation - Auto-generated API docs
Hosting Model: API and Blazor Server run in the same process (minimal overhead)
┌─────────────────────────────────────────────────────┐
│ SpotifyTools.Web (Single Process) │
├─────────────────────────────────────────────────────┤
│ Blazor Components HTTP Client (in-memory) │
│ ↓ ↓ │
│ API Controllers ← IAnalyticsService, IUnitOfWork │
│ ↓ │
│ SpotifyTools.Data (EF Core) → PostgreSQL │
└─────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────┐
│ Header: Search, Filters, "Create Playlist" Button │
├────────────────┬──────────────────────────┬──────────────────────┤
│ LEFT PANEL │ CENTER PANEL │ RIGHT PANEL │
│ (Filter) │ (Browse & Select) │ (Destination) │
│ │ │ │
│ Genre Filters │ Track List │ Target Playlist │
│ ┌────────────┐ │ ┌──────────────────────┐ │ ┌────────────────┐ │
│ │□ Rock (247)│ │ │☑ Track 1 Artist A ⋮ │ │ │🎵 My Rock Mix │ │
│ │☑ Pop (189) │ │ │☑ Track 2 Artist B ⋮ │ │ │ ├─ Track A │ │
│ │□ Metal(93) │ │ │□ Track 3 Artist C ⋮ │ │ │ ├─ Track B │ │
│ │□ Jazz (156)│ │ │ │ │ │ └─ Track C │ │
│ └────────────┘ │ │[Virtualized Scroll] │ │ │ │ │
│ │ └──────────────────────┘ │ │[+ New Playlist]│ │
│ Saved Clusters │ │ │ │ │
│ ┌────────────┐ │ ☑ Select All │ └────────────────┘ │
│ │Rock & Alt │ │ [Add 2 to Playlist ▼] │ │
│ │Pop & Dance │ │ │ │
│ └────────────┘ │ Showing: Pop (189 tracks)│ │
└────────────────┴──────────────────────────┴──────────────────────┘
- Genre list with track counts (checkbox filters)
- Saved clusters (expandable accordion)
- Quick filters:
- Not in any playlist
- Added in last 30 days
- Popularity threshold
- Multiple genres
- Virtualized track list (renders only visible items)
- Multi-select checkboxes (with Shift+click ranges)
- Bulk action toolbar when items selected
- Search/sort controls at top
- Genre tag pills per track (click to filter)
- Drag source for individual tracks
- Active playlist view with current tracks
- Create new playlist button
- Playlist selector dropdown
- Drop target for drag-drop
- Statistics: Total tracks, duration, duplicates
Problem: Libraries can contain 5,000-10,000 tracks. Loading all at once = slow UI.
Solution: Multi-stage progressive loading
GET /api/genres
Response: [
{ "name": "rock", "trackCount": 247 },
{ "name": "pop", "trackCount": 189 },
...
]GET /api/genres/rock/tracks?page=1&pageSize=50
Response: {
"items": [ /* 50 tracks */ ],
"totalCount": 247,
"page": 1,
"pageSize": 50
}- Blazor.Virtualizer automatically fetches next page as user scrolls
- Only renders ~20-30 visible items in DOM
- Total memory: ~2-3 pages in memory at once
Client-side (Blazor):
- Genre list: Cache for session
- Track pages: Cache last 3 pages per genre
- Playlists: Cache and invalidate on mutation
Server-side:
- No caching in API layer (services are scoped, EF handles caching)
- Optional: Add
IMemoryCachefor genre counts (updated on sync)
GET /api/genres # List all genres with counts
GET /api/genres/{name}/tracks # Paginated tracks for genre
GET /api/tracks # Paginated all tracks
GET /api/tracks/search?q={query} # Search tracks
GET /api/tracks/{id} # Single track details
GET /api/playlists # List user playlists
GET /api/playlists/{id} # Playlist details with tracks
POST /api/playlists # Create new playlist
PUT /api/playlists/{id} # Update playlist metadata
DELETE /api/playlists/{id} # Delete playlist
POST /api/playlists/{id}/tracks # Add tracks (bulk)
DELETE /api/playlists/{id}/tracks/{trackId} # Remove track
POST /api/playlists/{id}/sync # Sync to Spotify
GET /api/clusters # List saved clusters
GET /api/clusters/{id} # Cluster details
POST /api/clusters # Create/save cluster
PUT /api/clusters/{id} # Update cluster
DELETE /api/clusters/{id} # Delete cluster
POST /api/clusters/{id}/finalize # Mark as finalized
POST /api/clusters/{id}/create-playlist # Generate Spotify playlist
GET /api/analytics/genre-analysis # Genre analysis report
GET /api/analytics/sync-history # Recent sync history
// Response DTOs
public class GenreDto
{
public string Name { get; set; }
public int TrackCount { get; set; }
public int ArtistCount { get; set; }
}
public class TrackDto
{
public string Id { get; set; }
public string Name { get; set; }
public List<ArtistSummaryDto> Artists { get; set; }
public string AlbumName { get; set; }
public int DurationMs { get; set; }
public int Popularity { get; set; }
public List<string> Genres { get; set; }
public bool Explicit { get; set; }
}
public class ArtistSummaryDto
{
public string Id { get; set; }
public string Name { get; set; }
public List<string> Genres { get; set; }
}
public class PlaylistDto
{
public string Id { get; set; }
public string Name { get; set; }
public string? Description { get; set; }
public int TrackCount { get; set; }
public bool IsPublic { get; set; }
public string? SpotifyId { get; set; }
}
public class PlaylistDetailDto : PlaylistDto
{
public List<TrackDto> Tracks { get; set; }
public int TotalDurationMs { get; set; }
}
public class ClusterDto
{
public int Id { get; set; }
public string Name { get; set; }
public string? Description { get; set; }
public List<string> Genres { get; set; }
public int TrackCount { get; set; }
public bool IsFinalized { get; set; }
public bool IsAutoGenerated { get; set; }
public string? SpotifyPlaylistId { get; set; }
}
// Pagination wrapper
public class PagedResult<T>
{
public List<T> Items { get; set; } = new();
public int TotalCount { get; set; }
public int Page { get; set; }
public int PageSize { get; set; }
public int TotalPages => (int)Math.Ceiling(TotalCount / (double)PageSize);
public bool HasNextPage => Page < TotalPages;
public bool HasPreviousPage => Page > 1;
}
// Request DTOs
public class CreatePlaylistRequest
{
public string Name { get; set; }
public string? Description { get; set; }
public bool IsPublic { get; set; }
}
public class AddTracksRequest
{
public List<string> TrackIds { get; set; }
}Component: Blazor.Virtualizer (built-in .NET 6+)
<Virtualize Items="@tracks" Context="track">
<TrackRow Track="@track" OnSelected="HandleTrackSelected" />
</Virtualize>How it works:
- Renders only visible items + small buffer
- Automatically handles scroll events
- Dynamically loads more data via
ItemsProviderdelegate
Pattern: Checkbox selection with state management
<div class="bulk-actions" style="@(selectedTracks.Any() ? "" : "display:none")">
<span>@selectedTracks.Count selected</span>
<button @onclick="SelectAll">Select All</button>
<button @onclick="ClearSelection">Clear</button>
<select @onchange="HandleBulkAction">
<option value="">Add to Playlist...</option>
@foreach (var playlist in playlists)
{
<option value="@playlist.Id">@playlist.Name</option>
}
</select>
</div>
@code {
private HashSet<string> selectedTracks = new();
private void HandleTrackSelected(string trackId, bool isSelected)
{
if (isSelected) selectedTracks.Add(trackId);
else selectedTracks.Remove(trackId);
}
}Library: Minimal custom JavaScript or Blazor.DragDrop package
Implementation:
- Single track drag from center panel
- Drop zone on playlist in right panel
- Visual feedback (drag ghost, drop zone highlight)
- Fallback to checkbox + bulk action on mobile
Scenario: User creates playlist in another tab/CLI
Solution: SignalR hub for playlist mutations
// Server broadcasts changes
await Clients.All.SendAsync("PlaylistCreated", playlistDto);
// Blazor component listens
[Inject] HubConnection HubConnection { get; set; }
protected override async Task OnInitializedAsync()
{
HubConnection.On<PlaylistDto>("PlaylistCreated", (playlist) =>
{
playlists.Add(playlist);
StateHasChanged();
});
}- Tracks: 5,000-10,000 items
- Genres: 100-500 items
- Playlists: 10-100 items
- Concurrent users: 1-10 (personal/small team tool)
- Virtualization - Only render visible items (20-30 in DOM)
- Pagination - Server-side paging (50-100 items per page)
- Lazy Loading - Load data on-demand, not upfront
- Debouncing - Search input debounced (300ms)
- Caching - Cache genre list, invalidate on sync
- Indexing - Ensure DB indexes on
Genres,TrackId,PlaylistId
- Initial page load: <2 seconds
- Genre filter change: <500ms
- Search results: <1 second
- Add tracks to playlist: <2 seconds (for 50 tracks)
- OAuth via Spotify - Reuse existing
SpotifyClientService - ASP.NET Core Identity - Optional future addition for multi-user
- For now: Single-user mode (no auth required)
- JWT tokens for API access
- CORS configuration for external clients
- Rate limiting on API endpoints
- Create
SpotifyTools.Webproject - Define DTOs for all entities
- Implement controllers:
- GenresController
- TracksController
- PlaylistsController
- ClustersController
- Add Swagger/OpenAPI
- Test with Postman/curl
- Create three-panel layout
- Implement ApiClientService (typed HTTP client)
- Build genre list component (left panel)
- Build track list component (center panel) - basic
- Build playlist panel (right panel) - basic
- Wire up navigation between panels
- Add Blazor.Virtualizer to track list
- Implement multi-select checkboxes
- Add bulk actions toolbar
- Implement search/filter
- Add genre tag pills
- Create playlist modal
- Add drag-drop support
- Implement real-time updates (SignalR)
- Add loading states and error handling
- Implement duplicate detection
- Add playlist conflict warnings
- Responsive design (mobile/tablet)
- Cluster management UI
- Create Spotify playlists from clusters
- Track exclusions UI
- Sync progress visualization
- Export/import playlists
- Unit tests: Controller logic with mocked services
- Integration tests: Full request/response cycle with test DB
- Tools: xUnit, WebApplicationFactory, Testcontainers (PostgreSQL)
- Component tests: Blazor component isolation tests (bUnit)
- E2E tests: Playwright for critical workflows
- Manual testing: Browser DevTools, Lighthouse performance audits
- Docker - Existing
docker-compose.yml+ web service - IIS/Nginx - Traditional web server deployment
- Azure App Service - Cloud hosting (future)
appsettings.jsonfor connection strings (existing)- Environment variables for sensitive data
- Startup checks for database connectivity
- Multi-user support? - Currently single-user, add authentication later?
- Offline mode? - PWA with service workers?
- Mobile app? - REST API enables future mobile development
- Collaborative playlists? - Real-time multi-user editing?
- AI recommendations? - Suggest tracks for playlists based on similarity?