From 31829ad4f597905e4e466529014982227d845014 Mon Sep 17 00:00:00 2001 From: MrDulfin Date: Mon, 30 Dec 2024 03:02:11 -0500 Subject: [PATCH] more progress on RPC Integration --- .gitignore | 3 +- dmp-core/Cargo.toml | 1 + dmp-core/src/lib.rs | 1 + dmp-core/src/music_controller/connections.rs | 134 +++++++++++++++++++ dmp-core/src/music_controller/controller.rs | 127 +++++++++++------- src-tauri/Cargo.toml | 1 - src-tauri/src/lib.rs | 18 ++- src/App.tsx | 16 +-- src/types.ts | 3 +- 9 files changed, 234 insertions(+), 70 deletions(-) create mode 100644 dmp-core/src/music_controller/connections.rs diff --git a/.gitignore b/.gitignore index a4fc833..f28f2a1 100644 --- a/.gitignore +++ b/.gitignore @@ -30,4 +30,5 @@ target Cargo.lock .cargo -test-config \ No newline at end of file +test-config +.txt \ No newline at end of file diff --git a/dmp-core/Cargo.toml b/dmp-core/Cargo.toml index e7521ad..c64b603 100644 --- a/dmp-core/Cargo.toml +++ b/dmp-core/Cargo.toml @@ -39,3 +39,4 @@ ciborium = "0.2.2" itertools = "0.13.0" prismriver = { git = "https://github.com/Dangoware/prismriver.git"} parking_lot = "0.12.3" +discord-presence = { version = "1.4.1", features = ["activity_type"] } diff --git a/dmp-core/src/lib.rs b/dmp-core/src/lib.rs index 9731980..5f86f12 100644 --- a/dmp-core/src/lib.rs +++ b/dmp-core/src/lib.rs @@ -9,6 +9,7 @@ pub mod music_storage { } pub mod music_controller { + pub mod connections; pub mod controller; pub mod controller_handle; pub mod queue; diff --git a/dmp-core/src/music_controller/connections.rs b/dmp-core/src/music_controller/connections.rs new file mode 100644 index 0000000..28ae104 --- /dev/null +++ b/dmp-core/src/music_controller/connections.rs @@ -0,0 +1,134 @@ +#![allow(while_true)] +use std::time::Duration; + +use chrono::TimeDelta; +use crossbeam::scope; +use crossbeam_channel::{bounded, Receiver}; +use discord_presence::models::{Activity, ActivityTimestamps, ActivityType}; +use prismriver::State as PrismState; +use rayon::spawn; + +use crate::music_storage::library::{Song, Tag}; + +use super::controller::Controller; + +#[derive(Debug, Clone)] +pub(super) enum ConnectionsNotification { + Playback { + position: Option, + duration: Option + }, + StateChange(PrismState), + SongChange(Song), +} + +#[derive(Debug)] +pub struct ConnectionsInput { + pub discord_rpc_client_id: Option, +} + +pub(super) struct ControllerConnections { + pub notifications_tx: Receiver, + pub inner: ConnectionsInput +} + +impl Controller { + pub(super) fn handle_connections(ControllerConnections { + notifications_tx, + inner: ConnectionsInput { + discord_rpc_client_id + }, + }: ControllerConnections + ) { + let (dc_state_rx, dc_state_tx) = bounded::(1); + let (dc_song_rx, dc_song_tx) = bounded::(1); + scope(|s| { + s.builder().name("Notifications Sorter".to_string()).spawn(|_| { + use ConnectionsNotification::*; + while true { + match notifications_tx.recv().unwrap() { + Playback { position, duration } => { continue; } + StateChange(state) => { + dc_state_rx.send(state.clone()).unwrap(); + } + SongChange(song) => { + dc_song_rx.send(song).unwrap(); + } + } + } + }).unwrap(); + + if let Some(client_id) = discord_rpc_client_id { + println!("Discord thingy detected"); + s.builder().name("Discord RPC Handler".to_string()).spawn(move |_| { + Controller::discord_rpc(client_id, dc_song_tx, dc_state_tx); + }).unwrap(); + }; + }).unwrap(); + } + + fn discord_rpc(client_id: u64, song_tx: Receiver, state_tx: Receiver) { + spawn(move || { + let mut client = discord_presence::Client::new(client_id); + client.start(); + client.block_until_event(discord_presence::Event::Connected).unwrap(); + client.set_activity(|_| + Activity::new() + ).unwrap(); + println!("discord connected"); + + let mut state = "Started".to_string(); + let mut song: Option = None; + + 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 song = &mut song; + + if let Ok(state_) = state_res { + *state = match state_ { + PrismState::Playing => "Playing", + PrismState::Paused => "Paused", + PrismState::Stopped => "Stopped", + _ => "I'm Scared, Boss" + }.to_string() + } + if let Ok(song_) = song_res { + *song = Some(song_); + } + + client.set_activity(|activity| { + activity.state( + state.clone() + )._type(discord_presence::models::ActivityType::Listening) + .details( + if let Some(song) = song { + format!( + "{} - {}\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 { + String::new() + } + ) + // if let Some(song) = song { + // a.timestamps(|timestamp| { + // ActivityTimestamps::new() + // .start(timestamp.start.unwrap_or_default()) + // .end( + // song.duration.as_millis().clamp(u64::MIN as u128, u64::MAX as u128) as u64 + // ) + // }) + // } else { + // a + // } + }).unwrap(); + println!("Changed Status"); + } + }); + } +} \ No newline at end of file diff --git a/dmp-core/src/music_controller/controller.rs b/dmp-core/src/music_controller/controller.rs index 203a48c..823c63a 100644 --- a/dmp-core/src/music_controller/controller.rs +++ b/dmp-core/src/music_controller/controller.rs @@ -3,13 +3,14 @@ //! other functions #![allow(while_true)] -use async_channel::unbounded; +use async_channel::{bounded, unbounded}; use chrono::TimeDelta; use crossbeam::atomic::AtomicCell; use crossbeam_channel::{Receiver, Sender}; use kushi::{Queue, QueueItemType}; use kushi::{QueueError, QueueItem}; -use prismriver::{Prismriver, Volume, Error as PrismError}; +use parking_lot::RwLock; +use prismriver::{Error as PrismError, Prismriver, State as PrismState, Volume}; use rayon::iter::{IndexedParallelIterator, IntoParallelIterator, ParallelIterator}; use serde::{Deserialize, Serialize}; use serde_json::to_string_pretty; @@ -18,7 +19,7 @@ use std::error::Error; use std::fs::OpenOptions; use std::io::Write; use std::path::{Path, PathBuf}; -use std::sync::{Arc, RwLock}; +use std::sync::{Arc}; use std::time::Duration; use thiserror::Error; use uuid::Uuid; @@ -28,6 +29,7 @@ use crate::music_storage::library::Song; use crate::music_storage::playlist::{ExternalPlaylist, Playlist, PlaylistFolderItem}; use crate::{config::Config, music_storage::library::MusicLibrary}; +use super::connections::{ConnectionsInput, ConnectionsNotification, ControllerConnections}; use super::queue::{QueueAlbum, QueueSong}; pub struct Controller(); @@ -166,6 +168,7 @@ pub struct ControllerInput { config: Arc>, playback_info: Arc>, notify_next_song: Sender, + connections: Option } pub struct ControllerHandle { @@ -175,7 +178,7 @@ pub struct ControllerHandle { } impl ControllerHandle { - pub fn new(library: MusicLibrary, config: Arc>) -> (Self, ControllerInput, Arc>, Receiver) { + pub fn new(library: MusicLibrary, config: Arc>, connections: Option) -> (Self, ControllerInput, Arc>, Receiver) { let lib_mail = MailMan::double(); let player_mail = MailMan::double(); let queue_mail = MailMan::double(); @@ -195,6 +198,7 @@ impl ControllerHandle { config, playback_info: Arc::clone(&playback_info), notify_next_song: notify_next_song.0, + connections, }, playback_info, notify_next_song.1 @@ -246,6 +250,7 @@ impl Controller { config, playback_info, notify_next_song, + connections, }: ControllerInput ) -> Result<(), Box> { let queue: Queue = Queue { @@ -256,7 +261,7 @@ impl Controller { }; let state = { - let path = &config.read().unwrap().state_path; + let path = &config.read().state_path; if let Ok(state) = ControllerState::read_file(path) { state } else { @@ -264,24 +269,26 @@ impl Controller { } }; - let player = Arc::new(RwLock::new(Prismriver::new())); - std::thread::scope(|scope| { + let player = Prismriver::new(); + let player_state = player.state.clone(); + let player_timing = player.get_timing_recv(); + let finished_tx = player.get_finished_recv(); + let (notifications_rx, notifications_tx) = crossbeam_channel::unbounded::(); + let a = scope.spawn({ - let player = Arc::clone(&player); let queue_mail = queue_mail.clone(); move || { futures::executor::block_on(async { moro::async_scope!(|scope| { println!("async scope created"); - let _player = player.clone(); let _lib_mail = lib_mail.0.clone(); let _queue_mail = queue_mail.0.clone(); scope .spawn(async move { Controller::player_command_loop( - _player, + player, player_mail.1, _queue_mail, _lib_mail, @@ -314,14 +321,26 @@ impl Controller { let c = scope.spawn(|| { Controller::player_monitor_loop( - player, + player_state, + player_timing, + finished_tx, player_mail.0, queue_mail.0, - playback_info, notify_next_song, + notifications_rx, + playback_info, ).unwrap(); }); + if let Some(inner) = connections { + dbg!(&inner); + let d = scope.spawn(|| { + Controller::handle_connections( ControllerConnections { + notifications_tx, + inner, + }); + }); + } a.join().unwrap(); b.join().unwrap(); c.join().unwrap(); @@ -331,41 +350,42 @@ impl Controller { } async fn player_command_loop( - player: Arc>, + mut player: Prismriver, player_mail: MailMan, queue_mail: MailMan, lib_mail: MailMan, mut state: ControllerState, ) -> Result<(), ()> { - player.write().unwrap().set_volume(Volume::new(state.volume)); + player.set_volume(Volume::new(state.volume)); 'outer: while true { let _mail = player_mail.recv().await; if let Ok(mail) = _mail { match mail { PlayerCommand::Play => { - player.write().unwrap().play(); + player.play(); player_mail.send(PlayerResponse::Empty(Ok(()))).await.unwrap(); } PlayerCommand::Pause => { - player.write().unwrap().pause(); + player.pause(); player_mail.send(PlayerResponse::Empty(Ok(()))).await.unwrap(); } PlayerCommand::Stop => { - player.write().unwrap().stop(); + player.stop(); player_mail.send(PlayerResponse::Empty(Ok(()))).await.unwrap(); } PlayerCommand::Seek(time) => { - let res = player.write().unwrap().seek_to(TimeDelta::milliseconds(time)); + let res = player.seek_to(TimeDelta::milliseconds(time)); player_mail.send(PlayerResponse::Empty(res.map_err(|e| e.into()))).await.unwrap(); } PlayerCommand::SetVolume(volume) => { - player.write().unwrap().set_volume(Volume::new(volume)); + player.set_volume(Volume::new(volume)); player_mail.send(PlayerResponse::Empty(Ok(()))).await.unwrap(); + // make this async or something state.volume = volume; _ = state.write_file() } @@ -384,8 +404,8 @@ impl Controller { println!("Playing song at path: {:?}", prism_uri); // handle error here for unknown formats - player.write().unwrap().load_new(&prism_uri).unwrap(); - player.write().unwrap().play(); + player.load_new(&prism_uri).unwrap(); + player.play(); let QueueItemType::Single(np_song) = item.item else { panic!("This is temporary, handle queueItemTypes at some point")}; @@ -441,8 +461,8 @@ impl Controller { }; let prism_uri = prismriver::utils::path_to_uri(&uri.as_path().unwrap()).unwrap(); - player.write().unwrap().load_new(&prism_uri).unwrap(); - player.write().unwrap().play(); + player.load_new(&prism_uri).unwrap(); + player.play(); let QueueItemType::Single(np_song) = item.item else { panic!("This is temporary, handle queueItemTypes at some point")}; player_mail.send(PlayerResponse::NowPlaying(Ok(np_song.song.clone()))).await.unwrap(); @@ -467,8 +487,8 @@ impl Controller { match item.item { QueueItemType::Single(song) => { let prism_uri = prismriver::utils::path_to_uri(&song.song.primary_uri().unwrap().0.as_path().unwrap()).unwrap(); - player.write().unwrap().load_new(&prism_uri).unwrap(); - player.write().unwrap().play(); + player.load_new(&prism_uri).unwrap(); + player.play(); } _ => unimplemented!(), } @@ -508,8 +528,8 @@ impl Controller { // 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(); - player.write().unwrap().load_new(&prism_uri).unwrap(); - player.write().unwrap().play(); + player.load_new(&prism_uri).unwrap(); + player.play(); // how grab all the songs in a certain subset of the library, I reckon? // ... @@ -563,36 +583,33 @@ impl Controller { } fn player_monitor_loop( - player: Arc>, + playback_state: Arc>, + playback_time_tx: Receiver<(Option, Option)>, + finished_recv: Receiver<()>, player_mail: MailMan, queue_mail: MailMan, - player_info: Arc>, notify_next_song: Sender, + notify_connections_: Sender, + playback_info: Arc> ) -> Result<(), ()> { - - let finished_recv = player.read().unwrap().get_finished_recv(); - std::thread::scope(|s| { // Thread for timing and metadata + let notify_connections = notify_connections_.clone(); s.spawn({ - // let player = Arc::clone(&player); move || { + println!("playback monitor started"); while true { - let player = player.read().unwrap(); - player_info.store(PlaybackInfo { - duration: player.duration(), - position: player.position(), - metadata: player.metadata(), - }); - drop(player); - - std::thread::sleep(Duration::from_millis(100)); + let (position, duration) = playback_time_tx.recv().unwrap(); + notify_connections.send(ConnectionsNotification::Playback { position: position.clone(), duration: duration.clone() }).unwrap(); + playback_info.store(PlaybackInfo { position, duration }); } } }); // Thread for End of Track + let notify_connections = notify_connections_.clone(); s.spawn(move || { futures::executor::block_on(async { + println!("EOS monitor started"); while true { let _ = finished_recv.recv(); println!("End of song"); @@ -602,16 +619,31 @@ impl Controller { unreachable!() }; if let Ok(song) = res { - notify_next_song.send(song).unwrap(); + notify_next_song.send(song.clone()).unwrap(); + notify_connections.send(ConnectionsNotification::SongChange(song)).unwrap(); } } std::thread::sleep(Duration::from_millis(100)); });}); + + let notify_connections = notify_connections_.clone(); + s.spawn(move || { + let mut state = PrismState::Stopped; + while true { + let _state = playback_state.read().unwrap().to_owned(); + if _state != state { + state = _state; + println!("State Changed to {state:?}"); + notify_connections.send(ConnectionsNotification::StateChange(state.clone())).unwrap(); + } + std::thread::sleep(Duration::from_millis(100)); + } + }); }); - // Check for duration and spit it out - // Check for end of song to get the next + + println!("Monitor Loops Ended"); Ok(()) } @@ -643,7 +675,7 @@ impl Controller { lib_mail.send(LibraryResponse::ImportM3UPlayList(uuid, name)).await.unwrap(); } LibraryCommand::Save => { - library.save(config.read().unwrap().libraries.get_library(&library.uuid).unwrap().path.clone()).unwrap(); + library.save(config.read().libraries.get_library(&library.uuid).unwrap().path.clone()).unwrap(); lib_mail.send(LibraryResponse::Ok).await.unwrap(); } LibraryCommand::Playlists => { @@ -716,7 +748,6 @@ impl Controller { #[derive(Debug, Default, Serialize, Clone)] pub struct PlaybackInfo { - pub duration: Option, pub position: Option, - pub metadata: HashMap, + pub duration: Option, } diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index d39ce6f..9983ce6 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -36,7 +36,6 @@ rfd = "0.15.1" colog = "1.3.0" tempfile = "3.14.0" opener = "0.7.2" -discord-presence = { version = "1.4.1", features = ["activity_type"] } parking_lot = "0.12.3" [features] diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 7478ef3..6c9a3c1 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -3,8 +3,7 @@ use std::{borrow::BorrowMut, fs, ops::Deref, path::PathBuf, sync::{atomic::Ordering, Arc}, thread::{scope, spawn}, time::Duration}; use crossbeam::channel::{bounded, unbounded, Receiver, Sender}; -use discord_presence::{models::{Activity, ActivityButton, ActivityTimestamps, ActivityType}, Event}; -use dmp_core::{config::{Config, ConfigLibrary}, music_controller::controller::{Controller, ControllerHandle, LibraryResponse, PlaybackInfo}, music_storage::library::{MusicLibrary, Song}}; +use dmp_core::{config::{Config, ConfigLibrary}, music_controller::{connections::ConnectionsInput, controller::{Controller, ControllerHandle, LibraryResponse, PlaybackInfo}}, music_storage::library::{MusicLibrary, Song, Tag}}; use futures::channel::oneshot; use parking_lot::RwLock; use rfd::FileHandle; @@ -77,7 +76,10 @@ pub fn run() { next_song_notification, ) = ControllerHandle::new( library, - std::sync::Arc::new(std::sync::RwLock::new(config)) + std::sync::Arc::new(RwLock::new(config)), + Some(ConnectionsInput { + discord_rpc_client_id: std::option_env!("DISCORD_CLIENT_ID").map(|id| id.parse::().unwrap()), + }), ); handle_rx.send(handle).unwrap(); @@ -123,8 +125,8 @@ pub fn run() { std::thread::Builder::new() .name("PlaybackInfo handler".to_string()) .spawn(move || { - let mut _info = Arc::new(RwLock::new(PlaybackInfo::default())); - let mut _now_playing = Arc::new(RwLock::new(None)); + let mut _info: Arc> = Arc::new(RwLock::new(PlaybackInfo::default())); + let mut _now_playing: Arc>> = Arc::new(RwLock::new(None)); scope(|s| { let info = _info.clone(); @@ -148,13 +150,9 @@ pub fn run() { app.emit("now_playing_change", _Song::from(&song)).unwrap(); app.emit("queue_updated", ()).unwrap(); app.emit("playing", ()).unwrap(); - now_playing.write().insert(song); + _ = now_playing.write().insert(song); } }); - - let info = _info.clone(); - let now_playing = _now_playing.clone(); - }); }).unwrap(); diff --git a/src/App.tsx b/src/App.tsx index a6a405f..14d1e80 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -299,12 +299,12 @@ function PlayBar({ playing, setPlaying }: PlayBarProps) { useEffect(() => { const unlisten = appWindow.listen("playback_info", ({ payload, }) => { const info = payload as playbackInfo; - const _pos = Array.isArray(info.position) ? info.position![0] : 0; - const _dur = Array.isArray(info.duration) ? info.duration![0] : 0; + const pos_ = Array.isArray(info.position) ? info.position![0] : 0; + const dur_ = Array.isArray(info.duration) ? info.duration![0] : 0; - setPosition(_pos); - setDuration(_dur); - let progress = ((_pos/_dur) * 100); + setPosition(pos_); + setDuration(dur_); + let progress = ((dur_/pos_) * 100); setSeekBarSize(progress) }) return () => { unlisten.then((f) => f()) } @@ -313,7 +313,7 @@ function PlayBar({ playing, setPlaying }: PlayBarProps) { const seek = (event: React.MouseEvent) => { event.stopPropagation(); let rect = seekBarRef.current!.getBoundingClientRect(); - let val = ((event.clientX-rect.left) / (rect.width))*duration; + let val = ((event.clientX-rect.left) / (rect.width))*position; invoke('seek', { time: Math.round(val * 1000) }).then() }; @@ -340,9 +340,9 @@ function PlayBar({ playing, setPlaying }: PlayBarProps) { invoke('set_volume', { volume: volume.target.value }).then(() => {}) }} />

- { Math.round(+position / 60).toString().padStart(2, "0") }: - { (+position % 60).toString().padStart(2, "0") }/ { Math.round(+duration / 60).toString().padStart(2, "0") }: + { (+position % 60).toString().padStart(2, "0") }/ + { Math.round(+position / 60).toString().padStart(2, "0") }: { (+duration % 60).toString().padStart(2, "0") }

diff --git a/src/types.ts b/src/types.ts index eace57d..5c3a261 100644 --- a/src/types.ts +++ b/src/types.ts @@ -63,7 +63,6 @@ export enum BannedType { } export interface playbackInfo { - duration?: number[], position?: [number, number], - metadata: Map + duration?: [number, number], } \ No newline at end of file