Scrobbler
Port for recording that a track was listened to (Last.fm-style scrobbling), the IScrobbler contract.
The player calls scrobble() when a track has been played past the scrobble threshold (typically 50% of duration or 4 minutes, whichever comes first, per Last.fm rules).
TypeScript
import type { IScrobbler, ScrobbleContext } from '@nomercy-entertainment/nomercy-music-player/adapters/scrobbler';
Interface
TypeScript
interface IScrobbler<T extends BasePlaylistItem = BasePlaylistItem> {
readonly id: string;
scrobble(item: T, context: ScrobbleContext): Promise<void>;
nowPlaying?(item: T): Promise<void>;
}
interface ScrobbleContext {
startedAt: number; // Unix timestamp (seconds)
listenedSeconds: number; // actual seconds listened
durationSeconds: number; // total track duration
source: 'user' | 'auto' | 'radio';
}
scrobble(item, context)
Record that item was listened to. Called after the scrobble threshold is reached.
nowPlaying(item) (optional)
Signal that item started playing. Called at track start for services that display “now playing” status separately from the completed scrobble. Implementations that do not support now-playing may omit this method.
Built-in adapter
NoopScrobbler
TypeScript
import { NoopScrobbler } from '@nomercy-entertainment/nomercy-music-player/adapters/scrobbler';
No-op implementation. Default when no scrobbler is configured. Does nothing.
Custom implementation: Last.fm example
TypeScript
import type { IScrobbler, ScrobbleContext } from '@nomercy-entertainment/nomercy-music-player/adapters/scrobbler';
import type { MusicPlaylistItem } from '@nomercy-entertainment/nomercy-music-player';
class LastFmScrobbler implements IScrobbler<MusicPlaylistItem> {
readonly id = 'lastfm';
constructor(private sessionKey: string) {}
async scrobble(item: MusicPlaylistItem, context: ScrobbleContext): Promise<void> {
await fetch('https://ws.audioscrobbler.com/2.0/', {
method: 'POST',
body: new URLSearchParams({
method: 'track.scrobble',
artist: item.artist ?? '',
track: item.name,
timestamp: String(Math.floor(context.startedAt)),
sk: this.sessionKey,
api_key: MY_API_KEY,
format: 'json',
}),
});
}
async nowPlaying(item: MusicPlaylistItem): Promise<void> {
await fetch('https://ws.audioscrobbler.com/2.0/', {
method: 'POST',
body: new URLSearchParams({
method: 'track.updateNowPlaying',
artist: item.artist ?? '',
track: item.name,
sk: this.sessionKey,
api_key: MY_API_KEY,
format: 'json',
}),
});
}
}
See also
- MusicPlaylistItem:
artist,namefields read by the scrobbler - Configuration: where adapters are wired