Architecture
Spotter is built with a modern Go stack emphasizing simplicity and performance.
Technology Stack
Backend
- Language: Go 1.24+
- Router: chi - Lightweight HTTP router
- ORM: ent - Entity framework for Go
- Database: PostgreSQL (recommended), SQLite for development
Frontend
- Templating: templ - Type-safe Go templates
- Interactivity: HTMX - HTML over the wire
- Styling: Tailwind CSS + DaisyUI
- Icons: @iconify/tailwind
Real-time
- Event Bus: Internal pub/sub system
- SSE: Server-Sent Events for push notifications
Directory Structure
spotter/
├── cmd/
│ └── server/
│ └── main.go # Application entry point
├── internal/
│ ├── auth/ # JWT authentication
│ ├── config/ # Configuration management
│ ├── crypto/ # Encryption utilities
│ ├── handlers/ # HTTP handlers
│ ├── providers/ # External service providers
│ │ ├── navidrome/
│ │ ├── spotify/
│ │ └── lastfm/
│ ├── enrichers/ # Metadata enrichers
│ │ ├── musicbrainz/
│ │ ├── spotify/
│ │ ├── lastfm/
│ │ ├── fanart/
│ │ ├── lidarr/
│ │ └── openai/
│ ├── services/ # Business logic
│ ├── views/ # templ templates
│ └── middleware/ # HTTP middleware
├── ent/
│ └── schema/ # Database schemas
├── static/
│ └── css/ # Tailwind CSS
├── data/
│ ├── prompts/ # AI prompt templates
│ └── images/ # Downloaded images
└── docs-site/ # Docusaurus documentation
Request Flow
Browser → HTTP Handler → Service → Repository → Database
↓
Template → HTMX Response
↓
SSE Event (if applicable)
Key Components
Providers
Providers sync data from external services:
type Provider interface {
Name() string
SyncListens(ctx context.Context, user *ent.User) error
SyncPlaylists(ctx context.Context, user *ent.User) error
}
Enrichers
Enrichers enhance metadata from external sources:
type Enricher interface {
Name() string
EnrichArtist(ctx context.Context, artist *ent.Artist) error
EnrichAlbum(ctx context.Context, album *ent.Album) error
EnrichTrack(ctx context.Context, track *ent.Track) error
}
Event Bus
The event bus enables real-time updates:
// Publish an event
eventBus.Publish("listen:new", listenData)
// Subscribe to events
eventBus.Subscribe("listen:new", func(data interface{}) {
// Handle event
})
Database Schema
Key entities:
- User: Application users (linked to Navidrome)
- Artist: Music artists with metadata
- Album: Albums with artwork and tracks
- Track: Individual songs with audio features
- Listen: Listening history entries
- Playlist: Playlists from various sources
- DJ: Vibes Engine DJ personas
- Mixtape: Generated mixtapes
Background Jobs
Spotter runs several background processes:
- Sync Service: Periodically syncs from providers
- Enrichment Service: Runs metadata enrichment
- Playlist Sync: Keeps Navidrome playlists updated
Configuration
Configuration is loaded from environment variables using Viper:
type Config struct {
Server ServerConfig
Database DatabaseConfig
Sync SyncConfig
Metadata MetadataConfig
// ...
}
Error Handling
Errors are wrapped with context:
if err != nil {
return fmt.Errorf("failed to sync listens: %w", err)
}
Logging
Structured logging with slog:
slog.Info("syncing listens",
"provider", provider.Name(),
"user_id", user.ID,
)