This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Spotify Tools is a C# application that syncs your Spotify library to PostgreSQL for offline access and custom analytics. The application uses OAuth authentication, fetches all saved tracks/artists/albums/playlists, and stores them in a PostgreSQL database for analysis. Features an interactive CLI built with Spectre.Console for browsing and visualizing your music library.
Available Data for Analysis:
- Tracks: Names, duration, popularity, explicit flag, ISRC codes, added dates
- Artists: Names, genres, popularity, follower counts
- Albums: Names, release dates, labels, track counts, album types
- Playlists: User playlists with track relationships and positions
- Relationships: Track-artist mappings, track-album mappings, playlist contents
# Build the solution
dotnet build SpotifyGenreOrganizer.sln
# Run the application
dotnet run --project src/SpotifyGenreOrganizer
# Restore packages (if needed)
dotnet restoreThe application is configured via src/SpotifyGenreOrganizer/appsettings.json:
- Spotify Credentials:
ClientId,ClientSecret, andRedirectUrimust match settings in the Spotify Developer Dashboard - Connection String: PostgreSQL connection string with host, port (5433), database name, username, and password
- Redirect URI: Must be
http://127.0.0.1:5009/callback(matches Spotify Developer Dashboard)
Audio Features API Restricted: On November 27, 2024, Spotify restricted access to the /v1/audio-features endpoint for new applications. This endpoint provided metrics like danceability, energy, tempo, valence, acousticness, etc.
Impact:
- Audio features sync is disabled in the application (marked as unavailable in the UI)
- The
audio_featuresdatabase tables exist but will remain unpopulated - Analytics views that reference audio features will not have that data
- Apps created before Nov 27, 2024 may still have access (check your Spotify Developer Dashboard)
Workarounds Being Explored:
- Third-party APIs (Cyanite, Soundcharts, Musiio)
- Local audio analysis tools (Essentia, librosa equivalents)
- These will be implemented in future updates if needed
8 pre-built PostgreSQL views for analytics and visualization:
Currently Functional (with available data):
- v_tracks_with_artists - Denormalized track-artist relationships
- v_tracks_with_albums - Tracks with album information
- v_playlist_contents - Playlist contents with track and artist details
- v_artist_performance - Artist metrics including track counts and averages
- v_sync_summary - Human-readable sync history with duration calculations
- v_genre_stats - Genre statistics with track counts and popularity
Limited Functionality (audio features unavailable): 7. v_track_complete_details - Complete track info (audio features columns will be NULL) 8. v_high_energy_tracks - Cannot filter by energy/danceability (no data)
Views use snake_case column naming and can be queried from DataGrip or psql.
Analysis Possibilities with Current Data:
- Genre analysis (most popular genres, genre distribution)
- Artist analysis (top artists by follower count, track count, popularity)
- Temporal analysis (library growth over time, release date trends)
- Playlist analysis (playlist sizes, track overlap between playlists)
- Popularity trends (most popular tracks/artists/albums)
- Album analysis (album types, label distribution, release years)
SpotifyTools/
├── SpotifyTools.Domain # Entity models (Track, Artist, Album, AudioFeatures, etc.)
├── SpotifyTools.Data # EF Core, repositories, Unit of Work pattern
├── SpotifyTools.Sync # Sync orchestration with rate limiting and progress events
├── SpotifyTools.Analytics # Analytics queries and track detail reports
├── SpotifyClientService # Spotify API wrapper with OAuth
└── SpotifyGenreOrganizer # CLI interface with Spectre.Console
├── CliMenuService.cs # Main menu orchestration
└── UI/
├── MenuBuilder.cs # Spectre.Console menu factory
├── NavigationService.cs # Track browsing (artist/playlist/genre/search)
├── ProgressAdapter.cs # Sync progress visualization
└── SpectreReportFormatter.cs # Rich table/panel rendering
Naming Convention: All tables and columns use snake_case (e.g., track_id, duration_ms, first_synced_at), enforced by EFCore.NamingConventions package.
Tables:
tracks,artists,albums,playlists- Core music entitiestrack_artists,track_albums,playlist_tracks- Relationship tablesaudio_features,audio_analyses,audio_analysis_sections-⚠️ Not populated (Spotify API restricted)sync_history- Sync execution trackingspotify_tokens- OAuth token storage
Views: 8 pre-built analytics views for data visualization (see Database Views section for details)
- SpotifyAPI.Web (7.2.1): Main Spotify API wrapper
- Entity Framework Core (8.0.11): ORM with PostgreSQL provider (Npgsql)
- EFCore.NamingConventions (8.0.3): Enforces snake_case naming
- Spectre.Console (0.49.1): Beautiful CLI tables, menus, progress bars
- PostgreSQL 16: Database (Docker container)
Status: ✅ Core functionality complete (Jan 2026)
Goal: Organize user's Spotify library into genre-based playlists using intelligent clustering with full persistence and management capabilities.
Analytics Service Methods:
GetGenreAnalysisReportAsync()- Comprehensive genre landscape analysisGetAvailableGenreSeedsAsync()- Fetches official Spotify genre seedsSuggestGenreClustersAsync(minTracks)- Auto-generates genre clusters using pattern matchingGetTracksByGenreAsync()- Maps tracks to genres via artist relationshipsGetClusterPlaylistReportAsync(cluster)- Generates detailed track list for a cluster- ✅
SaveClusterAsync(cluster, customName)- Persists refined clusters to database - ✅
GetSavedClustersAsync()- Loads all saved clusters - ✅
GetSavedClusterByIdAsync(id)- Loads specific cluster - ✅
UpdateClusterAsync(id, cluster)- Updates existing cluster - ✅
DeleteClusterAsync(id)- Deletes cluster - ✅
FinalizeClusterAsync(id)- Marks cluster ready for playlist generation
Database Persistence:
- Table:
saved_clusters(snake_case columns) - Entity:
SavedClusterwith fields: id, name, description, genres (CSV), primary_genre, is_auto_generated, created_at, updated_at, is_finalized - Repository:
SavedClusterRepositorywith specialized queries - Migration:
20260103095757_AddSavedClustersTableSnakeCase
Clustering Algorithm:
- Uses 10 predefined patterns (Rock & Alternative, Pop & Dance, Electronic & EDM, Hip Hop & Rap, R&B & Soul, Metal & Heavy, Jazz & Blues, Folk & Acoustic, Classical & Orchestral, Latin & World)
- Matches library genres to patterns using keyword matching
- Creates individual clusters for large remaining genres (40+ tracks)
- Filters to minimum 20 tracks per cluster
Complete Workflow:
- Generate suggested clusters from library
- Review & Refine by removing genres that don't fit
- Save refined clusters with custom names
- View all saved clusters in management UI
- Edit saved clusters to further refine
- Finalize when ready for playlist generation
- Delete unwanted clusters
Interactive Cluster Refinement:
- View suggested clusters with track/artist counts
- Select cluster to review ALL genres with detailed breakdown
- Multi-select removal of genres that don't fit (e.g., remove "smooth jazz" from "hard bop")
- Description automatically updates to reflect refined genre list
- Orphaned Genre Handling (Option D):
- Shows removed genres with track counts
- Options: Create new clusters (if 20+ tracks), add to "Unclustered" bucket, suggest alternatives, or leave unclustered
- Large genres (20+ tracks) can become standalone clusters
- Small genres go to "Unclustered" for later review
Cluster Management UI:
- View all saved clusters in a table (ID, name, tracks, genres, status, type)
- Select cluster to view details (description, genre list, track counts)
- Edit cluster genres (remove unwanted genres, updates description)
- Delete clusters with confirmation
- Finalize clusters for playlist generation
- All operations persist to database
Models:
GenreCluster- In-memory cluster representation (id, name, description, genres list, track/artist counts, percentage)SavedCluster- Database entity (persisted clusters)GenreAnalysisReport- Full genre landscape with overlaps and statisticsClusterPlaylistReport- Track details for a cluster
Known Issues:
See issues.md for tracked bugs and UX improvements:
- Minor: Cannot exit edit screen without making changes
- Medium: Already-organized genres still appear in new suggestions
Implemented Features:
- ✅ Spotify playlist generation from finalized clusters (
CreatePlaylistFromClusterAsync) - ✅ Track exclusion system (
ExcludeTrackAsync,IncludeTrackAsync,GetExcludedTrackIdsAsync) - ✅ Playlist persistence with database fields (
spotify_playlist_id,playlist_created_at)
Planned Features:
- Track list preview within clusters (Artist | Song | Duration | Album | Genre)
- Filter already-organized genres from new suggestions
- Unclustered genre tracking and management
- Alternative cluster suggestions using genre overlap analysis
- Custom cluster creation from scratch
- Playlist sync back (detect manual changes to generated playlists)
User Feedback Incorporated:
- Genre clustering must respect subgenre differences (e.g., "hard bop" ≠ "smooth jazz")
- Interactive refinement needed instead of automatic clustering
- Removed genres need intelligent handling (not just dropped)
- Description must reflect actual refined genre list, not original
Status: ✅ Production Ready (Jan 2026)
Goal: Provide comprehensive playlist management better than native Spotify UI, especially for users with hundreds of playlists and thousands of songs.
Features:
- Manual Spotify Sync - Local-first workflow with explicit sync control
- Dirty Tracking - Visual indicators for playlists with unsaved changes
- Search & Filter - Real-time search with multiple filter options
- Detail Modal - View full track lists with metadata
- Bulk Operations - Select and manage multiple playlists
Visual Status Indicators:
- 🟨 Yellow "Has Changes" - Local modifications need syncing (
LastModifiedAt > LastSyncedAt) - 🟩 Green "Synced" - Up-to-date with Spotify
- ⚪ Gray "Local" - Not yet on Spotify (GUID ID)
Search & Filter Options:
- Real-time search by name/description
- Filter: All, Has Changes, Local Only, On Spotify, Empty Playlists
- Sort: By Name, Track Count, or Type
- Dynamic counts in dropdown
Sync Operations:
- New Playlists (GUID): Creates on Spotify with all tracks
- Existing Playlists (Spotify ID): Updates using ReplaceItems API
- Strategy: Local overwrites Spotify (one-way sync)
Database Schema:
-- playlists table additions
last_modified_at timestamp with time zone NOT NULL -- Tracks local changes
last_synced_at timestamp with time zone NOT NULL -- Tracks sync operationsMigration: 20260110212304_AddLastModifiedAtToPlaylists (snake_case verified)
API Endpoints:
GET /api/playlists- List all playlistsGET /api/playlists/{id}- Get playlist details with tracksPOST /api/playlists- Create new playlistPOST /api/playlists/{id}/sync- Sync playlist to SpotifyPOST /api/playlists/{id}/tracks- Add tracks (updates LastModifiedAt)DELETE /api/playlists/{id}/tracks/{trackId}- Remove track (updates LastModifiedAt)DELETE /api/playlists/{id}- Delete playlist
Service: PlaylistService in src/SpotifyTools.Web/Services/PlaylistService.cs
Documentation: See docs/PLAYLIST_MANAGEMENT.md for complete details.
Full Sync (FullSyncAsync):
- Imports all saved tracks, artists, albums, and playlists
- Creates artist/album stubs for quick initial import
- Critical Fix (Jan 2026): Now syncs ALL playlist tracks, including those not in saved library
- Creates complete metadata for playlist-only tracks (artists, albums, relationships)
- Time: ~30-45 minutes per 3,000 tracks (rate limited)
Incremental Sync (IncrementalSyncAsync):
- Status: ✅ Fully implemented (Jan 2026)
- Smart update that only processes changes since last sync
- Automatic fallback to full sync if no previous sync or >30 days old
How Incremental Sync Works:
- New Tracks - Filters by
AddedAtdate, syncs only tracks added since last sync - Artist Stubs - Enriches stubs (Genres.Length == 0) with full metadata
- Stale Artists - Refreshes artists with
LastSyncedAt> 7 days old - Album Stubs - Enriches stubs (Label is null/empty) with full metadata
- Stale Albums - Refreshes albums with
LastSyncedAt> 7 days old - Changed Playlists - Uses
SnapshotIdcomparison to detect changes, only re-syncs modified playlists
Configuration Constants:
METADATA_REFRESH_DAYS = 7- How often to refresh artist/album metadataFULL_SYNC_FALLBACK_DAYS = 30- Max days between syncs before forcing full sync
Benefits:
- Much faster than full sync (typically 2-5 minutes vs 30-45 minutes)
- Lower API usage (fewer rate limit concerns)
- Automatic stub enrichment (completes partial data from playlists)
- Smart change detection (only processes what's new/changed)
Bug 1: Playlist Track Position Calculation
- Issue: Used
offset + IndexOf(item)AFTER offset was incremented - Fix: Introduced
globalPositioncounter that increments sequentially - Impact: Track positions now accurately reflect playlist order
Bug 2: Missing Playlist Tracks (CRITICAL)
- Issue: Tracks in playlists but not in saved library were silently skipped
- Fix: Changed from
continueto full track sync with complete metadata - Impact: All playlist tracks now sync, regardless of saved library status
- Example: "80s Phoenix Radio" now syncs all 98 tracks instead of just 12
Implementation Details:
- Creates artist stubs for playlist-only track artists
- Creates album stubs for playlist-only track albums
- Establishes all relationship records (TrackArtist, TrackAlbum)
- Ensures referential integrity for all tracks
Migrations are stored in src/SpotifyTools.Data/Migrations/. To create or apply migrations:
cd src/SpotifyTools.Data
dotnet ef migrations add MigrationName --startup-project ../SpotifyGenreOrganizer
dotnet ef database update --startup-project ../SpotifyGenreOrganizerThe database uses snake_case for all tables and columns, configured via:
EFCore.NamingConventionspackage (v8.0.3) inSpotifyTools.Data.UseSnakeCaseNamingConvention()inProgram.csDbContext configuration
Migration 20260102082536_ConvertToSnakeCase converted all column names from PascalCase to snake_case.
SyncService implements rate limiting (60 requests/minute) with progress events. Rate limiter delays requests to respect Spotify API limits.
Required OAuth scopes (configured in SpotifyClientService):
UserLibraryRead: Access saved tracksPlaylistModifyPublic: Create/modify public playlistsPlaylistModifyPrivate: Create/modify private playlists
appsettings.json contains sensitive credentials and should be in .gitignore. Template file is appsettings.json.template.