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 album::Album;
pub use album::AlbumPage;
pub use client::LastFmEditClientImpl;
pub use discovery::AlbumTracksDiscovery;
pub use discovery::ArtistTracksDiscovery;
pub use discovery::ExactMatchDiscovery;
pub use discovery::TrackVariationsDiscovery;
pub use discovery_iterator::AsyncDiscoveryIterator;
pub use events::ClientEvent;
pub use events::ClientEventReceiver;
pub use events::ClientEventWatcher;
pub use events::RateLimitType;
pub use events::RequestInfo;
pub use login::LoginManager;
pub use trait::LastFmEditClient;
pub use edit::EditResponse;
pub use edit::ExactScrobbleEdit;
pub use edit::ScrobbleEdit;
pub use edit::SingleEditResponse;
pub use error::LastFmError;
pub use iterator::AsyncPaginatedIterator;
pub use session::LastFmEditSession;
pub use track::Track;
pub use track::TrackPage;

Modules§

album
client
discovery
discovery_iterator
edit
edit_analysis
error
events
Event system for monitoring HTTP client activity.
headers
iterator
login
parsing
HTML parsing utilities for Last.fm pages.
retry
session
track
trait

Structs§

Html
An HTML tree.

Type Aliases§

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