Skip to content

Instantly share code, notes, and snippets.

@AregevDev
Created October 17, 2024 08:37
Show Gist options
  • Select an option

  • Save AregevDev/e41438963145708c51d804c4999ffd46 to your computer and use it in GitHub Desktop.

Select an option

Save AregevDev/e41438963145708c51d804c4999ffd46 to your computer and use it in GitHub Desktop.
Spotify Playlist Server
[package]
name = "test_spotify"
version = "0.1.0"
edition = "2021"
[dependencies]
tokio = { version = "1.40.0", features = ["full"] }
tokio-util = { version = "0.7.12" , features = ["full"] }
tokio-cron-scheduler = { version = "0.13.0", features = ["signal"] }
spotify-rs = "0.3.14"
chrono = { version = "0.4.38" }
chrono-tz = "0.10.0"
warp = "0.3.7"
log = "0.4.22"
simple_logger = "5.0.0"
static_dir = "0.2.0"
[profile.release]
lto = true
panic = "abort"
opt-level = "s"
strip = true
<!-- Place in the static directory -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body style="background: black">
<h1 style="color: aquamarine">Auth successful</h1>
<p style="color: red">You can close this window now.</p>
</body>
</html>
use chrono::{NaiveDate, NaiveDateTime, NaiveTime, TimeZone, Utc};
use chrono_tz::Asia::Jerusalem;
use log::{error, info, warn, Level};
use spotify_rs::auth::{NoVerifier, Token};
use spotify_rs::client::Client;
use spotify_rs::{AuthCodePkceClient, AuthCodePkceFlow, RedirectUrl, SpotifyResult};
use static_dir::static_dir;
use std::collections::HashMap;
use std::env;
use std::error::Error;
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use std::sync::{mpsc, Arc};
use tokio::sync::Mutex;
use tokio::task::JoinSet;
use tokio_cron_scheduler::{JobBuilder, JobScheduler};
use tokio_util::sync::CancellationToken;
use warp::Filter;
const PLAYLIST_URI: &str = "4bqII0JQZllfclO6bplxWN";
const SCOPES: [&str; 2] = ["playlist-modify-public", "playlist-modify-private"];
async fn change_title(
spotify: Arc<Mutex<Client<Token, AuthCodePkceFlow, NoVerifier>>>,
) -> SpotifyResult<Result<String, ()>> {
let mut spotify = spotify.lock().await;
let now = Jerusalem
.from_utc_datetime(&Utc::now().naive_utc())
.date_naive();
let release_date = Jerusalem
.from_utc_datetime(&NaiveDateTime::new(
NaiveDate::from_ymd_opt(2024, 10, 15).unwrap(),
NaiveTime::default(),
))
.date_naive();
let days = release_date - now;
let days = days.num_days();
if days < 0 {
spotify
.change_playlist_details(PLAYLIST_URI)
.name("פלייליסט שחרור")
.send()
.await?;
return Ok(Err(()));
}
let change = match days {
0 => "פלייליסט שחרור (היום)".to_string(),
_ => format!("פלייליסט שחרור (עוד {} ימים)", days),
};
info!("Changing playlist title. (Days remaining {})", days);
spotify
.change_playlist_details(PLAYLIST_URI)
.name(&change)
.send()
.await
.map(|_| Ok(change))
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
simple_logger::init_with_level(Level::Info).expect("Failed to initialize logger.");
info!("Welcome to my Spotify program");
let mut joins = JoinSet::new();
let addr = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1));
let port = 6009;
let client_id = env::var("CLIENT_ID").expect("No client ID found.");
let redirect_url = RedirectUrl::from_url(
format!("http://{}:{}/callback", addr, port)
.parse()
.unwrap(), // Safe, SERVER_ADDRESS is valid
);
let auth_code_flow = AuthCodePkceFlow::new(client_id, SCOPES);
let (client, url) = AuthCodePkceClient::new(auth_code_flow, redirect_url, true);
let (auth_tx, auth_rx) = mpsc::channel();
info!("Waiting for authorization");
info!("{}", url);
let cancel = CancellationToken::new();
let s_cancel = cancel.clone();
let j_cancel = cancel.clone();
let c_cancel = cancel.clone();
joins.spawn(async move {
tokio::signal::ctrl_c().await.unwrap();
c_cancel.cancel();
});
let local = warp::path!("callback")
.and(warp::query::query::<HashMap<String, String>>())
.and(static_dir!("static"))
.map(move |params: HashMap<String, String>, s| {
let tx = auth_tx.clone();
// Safe, from Spotify redirect URL
let code = params.get("code").unwrap().clone();
let state = params.get("state").unwrap().clone();
tx.send((code, state))
.expect("Failed to send authorization code, aborting.");
s
});
let (_, server) = warp::serve(local).bind_with_graceful_shutdown(
SocketAddr::new(addr, port), // Safe, SERVER_ADDRESS is valid
async move {
s_cancel.cancelled().await;
warn!("Server shut down.");
},
);
joins.spawn(server);
let (code, state) = auth_rx.recv()?;
let spotify = Arc::new(Mutex::new(
client
.authenticate(code, state)
.await
.expect("Could not authorize"),
));
info!("Authorization Successful");
info!("Manually refreshing days left.");
let new = change_title(Arc::clone(&spotify)).await;
match new {
Ok(Err(_)) => {
info!("There are 0 days left, shutting down.");
cancel.cancel();
}
Ok(Ok(change)) => info!(
"Playlist title changed successfully! (Changed to \"{}\")",
change
),
Err(e) => error!("Failed to change playlist title: {}", e),
}
let mut sched = JobScheduler::new().await?;
sched.set_shutdown_handler(Box::new(move || {
Box::pin({
{
let value = j_cancel.clone();
async move {
warn!("Scheduler shut down.");
value.cancel();
}
}
})
}));
sched.shutdown_on_ctrl_c();
let job = JobBuilder::new()
.with_timezone(Jerusalem)
.with_cron_job_type()
.with_schedule("0 0 0 * * *")?
.with_run_async(Box::new(move |_, mut l| {
Box::pin({
let spotify = Arc::clone(&spotify);
async move {
let new = change_title(spotify).await;
match new {
Ok(Err(_)) => {
info!("There are 0 days left, shutting down.");
l.shutdown()
.await
.expect("Failed to shut down scheduler, aborting.");
}
Ok(Ok(change)) => info!(
"Playlist title changed successfully! (Changed to \"{}\")",
change
),
Err(e) => error!("Failed to change playlist title: {}", e),
}
}
})
}))
.build()?;
info!("Job scheduled successfully.");
sched.add(job).await?;
let f = async move {
sched
.start()
.await
.expect("Failed to start job scheduler, aborting.");
};
joins.spawn(f);
cancel.cancelled().await;
joins.shutdown().await;
info!("All tasks finished, exiting.");
Ok(())
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment