-
Notifications
You must be signed in to change notification settings - Fork 1
How It Works
This page explains the internal architecture and mechanisms of foo_mac_scrobble, detailing how the component integrates with foobar2000 and communicates with the Last.fm API.
┌─────────────────────────────────────────────────────────┐
│ foobar2000 Core │
└───────────────┬─────────────────────────────────────────┘
│
│ Playback Events
▼
┌─────────────────────────────────────────────────────────┐
│ Play Callback Handler │
│ (monitors track changes, playback position, etc.) │
└───────────────┬─────────────────────────────────────────┘
│
│ Track Info
▼
┌─────────────────────────────────────────────────────────┐
│ Scrobble Queue Manager │
│ • Validates track eligibility │
│ • Checks scrobble threshold │
│ • Queues tracks for submission │
└───────────────┬─────────────────────────────────────────┘
│
│ Queued Tracks
▼
┌─────────────────────────────────────────────────────────┐
│ Background Worker Thread │
│ • Processes queue periodically │
│ • Handles network operations asynchronously │
│ • Implements retry logic │
└───────────────┬─────────────────────────────────────────┘
│
│ HTTP Requests
▼
┌─────────────────────────────────────────────────────────┐
│ Last.fm API Client │
│ • Signs requests with API signature │
│ • Handles authentication flow │
│ • Submits scrobbles in batches │
└───────────────┬─────────────────────────────────────────┘
│
│ API Calls
▼
┌─────────────────────────────────────────────────────────┐
│ Last.fm Web Service │
└─────────────────────────────────────────────────────────┘
When foobar2000 starts:
- The component registers itself with the foobar2000 SDK
- Configuration values are loaded from foobar2000's preferences system
- The session manager checks for stored Last.fm credentials:
~/Library/foobar2000-v2/lastfm_session.json - The offline queue is loaded from disk:
~/Library/foobar2000-v2/lastfm_scrobble_queue.json - A background worker thread is started to process the queue
The component implements the play_callback interface to receive events:
void on_playback_new_track(metadb_handle_ptr track)
void on_playback_time(double time)
void on_playback_stop(play_control::t_stop_reason reason)
void on_playback_pause(bool state)Key events:
- New track starts: Track metadata is captured
- Playback time updates: Percentage complete is calculated
- Threshold reached: Track is queued for scrobbling
- Playback stops: Pending tracks are finalized
A track is eligible for scrobbling when:
- Threshold met: User-configurable percentage of track has been played (default: 50%)
- Minimum duration: Track is at least 30 seconds long (Last.fm requirement)
- Metadata present: Artist and track title are available
- Not already scrobbled: Each track is scrobbled only once per playback session
When a track becomes eligible:
- Track info (artist, title, album, timestamp) is added to the in-memory queue
- The queue is immediately persisted to disk as JSON:
[ { "artist": "Artist Name", "track": "Track Title", "timestamp": 1700000000, "album": "Album Name", "album_artist": "Album Artist", "track_number": "1", "mbid": "" } ] - If network is unavailable, tracks remain queued until connectivity returns
The worker thread runs periodically (every 30 seconds):
- Check authentication: Verify session key is valid
- Process queue: Attempt to submit queued tracks
- Batch submission: Up to 50 tracks sent per API request
-
Handle responses:
- Success: Remove submitted tracks from queue
- Failure: Keep tracks in queue for retry
- Invalid session: Prompt re-authentication
- Persist state: Save updated queue to disk
1. User enters API Key + Secret in Preferences
│
▼
2. User clicks "Authenticate"
│
▼
3. Component generates auth token
token = md5(api_key + timestamp)
│
▼
4. Browser opens with Last.fm auth URL:
https://www.last.fm/api/auth/?api_key=...&token=...
│
▼
5. User approves on Last.fm website
│
▼
6. Last.fm redirects to callback URL
(appears as "Invalid API key" error)
│
▼
7. User copies full URL and pastes into component
│
▼
8. Component extracts 'token' parameter from URL
│
▼
9. Component calls auth.getSession API
signature = md5("api_key...method...token...secret")
│
▼
10. Last.fm returns session key
│
▼
11. Session key stored in:
~/Library/foobar2000-v2/lastfm_session.json
The session key is stored in JSON format:
{
"session_key": "d580d57f32848420e4155f4r4d5e6d8e",
"username": "lastfm_username"
}This file persists across:
- foobar2000 restarts
- Component updates
- macOS reboots
Sessions remain valid until explicitly revoked on Last.fm.
All Last.fm API requests are signed:
- Sort parameters alphabetically by key
- Concatenate:
key1value1key2value2... - Append API secret
- Calculate MD5 hash
- Include as
api_sigparameter
Example:
Parameters: {api_key, method, session_key, timestamp, artist, track}
Sorted: api_key=XXX&artist=Queen&method=track.scrobble&session_key=YYY×tamp=1234&track=Bohemian
String: api_keyXXXartistQueenmethodtrack.scrobblesession_keyYYYtimestamp1234trackBohemianSECRET
MD5: a1b2c3d4e5f6...
POST https://ws.audioscrobbler.com/2.0/
Content-Type: application/x-www-form-urlencoded
method=track.scrobble
&artist[0]=Artist
&track[0]=Title
×tamp[0]=1700000000
&album[0]=Album
&api_key=YOUR_KEY
&sk=SESSION_KEY
&api_sig=SIGNATUREResponse:
<lfm status="ok">
<scrobbles accepted="1" ignored="0">
<scrobble>
<track corrected="0">Title</track>
<artist corrected="0">Artist</artist>
<timestamp>1700000000</timestamp>
</scrobble>
</scrobbles>
</lfm>Common API errors and component responses:
| Error Code | Meaning | Component Action |
|---|---|---|
9 |
Invalid session key | Prompt re-authentication |
11 |
Service offline | Keep in queue, retry later |
16 |
Temporarily unavailable | Exponential backoff |
26 |
Suspended API key | Log error, disable submission |
29 |
Rate limit exceeded | Wait and retry |
All network operations are non-blocking:
- Requests are dispatched to the background thread
- Main foobar2000 UI thread remains responsive
- Callbacks are invoked on completion
- Results are synchronized back to main thread
- Connection timeout: 10 seconds
- Request timeout: 30 seconds
- Retry interval: 30 seconds (exponential backoff on failure)
- Uses macOS native secure transport
- Validates Last.fm SSL certificates
- Supports TLS 1.2+ only
The preferences panel is implemented as a native macOS view:
@interface fooLastfmPreferences : NSViewController
- (IBAction)authenticateClicked:(id)sender;
- (IBAction)scrobbleThresholdChanged:(id)sender;
- (IBAction)debugLoggingToggled:(id)sender;
@endUser preferences are synchronized with foobar2000's configuration system:
cfg_string cfg_api_key(guid_cfg_api_key, "");
cfg_string cfg_api_secret(guid_cfg_api_secret, "");
cfg_int cfg_scrobble_threshold(guid_cfg_threshold, 50);
cfg_bool cfg_debug_logging(guid_cfg_debug, false);Changes are persisted immediately to:
~/Library/foobar2000-v2/configuration/
When enabled, the component logs to foobar2000's console:
Last.fm: Queue initialized, 5 tracks loaded from disk
Last.fm: Track eligible for scrobbling: Queen - Bohemian Rhapsody
Last.fm: Added track to queue (6 total)
Last.fm: Processing queue with 6 tracks
Last.fm: Successfully scrobbled 6 tracks
Last.fm: Queue saved to disk
Log messages include:
- Authentication events
- Queue operations
- API requests and responses
- Error conditions
- Queue: ~1 KB per track in memory
- Session data: < 1 KB
- Component overhead: < 500 KB total
- Queue writes: Only when queue changes
- Session writes: Only on authentication
- File format: Compact JSON (not pretty-printed)
- Scrobble batch: ~200 bytes per track
- Max batch size: 50 tracks (Last.fm limit)
- Compression: gzip supported
- Queue access: Protected by mutex
- Config reads: Thread-safe via SDK
- UI updates: Dispatched to main thread
- Network callbacks: Marshalled to worker thread
Critical sections are protected:
std::lock_guard<std::mutex> lock(queue_mutex);
// Safe queue access here- Technical Overview — Module and code structure
- Last.fm API Integration — Detailed API documentation
- foobar2000 SDK - Official foobar2000 SDK page and documentation
Questions? Open an issue on GitHub for technical discussions.