lastfm-edit 6.0.1

Rust crate for programmatic access to Last.fm's scrobble editing functionality via web scraping
Documentation
#[path = "shared/common.rs"]
mod common;

use lastfm_edit::{LastFmEditClient, Result};
use regex::Regex;

#[tokio::main]
async fn main() -> Result<()> {
    let client = common::setup_client().await?;

    println!("=== Remaster & Year Removal Tool ===\n");
    println!("๐ŸŽฏ This will remove 'remastered' text and year suffixes from track names");
    println!("๐Ÿ“ Patterns include: '- 2009', '(2009)', '[2009]', '- Remaster', etc.\n");

    let artist = std::env::args()
        .nth(1)
        .unwrap_or_else(|| "The Beatles".to_string());

    println!("๐ŸŽต Processing tracks for artist: {artist}\n");

    // Regex patterns to clean up remaster text and year suffixes
    // Note: Order matters! More specific patterns should come first
    let remaster_patterns = vec![
        // Patterns with "remaster" word (most specific)
        // "Track Name - 2009 Remaster" -> "Track Name"
        Regex::new(r"(?i)\s*-\s*\d{4}\s*remaster(ed)?.*$").unwrap(),
        // "Track Name - Remaster" or "Track Name - Remastered" -> "Track Name"
        Regex::new(r"(?i)\s*-\s*remaster(ed)?.*$").unwrap(),
        // "Track Name (2009 Remaster)" -> "Track Name"
        Regex::new(r"(?i)\s*\(\d{4}\s*remaster(ed)?.*\)\s*$").unwrap(),
        // "Track Name (Remaster)" or "Track Name (Remastered)" -> "Track Name"
        Regex::new(r"(?i)\s*\(remaster(ed)?.*\)\s*$").unwrap(),
        // "Track Name [2009 Remaster]" -> "Track Name"
        Regex::new(r"(?i)\s*\[\d{4}\s*remaster(ed)?.*\]\s*$").unwrap(),
        // "Track Name [Remaster]" or "Track Name [Remastered]" -> "Track Name"
        Regex::new(r"(?i)\s*\[remaster(ed)?.*\]\s*$").unwrap(),
        // "Track Name Remastered" -> "Track Name"
        Regex::new(r"(?i)\s*remaster(ed)?\s*(\d{4})?\s*$").unwrap(),
        // Years that are likely remaster years (1980-2030) - be more conservative
        // "Track Name - 2009" -> "Track Name" (only for likely remaster years)
        Regex::new(r"(?i)\s*-\s*(19[8-9]\d|20[0-3]\d)\s*$").unwrap(),
        // "Track Name (2009)" -> "Track Name" (only for likely remaster years)
        Regex::new(r"(?i)\s*\((19[8-9]\d|20[0-3]\d)\)\s*$").unwrap(),
        // "Track Name [2009]" -> "Track Name" (only for likely remaster years)
        Regex::new(r"(?i)\s*\[(19[8-9]\d|20[0-3]\d)\]\s*$").unwrap(),
        // Other common suffixes that should be removed
        // "Track Name - 2019 Mix" -> "Track Name"
        Regex::new(r"(?i)\s*-\s*\d{4}\s*mix.*$").unwrap(),
        // "Track Name - Mix" -> "Track Name"
        Regex::new(r"(?i)\s*-\s*mix.*$").unwrap(),
    ];

    // First, collect some tracks to process
    let mut tracks_to_process = Vec::new();
    let mut fetched_count = 0;
    let mut page = 1;

    loop {
        match client.get_artist_tracks_page(&artist, page).await {
            Ok(track_page) => {
                if track_page.tracks.is_empty() {
                    println!("\n๐Ÿ“š Fetched all {fetched_count} tracks for {artist}");
                    break;
                }

                for track in track_page.tracks {
                    fetched_count += 1;
                    println!("๐Ÿ” [{:3}] Found track: '{}'", fetched_count, track.name);

                    // Check if track name contains remaster text
                    let mut cleaned_name = track.name.clone();
                    let mut needs_cleaning = false;

                    for pattern in &remaster_patterns {
                        if pattern.is_match(&cleaned_name) {
                            cleaned_name = pattern.replace(&cleaned_name, "").trim().to_string();
                            needs_cleaning = true;
                        }
                    }

                    if needs_cleaning && !cleaned_name.is_empty() {
                        tracks_to_process.push((track, cleaned_name));
                    }
                }

                if !track_page.has_next_page {
                    println!("\n๐Ÿ“š Fetched all {fetched_count} tracks for {artist}");
                    break;
                }

                page += 1;
            }
            Err(e) => {
                println!("โŒ Error fetching tracks page {page}: {e}");
                break;
            }
        }
    }

    println!(
        "\n๐Ÿงน Starting remaster removal on {} tracks...\n",
        tracks_to_process.len()
    );

    let mut processed_count = 0;
    let mut edits_made = 0;
    let mut rate_limit_hits = 0;

    // Now process the collected tracks
    for (track, cleaned_name) in tracks_to_process {
        processed_count += 1;
        println!(
            "๐Ÿ”ง [{:3}] Processing: '{}' -> '{}'",
            processed_count, track.name, cleaned_name
        );
        println!("   ๐Ÿ”„ Applying change...");

        // Load edit form - this makes an HTTP request
        let edit_template =
            lastfm_edit::ScrobbleEdit::from_track_and_artist(&track.name, &track.artist);
        match client
            .discover_scrobble_edit_variations(&edit_template)
            .await
        {
            Ok(exact_edit_vec) => {
                if let Some(exact_edit) = exact_edit_vec.into_iter().next() {
                    let mut edit_data = exact_edit.to_scrobble_edit();
                    // Update track name
                    edit_data.track_name = Some(cleaned_name.clone());

                    // Submit edit - another HTTP request
                    match client.edit_scrobble(&edit_data).await {
                        Ok(_) => {
                            edits_made += 1;
                            println!("   โœ… Successfully cleaned track");
                        }
                        Err(e) => {
                            println!("   โŒ Error editing track: {e}");
                            if e.to_string().contains("RateLimit") {
                                rate_limit_hits += 1;
                                log::info!("Rate limit encountered during edit operation for track '{}' by '{}'", track.name, track.artist);
                                println!("   ๐Ÿšจ RATE LIMIT DETECTED during edit operation!");
                                break;
                            }
                        }
                    }
                } else {
                    println!("   โš ๏ธ  No edit data found for track");
                }
            }
            Err(e) => {
                println!("   โš ๏ธ  Couldn't load edit form: {e}");
                if e.to_string().contains("RateLimit") {
                    rate_limit_hits += 1;
                    log::info!(
                        "Rate limit encountered during form load for track '{}' by '{}'",
                        track.name,
                        track.artist
                    );
                    println!("   ๐Ÿšจ RATE LIMIT DETECTED during form load!");
                    break;
                }
            }
        }
    }

    println!("\n=== Summary ===");
    println!("๐Ÿ“Š Tracks processed: {processed_count}");
    println!("โœ๏ธ  Edits made: {edits_made}");
    println!("๐Ÿšจ Rate limit hits: {rate_limit_hits}");

    if rate_limit_hits > 0 {
        println!("\n๐ŸŽฏ Rate limiting was triggered.");
    } else {
        println!("\nโœจ All changes completed successfully!");
    }

    Ok(())
}