Skip to content

Instantly share code, notes, and snippets.

@w4
Last active January 9, 2022 17:11
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save w4/948f6d23507a1288cd0b4144b0ea00a9 to your computer and use it in GitHub Desktop.
Save w4/948f6d23507a1288cd0b4144b0ea00a9 to your computer and use it in GitHub Desktop.
spotify playlist scraper (api-based)
[package]
name = "spotify-playlist-scraper"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
rspotify = { version = "0.11.3", features = ["cli"] }
futures = "0.3.19"
tokio = { version = "1.15.0", features = ["macros", "rt-multi-thread"] }
serde_json = "1.0.74"
serde = "1.0.133"
use futures::{stream::FuturesUnordered, StreamExt};
use rspotify::{
clients::{BaseClient, OAuthClient},
model::{FullTrack, Id, PlayableItem, SimplifiedPlaylist, UserId},
scopes, AuthCodeSpotify, Credentials, OAuth,
};
use serde::Serialize;
use std::collections::HashMap;
#[derive(Debug, Serialize)]
pub struct Track {
title: String,
artists: Vec<String>,
album: String,
album_artists: Vec<String>,
}
#[tokio::main]
async fn main() {
let client = instantiate_client().await;
// grab a map of (playlist_name, tracks) for all the user's playlists
let playlist_tracks_map_fut = client
.current_user_playlists()
.filter_map(|playlist| async {
let playlist = playlist.expect("playlist");
if playlist.owner.id != UserId::from_id("11123510516").unwrap() {
return None;
}
Some(grab_tracks_for_playlist(&client, playlist))
})
.collect::<FuturesUnordered<_>>()
.await
.collect::<HashMap<String, Vec<Track>>>();
// grab tracks from the user's 'saved' list
let saved_tracks_fut = grab_saved_tracks(&client);
// run both futures to completion
let (mut playlist_tracks_map, saved_tracks) = tokio::join!(playlist_tracks_map_fut, saved_tracks_fut);
// add saved tracks as a playlist
playlist_tracks_map.insert("Spotify Saved".to_string(), saved_tracks);
// output as json
println!(
"{}",
serde_json::to_string_pretty(&playlist_tracks_map).unwrap()
);
}
async fn instantiate_client() -> AuthCodeSpotify {
let credentials = Credentials::from_env().expect("missing credentials from env");
let mut oauth = OAuth::default();
oauth.redirect_uri = "http://localhost:8888/callback".to_string();
oauth.scopes = scopes!(
"playlist-read-private",
"playlist-read-collaborative",
"user-library-read"
);
let mut client = AuthCodeSpotify::new(credentials, oauth);
client
.prompt_for_token(&client.get_authorize_url(false).unwrap())
.await
.unwrap();
client
}
async fn grab_tracks_for_playlist(
client: &AuthCodeSpotify,
playlist: SimplifiedPlaylist,
) -> (String, Vec<Track>) {
eprintln!("Grabbing tracks from: {}", playlist.name);
// `Paginator` isn't `Send` so we need to collect all at once rather than next().await
let output = client
.playlist_items(&playlist.id, None, None)
.filter_map(|item| async move {
if let Some(PlayableItem::Track(track)) = item.expect("playlist item").track {
Some(transform_track(track))
} else {
None
}
})
.collect()
.await;
eprintln!("{} done", playlist.name);
(playlist.name, output)
}
async fn grab_saved_tracks(client: &AuthCodeSpotify) -> Vec<Track> {
eprintln!("Grabbing tracks from: Saved");
let res = client
.current_user_saved_tracks(None)
.map(|saved_track| transform_track(saved_track.expect("saved track").track))
.collect()
.await;
eprintln!("Saved done");
res
}
fn transform_track(track: FullTrack) -> Track {
Track {
title: track.name,
artists: track.artists.into_iter().map(|v| v.name).collect(),
album: track.album.name,
album_artists: track.album.artists.into_iter().map(|v| v.name).collect(),
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment