Last active
January 9, 2022 17:11
-
-
Save w4/948f6d23507a1288cd0b4144b0ea00a9 to your computer and use it in GitHub Desktop.
spotify playlist scraper (api-based)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
[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" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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