use std::time::{SystemTime, UNIX_EPOCH}; use std::collections::BTreeMap; use async_trait::async_trait; use serde::{Serialize, Deserialize}; use md5::{Md5, Digest}; #[async_trait] pub trait MusicTracker { /// Adds one listen to a song halfway through playback async fn track_song(&self, song: &String) -> Result<(), surf::Error>; /// Adds a 'listening' status to the music tracker service of choice async fn track_now(&self, song: &String) -> Result<(), surf::Error>; /// Reads config files, and attempts authentication with service async fn test_tracker(&self) -> Result<(), surf::Error>; /// Returns plays for a given song according to tracker service async fn get_times_tracked(&self, song: &String) -> Result; } #[derive(Serialize, Deserialize)] pub struct LastFM { dango_api_key: String, auth_token: Option, shared_secret: Option, session_key: Option, } #[async_trait] impl MusicTracker for LastFM { async fn track_song(&self, song: &String) -> Result<(), surf::Error> { let mut params: BTreeMap<&str, &str> = BTreeMap::new(); // Sets timestamp of song beginning play time let timestamp = SystemTime::now().duration_since(UNIX_EPOCH).expect("Your time is off.").as_secs() - 30; let string_timestamp = timestamp.to_string(); params.insert("method", "track.scrobble"); params.insert("artist", "Kikuo"); params.insert("track", "A Happy Death - Again"); params.insert("timestamp", &string_timestamp); self.api_request(params).await?; Ok(()) } async fn track_now(&self, song: &String) -> Result<(), surf::Error> { let mut params: BTreeMap<&str, &str> = BTreeMap::new(); params.insert("method", "track.updateNowPlaying"); params.insert("artist", "Kikuo"); params.insert("track", "A Happy Death - Again"); self.api_request(params).await?; Ok(()) } async fn test_tracker(&self) -> Result<(), surf::Error> { let mut params: BTreeMap<&str, &str> = BTreeMap::new(); params.insert("method", "chart.getTopArtists"); self.api_request(params).await?; Ok(()) } async fn get_times_tracked(&self, song: &String) -> Result { todo!(); } } #[derive(Deserialize, Serialize)] struct AuthToken { token: String } #[derive(Deserialize, Serialize, Debug)] struct SessionResponse { name: String, key: String, subscriber: i32, } #[derive(Deserialize, Serialize, Debug)] struct Session { session: SessionResponse } impl LastFM { // Returns a url to be accessed by the user pub async fn get_auth_url(&mut self) -> Result { let method = String::from("auth.gettoken"); let api_key = self.dango_api_key.clone(); let api_request_url = format!("http://ws.audioscrobbler.com/2.0/?method={method}&api_key={api_key}&format=json"); let auth_token: AuthToken = surf::get(api_request_url).await?.body_json().await?; self.auth_token = Some(auth_token.token.clone()); let auth_url = format!("http://www.last.fm/api/auth/?api_key={api_key}&token={}", auth_token.token); return Ok(auth_url); } pub async fn set_session(&mut self) { let method = String::from("auth.getSession"); let api_key = self.dango_api_key.clone(); let auth_token = self.auth_token.clone().unwrap(); let shared_secret = self.shared_secret.clone().unwrap(); // Creates api_sig as defined in last.fm documentation let api_sig = format!("api_key{api_key}methodauth.getSessiontoken{auth_token}{shared_secret}"); // Creates insecure MD5 hash for last.fm api sig let mut hasher = Md5::new(); hasher.update(api_sig); let hash_result = hasher.finalize(); let hex_string_hash = format!("{:#02x}", hash_result); let api_request_url = format!("http://ws.audioscrobbler.com/2.0/?method={method}&api_key={api_key}&token={auth_token}&api_sig={hex_string_hash}&format=json"); let response = surf::get(api_request_url).recv_string().await.unwrap(); // Sets session key from received response let session_response: Session = serde_json::from_str(&response).unwrap(); self.session_key = Some(session_response.session.key.clone()); } // Creates a new LastFM struct pub fn new() -> LastFM { let last_fm = LastFM { // Grab this from config in future dango_api_key: String::from("29a071e3113ab8ed36f069a2d3e20593"), auth_token: None, // Also grab from config in future shared_secret: Some(String::from("5400c554430de5c5002d5e4bcc295b3d")), session_key: None, }; return last_fm; } // Creates an api request with the given parameters pub async fn api_request(&self, mut params: BTreeMap<&str, &str>) -> Result { params.insert("api_key", &self.dango_api_key); params.insert("sk", &self.session_key.as_ref().unwrap()); // Creates and sets api call signature let api_sig = LastFM::request_sig(¶ms, &self.shared_secret.as_ref().unwrap()); params.insert("api_sig", &api_sig); let mut string_params = String::from(""); // Creates method call string for key in params.keys() { let param_value = params.get(key).unwrap(); string_params.push_str(&format!("{key}={param_value}&")); } string_params.pop(); let url = "http://ws.audioscrobbler.com/2.0/"; let response = surf::post(url).body_string(string_params).await; return response; } // Returns an api signature as defined in the last.fm api documentation fn request_sig(params: &BTreeMap<&str, &str>, shared_secret: &str) -> String { let mut sig_string = String::new(); // Appends keys and values of parameters to the unhashed sig for key in params.keys() { let param_value = params.get(*key); sig_string.push_str(&format!("{key}{}", param_value.unwrap())); } sig_string.push_str(shared_secret); // Hashes signature using **INSECURE** MD5 (Required by last.fm api) let mut md5_hasher = Md5::new(); md5_hasher.update(sig_string); let hash_result = md5_hasher.finalize(); let hashed_sig = format!("{:#02x}", hash_result); return hashed_sig; } // Removes last.fm account from dango-music-player pub fn reset_account() { todo!(); } }