Crate lastfm_edit

Source
Expand description

§lastfm-edit

A Rust crate for programmatic access to Last.fm’s scrobble editing functionality via web scraping.

This crate provides a high-level interface for authenticating with Last.fm, browsing user libraries, and performing bulk edits on scrobbled tracks. It uses web scraping to access functionality not available through Last.fm’s public API.

§Features

  • Authentication: Login to Last.fm with username/password
  • Library browsing: Paginated access to tracks, albums, and recent scrobbles
  • Bulk editing: Edit track names, artist names, and album information
  • Async iterators: Stream large datasets efficiently
  • HTTP client abstraction: Works with any HTTP client implementation

§Quick Start

use lastfm_edit::{LastFmEditClient, LastFmEditClientImpl, AsyncPaginatedIterator, Result};

#[tokio::main]
async fn main() -> Result<()> {
    // Create HTTP client and login
    let http_client = http_client::native::NativeClient::new();
    let client = LastFmEditClientImpl::login_with_credentials(
        Box::new(http_client),
        "username",
        "password"
    ).await?;

    // Browse recent tracks
    let mut recent_tracks = client.recent_tracks();
    while let Some(track) = recent_tracks.next().await? {
        println!("{} - {}", track.artist, track.name);
    }

    Ok(())
}

§Core Components

§Installation

Add this to your Cargo.toml:

[dependencies]
lastfm-edit = "3.1.0"
http-client = { version = "^6.6.3", package = "http-client-2", features = ["curl_client"] }
tokio = { version = "1.0", features = ["full"] }

§Usage Patterns

§Basic Library Browsing

use lastfm_edit::{LastFmEditClient, LastFmEditClientImpl, AsyncPaginatedIterator, Result};

#[tokio::main]
async fn main() -> Result<()> {
    let http_client = http_client::native::NativeClient::new();
    let client = LastFmEditClientImpl::login_with_credentials(
        Box::new(http_client),
        "username",
        "password"
    ).await?;

    // Get all tracks by an artist
    let mut tracks = client.artist_tracks("Radiohead");
    while let Some(track) = tracks.next().await? {
        println!("{} - {}", track.artist, track.name);
    }

    Ok(())
}

§Bulk Track Editing

use lastfm_edit::{LastFmEditClient, LastFmEditClientImpl, ScrobbleEdit, AsyncPaginatedIterator, Result};

#[tokio::main]
async fn main() -> Result<()> {
    let http_client = http_client::native::NativeClient::new();
    let client = LastFmEditClientImpl::login_with_credentials(
        Box::new(http_client),
        "username",
        "password"
    ).await?;

    // Find and edit tracks
    let tracks = client.artist_tracks("Artist Name").collect_all().await?;
    for track in tracks {
        if track.name.contains("(Remaster)") {
            let new_name = track.name.replace(" (Remaster)", "");

            // Create edit for this track
            let edit = ScrobbleEdit::from_track_info(
                &track.name,
                &track.name, // Use track name as album fallback
                &track.artist,
                0 // No timestamp needed for bulk edit
            )
            .with_track_name(&new_name)
            .with_edit_all(true);

            let response = client.edit_scrobble(&edit).await?;
            if response.success() {
                println!("Successfully edited: {} -> {}", track.name, new_name);
            }
        }
    }

    Ok(())
}

§Recent Tracks Monitoring

use lastfm_edit::{LastFmEditClient, LastFmEditClientImpl, AsyncPaginatedIterator, Result};

#[tokio::main]
async fn main() -> Result<()> {
    let http_client = http_client::native::NativeClient::new();
    let client = LastFmEditClientImpl::login_with_credentials(
        Box::new(http_client),
        "username",
        "password"
    ).await?;

    // Get recent tracks (first 100)
    let recent_tracks = client.recent_tracks().take(100).await?;
    println!("Found {} recent tracks", recent_tracks.len());

    Ok(())
}

§Mocking for Testing

Enable the mock feature to use MockLastFmEditClient for testing:

[dev-dependencies]
lastfm-edit = { version = "3.1.0", features = ["mock"] }
mockall = "0.13"
#[cfg(feature = "mock")]
mod tests {
    use lastfm_edit::{LastFmEditClient, MockLastFmEditClient, Result, EditResponse, ScrobbleEdit};
    use mockall::predicate::*;

    #[tokio::test]
    async fn test_edit_workflow() -> Result<()> {
        let mut mock_client = MockLastFmEditClient::new();

        // Set up expectations
        mock_client
            .expect_login()
            .with(eq("testuser"), eq("testpass"))
            .times(1)
            .returning(|_, _| Ok(()));

        mock_client
            .expect_edit_scrobble()
            .times(1)
            .returning(|_| Ok(EditResponse {
                success: true,
                message: Some("Edit successful".to_string()),
            }));

        // Use as trait object
        let client: &dyn LastFmEditClient = &mock_client;

        client.login("testuser", "testpass").await?;

        let edit = ScrobbleEdit::new(
            Some("Old Track".to_string()),
            Some("Old Album".to_string()),
            Some("Old Artist".to_string()),
            Some("Old Artist".to_string()),
            "New Track".to_string(),
            "New Album".to_string(),
            "New Artist".to_string(),
            "New Artist".to_string(),
            1640995200,
            false,
        );

        let response = client.edit_scrobble(&edit).await?;
        assert!(response.success);

        Ok(())
    }
}

§License

MIT

Re-exports§

pub use client::LastFmEditClientImpl;
pub use discovery::AlbumTracksDiscovery;
pub use discovery::ArtistTracksDiscovery;
pub use discovery::AsyncDiscoveryIterator;
pub use discovery::ExactMatchDiscovery;
pub use discovery::TrackVariationsDiscovery;
pub use login::LoginManager;
pub use trait::LastFmEditClient;
pub use iterator::AsyncPaginatedIterator;
pub use types::Album;
pub use types::AlbumPage;
pub use types::Artist;
pub use types::ArtistPage;
pub use types::ClientConfig;
pub use types::ClientEvent;
pub use types::ClientEventReceiver;
pub use types::ClientEventWatcher;
pub use types::EditResponse;
pub use types::ExactScrobbleEdit;
pub use types::LastFmEditSession;
pub use types::LastFmError;
pub use types::OperationalDelayConfig;
pub use types::RateLimitConfig;
pub use types::RateLimitType;
pub use types::RequestInfo;
pub use types::RetryConfig;
pub use types::RetryResult;
pub use types::ScrobbleEdit;
pub use types::SharedEventBroadcaster;
pub use types::SingleEditResponse;
pub use types::Track;
pub use types::TrackPage;
pub use session_persistence::SessionManager;
pub use session_persistence::SessionPersistence;

Modules§

client
discovery
edit_analysis
headers
iterator
login
parsing
HTML parsing utilities for Last.fm pages.
retry
session_persistence
trait
types
Data types for Last.fm music metadata and operations.
vcr_form_data
vcr_matcher
vcr_test_utils

Structs§

Html
An HTML tree.

Type Aliases§

AlbumTracksIterator
ArtistAlbumsIterator
ArtistTracksDirectIterator
ArtistTracksIterator
ArtistsIterator
RecentTracksIterator
Result
A convenient type alias for Result with LastFmError as the error type.
SearchAlbumsIterator
SearchTracksIterator