rustfm-scraper 0.2.23

Scrapes listening history from Last.fm and stores it in a file
Documentation
use std::time;

use anyhow::{anyhow, Result};
use async_recursion::async_recursion;
use futures::prelude::*;
use indicatif::ProgressBar;

use crate::lastfm;
use crate::models::ApiResponse;
use crate::models::recent_tracks::{Attr, RecentTracksResponse, Track};
use crate::models::user::User;

// REFERENCE: https://stackoverflow.com/a/51047786
pub async fn fetch_tracks(
    user: &User,
    api_key: &str,
    page: i32,
    limit: i32,
    from: i64,
    to: i64,
) -> Result<Vec<Track>> {
    println!("\nFetching metadata...");
    let metadata = fetch_tracks_metadata(user, api_key, page, limit, from, to).await?;

    if metadata.single_page() && metadata.single_track() {
        println!("Fetching one new track...");
    } else if metadata.single_page() {
        println!(
            "Fetching {} tracks from one page...",
            metadata.total_tracks()
        );
    } else {
        println!(
            "Fetching {} tracks from {} pages...",
            metadata.total_tracks(),
            metadata.total_pages()
        );
    }

    let urls: Vec<String> = (1..=metadata.total_pages())
        .map(|p| lastfm::build_request_url(user, api_key, p, limit, from, to))
        .collect();

    let bar = ProgressBar::new(metadata.total_pages() as u64);
    let mut tracks = stream::iter(urls)
        .map(|url| {
            let client = reqwest::Client::builder()
                .timeout(time::Duration::from_secs(60))
                .build()
                .unwrap();

            let task = tokio::spawn(async move { fetch_page(&url, &client).await });

            bar.inc(1);

            task
        })
        .buffer_unordered(lastfm::PARALLEL_REQUESTS)
        .map(|t| t.unwrap())
        .collect::<Vec<Vec<Track>>>()
        .await
        .into_iter()
        .flatten()
        .collect::<Vec<Track>>();

    bar.finish();

    println!("Removing `Now Playing` track, if one exists...");
    if tracks.iter().any(|t| t.now_playing()) {
        tracks.retain(|t| !t.now_playing())
    }

    Ok(tracks)
}

#[async_recursion]
pub async fn fetch_page(url: &str, client: &reqwest::Client) -> Vec<Track> {
    let resp = match client.get(url).send().await {
        Ok(resp) => resp,
        Err(_) => return fetch_page(url, client).await,
    };

    let recent_tracks = match resp.json::<RecentTracksResponse>().await {
        Ok(rtr) => rtr.recent_tracks.tracks,
        Err(_) => return fetch_page(url, client).await,
    };

    recent_tracks
}

pub async fn fetch_tracks_metadata(
    user: &User,
    api_key: &str,
    page: i32,
    limit: i32,
    from: i64,
    to: i64,
) -> Result<Attr> {
    let url = format!("http://ws.audioscrobbler.com/2.0/?method={method}&user={user}&api_key={api_key}&format=json&extended=1&page={page}&limit={limit}&from={from}&to={to}",
                      method = "user.getRecentTracks",
                      user = user.name,
                      api_key = api_key,
                      page = page,
                      limit = limit,
                      from = from,
                      to = to,
    );

    let response: ApiResponse<RecentTracksResponse> = reqwest::get(&url).await?.json().await?;
    let attr = match response {
        ApiResponse::Success(recent_tracks_response) => recent_tracks_response.recent_tracks.attr,
        ApiResponse::Failure(error) => return Err(anyhow!("{}", error.message))
    };

    Ok(attr)
}