Skip to main content

Music sync daemon

info

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

  1. Flow Control
  2. Architecture
  3. Cursor sync daemon
  4. Music sync daemon - You are here
  5. Health sync daemon (disabled)
  6. HealthSync iOS app (recommended)
  7. 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?"
ChallengeSolution
Apple Music has no export APIJXA (JavaScript for Automation) queries playback state
Cannot run from KubernetesLocal daemon on Mac where Apple Music runs
Need accurate start/end times15-second polling with change detection
Must not lose events on crashGraceful 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:

  1. The previous track is recorded with its start and end timestamps
  2. Plays shorter than 5 seconds are discarded (likely skips)
  3. 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:

FieldDescription
trackNameTrack title
artistTrack artist
albumAlbum name
albumArtistAlbum artist (if different from track artist)
genreGenre tag from Apple Music metadata
trackNumberPosition on album
yearRelease year
durationTrack duration in seconds
startedAtWhen playback started (ISO timestamp)
endedAtWhen 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_USER with your macOS username
  • your-stats.example.com with your dashboard URL
  • your-api-key-here with 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

VariableDefaultDescription
STATS_DASHBOARD_URLRequiredDashboard base URL
STATS_API_KEYRequiredAPI key for authentication
POLL_INTERVAL15000Music polling interval in ms (15 seconds)
FLUSH_INTERVAL120000Event flush interval in ms (2 minutes)
DEBUGfalseEnable verbose logging

API endpoints

The daemon uses one endpoint for sending data. Two additional endpoints serve the Music page and flow timeline overlay.

EndpointMethodDescription
/api/import/music-eventsPOSTReceive music events from daemon (Zod validated, rate limited)
/api/music/timelineGETQuery music events for a time range (?from=...&to=...)
/api/music/top-flowGETTop 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 endedAt when a previously-open event receives its end time
  • Rate limits at 60 requests per minute per daemon ID
  • Logs imports to the ImportLog table for the Sources page

Heartbeat tracking

The daemon sends identification with every flush:

FieldExamplePurpose
daemon.idmy-mac-music-syncUnique identifier
daemon.hostnamemy-mac.localMachine name
daemon.version1.0.0Daemon 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

ControlImplementation
API key authenticationBearer token in Authorization header
Input validationZod schemas for all incoming events
Rate limiting60 requests/minute per daemon ID
HTTPS enforcementWarning 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

  1. Verify STATS_API_KEY matches DAEMON_API_KEY in your Kubernetes secret
  2. Check dashboard import logs on the Sources page
  3. 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.