From 43a98d115102b8029684b557613577ad1d8fb762 Mon Sep 17 00:00:00 2001 From: MrDulfin Date: Wed, 19 Mar 2025 02:48:08 -0400 Subject: [PATCH] Changed connections to take their input from the front-end --- dmp-core/Cargo.toml | 4 + dmp-core/src/config/mod.rs | 2 + dmp-core/src/music_controller/connections.rs | 484 +++++++++++-------- dmp-core/src/music_controller/controller.rs | 33 +- 4 files changed, 296 insertions(+), 227 deletions(-) diff --git a/dmp-core/Cargo.toml b/dmp-core/Cargo.toml index 481331e..dfe8173 100644 --- a/dmp-core/Cargo.toml +++ b/dmp-core/Cargo.toml @@ -41,3 +41,7 @@ prismriver = { git = "https://github.com/Dangoware/prismriver.git" } parking_lot = "0.12.3" discord-presence = { version = "1.4.1", features = ["activity_type"] } listenbrainz = "0.8.1" +rustfm-scrobble = "1.1.1" +reqwest = { version = "0.12.12", features = ["json"] } +tokio = { version = "1.43.0", features = ["macros"] } +opener = "0.7.2" diff --git a/dmp-core/src/config/mod.rs b/dmp-core/src/config/mod.rs index 931830b..86b6f65 100644 --- a/dmp-core/src/config/mod.rs +++ b/dmp-core/src/config/mod.rs @@ -94,7 +94,9 @@ impl ConfigLibraries { #[derive(Debug, Default, Serialize, Deserialize, Clone)] pub struct ConfigConnections { + pub discord_rpc_client_id: Option, pub listenbrainz_token: Option, + pub last_fm_session: Option, } #[derive(Debug, Default, Serialize, Deserialize, Clone)] diff --git a/dmp-core/src/music_controller/connections.rs b/dmp-core/src/music_controller/connections.rs index 82485c7..af18d4e 100644 --- a/dmp-core/src/music_controller/connections.rs +++ b/dmp-core/src/music_controller/connections.rs @@ -8,19 +8,20 @@ use std::{ }; use chrono::TimeDelta; -use crossbeam::{scope, select}; -use crossbeam_channel::{unbounded, Receiver}; +use crossbeam::select; +use crossbeam_channel::{unbounded, Receiver, Sender}; use discord_presence::Client; use listenbrainz::ListenBrainz; use parking_lot::RwLock; use prismriver::State as PrismState; +use rustfm_scrobble::Scrobbler; +use serde::Deserialize; use crate::{ config::Config, music_storage::library::{Song, Tag}, }; -use super::controller::Controller; #[derive(Debug, Clone)] pub(super) enum ConnectionsNotification { @@ -32,241 +33,306 @@ pub(super) enum ConnectionsNotification { SongChange(Song), AboutToFinish, EOS, + TryEnableConnection(TryConnectionType) } -#[derive(Debug)] -pub struct ConnectionsInput { - pub discord_rpc_client_id: Option, +#[derive(Debug, Clone)] +pub(super) enum TryConnectionType { + Discord(u64), + LastFM { + api_key: String, + api_secret: String, + session: Option + }, + ListenBrainz(String), + Custom(String) } pub(super) struct ControllerConnections { + pub notifications_rx: Sender, pub notifications_tx: Receiver, - pub inner: ConnectionsInput, } static DC_ACTIVE: AtomicBool = AtomicBool::new(false); static LB_ACTIVE: AtomicBool = AtomicBool::new(false); +static LAST_FM_ACTIVE: AtomicBool = AtomicBool::new(false); -impl Controller { - pub(super) fn handle_connections( - config: Arc>, - ControllerConnections { - notifications_tx, - inner: ConnectionsInput { - discord_rpc_client_id, - }, - }: ControllerConnections, - ) { - let (dc_state_rx, dc_state_tx) = unbounded::(); - let (dc_song_rx, dc_song_tx) = unbounded::(); - let (lb_song_rx, lb_song_tx) = unbounded::(); - let (lb_abt_fin_rx, lb_abt_fn_tx) = unbounded::<()>(); - let (lb_eos_rx, lb_eos_tx) = unbounded::<()>(); +pub(super) fn handle_connections( + config: Arc>, + ControllerConnections { + notifications_rx, + notifications_tx, + }: ControllerConnections, +) { + let (dc_state_rx, dc_state_tx) = unbounded::(); + let (dc_song_rx, dc_song_tx) = unbounded::(); + let (lb_song_rx, lb_song_tx) = unbounded::(); + let (lb_abt_fin_rx, lb_abt_fin_tx) = unbounded::<()>(); + let (lb_eos_rx, lb_eos_tx) = unbounded::<()>(); + let (last_song_rx, last_song_tx) = unbounded::(); + let (last_abt_fin_rx, last_abt_fin_tx) = unbounded::<()>(); + let (last_eos_rx, last_eos_tx) = unbounded::<()>(); - scope(|s| { - s.builder() - .name("Notifications Sorter".to_string()) - .spawn(|_| { - use ConnectionsNotification::*; - while true { - match notifications_tx.recv().unwrap() { - Playback { .. } => {} - StateChange(state) => { - if DC_ACTIVE.load(Ordering::Relaxed) { - dc_state_rx.send(state.clone()).unwrap(); - } - } - SongChange(song) => { - if DC_ACTIVE.load(Ordering::Relaxed) { - dc_song_rx.send(song.clone()).unwrap(); - } - if LB_ACTIVE.load(Ordering::Relaxed) { - lb_song_rx.send(song).unwrap(); - } - } - EOS => { - if LB_ACTIVE.load(Ordering::Relaxed) { - lb_eos_rx.send(()).unwrap(); - } - } - AboutToFinish => { - if LB_ACTIVE.load(Ordering::Relaxed) { - lb_abt_fin_rx.send(()).unwrap(); - } - } - } - } - }) - .unwrap(); - if let Some(client_id) = discord_rpc_client_id { - s.builder() - .name("Discord RPC Handler".to_string()) - .spawn(move |_| { - Controller::discord_rpc(client_id, dc_song_tx, dc_state_tx); - }) - .unwrap(); - }; - - if let Some(token) = config.read().connections.listenbrainz_token.clone() { - s.builder() - .name("ListenBrainz Handler".to_string()) - .spawn(move |_| { - Controller::listenbrainz_scrobble(&token, lb_song_tx, lb_abt_fn_tx, lb_eos_tx); - }) - .unwrap(); + use ConnectionsNotification::*; + while true { + match notifications_tx.recv().unwrap() { + Playback { .. } => {} + StateChange(state) => { + if DC_ACTIVE.load(Ordering::Relaxed) { + dc_state_rx.send(state.clone()).unwrap(); + } } - }) - .unwrap(); + SongChange(song) => { + if DC_ACTIVE.load(Ordering::Relaxed) { + dc_song_rx.send(song.clone()).unwrap(); + } + if LB_ACTIVE.load(Ordering::Relaxed) { + lb_song_rx.send(song).unwrap(); + } + } + EOS => { + if LB_ACTIVE.load(Ordering::Relaxed) { + lb_eos_rx.send(()).unwrap(); + } + } + AboutToFinish => { + if LB_ACTIVE.load(Ordering::Relaxed) { + lb_abt_fin_rx.send(()).unwrap(); + } + } + TryEnableConnection(c) => { match c { + TryConnectionType::Discord(client_id) => { + let (dc_song_tx, dc_state_tx) = (dc_song_tx.clone(), dc_state_tx.clone()); + std::thread::Builder::new() + .name("Discord RPC Handler".to_string()) + .spawn(move || { + // TODO: add proper error handling here + discord_rpc(client_id, dc_song_tx, dc_state_tx); + }) + .unwrap(); + }, + TryConnectionType::ListenBrainz(token) => { + let (lb_song_tx, lb_abt_fin_tx, lb_eos_tx) = (lb_song_tx.clone(), lb_abt_fin_tx.clone(), lb_eos_tx.clone()); + std::thread::Builder::new() + .name("ListenBrainz Handler".to_string()) + .spawn(move || { + listenbrainz_scrobble(&token, lb_song_tx, lb_abt_fin_tx, lb_eos_tx); + }) + .unwrap(); + } + TryConnectionType::LastFM { api_key, api_secret, session } => { + let (config, notifications_rx) = (config.clone(), notifications_rx.clone()); + std::thread::Builder::new() + .name("last.fm Handler".to_string()) + .spawn(move || { + let scrobbler = if let Some(session) = session { + let mut scrobbler = Scrobbler::new(&api_key, &api_secret); + scrobbler.authenticate_with_session_key(&session); + Ok(scrobbler) + } else { + last_fm_auth(config, notifications_rx, &api_key, &api_secret) + }; + // TODO: Add scrobbling support + }) + .unwrap(); + } + TryConnectionType::Custom(_) => unimplemented!() + }} + } } - fn discord_rpc(client_id: u64, song_tx: Receiver, state_tx: Receiver) { - // TODO: Handle seeking position change and pause - let mut client = - discord_presence::Client::with_error_config(client_id, Duration::from_secs(5), None); - client.start(); - while !Client::is_ready() { - sleep(Duration::from_millis(100)); +} + +fn discord_rpc(client_id: u64, song_tx: Receiver, state_tx: Receiver) { + // TODO: Handle seeking position change and pause + let mut client = + discord_presence::Client::with_error_config(client_id, Duration::from_secs(5), None); + client.start(); + while !Client::is_ready() { + sleep(Duration::from_millis(100)); + } + println!("discord connected"); + + let mut state = "Started".to_string(); + let mut song: Option = None; + let mut now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("Time went backwards?") + .as_secs(); + DC_ACTIVE.store(true, Ordering::Relaxed); + + while true { + let state = &mut state; + let song = &mut song; + select! { + recv(state_tx) -> res => { + if let Ok(state_) = res { + *state = match state_ { + PrismState::Playing => "Playing", + PrismState::Paused => "Paused", + PrismState::Stopped => "Stopped", + _ => "I'm Scared, Boss" + }.to_string(); + } + }, + recv(song_tx) -> res => { + if let Ok(song_) = res { + *song = Some(song_); + now = SystemTime::now().duration_since(UNIX_EPOCH).expect("Time went backwards?").as_secs(); + } + }, + default(Duration::from_millis(99)) => () } - println!("discord connected"); - let mut state = "Started".to_string(); - let mut song: Option = None; - let mut now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("Time went backwards?") - .as_secs(); - DC_ACTIVE.store(true, Ordering::Relaxed); - - while true { - let state = &mut state; - let song = &mut song; - select! { - recv(state_tx) -> res => { - if let Ok(state_) = res { - *state = match state_ { - PrismState::Playing => "Playing", - PrismState::Paused => "Paused", - PrismState::Stopped => "Stopped", - _ => "I'm Scared, Boss" - }.to_string(); - } - }, - recv(song_tx) -> res => { - if let Ok(song_) = res { - *song = Some(song_); - now = SystemTime::now().duration_since(UNIX_EPOCH).expect("Time went backwards?").as_secs(); - } - }, - default(Duration::from_millis(99)) => () - } - - client - .set_activity(|activity| { - let a = activity - .state(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) - .details(if let Some(song) = song { - song.get_tag(&Tag::Title) - .map_or(String::from("Unknown Title"), |title| title.clone()) - } else { - String::new() - }); - if let Some(s) = song { - if state.as_str() == "Playing" { - a.timestamps(|timestamps| { - timestamps.start(now).end(now + s.duration.as_secs()) - }) - } else { - a - } + client + .set_activity(|activity| { + let a = activity + .state(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) + .details(if let Some(song) = song { + song.get_tag(&Tag::Title) + .map_or(String::from("Unknown Title"), |title| title.clone()) + } else { + String::new() + }); + if let Some(s) = song { + if state.as_str() == "Playing" { + a.timestamps(|timestamps| { + timestamps.start(now).end(now + s.duration.as_secs()) + }) } else { a } - .assets(|a| a.large_text(state.clone())) - .instance(true) - }) - .unwrap(); - } - DC_ACTIVE.store(false, Ordering::Relaxed); + } else { + a + } + .assets(|a| a.large_text(state.clone())) + .instance(true) + }) + .unwrap(); + } + DC_ACTIVE.store(false, Ordering::Relaxed); +} + +fn listenbrainz_scrobble(token: &str, song_tx: Receiver, abt_fn_tx: Receiver<()>, eos_tx: Receiver<()>) { + let mut client = ListenBrainz::new(); + client.authenticate(token).unwrap(); + if !client.is_authenticated() { + return; } - fn listenbrainz_scrobble(token: &str, song_tx: Receiver, abt_fn_tx: Receiver<()>, eos_tx: Receiver<()>) { - let mut client = ListenBrainz::new(); - client.authenticate(token).unwrap(); - if !client.is_authenticated() { - return; - } + let mut song: Option = None; + let mut last_song: Option = None; + LB_ACTIVE.store(true, Ordering::Relaxed); + println!("ListenBrainz connected"); - let mut song: Option = None; - let mut last_song: Option = None; - LB_ACTIVE.store(true, Ordering::Relaxed); - println!("ListenBrainz connected"); + while true { + let song = &mut song; + let last_song = &mut last_song; - while true { - let song = &mut song; - let last_song = &mut last_song; + let client = &client; + select! { + recv(song_tx) -> res => { + if let Ok(_song) = res { + let artist = if let Some(tag) = _song.get_tag(&Tag::Artist) { + tag.as_str() + } else { + continue + }; + let title = if let Some(tag) = _song.get_tag(&Tag::Title) { + tag.as_str() + } else { + continue + }; + let release = _song.get_tag(&Tag::Key(String::from("MusicBrainzReleaseId"))).map(|id| id.as_str()); - let client = &client; - select! { - recv(song_tx) -> res => { - if let Ok(_song) = res { - let artist = if let Some(tag) = _song.get_tag(&Tag::Artist) { - tag.as_str() - } else { - continue - }; - let title = if let Some(tag) = _song.get_tag(&Tag::Title) { - tag.as_str() - } else { - continue - }; - let release = _song.get_tag(&Tag::Key(String::from("MusicBrainzReleaseId"))).map(|id| id.as_str()); + client.playing_now(artist, title, release).unwrap(); + println!("Song Listening = {artist} - {title}"); + *song = Some(_song); + } + }, + recv(abt_fn_tx) -> _ => { + *last_song = song.take(); + println!("song = {:?}", last_song.as_ref().map(|s| s.get_tag(&Tag::Title).map_or("No Title", |t| t.as_str()))); + }, + recv(eos_tx) -> _ => { + if let Some(song) = last_song { + let artist = if let Some(tag) = song.get_tag(&Tag::Artist) { + tag.as_str() + } else { + continue + }; + let title = if let Some(tag) = song.get_tag(&Tag::Title) { + tag.as_str() + } else { + continue + }; + let release = song.get_tag(&Tag::Key(String::from("MusicBrainzReleaseId"))).map(|id| id.as_str()); - client.playing_now(artist, title, release).unwrap(); - println!("Song Listening = {artist} - {title}"); - *song = Some(_song); - } - }, - recv(abt_fn_tx) -> _ => { - *last_song = song.take(); - println!("song = {:?}", last_song.as_ref().map(|s| s.get_tag(&Tag::Title).map_or("No Title", |t| t.as_str()))); - }, - recv(eos_tx) -> _ => { - if let Some(song) = last_song { - let artist = if let Some(tag) = song.get_tag(&Tag::Artist) { - tag.as_str() - } else { - continue - }; - let title = if let Some(tag) = song.get_tag(&Tag::Title) { - tag.as_str() - } else { - continue - }; - let release = song.get_tag(&Tag::Key(String::from("MusicBrainzReleaseId"))).map(|id| id.as_str()); - - client.listen(artist, title, release).unwrap(); - println!("Song Scrobbled"); - } + client.listen(artist, title, release).unwrap(); + println!("Song Scrobbled"); } } } - LB_ACTIVE.store(false, Ordering::Relaxed); } + LB_ACTIVE.store(false, Ordering::Relaxed); +} +pub(super) fn last_fm_auth( + config: Arc>, + notifications_rx: Sender, + api_key: &str, + api_secret: &str +) -> Result> { + let token = { + tokio::runtime::Builder::new_current_thread() + .build() + .unwrap() + .block_on( + async { + reqwest::get( + format!("http://ws.audioscrobbler.com/2.0/?method=auth.gettoken&api_key={api_key}&format=json")) + .await + .unwrap() + .json::() + .await + .unwrap() + } + ) + }; + let mut scrobbler = Scrobbler::new(api_key, api_secret); + println!("Token: {}", token.token); + opener::open_browser(format!("http://www.last.fm/api/auth/?api_key={api_key}&token={}", token.token)).unwrap(); + + let session = loop { + if let Ok(session) = scrobbler.authenticate_with_token(&token.token) { + break session; + } + sleep(Duration::from_millis(1000)); + }; + println!("Session: {}", session.key); + + config.write().connections.last_fm_session = Some(session.key); + Ok(scrobbler) +} + + +#[derive(Deserialize)] +pub struct Token { + token: String, } \ 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 ea63369..aed4ece 100644 --- a/dmp-core/src/music_controller/controller.rs +++ b/dmp-core/src/music_controller/controller.rs @@ -20,11 +20,12 @@ use thiserror::Error; use uuid::Uuid; use crate::config::ConfigError; +use crate::music_controller::connections::handle_connections; use crate::music_storage::library::Song; use crate::music_storage::playlist::{ExternalPlaylist, Playlist}; use crate::{config::Config, music_storage::library::MusicLibrary}; -use super::connections::{ConnectionsInput, ConnectionsNotification, ControllerConnections}; +use super::connections::{ConnectionsNotification, ControllerConnections}; use super::controller_handle::{LibraryCommandInput, PlayerCommandInput, QueueCommandInput}; use super::queue::{QueueAlbum, QueueSong}; @@ -163,7 +164,6 @@ pub struct ControllerInput { config: Arc>, playback_info: Arc>, notify_next_song: Sender, - connections: Option, } pub struct ControllerHandle { @@ -176,7 +176,6 @@ impl ControllerHandle { pub fn new( library: MusicLibrary, config: Arc>, - connections: Option, ) -> ( Self, ControllerInput, @@ -202,7 +201,6 @@ impl ControllerHandle { config, playback_info: Arc::clone(&playback_info), notify_next_song: notify_next_song.0, - connections, }, playback_info, notify_next_song.1, @@ -254,7 +252,6 @@ impl Controller { config, playback_info, notify_next_song, - connections, }: ControllerInput, ) -> Result<(), Box> { let queue: Queue = Queue { @@ -322,6 +319,7 @@ impl Controller { }) }); + let _notifications_rx = notifications_rx.clone(); let c = scope.spawn(|| { Controller::player_monitor_loop( player_state, @@ -330,27 +328,26 @@ impl Controller { finished_tx, player_mail.0, notify_next_song, - notifications_rx, + _notifications_rx, playback_info, ) .unwrap(); }); - if let Some(inner) = connections { - dbg!(&inner); - let d = scope.spawn(|| { - Controller::handle_connections( - config, - ControllerConnections { - notifications_tx, - inner, - }, - ); - }); - } + let d = scope.spawn(|| { + handle_connections( + config, + ControllerConnections { + notifications_rx, + notifications_tx, + }, + ); + }); + a.join().unwrap(); b.join().unwrap(); c.join().unwrap(); + d.join().unwrap(); }); Ok(())