From 14edbe7c465af3d6e64a477f8f1d28b08239ba01 Mon Sep 17 00:00:00 2001 From: G2-Games Date: Fri, 19 Jan 2024 18:22:36 -0600 Subject: [PATCH] Refactored music_player, integrated music_tracker into it. Closes #7 --- src/music_controller/config.rs | 10 +- src/music_controller/music_controller.rs | 33 +- src/music_player/music_player.rs | 443 +++++++++++++++-------- src/music_processor/music_processor.rs | 8 + src/music_storage/music_db.rs | 31 +- src/music_tracker/music_tracker.rs | 83 +++-- 6 files changed, 378 insertions(+), 230 deletions(-) diff --git a/src/music_controller/config.rs b/src/music_controller/config.rs index d018088..c77a02a 100644 --- a/src/music_controller/config.rs +++ b/src/music_controller/config.rs @@ -6,7 +6,7 @@ use serde::{Deserialize, Serialize}; use crate::music_tracker::music_tracker::{LastFM, LastFMConfig, DiscordRPCConfig}; -#[derive(Serialize, Deserialize)] +#[derive(Serialize, Deserialize, PartialEq, Eq)] pub struct Config { pub db_path: Box, pub lastfm: Option, @@ -20,13 +20,7 @@ impl Default for Config { 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, - }), + lastfm: None, discord: Some(DiscordRPCConfig { enabled: true, diff --git a/src/music_controller/music_controller.rs b/src/music_controller/music_controller.rs index bb1ab66..29f4fb7 100644 --- a/src/music_controller/music_controller.rs +++ b/src/music_controller/music_controller.rs @@ -1,22 +1,23 @@ use std::path::PathBuf; +use std::sync::{RwLock, Arc, Mutex}; use rusqlite::Result; use crate::music_controller::config::Config; -use crate::music_player::music_player::{MusicPlayer, PlayerStatus, PlayerMessage, DSPMessage}; +use crate::music_player::music_player::{MusicPlayer, PlayerStatus, DecoderMessage, DSPMessage}; use crate::music_processor::music_processor::MusicProcessor; -use crate::music_storage::music_db::URI; +use crate::music_storage::music_db::{URI, Song}; pub struct MusicController { - pub config: Config, + pub config: Arc>, music_player: MusicPlayer, } impl MusicController { /// Creates new MusicController with config at given path pub fn new(config_path: &PathBuf) -> Result{ - let config = Config::new(config_path)?; - let music_player = MusicPlayer::new(); + let config = Arc::new(RwLock::new(Config::new(config_path)?)); + let music_player = MusicPlayer::new(config.clone()); let controller = MusicController { config, @@ -28,8 +29,8 @@ impl MusicController { /// Creates new music controller from a config at given path pub fn from(config_path: &PathBuf) -> std::result::Result { - let config = Config::from(config_path)?; - let music_player = MusicPlayer::new(); + let config = Arc::new(RwLock::new(Config::from(config_path)?)); + let music_player = MusicPlayer::new(config.clone()); let controller = MusicController { config, @@ -39,13 +40,8 @@ impl MusicController { return Ok(controller) } - /// Opens and plays song at given URI - pub fn open_song(&mut self, uri: &URI) { - self.music_player.open_song(uri); - } - - /// Sends given message to music player - pub fn song_control(&mut self, message: PlayerMessage) { + /// Sends given message to control music player + pub fn song_control(&mut self, message: DecoderMessage) { self.music_player.send_message(message); } @@ -54,15 +50,20 @@ impl MusicController { return self.music_player.get_status(); } + /// Gets current song being controlled, if any + pub fn get_current_song(&self) -> Option { + return self.music_player.get_current_song(); + } + /// Gets audio playback volume - pub fn get_vol(&mut self) -> f32 { + pub fn get_vol(&self) -> f32 { return self.music_player.music_processor.audio_volume; } /// Sets audio playback volume on a scale of 0.0 to 1.0 pub fn set_vol(&mut self, volume: f32) { self.music_player.music_processor.audio_volume = volume; - self.song_control(PlayerMessage::DSP(DSPMessage::UpdateProcessor(Box::new(self.music_player.music_processor.clone())))); + self.song_control(DecoderMessage::DSP(DSPMessage::UpdateProcessor(Box::new(self.music_player.music_processor.clone())))); } } diff --git a/src/music_player/music_player.rs b/src/music_player/music_player.rs index 1996240..2944788 100644 --- a/src/music_player/music_player.rs +++ b/src/music_player/music_player.rs @@ -1,41 +1,50 @@ use std::sync::mpsc::{self, Sender, Receiver}; +use std::sync::{Arc, RwLock}; use std::thread; use std::io::SeekFrom; use async_std::io::ReadExt; use async_std::task; +use futures::future::join_all; use symphonia::core::codecs::{CODEC_TYPE_NULL, DecoderOptions, Decoder}; use symphonia::core::formats::{FormatOptions, FormatReader, SeekMode, SeekTo}; use symphonia::core::io::{MediaSourceStream, MediaSource}; use symphonia::core::meta::MetadataOptions; use symphonia::core::probe::Hint; use symphonia::core::errors::Error; -use symphonia::core::units::Time; +use symphonia::core::units::{Time, TimeBase}; use futures::AsyncBufRead; +use crate::music_controller::config::Config; use crate::music_player::music_output::AudioStream; use crate::music_processor::music_processor::MusicProcessor; -use crate::music_storage::music_db::URI; +use crate::music_storage::music_db::{URI, Song}; +use crate::music_tracker::music_tracker::{MusicTracker, LastFM, DiscordRPCConfig, DiscordRPC, TrackerError}; // Struct that controls playback of music pub struct MusicPlayer { pub music_processor: MusicProcessor, player_status: PlayerStatus, - message_sender: Option>, - status_receiver: Option>, + music_trackers: Vec>, + current_song: Arc>>, + message_sender: Sender, + status_receiver: Receiver, + config: Arc>, } -#[derive(Clone, Copy)] +#[derive(Clone, Copy, Debug)] pub enum PlayerStatus { - Playing, + Playing(f64), Paused, Stopped, Error, } -pub enum PlayerMessage { +#[derive(Debug, Clone)] +pub enum DecoderMessage { + OpenSong(Song), Play, Pause, Stop, @@ -43,143 +52,43 @@ pub enum PlayerMessage { DSP(DSPMessage) } +#[derive(Clone)] +pub enum TrackerMessage { + Track(Song), + TrackNow(Song) +} + +#[derive(Debug, Clone)] pub enum DSPMessage { UpdateProcessor(Box) } -impl MusicPlayer { - pub fn new() -> Self { - MusicPlayer { - music_processor: MusicProcessor::new(), - player_status: PlayerStatus::Stopped, - message_sender: None, - status_receiver: None, - } - } - - // Opens and plays song with given path in separate thread - pub fn open_song(&mut self, uri: &URI) { - // Creates mpsc channels to communicate with thread - let (message_sender, message_receiver) = mpsc::channel(); - let (status_sender, status_receiver) = mpsc::channel(); - self.message_sender = Some(message_sender); - self.status_receiver = Some(status_receiver); - - let owned_uri = uri.clone(); +// Holds a song decoder reader, etc +struct SongHandler { + pub reader: Box, + pub decoder: Box, + pub time_base: Option, + pub duration: Option, +} - // Creates thread that audio is decoded in - thread::spawn(move || { - let (mut reader, mut decoder) = MusicPlayer::get_reader_and_dec(&owned_uri); - - let mut seek_time: Option = None; - - let mut audio_output: Option> = None; - - let mut music_processor = MusicProcessor::new(); - - 'main_decode: loop { - // Handles message received from the MusicPlayer if there is one // TODO: Refactor - let received_message = message_receiver.try_recv(); - match received_message { - Ok(PlayerMessage::Pause) => { - status_sender.send(PlayerStatus::Paused).unwrap(); - // Loops on a blocking message receiver to wait for a play/stop message - 'inner_pause: loop { - let message = message_receiver.try_recv(); - match message { - Ok(PlayerMessage::Play) => { - status_sender.send(PlayerStatus::Playing).unwrap(); - break 'inner_pause - }, - Ok(PlayerMessage::Stop) => { - status_sender.send(PlayerStatus::Stopped).unwrap(); - break 'main_decode - }, - _ => {}, - } - } - }, - // Exits main decode loop and subsequently ends thread (?) - Ok(PlayerMessage::Stop) => { - status_sender.send(PlayerStatus::Stopped).unwrap(); - break 'main_decode - }, - Ok(PlayerMessage::SeekTo(time)) => seek_time = Some(time), - Ok(PlayerMessage::DSP(dsp_message)) => { - match dsp_message { - DSPMessage::UpdateProcessor(new_processor) => music_processor = *new_processor, - } - } - _ => {}, - } - - match seek_time { - Some(time) => { - let seek_to = SeekTo::Time { time: Time::from(time), track_id: Some(0) }; - reader.seek(SeekMode::Accurate, seek_to).unwrap(); - seek_time = None; - } - None => {} //Nothing to do! - } - - let packet = match reader.next_packet() { - Ok(packet) => packet, - Err(Error::ResetRequired) => panic!(), //TODO, - Err(err) => { - //Unrecoverable? - panic!("{}", err); - } - }; - - match decoder.decode(&packet) { - Ok(decoded) => { - // Opens audio stream if there is not one - if audio_output.is_none() { - let spec = *decoded.spec(); - let duration = decoded.capacity() as u64; - - audio_output.replace(crate::music_player::music_output::open_stream(spec, duration).unwrap()); - } - - // Handles audio normally provided there is an audio stream - if let Some(ref mut audio_output) = audio_output { - // Changes buffer of the MusicProcessor if the packet has a differing capacity or spec - if music_processor.audio_buffer.capacity() != decoded.capacity() ||music_processor.audio_buffer.spec() != decoded.spec() { - let spec = *decoded.spec(); - let duration = decoded.capacity() as u64; - - music_processor.set_buffer(duration, spec); - } - - let transformed_audio = music_processor.process(&decoded); - - // Writes transformed packet to audio out - audio_output.write(transformed_audio).unwrap() - } - }, - Err(Error::IoError(_)) => { - // rest in peace packet - continue; - }, - Err(Error::DecodeError(_)) => { - // may you one day be decoded - continue; - }, - Err(err) => { - // Unrecoverable, though shouldn't panic here - panic!("{}", err); - } - } - } - }); - } - - fn get_reader_and_dec(uri: &URI) -> (Box, Box) { +// TODO: actual error handling here +impl SongHandler { + pub fn new(uri: &URI) -> Result { // Opens remote/local source and creates MediaSource for symphonia - let config = RemoteOptions { media_buffer_len: 10000, forward_buffer_len: 10000}; + let config = RemoteOptions {media_buffer_len: 10000, forward_buffer_len: 10000}; let src: Box = match uri { - URI::Local(path) => Box::new(std::fs::File::open(path).expect("Failed to open file")), - URI::Remote(_, location) => Box::new(RemoteSource::new(location.as_ref(), &config).unwrap()), + URI::Local(path) => { + match std::fs::File::open(path) { + Ok(file) => Box::new(file), + Err(_) => return Err(()), + } + }, + URI::Remote(_, location) => { + match RemoteSource::new(location.as_ref(), &config) { + Ok(remote_source) => Box::new(remote_source), + Err(_) => return Err(()), + } + }, }; let mss = MediaSourceStream::new(src, Default::default()); @@ -198,42 +107,262 @@ impl MusicPlayer { .iter() .find(|t| t.codec_params.codec != CODEC_TYPE_NULL) .expect("no supported audio tracks"); + + let time_base = track.codec_params.time_base; + let duration = track.codec_params.n_frames; let dec_opts: DecoderOptions = Default::default(); let mut decoder = symphonia::default::get_codecs().make(&track.codec_params, &dec_opts) .expect("unsupported codec"); - return (reader, decoder); + return Ok(SongHandler {reader, decoder, time_base, duration}); + } +} + +impl MusicPlayer { + pub fn new(config: Arc>) -> Self { + // Creates mpsc channels to communicate with music player threads + let (message_sender, message_receiver) = mpsc::channel(); + let (status_sender, status_receiver) = mpsc::channel(); + let current_song = Arc::new(RwLock::new(None)); + + MusicPlayer::start_player(message_receiver, status_sender, config.clone(), current_song.clone()); + + MusicPlayer { + music_processor: MusicProcessor::new(), + music_trackers: Vec::new(), + player_status: PlayerStatus::Stopped, + current_song, + message_sender, + status_receiver, + config, + } + } + + fn start_tracker(status_sender: Sender>, tracker_receiver: Receiver, config: Arc>) { + thread::spawn(move || { + let global_config = &*config.read().unwrap(); + // Sets local config for trackers to detect changes + let local_config = global_config.clone(); + let mut trackers: Vec> = Vec::new(); + // Updates local trackers to the music controller config + let update_trackers = |trackers: &mut Vec>|{ + if let Some(lastfm_config) = global_config.lastfm.clone() { + trackers.push(Box::new(LastFM::new(&lastfm_config))); + } + if let Some(discord_config) = global_config.discord.clone() { + trackers.push(Box::new(DiscordRPC::new(&discord_config))); + } + }; + update_trackers(&mut trackers); + loop { + if let message = tracker_receiver.recv() { + if local_config != global_config { + update_trackers(&mut trackers); + } + + let mut results = Vec::new(); + task::block_on(async { + let mut futures = Vec::new(); + for tracker in trackers.iter_mut() { + match message.clone() { + Ok(TrackerMessage::Track(song)) => futures.push(tracker.track_song(song)), + Ok(TrackerMessage::TrackNow(song)) => futures.push(tracker.track_now(song)), + Err(_) => {}, + } + } + results = join_all(futures).await; + }); + + for result in results { + status_sender.send(result).unwrap_or_default() + } + } + } + }); + } + + // Opens and plays song with given path in separate thread + fn start_player(message_receiver: Receiver, status_sender: Sender, config: Arc>, current_song: Arc>>) { + // Creates thread that audio is decoded in + thread::spawn(move || { + let current_song = current_song; + + let mut song_handler = None; + + let mut seek_time: Option = None; + + let mut audio_output: Option> = None; + + let mut music_processor = MusicProcessor::new(); + + let (tracker_sender, tracker_receiver): (Sender, Receiver) = mpsc::channel(); + let (tracker_status_sender, tracker_status_receiver): (Sender>, Receiver>) = mpsc::channel(); + + MusicPlayer::start_tracker(tracker_status_sender, tracker_receiver, config); + + let mut song_tracked = false; + let mut song_time = 0.0; + let mut paused = true; + 'main_decode: loop { + 'handle_message: loop { + let message = if paused { + // Pauses playback by blocking on waiting for new player messages + match message_receiver.recv() { + Ok(message) => Some(message), + Err(_) => None, + } + } else { + // Resumes playback by not blocking + match message_receiver.try_recv() { + Ok(message) => Some(message), + Err(_) => break 'handle_message, + } + }; + // Handles message received from MusicPlayer struct + match message { + Some(DecoderMessage::OpenSong(song)) => { + let song_uri = song.path.clone(); + match SongHandler::new(&song_uri) { + Ok(new_handler) => { + song_handler = Some(new_handler); + *current_song.write().unwrap() = Some(song); + paused = false; + song_tracked = false; + } + Err(_) => status_sender.send(PlayerStatus::Error).unwrap(), + } + } + Some(DecoderMessage::Play) => { + if song_handler.is_some() { + paused = false; + } + } + Some(DecoderMessage::Pause) => { + paused = true; + status_sender.send(PlayerStatus::Paused).unwrap(); + } + Some(DecoderMessage::SeekTo(time)) => seek_time = Some(time), + Some(DecoderMessage::DSP(dsp_message)) => { + match dsp_message { + DSPMessage::UpdateProcessor(new_processor) => music_processor = *new_processor, + } + } + // Exits main decode loop and subsequently ends thread + Some(DecoderMessage::Stop) => { + status_sender.send(PlayerStatus::Stopped).unwrap(); + break 'main_decode + } + None => {}, + } + status_sender.send(PlayerStatus::Error).unwrap(); + } + // In theory this check should not need to occur? + if let (Some(song_handler), current_song) = (&mut song_handler, &*current_song.read().unwrap()) { + match seek_time { + Some(time) => { + let seek_to = SeekTo::Time { time: Time::from(time), track_id: Some(0) }; + song_handler.reader.seek(SeekMode::Accurate, seek_to).unwrap(); + seek_time = None; + } + None => {} //Nothing to do! + } + let packet = match song_handler.reader.next_packet() { + Ok(packet) => packet, + Err(Error::ResetRequired) => panic!(), //TODO, + Err(err) => { + // Unrecoverable? + panic!("{}", err); + } + }; + + if let (Some(time_base), Some(song)) = (song_handler.time_base, current_song) { + let time_units = time_base.calc_time(packet.ts); + song_time = time_units.seconds as f64 + time_units.frac; + // Tracks song now if song has just started + if song_time == 0.0 { + tracker_sender.send(TrackerMessage::TrackNow(song.clone())).unwrap(); + } + + if let Some(duration) = song_handler.duration { + let song_duration = time_base.calc_time(duration); + let song_duration_secs = song_duration.seconds as f64 + song_duration.frac; + // Tracks song if current time is past half of total song duration or past 4 minutes + if (song_duration_secs / 2.0 < song_time || song_time > 240.0) && !song_tracked { + song_tracked = true; + tracker_sender.send(TrackerMessage::Track(song.clone())).unwrap(); + } + } + } + + status_sender.send(PlayerStatus::Playing(song_time)).unwrap(); + + match song_handler.decoder.decode(&packet) { + Ok(decoded) => { + // Opens audio stream if there is not one + if audio_output.is_none() { + let spec = *decoded.spec(); + let duration = decoded.capacity() as u64; + + audio_output.replace(crate::music_player::music_output::open_stream(spec, duration).unwrap()); + } + // Handles audio normally provided there is an audio stream + if let Some(ref mut audio_output) = audio_output { + // Changes buffer of the MusicProcessor if the packet has a differing capacity or spec + if music_processor.audio_buffer.capacity() != decoded.capacity() ||music_processor.audio_buffer.spec() != decoded.spec() { + let spec = *decoded.spec(); + let duration = decoded.capacity() as u64; + + music_processor.set_buffer(duration, spec); + } + let transformed_audio = music_processor.process(&decoded); + + // Writes transformed packet to audio out + audio_output.write(transformed_audio).unwrap() + } + }, + Err(Error::IoError(_)) => { + // rest in peace packet + continue; + }, + Err(Error::DecodeError(_)) => { + // may you one day be decoded + continue; + }, + Err(err) => { + // Unrecoverable, though shouldn't panic here + panic!("{}", err); + } + } + } + } + }); } // Updates status by checking on messages from spawned thread - fn update_status(&mut self) { - let status = self.status_receiver.as_mut().unwrap().try_recv(); - if status.is_ok() { - self.player_status = status.unwrap(); - match status.unwrap() { - // Removes receiver and sender since spawned thread no longer exists - PlayerStatus::Stopped => { - self.status_receiver = None; - self.message_sender = None; - } - _ => {} - } + fn update_player(&mut self) { + for message in self.status_receiver.try_recv() { + self.player_status = message; + } + } + + pub fn get_current_song(&self) -> Option{ + match self.current_song.try_read() { + Ok(song) => return (*song).clone(), + Err(_) => return None, } } // Sends message to spawned thread - pub fn send_message(&mut self, message: PlayerMessage) { - self.update_status(); + pub fn send_message(&mut self, message: DecoderMessage) { + self.update_player(); // Checks that message sender exists before sending a message off - if self.message_sender.is_some() { - self.message_sender.as_mut().unwrap().send(message).unwrap(); - } + self.message_sender.send(message).unwrap(); } pub fn get_status(&mut self) -> PlayerStatus { - self.update_status(); + self.update_player(); return self.player_status; } } @@ -361,4 +490,4 @@ impl MediaSource for RemoteSource { fn byte_len(&self) -> Option { return None; } -} \ No newline at end of file +} diff --git a/src/music_processor/music_processor.rs b/src/music_processor/music_processor.rs index dd7effc..e6d6608 100644 --- a/src/music_processor/music_processor.rs +++ b/src/music_processor/music_processor.rs @@ -1,3 +1,5 @@ +use std::fmt::Debug; + use symphonia::core::audio::{AudioBuffer, AudioBufferRef, Signal, AsAudioBufferRef, SignalSpec}; #[derive(Clone)] @@ -6,6 +8,12 @@ pub struct MusicProcessor { pub audio_volume: f32, } +impl Debug for MusicProcessor { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("MusicProcessor").field("audio_volume", &self.audio_volume).finish() + } +} + impl MusicProcessor { /// Returns new MusicProcessor with blank buffer and 100% volume pub fn new() -> Self { diff --git a/src/music_storage/music_db.rs b/src/music_storage/music_db.rs index 72d3053..7ed59fe 100644 --- a/src/music_storage/music_db.rs +++ b/src/music_storage/music_db.rs @@ -11,28 +11,29 @@ use walkdir::WalkDir; use crate::music_controller::config::Config; -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct Song { - pub path: Box, + pub path: URI, pub title: Option, pub album: Option, - tracknum: Option, + pub tracknum: Option, pub artist: Option, - date: Option, - genre: Option, - plays: Option, - favorited: Option, - format: Option, // TODO: Make this a proper FileFormat eventually - duration: Option, + pub date: Option, + pub genre: Option, + pub plays: Option, + pub favorited: Option, + pub format: Option, // TODO: Make this a proper FileFormat eventually + pub duration: Option, pub custom_tags: Option>, } -#[derive(Clone)] + +#[derive(Clone, Debug)] pub enum URI{ Local(String), Remote(Service, String), } -#[derive(Clone, Copy)] +#[derive(Clone, Copy, Debug)] pub enum Service { InternetRadio, Spotify, @@ -143,7 +144,7 @@ fn parse_cuesheet( let tags = track.get_cdtext(); let cue_song = Song { - path: track_path.into(), + path: URI::Local(String::from("URI")), title: tags.read(PTI::Title), album: album.clone(), tracknum: Some(index + 1), @@ -311,7 +312,7 @@ pub fn add_file_to_db(target_file: &Path, connection: &Connection) { } } -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, Clone)] pub enum Tag { SongPath, Title, @@ -363,7 +364,7 @@ impl MusicObject { } /// Query the database, returning a list of items -pub fn query( +pub fn query ( config: &Config, text_input: &String, queried_tags: &Vec<&Tag>, @@ -435,7 +436,7 @@ pub fn query( let new_song = Song { // TODO: Implement proper errors here - path: Path::new(&row.get::(0).unwrap_or("".to_owned())).into(), + path: URI::Local(String::from("URI")), title: row.get::(1).ok(), album: row.get::(2).ok(), tracknum: row.get::(3).ok(), diff --git a/src/music_tracker/music_tracker.rs b/src/music_tracker/music_tracker.rs index 9568d85..7dd1d8a 100644 --- a/src/music_tracker/music_tracker.rs +++ b/src/music_tracker/music_tracker.rs @@ -7,19 +7,21 @@ use md5::{Md5, Digest}; use discord_presence::{ Event, DiscordError}; use surf::StatusCode; +use crate::music_storage::music_db::Song; + #[async_trait] pub trait MusicTracker { /// Adds one listen to a song halfway through playback - async fn track_song(&mut self, song: &String) -> Result<(), TrackerError>; + async fn track_song(&mut self, song: Song) -> Result<(), TrackerError>; /// Adds a 'listening' status to the music tracker service of choice - async fn track_now(&mut self, song: &String) -> Result<(), TrackerError>; + async fn track_now(&mut self, song: Song) -> Result<(), TrackerError>; /// Reads config files, and attempts authentication with service async fn test_tracker(&mut self) -> Result<(), TrackerError>; /// Returns plays for a given song according to tracker service - async fn get_times_tracked(&mut self, song: &String) -> Result; + async fn get_times_tracked(&mut self, song: &Song) -> Result; } #[derive(Debug)] @@ -52,13 +54,12 @@ impl TrackerError { } } -#[derive(Serialize, Deserialize, Clone)] +#[derive(Serialize, Deserialize, Clone, PartialEq, Eq)] pub struct LastFMConfig { pub enabled: bool, pub dango_api_key: String, - pub auth_token: Option, - pub shared_secret: Option, - pub session_key: Option, + pub shared_secret: String, + pub session_key: String, } pub struct LastFM { @@ -67,15 +68,21 @@ pub struct LastFM { #[async_trait] impl MusicTracker for LastFM { - async fn track_song(&mut self, song: &String) -> Result<(), TrackerError> { + async fn track_song(&mut self, song: Song) -> Result<(), TrackerError> { 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(); + + let (artist, track) = match (song.artist, song.title) { + (Some(artist), Some(track)) => (artist, track), + _ => return Err(TrackerError::InvalidSong) + }; + params.insert("method", "track.scrobble"); - params.insert("artist", "Kikuo"); - params.insert("track", "A Happy Death - Again"); + params.insert("artist", &artist); + params.insert("track", &track); params.insert("timestamp", &string_timestamp); return match self.api_request(params).await { @@ -84,11 +91,17 @@ impl MusicTracker for LastFM { } } - async fn track_now(&mut self, song: &String) -> Result<(), TrackerError> { + async fn track_now(&mut self, song: Song) -> Result<(), TrackerError> { let mut params: BTreeMap<&str, &str> = BTreeMap::new(); + + let (artist, track) = match (song.artist, song.title) { + (Some(artist), Some(track)) => (artist, track), + _ => return Err(TrackerError::InvalidSong) + }; + params.insert("method", "track.updateNowPlaying"); - params.insert("artist", "Kikuo"); - params.insert("track", "A Happy Death - Again"); + params.insert("artist", &artist); + params.insert("track", &track); return match self.api_request(params).await { Ok(_) => Ok(()), @@ -106,7 +119,7 @@ impl MusicTracker for LastFM { } } - async fn get_times_tracked(&mut self, song: &String) -> Result { + async fn get_times_tracked(&mut self, song: &Song) -> Result { todo!(); } } @@ -129,26 +142,21 @@ struct Session { } impl LastFM { - // Returns a url to be accessed by the user - pub async fn get_auth_url(&mut self) -> Result { + /// Returns a url to be approved by the user along with the auth token + pub async fn get_auth(api_key: &String) -> Result { let method = String::from("auth.gettoken"); - 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 auth_token: AuthToken = surf::get(api_request_url).await?.body_json().await?; - 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); return Ok(auth_url); } - pub async fn set_session(&mut self) { + /// Returns a LastFM session key + pub async fn get_session_key(api_key: &String, shared_secret: &String, auth_token: &String) -> Result { let method = String::from("auth.getSession"); - let api_key = self.config.dango_api_key.clone(); - let auth_token = self.config.auth_token.clone().unwrap(); - let shared_secret = self.config.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}"); @@ -160,14 +168,14 @@ impl LastFM { 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(); + let response = surf::get(api_request_url).recv_string().await?; // Sets session key from received response - let session_response: Session = serde_json::from_str(&response).unwrap(); - self.config.session_key = Some(session_response.session.key.clone()); + let session_response: Session = serde_json::from_str(&response)?; + return Ok(session_response.session.key); } - // Creates a new LastFM struct + /// Creates a new LastFM struct with a given config pub fn new(config: &LastFMConfig) -> LastFM { let last_fm = LastFM { config: config.clone() @@ -178,10 +186,10 @@ impl LastFM { // 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.config.dango_api_key); - params.insert("sk", &self.config.session_key.as_ref().unwrap()); + params.insert("sk", &self.config.session_key); // Creates and sets api call signature - let api_sig = LastFM::request_sig(¶ms, &self.config.shared_secret.as_ref().unwrap()); + let api_sig = LastFM::request_sig(¶ms, &self.config.shared_secret); params.insert("api_sig", &api_sig); let mut string_params = String::from(""); @@ -226,7 +234,7 @@ impl LastFM { } } -#[derive(Serialize, Deserialize, Clone)] +#[derive(Serialize, Deserialize, Clone, PartialEq, Eq)] pub struct DiscordRPCConfig { pub enabled: bool, pub dango_client_id: u64, @@ -250,7 +258,14 @@ impl DiscordRPC { #[async_trait] impl MusicTracker for DiscordRPC { - async fn track_now(&mut self, song: &String) -> Result<(), TrackerError> { + async fn track_now(&mut self, song: Song) -> Result<(), TrackerError> { + // Sets song title + let song_name = if let Some(song_name) = song.title { + song_name + } else { + String::from("Unknown") + }; + let _client_thread = self.client.start(); // Blocks thread execution until it has connected to local discord client @@ -263,7 +278,7 @@ impl MusicTracker for DiscordRPC { // Sets discord account activity to current playing song let send_activity = self.client.set_activity(|activity| { activity - .state(song) + .state(format!("Listening to: {}", song_name)) .assets(|assets| assets.large_image(&self.config.dango_icon)) .timestamps(|time| time.start(start_time)) }); @@ -274,7 +289,7 @@ impl MusicTracker for DiscordRPC { } } - async fn track_song(&mut self, song: &String) -> Result<(), TrackerError> { + async fn track_song(&mut self, song: Song) -> Result<(), TrackerError> { return Ok(()) } @@ -282,7 +297,7 @@ impl MusicTracker for DiscordRPC { return Ok(()) } - async fn get_times_tracked(&mut self, song: &String) -> Result { + async fn get_times_tracked(&mut self, song: &Song) -> Result { return Ok(0); } } \ No newline at end of file