A lightweight Apple Music scrobbler for Last.fm that runs as a macOS daemon.
- Automatic Scrobbling: Monitors Apple Music and scrobbles tracks to Last.fm following official scrobbling rules (50% or 4 minutes)
- Background Daemon: Runs unobtrusively in the background via launchd
- Smart Tracking: Handles pause/resume, track skips, and repeats correctly
- Offline Queue: Queues scrobbles when offline and retries automatically
- Discord Rich Presence: Show current track in your Discord profile
- CLI Status: Query current track for tmux/status bars
- Easy Setup: Simple authentication flow and automatic installation
This project includes a modern, reusable Last.fm API client library at
pkg/lastfm/. It can be used independently in your own Go projects.
go get github.com/jfmyers9/scribbles/pkg/lastfmimport "github.com/jfmyers9/scribbles/pkg/lastfm"
// Create client
client, err := lastfm.NewClient(lastfm.Config{
APIKey: "your-api-key",
APISecret: "your-api-secret",
})
// Authenticate
token, _ := client.Auth().GetToken(ctx)
fmt.Println("Visit:", client.Auth().GetAuthURL(token.Token))
session, _ := client.Auth().GetSession(ctx, token.Token)
client.SetSessionKey(session.Key)
// Scrobble
track := lastfm.Track{
Artist: "The Beatles",
Track: "Yesterday",
}
client.Scrobble().Scrobble(ctx, track, time.Now())- Clean, type-safe API with context support
- Automatic retry with exponential backoff
- Batch scrobbling (up to 50 tracks)
- Structured error types
- Comprehensive godoc and examples
- Zero dependencies outside stdlib
For detailed documentation, see pkg/lastfm/README.md or view on pkg.go.dev.
git clone https://github.com/jfmyers9/scribbles.git
cd scribbles
go build -o scribbles .
sudo mv scribbles /usr/local/bin/First, get your Last.fm API credentials:
- Visit https://www.last.fm/api/account/create
- Create an application to get your API key and secret
Then authenticate:
scribbles authThis will:
- Prompt you for your API key and secret
- Generate an authorization URL for you to visit
- Wait for you to authorize the application
- Save your session key to the config file
Install and start the background daemon:
scribbles installThis will:
- Create a launchd plist at
~/Library/LaunchAgents/com.scribbles.daemon.plist - Start the daemon automatically
- Configure it to start on login
The daemon will now monitor Apple Music and scrobble tracks to Last.fm.
Query what's currently playing:
scribbles nowConfiguration is stored in ~/.config/scribbles/config.yaml.
Example configuration:
# Output format template for the "now" command
# Available fields: .Name, .Artist, .Album, .Duration, .Position, .State
output_format: "{{.Artist}} - {{.Name}}"
# Fixed output width for the "now" command (0=disabled)
# Useful for tmux status bars to prevent layout shifts
output_width: 0
# Marquee scrolling for long track names (requires output_width > 0)
marquee_enabled: false # Enable marquee scrolling
marquee_speed: 2 # Scroll speed in characters per second
marquee_separator: " • " # Separator between text repetitions
# Polling interval for the daemon (in seconds)
poll_interval: 3
# Logging configuration
logging:
level: info # debug, info, warn, error
file: "" # empty for stderr, or path to log file
# Last.fm API credentials (set via "scribbles auth")
lastfm:
api_key: "your-api-key"
api_secret: "your-api-secret"
session_key: "your-session-key"
# Discord Rich Presence
discord:
enabled: false
app_id: "" # Create at https://discord.com/developers/applicationsRun the scrobbling daemon in the foreground.
scribbles daemon [flags]Flags:
--log-file <path>: Log to a file instead of stderr--log-level <level>: Set log level (debug, info, warn, error)--data-dir <path>: Data directory for state and queue (default:~/.local/share/scribbles)--tui: Enable terminal UI for now playing display--discord: Enable Discord Rich Presence
The daemon:
- Polls Apple Music every 3 seconds (configurable)
- Tracks playback time and handles pause/resume
- Scrobbles tracks when they reach 50% or 4 minutes
- Queues failed scrobbles for retry
- Handles graceful shutdown on SIGINT/SIGTERM
Display the currently playing track.
scribbles now [flags]Flags:
--format <template>: Override the output format template--width <n>: Set fixed output width (0=disabled, overrides config)--marquee: Enable marquee scrolling for long text (requires --width)
Examples:
# Default format
scribbles now
# Output: Artist Name - Track Name
# Custom format
scribbles now --format "{{.Name}} by {{.Artist}}"
# Output: Track Name by Artist Name
# Full format
scribbles now --format "{{.Artist}} - {{.Name}} ({{.Album}})"
# Output: Artist Name - Track Name (Album Name)
# Fixed width output (useful for tmux status bars)
scribbles now --width 30
# Output: Artist Name - Track Name
# (padded to exactly 30 characters)
# Truncate long output
scribbles now --width 20
# Output: Artist Name - Tra...
# (truncated with "..." if longer than 20 characters)
# Marquee scrolling for long text
scribbles now --width 25 --marquee
# Output: (scrolls left to reveal full text over time)
# t=0s: Artist Name - Very L
# t=5s: y Long Track Name • A
# t=10s: Track Name • Artist NExit codes:
0: Music is playing1: Music is stopped or paused
Control Apple Music playback directly from the command line. These commands are useful for creating keyboard shortcuts or integrating with tmux.
Resume playback in Apple Music.
scribbles playPause playback in Apple Music.
scribbles pauseToggle between play and pause.
scribbles playpauseSkip to the next track.
scribbles nextGo to the previous track.
scribbles prevSet shuffle mode.
scribbles shuffle on
scribbles shuffle offSet playback volume (0-100).
scribbles volume 50
scribbles volume 100You can bind keys in tmux to control music playback without leaving your terminal:
# Add to your ~/.tmux.conf
# Alt+Space: toggle play/pause
bind-key -n M-Space run-shell "scribbles playpause"
# Alt+N: next track
bind-key -n M-n run-shell "scribbles next"
# Alt+P: previous track
bind-key -n M-p run-shell "scribbles prev"
# Alt+Plus: volume high
bind-key -n M-+ run-shell "scribbles volume 75"
# Alt+Minus: volume low
bind-key -n M-- run-shell "scribbles volume 25"After adding these bindings, reload your tmux config:
tmux source-file ~/.tmux.confNow you can control music playback with keyboard shortcuts:
- Alt+Space: Play/Pause
- Alt+N: Next track
- Alt+P: Previous track
- Alt++: Volume up
- Alt+-: Volume down
Authenticate with Last.fm.
scribbles authInteractive command that:
- Prompts for API key and secret
- Generates an authorization URL
- Opens your browser to authorize the application
- Saves the session key to your config file
Install the daemon as a launchd agent.
scribbles installThis creates and loads a launchd plist that:
- Starts the daemon automatically on login
- Restarts it if it crashes
- Logs to
~/.local/share/scribbles/logs/
Uninstall the daemon.
scribbles uninstallStops the daemon and removes the launchd plist.
Add the current track to your tmux status line:
set -g status-right "#(scribbles now 2>/dev/null || echo '')"Or with a prefix:
set -g status-right "♫ #(scribbles now 2>/dev/null || echo 'Not playing')"To prevent the status bar from shifting as track names change length, use the
--width flag or set output_width in your config:
# Using the --width flag (25 characters fixed width)
set -g status-right "♫ #(scribbles now --width 25 2>/dev/null || echo '— ')"
set -g status-interval 5Or configure it globally in ~/.config/scribbles/config.yaml:
output_format: "🎵 {{.Name}} - {{.Artist}}"
output_width: 25Then use in tmux:
set -g status-right "#(scribbles now 2>/dev/null || echo '— ')"The width is measured in display columns, accounting for Unicode characters like emoji. When output is longer than the specified width, it's truncated with "...". When shorter, it's padded with spaces.
For track names longer than the fixed width, you can enable marquee scrolling to reveal the full text over time instead of truncating it:
# Enable marquee scrolling with the --marquee flag
set -g status-right "♫ #(scribbles now --width 25 --marquee 2>/dev/null || echo '— ')"
set -g status-interval 5Or configure it globally in ~/.config/scribbles/config.yaml:
output_format: "🎵 {{.Name}} - {{.Artist}}"
output_width: 25
marquee_enabled: true
marquee_speed: 2
marquee_separator: " • "Then use in tmux:
set -g status-right "#(scribbles now 2>/dev/null || echo '— ')"- Short text (fits within width): Displayed statically with padding (no scrolling)
- Long text (exceeds width): Scrolls left to reveal the full text over time
- Continuous loop: Text wraps around with a separator (default: " • ")
- Deterministic: Same timestamp produces same output (consistent across tmux refreshes)
marquee_enabled(boolean, default:false): Enable marquee scrolling globallymarquee_speed(integer, default:2): Scroll speed in characters per second- With tmux
status-interval: 5, speed 2 = 10 characters per refresh - Higher values scroll faster but may be harder to read
- Lower values scroll slower but are more readable
- With tmux
marquee_separator(string, default:" • "): Separator shown between the end and beginning of the text
The visual scrolling effect depends on your tmux status-interval:
- status-interval: 5s, speed: 2 → 10 chars per update (recommended)
- status-interval: 5s, speed: 1 → 5 chars per update (slower, more readable)
- status-interval: 5s, speed: 3 → 15 chars per update (faster, less readable)
Adjust marquee_speed based on your preferred refresh interval for optimal
readability.
Show the currently playing track in your Discord profile with "Listening to" status.
- Create a Discord application at https://discord.com/developers/applications
- Copy the Application ID
- (Optional) Upload an icon in the app's Rich Presence > Art
Assets section — name it
scribblesfor the small icon - Add to your config:
discord:
enabled: true
app_id: "your-application-id"Or use the --discord flag:
scribbles daemon --discordDiscord displays: track name, artist, album (on hover), and elapsed/remaining time. The presence clears when music is paused or stopped.
Scribbles follows the official Last.fm scrobbling rules:
- Track must be longer than 30 seconds
- Track must be played for at least:
- 50% of its duration, OR
- 4 minutes (whichever comes first)
Examples:
- 3 minute track: scrobbles at 1:30
- 10 minute track: scrobbles at 4:00
- 20 second track: never scrobbles (too short)
- Pause/Resume: Playback time is accumulated across pauses
- Skip: If you skip before the threshold, the track is not scrobbled
- Repeat: Each play of the same track is scrobbled separately
- Offline: Scrobbles are queued and submitted when online
- Config:
~/.config/scribbles/config.yaml - State:
~/.local/share/scribbles/state.json(daemon runtime state) - Queue:
~/.local/share/scribbles/queue.db(SQLite database for scrobble queue) - Logs:
~/.local/share/scribbles/logs/(when running via launchd)
-
Check if the daemon is running:
launchctl list | grep scribbles -
Check the logs:
tail -f ~/.local/share/scribbles/logs/scribbles.log tail -f ~/.local/share/scribbles/logs/scribbles.err
-
Verify Last.fm credentials:
cat ~/.config/scribbles/config.yaml -
Restart the daemon:
scribbles uninstall scribbles install
- Check that tracks are longer than 30 seconds
- Verify you're playing tracks for at least 50% or 4 minutes
- Check Last.fm is online and accepting scrobbles
- Look for errors in the daemon logs
The daemon requires Apple Music to be running. Start Music and the daemon will automatically detect it.
- Increase the poll interval in the config (default: 3 seconds)
- Check for errors in the logs that might cause rapid retries
The now command calls AppleScript to query Apple Music, which takes
~200-300ms. This is unavoidable due to macOS limitations. For best
performance:
- Set tmux
status-intervalto 5 seconds or more - Ensure Apple Music is running (faster when app is active)
go build -o scribbles .# Unit tests only
go test ./...
# With integration tests (requires Last.fm credentials)
go test -tags=integration ./...scribbles/
├── cmd/ # CLI commands (Cobra)
│ ├── root.go
│ ├── daemon.go
│ ├── now.go
│ ├── auth.go
│ ├── install.go
│ └── uninstall.go
├── internal/
│ ├── music/ # Apple Music client
│ │ ├── client.go # Interface
│ │ └── applescript.go # AppleScript implementation
│ ├── scrobbler/ # Last.fm client
│ │ ├── client.go # Last.fm API wrapper
│ │ ├── queue.go # SQLite scrobble queue
│ │ └── rules.go # Scrobbling rules
│ ├── daemon/ # Daemon implementation
│ │ ├── daemon.go # Main daemon loop
│ │ ├── state.go # Track state management
│ │ ├── poller.go # Music polling
│ │ └── launchd.go # launchd plist generation
│ ├── discord/ # Discord Rich Presence
│ │ └── presence.go # IPC client and activity updates
│ └── config/ # Configuration
│ └── config.go
├── go.mod
├── go.sum
└── README.md
MIT License - see LICENSE for details.
- Cobra - CLI framework
- Viper - Configuration management
- zerolog - Structured logging
- modernc.org/sqlite - Pure Go SQLite
- rescrobbled - MPRIS scrobbler for Linux
- pScrobbler - Plex scrobbler
- mpdscribble - MPD scrobbler