mirror of
https://github.com/Dangoware/dmp-core.git
synced 2025-04-19 17:42:55 -05:00
Added preliminary Discord RPC support, updated configuration and error handling for music trackers. Closes #2
This commit is contained in:
parent
8fec560bcf
commit
c8f16f6b35
3 changed files with 168 additions and 45 deletions
|
@ -32,3 +32,4 @@ surf = "2.3.2"
|
||||||
futures = "0.3.28"
|
futures = "0.3.28"
|
||||||
rubato = "0.12.0"
|
rubato = "0.12.0"
|
||||||
arrayvec = "0.7.4"
|
arrayvec = "0.7.4"
|
||||||
|
discord-presence = "0.5.18"
|
||||||
|
|
|
@ -4,23 +4,43 @@ use std::fs;
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::music_tracker::music_tracker::LastFM;
|
use crate::music_tracker::music_tracker::{LastFM, LastFMConfig, DiscordRPCConfig};
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
pub db_path: Box<PathBuf>,
|
pub db_path: Box<PathBuf>,
|
||||||
pub lastfm: Option<LastFM>,
|
pub lastfm: Option<LastFMConfig>,
|
||||||
|
pub discord: Option<DiscordRPCConfig>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Config {
|
||||||
|
fn default() -> Self {
|
||||||
|
let path = PathBuf::from("./music_database.db3");
|
||||||
|
|
||||||
|
return Config {
|
||||||
|
db_path: Box::new(path),
|
||||||
|
|
||||||
|
lastfm: Some (LastFMConfig {
|
||||||
|
enabled: true,
|
||||||
|
dango_api_key: String::from("29a071e3113ab8ed36f069a2d3e20593"),
|
||||||
|
auth_token: None,
|
||||||
|
shared_secret: Some(String::from("5400c554430de5c5002d5e4bcc295b3d")),
|
||||||
|
session_key: None,
|
||||||
|
}),
|
||||||
|
|
||||||
|
discord: Some(DiscordRPCConfig {
|
||||||
|
enabled: true,
|
||||||
|
dango_client_id: 1144475145864499240,
|
||||||
|
dango_icon: String::from("flat"),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Config {
|
impl Config {
|
||||||
// Creates and saves a new config with default values
|
// Creates and saves a new config with default values
|
||||||
pub fn new(config_file: &PathBuf) -> std::io::Result<Config> {
|
pub fn new(config_file: &PathBuf) -> std::io::Result<Config> {
|
||||||
let path = PathBuf::from("./music_database.db3");
|
let config = Config::default();
|
||||||
|
|
||||||
let config = Config {
|
|
||||||
db_path: Box::new(path),
|
|
||||||
lastfm: None,
|
|
||||||
};
|
|
||||||
config.save(config_file)?;
|
config.save(config_file)?;
|
||||||
|
|
||||||
Ok(config)
|
Ok(config)
|
||||||
|
|
|
@ -2,35 +2,72 @@ use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use serde::{Serialize, Deserialize};
|
use serde::{Serialize, Deserialize, Serializer};
|
||||||
use md5::{Md5, Digest};
|
use md5::{Md5, Digest};
|
||||||
|
use discord_presence::{ Event, DiscordError};
|
||||||
|
use surf::StatusCode;
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait MusicTracker {
|
pub trait MusicTracker {
|
||||||
/// Adds one listen to a song halfway through playback
|
/// Adds one listen to a song halfway through playback
|
||||||
async fn track_song(&self, song: &String) -> Result<(), surf::Error>;
|
async fn track_song(&mut self, song: &String) -> Result<(), TrackerError>;
|
||||||
|
|
||||||
/// Adds a 'listening' status to the music tracker service of choice
|
/// Adds a 'listening' status to the music tracker service of choice
|
||||||
async fn track_now(&self, song: &String) -> Result<(), surf::Error>;
|
async fn track_now(&mut self, song: &String) -> Result<(), TrackerError>;
|
||||||
|
|
||||||
/// Reads config files, and attempts authentication with service
|
/// Reads config files, and attempts authentication with service
|
||||||
async fn test_tracker(&self) -> Result<(), surf::Error>;
|
async fn test_tracker(&mut self) -> Result<(), TrackerError>;
|
||||||
|
|
||||||
/// Returns plays for a given song according to tracker service
|
/// Returns plays for a given song according to tracker service
|
||||||
async fn get_times_tracked(&self, song: &String) -> Result<u32, surf::Error>;
|
async fn get_times_tracked(&mut self, song: &String) -> Result<u32, TrackerError>;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum TrackerError {
|
||||||
|
/// Tracker does not accept the song's format/content
|
||||||
|
InvalidSong,
|
||||||
|
/// Tracker requires authentication
|
||||||
|
InvalidAuth,
|
||||||
|
/// Tracker request was malformed
|
||||||
|
InvalidRequest,
|
||||||
|
/// Tracker is unavailable
|
||||||
|
ServiceUnavailable,
|
||||||
|
/// Unknown tracker error
|
||||||
|
Unknown,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
impl TrackerError {
|
||||||
|
pub fn from_surf_error(error: surf::Error) -> TrackerError {
|
||||||
|
return match error.status() {
|
||||||
|
StatusCode::Forbidden => TrackerError::InvalidAuth,
|
||||||
|
StatusCode::Unauthorized => TrackerError::InvalidAuth,
|
||||||
|
StatusCode::NetworkAuthenticationRequired => TrackerError::InvalidAuth,
|
||||||
|
StatusCode::BadRequest => TrackerError::InvalidRequest,
|
||||||
|
StatusCode::BadGateway => TrackerError::ServiceUnavailable,
|
||||||
|
StatusCode::ServiceUnavailable => TrackerError::ServiceUnavailable,
|
||||||
|
StatusCode::NotFound => TrackerError::ServiceUnavailable,
|
||||||
|
_ => TrackerError::Unknown,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone)]
|
||||||
|
pub struct LastFMConfig {
|
||||||
|
pub enabled: bool,
|
||||||
|
pub dango_api_key: String,
|
||||||
|
pub auth_token: Option<String>,
|
||||||
|
pub shared_secret: Option<String>,
|
||||||
|
pub session_key: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
|
||||||
pub struct LastFM {
|
pub struct LastFM {
|
||||||
dango_api_key: String,
|
config: LastFMConfig
|
||||||
auth_token: Option<String>,
|
|
||||||
shared_secret: Option<String>,
|
|
||||||
session_key: Option<String>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl MusicTracker for LastFM {
|
impl MusicTracker for LastFM {
|
||||||
async fn track_song(&self, song: &String) -> Result<(), surf::Error> {
|
async fn track_song(&mut self, song: &String) -> Result<(), TrackerError> {
|
||||||
let mut params: BTreeMap<&str, &str> = BTreeMap::new();
|
let mut params: BTreeMap<&str, &str> = BTreeMap::new();
|
||||||
|
|
||||||
// Sets timestamp of song beginning play time
|
// Sets timestamp of song beginning play time
|
||||||
|
@ -41,27 +78,35 @@ impl MusicTracker for LastFM {
|
||||||
params.insert("track", "A Happy Death - Again");
|
params.insert("track", "A Happy Death - Again");
|
||||||
params.insert("timestamp", &string_timestamp);
|
params.insert("timestamp", &string_timestamp);
|
||||||
|
|
||||||
self.api_request(params).await?;
|
return match self.api_request(params).await {
|
||||||
Ok(())
|
Ok(_) => Ok(()),
|
||||||
|
Err(err) => Err(TrackerError::from_surf_error(err)),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn track_now(&self, song: &String) -> Result<(), surf::Error> {
|
async fn track_now(&mut self, song: &String) -> Result<(), TrackerError> {
|
||||||
let mut params: BTreeMap<&str, &str> = BTreeMap::new();
|
let mut params: BTreeMap<&str, &str> = BTreeMap::new();
|
||||||
params.insert("method", "track.updateNowPlaying");
|
params.insert("method", "track.updateNowPlaying");
|
||||||
params.insert("artist", "Kikuo");
|
params.insert("artist", "Kikuo");
|
||||||
params.insert("track", "A Happy Death - Again");
|
params.insert("track", "A Happy Death - Again");
|
||||||
self.api_request(params).await?;
|
|
||||||
Ok(())
|
return match self.api_request(params).await {
|
||||||
|
Ok(_) => Ok(()),
|
||||||
|
Err(err) => Err(TrackerError::from_surf_error(err)),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn test_tracker(&self) -> Result<(), surf::Error> {
|
async fn test_tracker(&mut self) -> Result<(), TrackerError> {
|
||||||
let mut params: BTreeMap<&str, &str> = BTreeMap::new();
|
let mut params: BTreeMap<&str, &str> = BTreeMap::new();
|
||||||
params.insert("method", "chart.getTopArtists");
|
params.insert("method", "chart.getTopArtists");
|
||||||
self.api_request(params).await?;
|
|
||||||
Ok(())
|
return match self.api_request(params).await {
|
||||||
|
Ok(_) => Ok(()),
|
||||||
|
Err(err) => Err(TrackerError::from_surf_error(err)),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_times_tracked(&self, song: &String) -> Result<u32, surf::Error> {
|
async fn get_times_tracked(&mut self, song: &String) -> Result<u32, TrackerError> {
|
||||||
todo!();
|
todo!();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -87,11 +132,11 @@ impl LastFM {
|
||||||
// Returns a url to be accessed by the user
|
// Returns a url to be accessed by the user
|
||||||
pub async fn get_auth_url(&mut self) -> Result<String, surf::Error> {
|
pub async fn get_auth_url(&mut self) -> Result<String, surf::Error> {
|
||||||
let method = String::from("auth.gettoken");
|
let method = String::from("auth.gettoken");
|
||||||
let api_key = self.dango_api_key.clone();
|
let api_key = self.config.dango_api_key.clone();
|
||||||
let api_request_url = format!("http://ws.audioscrobbler.com/2.0/?method={method}&api_key={api_key}&format=json");
|
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?;
|
let auth_token: AuthToken = surf::get(api_request_url).await?.body_json().await?;
|
||||||
self.auth_token = Some(auth_token.token.clone());
|
self.config.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);
|
let auth_url = format!("http://www.last.fm/api/auth/?api_key={api_key}&token={}", auth_token.token);
|
||||||
|
|
||||||
|
@ -100,9 +145,9 @@ impl LastFM {
|
||||||
|
|
||||||
pub async fn set_session(&mut self) {
|
pub async fn set_session(&mut self) {
|
||||||
let method = String::from("auth.getSession");
|
let method = String::from("auth.getSession");
|
||||||
let api_key = self.dango_api_key.clone();
|
let api_key = self.config.dango_api_key.clone();
|
||||||
let auth_token = self.auth_token.clone().unwrap();
|
let auth_token = self.config.auth_token.clone().unwrap();
|
||||||
let shared_secret = self.shared_secret.clone().unwrap();
|
let shared_secret = self.config.shared_secret.clone().unwrap();
|
||||||
|
|
||||||
// Creates api_sig as defined in last.fm documentation
|
// Creates api_sig as defined in last.fm documentation
|
||||||
let api_sig = format!("api_key{api_key}methodauth.getSessiontoken{auth_token}{shared_secret}");
|
let api_sig = format!("api_key{api_key}methodauth.getSessiontoken{auth_token}{shared_secret}");
|
||||||
|
@ -119,33 +164,29 @@ impl LastFM {
|
||||||
|
|
||||||
// Sets session key from received response
|
// Sets session key from received response
|
||||||
let session_response: Session = serde_json::from_str(&response).unwrap();
|
let session_response: Session = serde_json::from_str(&response).unwrap();
|
||||||
self.session_key = Some(session_response.session.key.clone());
|
self.config.session_key = Some(session_response.session.key.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Creates a new LastFM struct
|
// Creates a new LastFM struct
|
||||||
pub fn new() -> LastFM {
|
pub fn new(config: &LastFMConfig) -> LastFM {
|
||||||
let last_fm = LastFM {
|
let last_fm = LastFM {
|
||||||
// Grab this from config in future
|
config: config.clone()
|
||||||
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;
|
return last_fm;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Creates an api request with the given parameters
|
// Creates an api request with the given parameters
|
||||||
pub async fn api_request(&self, mut params: BTreeMap<&str, &str>) -> Result<surf::Response, surf::Error> {
|
pub async fn api_request(&self, mut params: BTreeMap<&str, &str>) -> Result<surf::Response, surf::Error> {
|
||||||
params.insert("api_key", &self.dango_api_key);
|
params.insert("api_key", &self.config.dango_api_key);
|
||||||
params.insert("sk", &self.session_key.as_ref().unwrap());
|
params.insert("sk", &self.config.session_key.as_ref().unwrap());
|
||||||
|
|
||||||
// Creates and sets api call signature
|
// Creates and sets api call signature
|
||||||
let api_sig = LastFM::request_sig(¶ms, &self.shared_secret.as_ref().unwrap());
|
let api_sig = LastFM::request_sig(¶ms, &self.config.shared_secret.as_ref().unwrap());
|
||||||
params.insert("api_sig", &api_sig);
|
params.insert("api_sig", &api_sig);
|
||||||
let mut string_params = String::from("");
|
let mut string_params = String::from("");
|
||||||
|
|
||||||
// Creates method call string
|
// Creates method call string
|
||||||
|
// Just iterate over values???
|
||||||
for key in params.keys() {
|
for key in params.keys() {
|
||||||
let param_value = params.get(key).unwrap();
|
let param_value = params.get(key).unwrap();
|
||||||
string_params.push_str(&format!("{key}={param_value}&"));
|
string_params.push_str(&format!("{key}={param_value}&"));
|
||||||
|
@ -183,4 +224,65 @@ impl LastFM {
|
||||||
pub fn reset_account() {
|
pub fn reset_account() {
|
||||||
todo!();
|
todo!();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone)]
|
||||||
|
pub struct DiscordRPCConfig {
|
||||||
|
pub enabled: bool,
|
||||||
|
pub dango_client_id: u64,
|
||||||
|
pub dango_icon: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct DiscordRPC {
|
||||||
|
config: DiscordRPCConfig,
|
||||||
|
pub client: discord_presence::client::Client
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DiscordRPC {
|
||||||
|
pub fn new(config: &DiscordRPCConfig) -> Self {
|
||||||
|
let rpc = DiscordRPC {
|
||||||
|
client: discord_presence::client::Client::new(config.dango_client_id),
|
||||||
|
config: config.clone(),
|
||||||
|
};
|
||||||
|
return rpc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl MusicTracker for DiscordRPC {
|
||||||
|
async fn track_now(&mut self, song: &String) -> Result<(), TrackerError> {
|
||||||
|
let _client_thread = self.client.start();
|
||||||
|
|
||||||
|
// Blocks thread execution until it has connected to local discord client
|
||||||
|
let ready = self.client.block_until_event(Event::Ready);
|
||||||
|
if ready.is_err() {
|
||||||
|
return Err(TrackerError::ServiceUnavailable);
|
||||||
|
}
|
||||||
|
|
||||||
|
let start_time = std::time::SystemTime::now().duration_since(UNIX_EPOCH).unwrap_or_default().as_millis() as u64;
|
||||||
|
// Sets discord account activity to current playing song
|
||||||
|
let send_activity = self.client.set_activity(|activity| {
|
||||||
|
activity
|
||||||
|
.state(song)
|
||||||
|
.assets(|assets| assets.large_image(&self.config.dango_icon))
|
||||||
|
.timestamps(|time| time.start(start_time))
|
||||||
|
});
|
||||||
|
|
||||||
|
match send_activity {
|
||||||
|
Ok(_) => return Ok(()),
|
||||||
|
Err(_) => return Err(TrackerError::ServiceUnavailable),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn track_song(&mut self, song: &String) -> Result<(), TrackerError> {
|
||||||
|
return Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn test_tracker(&mut self) -> Result<(), TrackerError> {
|
||||||
|
return Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_times_tracked(&mut self, song: &String) -> Result<u32, TrackerError> {
|
||||||
|
return Ok(0);
|
||||||
|
}
|
||||||
}
|
}
|
Loading…
Reference in a new issue