Initial commit
This commit is contained in:
commit
3818e91ef8
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
/target
|
1999
Cargo.lock
generated
Normal file
1999
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
14
Cargo.toml
Normal file
14
Cargo.toml
Normal file
|
@ -0,0 +1,14 @@
|
|||
[package]
|
||||
name = "spotify-bpm-filter"
|
||||
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.13", features = ["cli"] }
|
||||
tokio = { version = "1.38", features = ["full"] }
|
||||
|
||||
futures = "*"
|
||||
futures-util = "*"
|
61
flake.lock
Normal file
61
flake.lock
Normal file
|
@ -0,0 +1,61 @@
|
|||
{
|
||||
"nodes": {
|
||||
"flake-utils": {
|
||||
"inputs": {
|
||||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1710146030,
|
||||
"narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1718208800,
|
||||
"narHash": "sha256-US1tAChvPxT52RV8GksWZS415tTS7PV42KTc2PNDBmc=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "cc54fb41d13736e92229c21627ea4f22199fee6b",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixos-24.05",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils",
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
},
|
||||
"systems": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
29
flake.nix
Normal file
29
flake.nix
Normal file
|
@ -0,0 +1,29 @@
|
|||
{
|
||||
inputs = {
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.05";
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
};
|
||||
|
||||
outputs = { self, nixpkgs, flake-utils, ... }:
|
||||
flake-utils.lib.eachDefaultSystem (system:
|
||||
let
|
||||
pkgs = import nixpkgs { inherit system; };
|
||||
in
|
||||
{
|
||||
devShells.default = pkgs.mkShell {
|
||||
buildInputs = with pkgs; [
|
||||
cargo
|
||||
clippy
|
||||
rustPackages.clippy
|
||||
rustc
|
||||
rustfmt
|
||||
rust-analyzer
|
||||
|
||||
openssl
|
||||
pkg-config
|
||||
];
|
||||
|
||||
RUST_SRC_PATH = pkgs.rustPlatform.rustLibSrc;
|
||||
};
|
||||
});
|
||||
}
|
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…
Reference in a new issue