lastfm-client
A modern, async Rust library for fetching and analyzing Last.fm user data with ease.
What's new in v3.4
- NDJSON export:
FileFormat::Ndjsonproduces.ndjsonfiles (one compact JSON object per line).fetch_and_saveandfetch_and_updateboth support it - incremental updates append new lines without rewriting the whole file.FileHandler::load_ndjsonandFileHandler::append_or_create_ndjsonare also public. Way faster than JSON for updates.
What's new in v3.3
- SQLite export (
sqlitefeature):fetch_and_save_sqliteandfetch_and_update_sqliteon all resource builders - see SQLite Export
What's new in v3.0
- Breaking: Removed the deprecated
lastfm_handlermodule (LastFMHandlerand all v1.x methods) - Top Artists: Fetch top artists via
client.top_artists("username") - Top Albums: Fetch top albums via
client.top_albums("username") - Unified
ResourceContainertrait across all resource types
If you were still using the v1.x API, see the migration section below.
Installation
Add this to your Cargo.toml:
[]
= "3.5"
Optional Features
| Feature | Description |
|---|---|
sqlite |
SQLite export via rusqlite (bundled SQLite, no system dependency required) |
[]
= { = "3.5", = ["sqlite"] }
Features
- Builder Pattern: Fluent, discoverable API design
- Automatic Retries: Configurable retry logic with exponential or linear backoff
- Rate Limiting: Prevent API abuse with built-in rate limiting
- Enhanced Error Handling: Rich error types with retry hints and context
- Testable: HTTP abstraction layer for easy mocking
- Type Safe: Leverages Rust's type system for compile-time guarantees
Data Fetching
- Async API Integration: Modern asynchronous Last.fm API communication
- Flexible Fetching: Get recent tracks, loved tracks, top tracks, top artists, and top albums with configurable limits
- Advanced Filtering: Time-based filtering (
from/totimestamps) and period-based filtering for top resources - Extended Data Support: Fetch extended track information with additional artist details
- Efficient Pagination: Smart handling of Last.fm's pagination system with chunked concurrent requests
Analytics
- Comprehensive Statistics: Total play counts, artist-level analytics, track-level analytics, most played artists/tracks, play count thresholds
- Custom Analysis: Extensible analysis framework with the
TrackAnalyzabletrait
Data Export
- Multiple Formats: Export data in JSON, NDJSON, CSV, and SQLite formats
- Timestamp-based Filenames: Automatic file naming with timestamps
- Organized Storage: Structured data directory management
- Incremental Updates:
fetch_and_updatewrites only new entries to an existing file (prepend for JSON, append for CSV and NDJSON); a sidecar metadata file keeps repeated calls fast regardless of file size - SQLite Export (
sqlitefeature): Export to a queryable.dbfile; incremental updates useMAX(date_uts)directly from the database with no sidecar needed
Configuration
Create a .env file in your project root:
LAST_FM_API_KEY=your_api_key_here
Usage
Quick Start
use LastFmClient;
async
Custom Configuration
use LastFmClient;
use Duration;
let client = builder
.api_key
.user_agent
.timeout
.max_concurrent_requests
.retry_attempts
.rate_limit // 10 requests per second
.build_client?;
Fetching Recent Tracks
// Limited tracks
let tracks = client
.recent_tracks
.limit
.fetch
.await?;
// All available tracks
let all_tracks = client
.recent_tracks
.unlimited
.fetch
.await?;
// Tracks from specific date
let since_timestamp = 1704067200; // Jan 1, 2024
let recent = client
.recent_tracks
.since
.fetch
.await?;
// Tracks between two dates
let from = 1704067200; // Jan 1, 2024
let to = 1706745600; // Feb 1, 2024
let tracks = client
.recent_tracks
.between
.fetch
.await?;
// Extended track information (includes full artist details)
let extended_tracks = client
.recent_tracks
.limit
.extended
.fetch_extended
.await?;
// Check if user is currently playing
let currently_playing = client
.recent_tracks
.check_currently_playing
.await?;
// Analyze tracks and get statistics
let stats = client
.recent_tracks
.limit
.analyze
.await?;
// Fetch and save to file
let filename = client
.recent_tracks
.limit
.fetch_and_save
.await?;
// Track progress during a long fetch
let filename = client
.recent_tracks
.unlimited
.on_progress
.fetch_extended_and_save
.await?;
// Incremental update: only fetch tracks newer than the last entry in the file.
// On the first call the file is created with a full fetch. Subsequent calls are
// fast because the latest timestamp is read from a small sidecar file instead
// of deserializing the entire data file.
let new_count = client
.recent_tracks
.on_progress
.fetch_and_update
.await?;
println!;
// Incremental update (CSV): same logic, but new rows are appended to the CSV.
// A sidecar (data/scrobbles.csv.meta) tracks the latest timestamp.
let new_count = client
.recent_tracks
.fetch_and_update
.await?;
println!;
// Incremental update (NDJSON): new records are appended as lines to the .ndjson file.
// A sidecar (data/scrobbles.ndjson.meta) tracks the latest timestamp.
let new_count = client
.recent_tracks
.fetch_and_update
.await?;
println!;
// Same for extended tracks (JSON or CSV path works)
let new_count = client
.recent_tracks
.fetch_extended_and_update
.await?;
// SQLite export (requires `sqlite` feature)
let db_path = client
.recent_tracks
.unlimited
.fetch_and_save_sqlite
.await?;
println!;
// SQLite incremental update - reads MAX(date_uts) from the DB, no sidecar file needed
let new_count = client
.recent_tracks
.fetch_and_update_sqlite
.await?;
println!;
SQLite Export (optional feature)
Enable the sqlite feature in Cargo.toml:
= { = "3.5", = ["sqlite"] }
All five resource types support fetch_and_save_sqlite(prefix). Recent tracks and loved tracks additionally support fetch_and_update_sqlite(db_path) for incremental updates. Extended recent tracks have their own pair of methods: fetch_extended_and_save_sqlite(prefix) and fetch_extended_and_update_sqlite(db_path). The databases can be queried with any SQLite tool:
Schema overview:
| Resource | Table | Notable columns |
|---|---|---|
RecentTrack |
recent_tracks |
name, artist, album, date_uts (NULL when now-playing) |
RecentTrackExtended |
recent_tracks_extended |
name, url, mbid, artist, artist_url, album, album_url, date_uts (NULL when now-playing) |
LovedTrack |
loved_tracks |
name, artist, date_uts |
TopTrack |
top_tracks |
name, artist, playcount, rank |
TopArtist |
top_artists |
name, playcount, rank |
TopAlbum |
top_albums |
name, artist, playcount, rank |
Progress Tracking
For large libraries, .on_progress() fires after the total is known (fetched = 0) and then after each batch (~5000 tracks):
use ;
let pb = new;
pb.set_style;
let pb_clone = pb.clone;
let filename = client
.recent_tracks
.unlimited
.on_progress
.fetch_extended_and_save
.await?;
pb.finish_and_clear;
println!;
Fetching Loved Tracks
// Limited loved tracks
let loved_tracks = client
.loved_tracks
.limit
.fetch
.await?;
// All loved tracks
let all_loved = client
.loved_tracks
.unlimited
.fetch
.await?;
// Analyze loved tracks
let stats = client
.loved_tracks
.analyze
.await?;
// Fetch and save loved tracks
let filename = client
.loved_tracks
.fetch_and_save
.await?;
// Incremental update for loved tracks
let new_count = client
.loved_tracks
.fetch_and_update
.await?;
println!;
Fetching Top Tracks
use Period;
// Top tracks with period filter
let top_tracks = client
.top_tracks
.limit
.period
.fetch
.await?;
// All-time top tracks
let all_time_top = client
.top_tracks
.unlimited
.period
.fetch
.await?;
// Fetch and save top tracks
let filename = client
.top_tracks
.limit
.period
.fetch_and_save
.await?;
Fetching Top Artists
use Period;
let top_artists = client
.top_artists
.limit
.period
.fetch
.await?;
Fetching Top Albums
use Period;
let top_albums = client
.top_albums
.limit
.period
.fetch
.await?;
Error Handling with Retry Hints
use LastFmError;
match client.recent_tracks.limit.fetch.await
Friendly error messages (Display vs Debug)
If you see output like MissingEnvVar("LAST_FM_API_KEY"), the error is being printed with Debug formatting ({:?}) somewhere. This library implements friendly Display messages (via #[error("...")]), so prefer Display ({}) when printing errors.
Use an explicit main error handler to guarantee Display formatting:
use dotenv;
use LastFmClient;
async
async
Tips:
- Use
eprintln!("{}", err)oreprintln!("Error: {err}")(Display), avoid{:?}/{:#?}(Debug). - If you keep
fn main() -> Result<...>, your runtime may show Debug output on failure. The explicit handler above guarantees Display. - This applies to all errors from this library, including configuration errors like missing
LAST_FM_API_KEY.
API Methods Reference
Client Creation
use LastFmClient;
// Simple: with defaults (loads API key from LAST_FM_API_KEY env var)
let client = new?;
// With custom configuration
let client = builder
.api_key
.retry_attempts
.rate_limit
.build_client?;
Recent Tracks
client.recent_tracks
.limit // Limit number of tracks
.unlimited // Fetch all available tracks
.since // Tracks since timestamp
.between // Tracks between two timestamps
.extended // Include extended info
.on_progress // Progress callback (fetched, total)
.fetch // Execute and get Vec<RecentTrack>
.fetch_extended // Execute and get Vec<RecentTrackExtended>
.check_currently_playing // Check if currently playing
.analyze // Analyze tracks and get statistics
.analyze_and_print // Analyze and print statistics
.fetch_and_save // Fetch and save to a new timestamped file
.fetch_extended_and_save // Fetch extended and save
.fetch_and_update // Fetch only new tracks and prepend to file -> usize
.fetch_extended_and_update // Same for extended tracks -> usize
// sqlite feature only:
.fetch_and_save_sqlite // Fetch and save to a new .db file -> String
.fetch_and_update_sqlite // Fetch only new tracks and insert into .db -> usize
.fetch_extended_and_save_sqlite // Fetch extended and save to a new .db file -> String
.fetch_extended_and_update_sqlite // Fetch only new extended tracks and insert -> usize
Loved Tracks
client.loved_tracks
.limit // Limit number of tracks
.unlimited // Fetch all available tracks
.fetch // Execute and get Vec<LovedTrack>
.analyze // Analyze tracks and get statistics
.analyze_and_print // Analyze and print statistics
.fetch_and_save // Fetch and save to a new timestamped file
.fetch_and_update // Fetch only new tracks and prepend to file -> usize
// sqlite feature only:
.fetch_and_save_sqlite // Fetch and save to a new .db file -> String
.fetch_and_update_sqlite // Fetch only new tracks and insert into .db -> usize
Top Tracks
client.top_tracks
.limit // Limit number of tracks
.unlimited // Fetch all available tracks
.period // Time period filter
.fetch // Execute and get Vec<TopTrack>
.fetch_and_save // Fetch and save to file
.fetch_and_save_sqlite // sqlite feature only: save to .db file
Top Artists
client.top_artists
.limit // Limit number of artists
.unlimited // Fetch all available artists
.period // Time period filter
.fetch // Execute and get Vec<TopArtist>
.fetch_and_save // Fetch and save to file
.fetch_and_save_sqlite // sqlite feature only: save to .db file
Top Albums
client.top_albums
.limit // Limit number of albums
.unlimited // Fetch all available albums
.period // Time period filter
.fetch // Execute and get Vec<TopAlbum>
.fetch_and_save // Fetch and save to file
.fetch_and_save_sqlite // sqlite feature only: save to .db file
Available Period Options
Period::Overall- All timePeriod::Week- Last 7 daysPeriod::Month- Last monthPeriod::ThreeMonth- Last 3 monthsPeriod::SixMonth- Last 6 monthsPeriod::TwelveMonth- Last 12 months
Parameter Types
limit:impl Into<TrackLimit>- UseSome(n)for limited results,NoneorTrackLimit::Unlimitedfor allfrom/to:Option<i64>- Unix timestamps in secondsextended:bool- Whether to fetch extended track informationperiod:Option<Period>- Time period filter (Week, Month, ThreeMonth, etc.)format:FileFormat-FileFormat::Json,FileFormat::Csv, orFileFormat::Ndjson
Testing
Run the test suite:
The library includes extensive test coverage with mock HTTP clients for reliable testing.
Advanced Features
Retry Logic
Configure automatic retries with exponential or linear backoff:
use ;
use Duration;
// Exponential backoff: 100ms -> 200ms -> 400ms -> 800ms
let client = builder
.api_key
.retry_attempts
.build_client?;
// Custom retry policy
let policy = exponential
.with_base_delay
.with_max_delay;
Rate Limiting
Prevent API throttling with sliding window rate limiting:
use Duration;
let client = builder
.api_key
.rate_limit // 10 requests per second
.build_client?;
Testing with Mocks
Use mock HTTP clients for testing:
use MockClient;
use HashMap;
let mut responses = new;
responses.insert;
let mock_client = new;
// Use mock_client in your tests
Architecture
src/
├── client/
│ ├── lastfm.rs # Main LastFmClient entry point
│ ├── http.rs # HTTP abstraction (trait + implementations)
│ ├── retry.rs # Retry logic with backoff strategies
│ └── rate_limiter.rs # Rate limiting with sliding window
├── api/
│ ├── fetch_utils.rs # Generic pagination with ResourceContainer trait
│ ├── recent_tracks.rs # RecentTracksClient with builder pattern
│ ├── loved_tracks.rs # LovedTracksClient with builder pattern
│ ├── top_tracks.rs # TopTracksClient with builder pattern
│ ├── top_artists.rs # TopArtistsClient with builder pattern
│ └── top_albums.rs # TopAlbumsClient with builder pattern
├── types/
│ ├── tracks.rs # Track type definitions
│ ├── artists.rs # Artist type definitions
│ ├── albums.rs # Album type definitions
│ └── period.rs # Period and TrackLimit enums
├── config.rs # Configuration with builder
├── error.rs # Rich error types with retry hints
├── analytics.rs # TrackAnalyzable trait + AnalysisHandler
├── file_handler.rs # JSON/NDJSON/CSV/SQLite export
└── sqlite.rs # SqliteExportable trait (sqlite feature only)
Key Design Principles
- Trait-based HTTP abstraction: Easy to test with mocks
- Builder patterns: Fluent, discoverable APIs
- Type safety: Leverages Rust's type system
- Zero-cost abstractions: No runtime overhead
Migrating from v2.x
v3.0 removes the lastfm_handler module (LastFMHandler) that was deprecated since v2.0. If you were using it:
| v2.x (removed) | v3.0 equivalent |
|---|---|
LastFMHandler::new("user") |
LastFmClient::new()? |
handler.get_user_recent_tracks(Some(100)) |
client.recent_tracks("user").limit(100).fetch().await? |
handler.get_user_recent_tracks_between(from, to, false) |
client.recent_tracks("user").between(from, to).fetch().await? |
handler.get_user_top_tracks(Some(50), Some(Period::Week)) |
client.top_tracks("user").limit(50).period(Period::Week).fetch().await? |
handler.get_user_loved_tracks(Some(100)) |
client.loved_tracks("user").limit(100).fetch().await? |
TrackPlayInfo has moved from lastfm_client::lastfm_handler::TrackPlayInfo to lastfm_client::types::TrackPlayInfo.
License
This project is licensed under the MIT License - see the LICENSE file for details.
Acknowledgments
Built with Rust and powered by the Last.fm API.