Created
October 17, 2024 08:37
-
-
Save AregevDev/e41438963145708c51d804c4999ffd46 to your computer and use it in GitHub Desktop.
Spotify Playlist Server
This file contains hidden or 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 = "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 |
This file contains hidden or 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
| <!-- 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> |
This file contains hidden or 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 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