Architecture
Drift FM is a mood-based internet radio you host yourself. Pick a mood, get continuous music. No algorithms, no accounts, no tracking.
Design Principles
- Simple by default. Single binary, SQLite, vanilla JS. No build step.
- Bring your own music. The platform is the player + shuffle engine. You supply content.
- Mood-first. Every track belongs to a mood. Playlists are per-mood shuffle with recency awareness.
- No frameworks. Go stdlib for HTTP, vanilla JS for frontend, SQLite for storage.
- Deploy anywhere. Runs on a $5 VPS, a Raspberry Pi, or localhost.
System Overview
┌─────────────┐ ┌──────────────┐ ┌──────────┐
│ Browser │────▶│ Go Server │────▶│ SQLite │
│ (vanilla │◀────│ (net/http) │ │ (WAL) │
│ JS/CSS) │ │ │ └──────────┘
└─────────────┘ │ :8080 │
│ │────▶ audio/tracks/
└──────────────┘
Request Flow
- Browser loads
/→ servesweb/index.html(single page) - JS calls
GET /api/moods→ returns mood list with track counts - User picks mood → JS calls
GET /api/moods/:mood/playlist - Server generates shuffled playlist from SQLite
- JS plays tracks sequentially via
<audio>element - Audio served from local filesystem via
/audio/path
Backend Packages
cmd/server/ Entry point, wiring
internal/
├── api/ HTTP handlers, routing
├── audio/ Audio file path resolution
├── cache/ In-memory cache with TTL
├── config/ YAML + environment configuration
├── inventory/ SQLite track management, queries
├── metrics/ Runtime and application metrics
└── radio/ Playlist generation, shuffle with recency
Key Design Decisions
SQLite over Postgres/MySQL: A music library of thousands of tracks fits comfortably in SQLite. WAL mode handles concurrent reads. No external dependencies to manage.
Pure Go SQLite (modernc.org/sqlite): No CGO required. Cross-compiles cleanly to any platform. Slightly slower than CGO sqlite3 but the workload is tiny.
Shuffle with recency: Tracks are Fisher-Yates shuffled per mood. Recently played tracks are pushed to the end of the playlist to avoid immediate repeats.
Content-addressed paths: Audio files live at audio/tracks/<prefix>/<slug>-<hex-id>.mp3. The hex prefix distributes files across subdirectories for filesystem performance.
No SPA framework: The player is vanilla JS (~1000 lines in app.js). CSS variables handle theming. No build step, no node_modules, no bundler.
Data Model
tracks
| Column | Type | Description | |——–|——|————-| | id | INTEGER | Primary key | | title | TEXT | Track title | | artist | TEXT | Artist name | | mood | TEXT | Primary mood (focus, calm, etc.) | | file_path | TEXT | Path relative to audio root | | duration_seconds | INTEGER | Track length | | energy | TEXT | low / medium / high | | intensity | INTEGER | 1-10 scale | | tempo_bpm | INTEGER | Beats per minute | | has_vocals | BOOLEAN | Instrumental flag | | lyrics | TEXT | Display lyrics (cleaned) | | status | TEXT | approved / pending / rejected |
play_stats
| Column | Type | Description |
|---|---|---|
| file_path | TEXT | PK, FK to tracks.file_path |
| play_count | INTEGER | Total plays |
| last_played_at | DATETIME | Last play timestamp |
listen_events
| Column | Type | Description |
|---|---|---|
| id | INTEGER | Primary key |
| track_id | INTEGER | FK to tracks |
| mood | TEXT | Mood during play |
| event_type | TEXT | play / skip / complete |
| listen_seconds | REAL | Duration listened |
| playlist_position | INTEGER | Position in playlist |
| created_at | DATETIME | Event timestamp |
Frontend Architecture
web/
├── index.html Single page, no routing
├── app.js Main application logic (~1000 lines)
├── galaxy.js Mood galaxy visualization
├── style.css Layout, responsive styles
├── tokens.css CSS variables for theming
├── base.css Reset and base styles
├── core/
│ ├── listen-reporter.js Listen event reporting
│ ├── events.js Event bus
│ ├── storage.js Local storage abstraction
│ ├── theme.js Theme switching
│ └── a11y.js Accessibility helpers
├── ui/
│ ├── about.js About panel
│ ├── lyrics.js Lyrics display
│ └── settings.js Settings panel
├── utils/
│ └── format.js Display formatting helpers
├── components/ Component CSS (mood-space, player-bar, etc.)
├── icons/ PWA icons
├── manifest.json PWA manifest
└── tests/ Frontend unit tests
Player engine: Uses HTML5 <audio> element with preloading. When current track reaches 80% completion, the next track starts preloading to eliminate gaps between tracks.
Mood selection: Mood grid with visual indicators. Selecting a mood fetches a fresh playlist and begins playback immediately.
No tracking: No analytics, no cookies, no third-party scripts. Listen events are stored locally in SQLite for playlist optimization only.
Audio Storage
Audio files live on the server filesystem:
audio/tracks/
├── 0/
│ ├── morning-coffee-0010.mp3
│ └── deep-current-04a0.mp3
├── 1/
│ ├── quiet-studio-0041.mp3
│ └── typewriter-04a1.mp3
...
Files are served directly by the Go server with appropriate cache headers.
Adding Custom Moods
Moods are derived from the tracks.mood column. To add a new mood:
- Import tracks with the new mood name
- The API automatically includes it in
/api/moods - The frontend dynamically renders mood buttons
No code changes needed.
Performance Characteristics
- Startup: < 1 second (single binary + SQLite)
- Memory: ~20-30 MB for a library of 200 tracks
- Playlist generation: < 5ms for shuffle of 50 tracks
- Concurrent users: SQLite WAL handles dozens of readers comfortably
- Storage: Bottleneck is audio files, not the application