Music sync daemon
This page covers the music-sync daemon that runs locally on your Mac to capture Apple Music listening data and correlate it with flow scores.
Flow Control series
- Flow Control
- Architecture
- Cursor sync daemon
- Music sync daemon - You are here
- Health sync daemon (disabled)
- HealthSync iOS app (recommended)
- Deployment
Why a music daemon?
Music is a significant environmental factor during coding sessions, but no tool connects what you are listening to with your productivity data. I wanted to answer:
- "What was I listening to at flow score 9?"
- "Which songs correlate with rising flow?"
- "What genre am I most productive with?"
| Challenge | Solution |
|---|---|
| Apple Music has no export API | JXA (JavaScript for Automation) queries playback state |
| Cannot run from Kubernetes | Local daemon on Mac where Apple Music runs |
| Need accurate start/end times | 15-second polling with change detection |
| Must not lose events on crash | Graceful shutdown flushes buffered events |
How it works
Polling mechanism
The daemon uses osascript -l JavaScript to query Apple Music every 15 seconds. The JXA script runs inside a try/catch block so that if Music.app is not running, it returns { state: 'stopped' } instead of throwing an error.
Change detection
Each poll compares the current track key (name|artist|album) against the previous track. When the key changes or playback stops:
- The previous track is recorded with its start and end timestamps
- Plays shorter than 5 seconds are discarded (likely skips)
- The new track becomes the current track
Event buffering
Events accumulate in memory up to a maximum of 20 before a forced flush. Under normal conditions, the 2-minute flush timer sends events to the dashboard in batches.
If a flush fails (dashboard unreachable), events are re-queued for retry on the next cycle.
Data extracted
Each event includes:
| Field | Description |
|---|---|
| trackName | Track title |
| artist | Track artist |
| album | Album name |
| albumArtist | Album artist (if different from track artist) |
| genre | Genre tag from Apple Music metadata |
| trackNumber | Position on album |
| year | Release year |
| duration | Track duration in seconds |
| startedAt | When playback started (ISO timestamp) |
| endedAt | When playback ended or track changed (ISO timestamp) |
Installation
1. Build the daemon
cd ~/Projects/tools/music-sync
npm install
npm run build
2. Configure launchd
Create ~/Library/LaunchAgents/com.example.music-sync.plist:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.example.music-sync</string>
<key>ProgramArguments</key>
<array>
<string>/usr/local/bin/node</string>
<string>/Users/YOUR_USER/Projects/tools/music-sync/dist/index.js</string>
</array>
<key>EnvironmentVariables</key>
<dict>
<key>STATS_DASHBOARD_URL</key>
<string>https://your-stats.example.com</string>
<key>STATS_API_KEY</key>
<string>your-api-key-here</string>
<key>DEBUG</key>
<string>false</string>
</dict>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<dict>
<key>SuccessfulExit</key>
<false/>
</dict>
<key>StandardOutPath</key>
<string>/tmp/music-sync.log</string>
<key>StandardErrorPath</key>
<string>/tmp/music-sync.error.log</string>
<key>ThrottleInterval</key>
<integer>30</integer>
</dict>
</plist>
Replace:
YOUR_USERwith your macOS usernameyour-stats.example.comwith your dashboard URLyour-api-key-herewith the API key from your Kubernetes secret
3. Load the service
launchctl load ~/Library/LaunchAgents/com.example.music-sync.plist
4. Verify
# Check if running
launchctl list | grep music-sync
# View logs
tail -f /tmp/music-sync.log
Configuration
| Variable | Default | Description |
|---|---|---|
STATS_DASHBOARD_URL | Required | Dashboard base URL |
STATS_API_KEY | Required | API key for authentication |
POLL_INTERVAL | 15000 | Music polling interval in ms (15 seconds) |
FLUSH_INTERVAL | 120000 | Event flush interval in ms (2 minutes) |
DEBUG | false | Enable verbose logging |
API endpoints
The daemon uses one endpoint for sending data. Two additional endpoints serve the Music page and flow timeline overlay.
| Endpoint | Method | Description |
|---|---|---|
/api/import/music-events | POST | Receive music events from daemon (Zod validated, rate limited) |
/api/music/timeline | GET | Query music events for a time range (?from=...&to=...) |
/api/music/top-flow | GET | Top songs during flow-building periods (?period=week|month|all) |
Import endpoint details
The import endpoint:
- Validates all events with Zod schemas (max 1000 per request)
- Deduplicates by
trackName + artist + startedAt(2-second tolerance window) - Updates
endedAtwhen a previously-open event receives its end time - Rate limits at 60 requests per minute per daemon ID
- Logs imports to the
ImportLogtable for the Sources page
Heartbeat tracking
The daemon sends identification with every flush:
| Field | Example | Purpose |
|---|---|---|
daemon.id | my-mac-music-sync | Unique identifier |
daemon.hostname | my-mac.local | Machine name |
daemon.version | 1.0.0 | Daemon version |
The Sources page shows daemon health based on the DaemonHeartbeat table (stale threshold: 5 minutes).
Dashboard integration
Music page
The /music page shows:
- Summary cards: unique tracks, total listening time, top genre, top flow-building track
- Flow-building tracks table: songs playing while flow score was rising, ranked by average flow score
- Most listened table: all tracks with Last Played column (sortable, default sort newest first), total listening time
- Period toggle: 7 days, 30 days, all time
- Sortable columns: click any column header to sort ascending/descending
- Text filter: search by track name, artist, album, or genre
Flow timeline overlay
When hovering over a data point on the flow timeline chart, if a track was playing during that 5-minute time bucket, the tooltip shows:
- Track name
- Artist and album
This enables instant correlation between flow peaks and music.
Security
| Control | Implementation |
|---|---|
| API key authentication | Bearer token in Authorization header |
| Input validation | Zod schemas for all incoming events |
| Rate limiting | 60 requests/minute per daemon ID |
| HTTPS enforcement | Warning logged for non-HTTPS URLs |
Troubleshooting
Daemon not recording tracks
# Check if Music.app is running and playing
osascript -l JavaScript -e 'Application("Music").playerState()'
# Check logs for errors
tail -50 /tmp/music-sync.log
Events not appearing on dashboard
- Verify
STATS_API_KEYmatchesDAEMON_API_KEYin your Kubernetes secret - Check dashboard import logs on the Sources page
- Ensure Music.app is the system music player (not Spotify or other players)
No flow correlation data
Flow-building track detection requires at least 2 flow data points during a track's playback. Flow data points are 5-minute buckets, so very short tracks may not overlap with any data points. Longer listening sessions produce better correlations.
Daemon not starting
# Check launchd status
launchctl list | grep music-sync
# Check error log
tail -50 /tmp/music-sync.error.log
# Common issues:
# - Node.js path incorrect in plist
# - STATS_API_KEY not set
# - Dashboard URL unreachable
Next: Health sync daemon covers the legacy daemon that syncs Apple Health data.