Skip to content

How It Works

avelytchko edited this page Nov 17, 2025 · 1 revision

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.

Architecture Overview

┌─────────────────────────────────────────────────────────┐
│                    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                        │
└─────────────────────────────────────────────────────────┘

Component Lifecycle

1. Plugin Initialization (initquit.cpp)

When foobar2000 starts:

  1. The component registers itself with the foobar2000 SDK
  2. Configuration values are loaded from foobar2000's preferences system
  3. The session manager checks for stored Last.fm credentials:
    ~/Library/foobar2000-v2/lastfm_session.json
    
  4. The offline queue is loaded from disk:
    ~/Library/foobar2000-v2/lastfm_scrobble_queue.json
    
  5. A background worker thread is started to process the queue

2. Playback Monitoring (play_callback.cpp)

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

3. Scrobble Eligibility

A track is eligible for scrobbling when:

  1. Threshold met: User-configurable percentage of track has been played (default: 50%)
  2. Minimum duration: Track is at least 30 seconds long (Last.fm requirement)
  3. Metadata present: Artist and track title are available
  4. Not already scrobbled: Each track is scrobbled only once per playback session

4. Offline Queueing (scrobble_queue.*)

When a track becomes eligible:

  1. Track info (artist, title, album, timestamp) is added to the in-memory queue
  2. 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": ""
      }
    ]
  3. If network is unavailable, tracks remain queued until connectivity returns

5. Background Processing

The worker thread runs periodically (every 30 seconds):

  1. Check authentication: Verify session key is valid
  2. Process queue: Attempt to submit queued tracks
  3. Batch submission: Up to 50 tracks sent per API request
  4. Handle responses:
    • Success: Remove submitted tracks from queue
    • Failure: Keep tracks in queue for retry
    • Invalid session: Prompt re-authentication
  5. Persist state: Save updated queue to disk

Authentication Flow

Initial Authentication

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

Session Persistence

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.


API Integration (lastfm_api.*)

Request Signing

All Last.fm API requests are signed:

  1. Sort parameters alphabetically by key
  2. Concatenate: key1value1key2value2...
  3. Append API secret
  4. Calculate MD5 hash
  5. Include as api_sig parameter

Example:

Parameters: {api_key, method, session_key, timestamp, artist, track}
Sorted: api_key=XXX&artist=Queen&method=track.scrobble&session_key=YYY&timestamp=1234&track=Bohemian
String: api_keyXXXartistQueenmethodtrack.scrobblesession_keyYYYtimestamp1234trackBohemianSECRET
MD5: a1b2c3d4e5f6...

Scrobble Submission

POST https://ws.audioscrobbler.com/2.0/
Content-Type: application/x-www-form-urlencoded

method=track.scrobble
&artist[0]=Artist
&track[0]=Title
&timestamp[0]=1700000000
&album[0]=Album
&api_key=YOUR_KEY
&sk=SESSION_KEY
&api_sig=SIGNATURE

Response:

<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>

Error Handling

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

Networking (libcurl)

Asynchronous I/O

All network operations are non-blocking:

  1. Requests are dispatched to the background thread
  2. Main foobar2000 UI thread remains responsive
  3. Callbacks are invoked on completion
  4. Results are synchronized back to main thread

Timeout Configuration

  • Connection timeout: 10 seconds
  • Request timeout: 30 seconds
  • Retry interval: 30 seconds (exponential backoff on failure)

SSL/TLS

  • Uses macOS native secure transport
  • Validates Last.fm SSL certificates
  • Supports TLS 1.2+ only

Preferences UI (fooLastfmMacPreferences.*)

Cocoa Integration

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;
@end

Data Binding

User 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/

Debug Logging

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

Performance Considerations

Memory Usage

  • Queue: ~1 KB per track in memory
  • Session data: < 1 KB
  • Component overhead: < 500 KB total

Disk I/O

  • Queue writes: Only when queue changes
  • Session writes: Only on authentication
  • File format: Compact JSON (not pretty-printed)

Network Usage

  • Scrobble batch: ~200 bytes per track
  • Max batch size: 50 tracks (Last.fm limit)
  • Compression: gzip supported

Thread Safety

Synchronization Points

  • Queue access: Protected by mutex
  • Config reads: Thread-safe via SDK
  • UI updates: Dispatched to main thread
  • Network callbacks: Marshalled to worker thread

Race Condition Prevention

Critical sections are protected:

std::lock_guard<std::mutex> lock(queue_mutex);
// Safe queue access here

Next Steps


Questions? Open an issue on GitHub for technical discussions.

Clone this wiki locally