Initial commit

This commit is contained in:
Antoine Martin 2024-06-17 23:09:26 +02:00
commit 3818e91ef8
6 changed files with 2196 additions and 0 deletions

92
src/main.rs Normal file
View file

@ -0,0 +1,92 @@
use futures::stream::TryStreamExt;
use futures_util::pin_mut;
use rspotify::{prelude::*, scopes, AuthCodeSpotify, Credentials, OAuth};
const SPOTIFY_WEB_API_ADD_PLAYLIST_MAX_ID_NUM: usize = 100;
const SPOTIFY_WEB_API_TRACK_FEATURES_MAX_ID_NUM: usize = 100;
#[tokio::main]
async fn main() {
let args: Vec<String> = std::env::args().collect();
if args.len() != 4 {
eprintln!("usage: {} PLAYLIST_TITLE MIN_TEMPO MAX_TEMPO", args[0]);
std::process::exit(1);
}
let playlist_title = &args[1];
let min_tempo = args[2].parse::<u64>().unwrap();
let max_tempo = args[3].parse::<u64>().unwrap();
let tempo_range = min_tempo..=max_tempo;
// export RSPOTIFY_CLIENT_ID="your client_id"
// export RSPOTIFY_CLIENT_SECRET="secret"
//
// See https://developer.spotify.com/documentation/web-api/tutorials/getting-started#create-an-app
let creds = Credentials::from_env().unwrap();
let oauth = OAuth {
redirect_uri: "http://localhost:8888/callback".to_string(),
scopes: scopes!("playlist-modify-private", "user-library-read"),
..Default::default()
};
let spotify = AuthCodeSpotify::new(creds, oauth);
// Obtaining the access token
let url = spotify.get_authorize_url(false).unwrap();
// This function requires the `cli` feature enabled.
spotify.prompt_for_token(&url).await.unwrap();
let user = spotify.current_user().await.unwrap();
eprintln!("Collecting saved tracks...");
let stream = spotify.current_user_saved_tracks(None);
pin_mut!(stream);
let mut track_ids = Vec::new();
while let Some(saved_track) = stream.try_next().await.unwrap() {
track_ids.push(saved_track.track.id.unwrap());
}
eprintln!("Fetching track features...");
let mut features = Vec::new();
for chunk in track_ids.chunks(SPOTIFY_WEB_API_TRACK_FEATURES_MAX_ID_NUM) {
// FIXME: why does rspotify return a Result<Option<Vec<Features>>>? I think this should be a
// Result<Vec<Option<Features>>>, the Spotify API returns `null` for a single track if the
// provided Id isn't valid
if let Ok(Some(mut features_chunk)) = spotify.tracks_features(chunk.iter().cloned()).await {
features.append(&mut features_chunk);
}
}
// if this fails then Spotify probably did not return some track's features, we can't rely on
// features and ids having the same index anymore!
assert_eq!(track_ids.len(), features.len());
let mut ids: Vec<PlayableId<'_>> = track_ids
.into_iter()
.zip(features.iter())
.filter(|(_, features)| {
let tempo = features.tempo.round() as u64;
let doubled_tempo = tempo * 2;
tempo_range.contains(&tempo) || tempo_range.contains(&doubled_tempo)
})
.map(|(id, _)| PlayableId::from(id))
.collect();
eprintln!("Creating playlist...");
let new_playlist = spotify
.user_playlist_create(user.id, playlist_title, Some(false), None, None)
.await
.unwrap();
while !ids.is_empty() {
// NOTE: Can't use ids.chunks() because PlayableId isn't Clone :(
//
// (chunks doesn't need Clone but playlist_add_items needs IntoIterator<Item = PlayableId>)
let ids_chunk = ids.drain(0..(SPOTIFY_WEB_API_ADD_PLAYLIST_MAX_ID_NUM.min(ids.len())));
spotify
.playlist_add_items(new_playlist.id.clone(), ids_chunk, None)
.await
.unwrap();
}
}