Initial commit
This commit is contained in:
commit
3818e91ef8
6 changed files with 2196 additions and 0 deletions
92
src/main.rs
Normal file
92
src/main.rs
Normal 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();
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue