Finished Initial implementation of Discord RPC

This commit is contained in:
MrDulfin 2024-12-30 23:49:19 -05:00
parent 31829ad4f5
commit 5be0df4dc2
3 changed files with 97 additions and 56 deletions

View file

@ -1,12 +1,11 @@
#![allow(while_true)] #![allow(while_true)]
use std::time::Duration; use std::{thread::sleep, time::{Duration, SystemTime, UNIX_EPOCH}};
use chrono::TimeDelta; use chrono::TimeDelta;
use crossbeam::scope; use crossbeam::{scope, select};
use crossbeam_channel::{bounded, Receiver}; use crossbeam_channel::{bounded, Receiver};
use discord_presence::models::{Activity, ActivityTimestamps, ActivityType}; use discord_presence::Client;
use prismriver::State as PrismState; use prismriver::State as PrismState;
use rayon::spawn;
use crate::music_storage::library::{Song, Tag}; use crate::music_storage::library::{Song, Tag};
@ -68,67 +67,98 @@ impl Controller {
} }
fn discord_rpc(client_id: u64, song_tx: Receiver<Song>, state_tx: Receiver<PrismState>) { fn discord_rpc(client_id: u64, song_tx: Receiver<Song>, state_tx: Receiver<PrismState>) {
spawn(move || { // TODO: Handle seeking position change
std::thread::spawn(move || {
let mut client = discord_presence::Client::new(client_id); let mut client = discord_presence::Client::new(client_id);
client.start(); client.start();
client.block_until_event(discord_presence::Event::Connected).unwrap(); while !Client::is_ready() { sleep(Duration::from_millis(100)); }
client.set_activity(|_|
Activity::new()
).unwrap();
println!("discord connected"); println!("discord connected");
let mut state = "Started".to_string(); let mut state = "Started".to_string();
let mut song: Option<Song> = None; let mut song: Option<Song> = None;
let mut now = SystemTime::now().duration_since(UNIX_EPOCH).expect("Time went backwards?").as_secs();
while true { while true {
let state_res = state_tx.recv_timeout(Duration::from_secs(5));
let song_res = song_tx.recv_timeout(Duration::from_millis(100));
let state = &mut state; let state = &mut state;
let song = &mut song; let song = &mut song;
select! {
if let Ok(state_) = state_res { recv(state_tx) -> res => {
if let Ok(state_) = res {
*state = match state_ { *state = match state_ {
PrismState::Playing => "Playing", PrismState::Playing => "Playing",
PrismState::Paused => "Paused", PrismState::Paused => "Paused",
PrismState::Stopped => "Stopped", PrismState::Stopped => "Stopped",
_ => "I'm Scared, Boss" _ => "I'm Scared, Boss"
}.to_string() }.to_string();
} }
if let Ok(song_) = song_res { },
recv(song_tx) -> res => {
if let Ok(song_) = res {
*song = Some(song_); *song = Some(song_);
now = SystemTime::now().duration_since(UNIX_EPOCH).expect("Time went backwards?").as_secs();
}
}
default(Duration::from_millis(4500)) => ()
} }
client.set_activity(|activity| { client.set_activity(|activity| {
activity.state( let a = activity.state(
state.clone() song.as_ref().map_or(String::new(), |s| format!(
"{}{}{}",
s.get_tag(&Tag::Artist).map_or(String::new(), |album| album.clone()),
if s.get_tag(&Tag::Album).is_some() && s.get_tag(&Tag::Artist).is_some() { " - " } else { "" },
s.get_tag(&Tag::Album).map_or(String::new(), |album| album.clone())
)
)
)._type(discord_presence::models::ActivityType::Listening) )._type(discord_presence::models::ActivityType::Listening)
.details( .details(
if let Some(song) = song { if let Some(song) = song {
format!( song.get_tag(&Tag::Title).map_or(String::from("Unknown Title"), |title| title.clone())
"{} - {}\n{}",
song.get_tag(&Tag::Title).map_or(String::from("No Title"), |title| title.clone()),
song.get_tag(&Tag::Artist).map_or(String::from("No Artist"), |artist| artist.clone()),
song.get_tag(&Tag::Album).map_or(String::from("No Album"), |album| album.clone())
)
} else { } else {
String::new() String::new()
} }
) );
// if let Some(song) = song { if let Some(s) = song {
// a.timestamps(|timestamp| { if state.as_str() == "Playing" {
// ActivityTimestamps::new() a.timestamps(|timestamps| {
// .start(timestamp.start.unwrap_or_default()) timestamps.start(now)
// .end( .end(now + s.duration.as_secs())
// song.duration.as_millis().clamp(u64::MIN as u128, u64::MAX as u128) as u64 })
// ) } else {
// }) a
// } else { }
// a } else {
// } a
}.assets(|a| {
a.large_text(state.clone())
})
}).unwrap(); }).unwrap();
println!("Changed Status"); println!("Updated Discord Status");
} }
}); });
} }
} }
#[cfg(test)]
mod test_super {
use std::thread::sleep;
use crossbeam_channel::unbounded;
use crate::config::tests::read_config_lib;
use super::*;
#[test]
fn discord_test() {
let client_id = std::env!("DISCORD_CLIENT_ID").parse::<u64>().unwrap();
let (song_rx, song_tx) = unbounded();
let (_, state_tx) = unbounded();
let (_, lib ) = read_config_lib();
song_rx.send(lib.library[0].clone()).unwrap();
Controller::discord_rpc(client_id, song_tx, state_tx);
sleep(Duration::from_secs(150));
}
}

View file

@ -3,7 +3,6 @@
//! other functions //! other functions
#![allow(while_true)] #![allow(while_true)]
use async_channel::{bounded, unbounded};
use chrono::TimeDelta; use chrono::TimeDelta;
use crossbeam::atomic::AtomicCell; use crossbeam::atomic::AtomicCell;
use crossbeam_channel::{Receiver, Sender}; use crossbeam_channel::{Receiver, Sender};
@ -14,12 +13,11 @@ use prismriver::{Error as PrismError, Prismriver, State as PrismState, Volume};
use rayon::iter::{IndexedParallelIterator, IntoParallelIterator, ParallelIterator}; use rayon::iter::{IndexedParallelIterator, IntoParallelIterator, ParallelIterator};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_json::to_string_pretty; use serde_json::to_string_pretty;
use std::collections::HashMap;
use std::error::Error; use std::error::Error;
use std::fs::OpenOptions; use std::fs::OpenOptions;
use std::io::Write; use std::io::Write;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::sync::{Arc}; use std::sync::Arc;
use std::time::Duration; use std::time::Duration;
use thiserror::Error; use thiserror::Error;
use uuid::Uuid; use uuid::Uuid;
@ -278,6 +276,7 @@ impl Controller {
let a = scope.spawn({ let a = scope.spawn({
let queue_mail = queue_mail.clone(); let queue_mail = queue_mail.clone();
let _notifications_rx = notifications_rx.clone();
move || { move || {
futures::executor::block_on(async { futures::executor::block_on(async {
moro::async_scope!(|scope| { moro::async_scope!(|scope| {
@ -292,6 +291,7 @@ impl Controller {
player_mail.1, player_mail.1,
_queue_mail, _queue_mail,
_lib_mail, _lib_mail,
_notifications_rx,
state, state,
) )
.await .await
@ -354,6 +354,7 @@ impl Controller {
player_mail: MailMan<PlayerResponse, PlayerCommand>, player_mail: MailMan<PlayerResponse, PlayerCommand>,
queue_mail: MailMan<QueueCommand, QueueResponse>, queue_mail: MailMan<QueueCommand, QueueResponse>,
lib_mail: MailMan<LibraryCommand, LibraryResponse>, lib_mail: MailMan<LibraryCommand, LibraryResponse>,
notify_connections_: Sender<ConnectionsNotification>,
mut state: ControllerState, mut state: ControllerState,
) -> Result<(), ()> { ) -> Result<(), ()> {
player.set_volume(Volume::new(state.volume)); player.set_volume(Volume::new(state.volume));
@ -444,6 +445,7 @@ impl Controller {
state.now_playing = np_song.song.uuid; state.now_playing = np_song.song.uuid;
_ = state.write_file(); _ = state.write_file();
notify_connections_.send(ConnectionsNotification::SongChange(np_song.song)).unwrap();
} QueueResponse::Item(Err(e)) => { } QueueResponse::Item(Err(e)) => {
player_mail.send(PlayerResponse::NowPlaying(Err(e.into()))).await.unwrap(); player_mail.send(PlayerResponse::NowPlaying(Err(e.into()))).await.unwrap();
} }
@ -469,6 +471,7 @@ impl Controller {
state.now_playing = np_song.song.uuid; state.now_playing = np_song.song.uuid;
_ = state.write_file(); _ = state.write_file();
notify_connections_.send(ConnectionsNotification::SongChange(np_song.song)).unwrap();
} }
QueueResponse::Item(Err(e)) => { QueueResponse::Item(Err(e)) => {
player_mail.send(PlayerResponse::NowPlaying(Err(e.into()))).await.unwrap(); player_mail.send(PlayerResponse::NowPlaying(Err(e.into()))).await.unwrap();
@ -485,10 +488,14 @@ impl Controller {
match queue_mail.recv().await.unwrap() { match queue_mail.recv().await.unwrap() {
QueueResponse::Item(Ok(item)) => { QueueResponse::Item(Ok(item)) => {
match item.item { match item.item {
QueueItemType::Single(song) => { QueueItemType::Single(np_song) => {
let prism_uri = prismriver::utils::path_to_uri(&song.song.primary_uri().unwrap().0.as_path().unwrap()).unwrap(); let prism_uri = prismriver::utils::path_to_uri(&np_song.song.primary_uri().unwrap().0.as_path().unwrap()).unwrap();
player.load_new(&prism_uri).unwrap(); player.load_new(&prism_uri).unwrap();
player.play(); player.play();
state.now_playing = np_song.song.uuid;
_ = state.write_file();
notify_connections_.send(ConnectionsNotification::SongChange(np_song.song)).unwrap();
} }
_ => unimplemented!(), _ => unimplemented!(),
} }
@ -504,7 +511,7 @@ impl Controller {
PlayerCommand::PlayNow(uuid, location) => { PlayerCommand::PlayNow(uuid, location) => {
// TODO: This assumes the uuid doesn't point to an album. we've been over this. // TODO: This assumes the uuid doesn't point to an album. we've been over this.
lib_mail.send(LibraryCommand::Song(uuid)).await.unwrap(); lib_mail.send(LibraryCommand::Song(uuid)).await.unwrap();
let LibraryResponse::Song(song, index) = lib_mail.recv().await.unwrap() else { let LibraryResponse::Song(np_song, index) = lib_mail.recv().await.unwrap() else {
unreachable!() unreachable!()
}; };
queue_mail.send(QueueCommand::Clear).await.unwrap(); queue_mail.send(QueueCommand::Clear).await.unwrap();
@ -516,7 +523,7 @@ impl Controller {
} }
_ => unreachable!() _ => unreachable!()
} }
queue_mail.send(QueueCommand::Append(QueueItem::from_item_type(QueueItemType::Single(QueueSong { song: song.clone(), location })), true)).await.unwrap(); queue_mail.send(QueueCommand::Append(QueueItem::from_item_type(QueueItemType::Single(QueueSong { song: np_song.clone(), location })), true)).await.unwrap();
match queue_mail.recv().await.unwrap() { match queue_mail.recv().await.unwrap() {
QueueResponse::Empty(Ok(())) => (), QueueResponse::Empty(Ok(())) => (),
QueueResponse::Empty(Err(e)) => { QueueResponse::Empty(Err(e)) => {
@ -527,7 +534,7 @@ impl Controller {
} }
// TODO: Handle non Local URIs here, and whenever `load_new()` or `load_gapless()` is called // TODO: Handle non Local URIs here, and whenever `load_new()` or `load_gapless()` is called
let prism_uri = prismriver::utils::path_to_uri(&song.primary_uri().unwrap().0.as_path().unwrap()).unwrap(); let prism_uri = prismriver::utils::path_to_uri(&np_song.primary_uri().unwrap().0.as_path().unwrap()).unwrap();
player.load_new(&prism_uri).unwrap(); player.load_new(&prism_uri).unwrap();
player.play(); player.play();
@ -548,7 +555,7 @@ impl Controller {
let LibraryResponse::ExternalPlaylist(list) = lib_mail.recv().await.unwrap() else { let LibraryResponse::ExternalPlaylist(list) = lib_mail.recv().await.unwrap() else {
unreachable!() unreachable!()
}; };
let index = list.get_index(song.uuid).unwrap(); let index = list.get_index(np_song.uuid).unwrap();
(list.tracks, index) (list.tracks, index)
} }
_ => todo!("Got Location other than Library or Playlist") _ => todo!("Got Location other than Library or Playlist")
@ -572,7 +579,11 @@ impl Controller {
} }
} }
// ^ This be my solution for now ^ // ^ This be my solution for now ^
player_mail.send(PlayerResponse::NowPlaying(Ok(song.clone()))).await.unwrap(); player_mail.send(PlayerResponse::NowPlaying(Ok(np_song.clone()))).await.unwrap();
state.now_playing = np_song.uuid;
_ = state.write_file();
notify_connections_.send(ConnectionsNotification::SongChange(np_song)).unwrap();
} }
} }
} else { } else {

View file

@ -340,9 +340,9 @@ function PlayBar({ playing, setPlaying }: PlayBarProps) {
invoke('set_volume', { volume: volume.target.value }).then(() => {}) invoke('set_volume', { volume: volume.target.value }).then(() => {})
}} /> }} />
<p id="timeDisplay"> <p id="timeDisplay">
{ Math.round(+duration / 60).toString().padStart(2, "0") }: { Math.floor(+position / 60).toString().padStart(2, "0") }:
{ (+position % 60).toString().padStart(2, "0") }/ { (+position % 60).toString().padStart(2, "0") }/
{ Math.round(+position / 60).toString().padStart(2, "0") }: { Math.floor(+duration / 60).toString().padStart(2, "0") }:
{ (+duration % 60).toString().padStart(2, "0") } { (+duration % 60).toString().padStart(2, "0") }
</p> </p>
</div> </div>