diff --git a/dmp-core/src/music_controller/connections.rs b/dmp-core/src/music_controller/connections.rs index 140ec16..d6dccb5 100644 --- a/dmp-core/src/music_controller/connections.rs +++ b/dmp-core/src/music_controller/connections.rs @@ -36,6 +36,7 @@ pub(super) enum ConnectionsNotification { TryEnableConnection(TryConnectionType) } +#[non_exhaustive] #[derive(Debug, Clone)] pub(super) enum TryConnectionType { Discord(u64), @@ -45,11 +46,10 @@ pub(super) enum TryConnectionType { auth: LastFMAuth }, ListenBrainz(String), - Custom(String) } #[derive(Debug, Clone)] -pub(super) enum LastFMAuth { +pub enum LastFMAuth { Session(Option), UserPass { username: String, @@ -74,45 +74,59 @@ pub(super) fn handle_connections( }: 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::<()>(); + let (dc_now_playing_rx, dc_now_playing_tx) = unbounded::(); + let (lb_now_playing_rx, lb_now_playing_tx) = unbounded::(); + let (lb_scrobble_rx, lb_scrobble_tx) = unbounded::<()>(); + let (last_now_playing_rx, last_now_playing_tx) = unbounded::(); + let (last_scrobble_rx, last_scrobble_tx) = unbounded::<()>(); + let mut song_scrobbled = false; use ConnectionsNotification::*; while true { match notifications_tx.recv().unwrap() { - Playback { .. } => {} + Playback { position: _position, duration: _duration } => { + if song_scrobbled { continue } + + let Some(position) = _position.map(|t| t.num_milliseconds()) else { continue }; + let Some(duration) = _duration.map(|t| t.num_milliseconds()) else { continue }; + + // Scrobble at 50% or at 4 minutes + if duration < 30000 || position == 0 { continue } + let percent_played = position as f32 / duration as f32; + + if percent_played != 0.0 && (percent_played > 0.5 || position >= 240000) { + if LB_ACTIVE.load(Ordering::Relaxed) { + lb_scrobble_rx.send(()).unwrap(); + } + if LAST_FM_ACTIVE.load(Ordering::Relaxed) { + last_scrobble_rx.send(()).unwrap(); + } + song_scrobbled = true; + } + } StateChange(state) => { if DC_ACTIVE.load(Ordering::Relaxed) { dc_state_rx.send(state.clone()).unwrap(); } } SongChange(song) => { + song_scrobbled = false; if DC_ACTIVE.load(Ordering::Relaxed) { - dc_song_rx.send(song.clone()).unwrap(); + dc_now_playing_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(); + lb_now_playing_rx.send(song.clone()).unwrap(); + } + if LAST_FM_ACTIVE.load(Ordering::Relaxed) { + last_now_playing_rx.send(song.clone()).unwrap(); } } + EOS => { continue } + AboutToFinish => { continue } TryEnableConnection(c) => { match c { TryConnectionType::Discord(client_id) => { - let (dc_song_tx, dc_state_tx) = (dc_song_tx.clone(), dc_state_tx.clone()); + let (dc_song_tx, dc_state_tx) = (dc_now_playing_tx.clone(), dc_state_tx.clone()); std::thread::Builder::new() .name("Discord RPC Handler".to_string()) .spawn(move || { @@ -122,17 +136,17 @@ pub(super) fn handle_connections( .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()); + let (lb_now_playing_tx, lb_scrobble_tx) = (lb_now_playing_tx.clone(), lb_scrobble_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); + listenbrainz_scrobble(&token, lb_now_playing_tx, lb_scrobble_tx); }) .unwrap(); } TryConnectionType::LastFM { api_key, api_secret, auth } => { let (config, notifications_rx) = (config.clone(), notifications_rx.clone()); - let (last_song_tx, last_abt_fin_tx, last_eos_tx) = (last_song_tx.clone(), last_abt_fin_tx.clone(), last_eos_tx.clone()); + let (last_now_playing_tx, last_scrobble_tx) = (last_now_playing_tx.clone(), last_scrobble_tx.clone()); std::thread::Builder::new() .name("last.fm Handler".to_string()) .spawn(move || { @@ -152,15 +166,13 @@ pub(super) fn handle_connections( scrobbler } }; - last_fm_scrobble(scrobbler, last_song_tx, last_abt_fin_tx, last_eos_tx); + last_fm_scrobble(scrobbler, last_now_playing_tx, last_scrobble_tx); }) .unwrap(); } - TryConnectionType::Custom(_) => unimplemented!() }} } } - } fn discord_rpc(client_id: u64, song_tx: Receiver, state_tx: Receiver) { @@ -249,7 +261,7 @@ fn discord_rpc(client_id: u64, song_tx: Receiver, state_tx: Receiver, abt_fn_tx: Receiver<()>, eos_tx: Receiver<()>) { +fn listenbrainz_scrobble(token: &str, now_playing_tx: Receiver, scrobble_tx: Receiver<()>) { let mut client = ListenBrainz::new(); client.authenticate(token).unwrap(); if !client.is_authenticated() { @@ -257,17 +269,15 @@ fn listenbrainz_scrobble(token: &str, song_tx: Receiver, abt_fn_tx: Receiv } 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; + let now_playing = &mut song; let client = &client; select! { - recv(song_tx) -> res => { + recv(now_playing_tx) -> res => { if let Ok(_song) = res { let artist = if let Some(tag) = _song.get_tag(&Tag::Artist) { tag.as_str() @@ -283,15 +293,11 @@ fn listenbrainz_scrobble(token: &str, song_tx: Receiver, abt_fn_tx: Receiv client.playing_now(artist, title, release).unwrap(); println!("Song Listening = {artist} - {title}"); - *song = Some(_song); + *now_playing = 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 { + recv(scrobble_tx) -> _ => { + if let Some(song) = now_playing.take() { let artist = if let Some(tag) = song.get_tag(&Tag::Artist) { tag.as_str() } else { @@ -305,7 +311,7 @@ fn listenbrainz_scrobble(token: &str, song_tx: Receiver, abt_fn_tx: Receiv let release = song.get_tag(&Tag::Key(String::from("MusicBrainzReleaseId"))).map(|id| id.as_str()); client.listen(artist, title, release).unwrap(); - println!("Song Scrobbled"); + println!("Song {title} Listened"); } } } @@ -320,6 +326,7 @@ fn last_fm_auth( ) -> Result> { let token = { tokio::runtime::Builder::new_current_thread() + .enable_all() .build() .unwrap() .block_on( @@ -345,27 +352,29 @@ fn last_fm_auth( sleep(Duration::from_millis(1000)); }; println!("Session: {}", session.key); - - config.write().connections.last_fm_session = Some(session.key); + { + let mut config = config.write(); + config.connections.last_fm_session = Some(session.key); + config.write_file().unwrap(); + } Ok(scrobbler) } -fn last_fm_scrobble(scrobbler: Scrobbler, song_tx: Receiver, abt_fn_tx: Receiver<()>, eos_tx: Receiver<()>) { +fn last_fm_scrobble(scrobbler: Scrobbler, now_playing_tx: Receiver, scrobble_tx: Receiver<()>) { // TODO: Add support for scrobble storage for later let mut song: Option = None; let mut last_song: Option = None; LAST_FM_ACTIVE.store(true, Ordering::Relaxed); - println!("ListenBrainz connected"); + println!("last.fm connected"); while true { - let song = &mut song; - let last_song = &mut last_song; + let now_playing = &mut song; let scrobbler = &scrobbler; select! { - recv(song_tx) -> res => { + recv(now_playing_tx) -> res => { if let Ok(_song) = res { let title = if let Some(tag) = _song.get_tag(&Tag::Title) { tag.as_str() @@ -388,15 +397,11 @@ fn last_fm_scrobble(scrobbler: Scrobbler, song_tx: Receiver, abt_fn_tx: Re Err(e) => println!("Error at last.fm now playing:\n{e}") }; - *song = Some(_song); + *now_playing = 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 { + recv(scrobble_tx) -> _ => { + if let Some(song) = now_playing.take() { let title = if let Some(tag) = song.get_tag(&Tag::Title) { tag.as_str() } else { @@ -414,7 +419,7 @@ fn last_fm_scrobble(scrobbler: Scrobbler, song_tx: Receiver, abt_fn_tx: Re }; match scrobbler.scrobble(&Scrobble::new(artist, title, album)) { - Ok(_) => println!("Song Scrobbled"), + Ok(_) => println!("Song {title} Scrobbled"), Err(e) => println!("Error at last.fm scrobbler:\n{e:?}") } } diff --git a/dmp-core/src/music_controller/controller.rs b/dmp-core/src/music_controller/controller.rs index aed4ece..261d633 100644 --- a/dmp-core/src/music_controller/controller.rs +++ b/dmp-core/src/music_controller/controller.rs @@ -160,6 +160,10 @@ pub struct ControllerInput { async_channel::Sender, async_channel::Receiver, ), + connections_mail: ( + crossbeam_channel::Sender, + crossbeam_channel::Receiver + ), library: MusicLibrary, config: Arc>, playback_info: Arc>, @@ -170,6 +174,7 @@ pub struct ControllerHandle { pub(super) lib_mail_rx: async_channel::Sender, pub(super) player_mail_rx: async_channel::Sender, pub(super) queue_mail_rx: async_channel::Sender, + pub(super) connections_rx: crossbeam_channel::Sender, } impl ControllerHandle { @@ -185,6 +190,7 @@ impl ControllerHandle { let (lib_mail_rx, lib_mail_tx) = async_channel::unbounded(); let (player_mail_rx, player_mail_tx) = async_channel::unbounded(); let (queue_mail_rx, queue_mail_tx) = async_channel::unbounded(); + let (connections_mail_rx, connections_mail_tx) = crossbeam_channel::unbounded(); let playback_info = Arc::new(AtomicCell::new(PlaybackInfo::default())); let notify_next_song = crossbeam::channel::unbounded::(); ( @@ -192,11 +198,13 @@ impl ControllerHandle { lib_mail_rx: lib_mail_rx.clone(), player_mail_rx: player_mail_rx.clone(), queue_mail_rx: queue_mail_rx.clone(), + connections_rx: connections_mail_rx.clone(), }, ControllerInput { player_mail: (player_mail_rx, player_mail_tx), lib_mail: (lib_mail_rx, lib_mail_tx), queue_mail: (queue_mail_rx, queue_mail_tx), + connections_mail: (connections_mail_rx, connections_mail_tx), library, config, playback_info: Arc::clone(&playback_info), @@ -241,13 +249,14 @@ impl ControllerState { } } -#[allow(unused_variables)] +// #[allow(unused_variables)] impl Controller { pub async fn start( ControllerInput { player_mail, lib_mail, queue_mail, + connections_mail, mut library, config, playback_info, @@ -276,12 +285,10 @@ impl Controller { let player_timing = player.get_timing_recv(); let about_to_finish_tx = player.get_about_to_finish_recv(); let finished_tx = player.get_finished_recv(); - let (notifications_rx, notifications_tx) = - crossbeam_channel::unbounded::(); let a = scope.spawn({ let queue_mail = queue_mail.clone(); - let _notifications_rx = notifications_rx.clone(); + let _notifications_rx = connections_mail.0.clone(); let _config = config.clone(); move || { futures::executor::block_on(async { @@ -319,7 +326,7 @@ impl Controller { }) }); - let _notifications_rx = notifications_rx.clone(); + let _notifications_rx = connections_mail.0.clone(); let c = scope.spawn(|| { Controller::player_monitor_loop( player_state, @@ -338,8 +345,8 @@ impl Controller { handle_connections( config, ControllerConnections { - notifications_rx, - notifications_tx, + notifications_rx: connections_mail.0, + notifications_tx: connections_mail.1, }, ); }); diff --git a/dmp-core/src/music_controller/controller_handle.rs b/dmp-core/src/music_controller/controller_handle.rs index 5c518aa..e31ed04 100644 --- a/dmp-core/src/music_controller/controller_handle.rs +++ b/dmp-core/src/music_controller/controller_handle.rs @@ -176,6 +176,21 @@ impl ControllerHandle { }; res } + + // The Connections Section + pub fn discord_rpc(&self, client_id: u64) { + self.connections_rx.send(super::connections::ConnectionsNotification::TryEnableConnection(super::connections::TryConnectionType::Discord(client_id))).unwrap(); + } + + pub fn listenbrainz_scrobble(&self, token: String) { + self.connections_rx.send(super::connections::ConnectionsNotification::TryEnableConnection(super::connections::TryConnectionType::ListenBrainz(token))).unwrap(); + } + + pub fn last_fm_scrobble_auth(&self, api_key: String, api_secret: String, auth: super::connections::LastFMAuth) { + self.connections_rx.send(super::connections::ConnectionsNotification::TryEnableConnection(super::connections::TryConnectionType::LastFM { api_key, api_secret, auth })).unwrap(); + } + + } pub(super) struct LibraryCommandInput { diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index 984e8e4..03deecc 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -1,15 +1,14 @@ use std::{fs::OpenOptions, io::Write}; use dmp_core::music_controller::{ - controller::{ControllerHandle, PlayerLocation}, - queue::QueueSong, + connections::LastFMAuth, controller::{ControllerHandle, PlayerLocation}, queue::QueueSong }; use kushi::QueueItem; use tauri::{AppHandle, Emitter, State, Wry}; use tempfile::TempDir; use uuid::Uuid; -use crate::wrappers::_Song; +use crate::{wrappers::_Song, LAST_FM_API_KEY, LAST_FM_API_SECRET}; #[tauri::command] pub async fn add_song_to_queue( @@ -80,3 +79,9 @@ pub async fn display_album_art( }; Ok(()) } + +#[tauri::command] +pub async fn last_fm_init_auth(ctrl_handle: State<'_, ControllerHandle>) -> Result<(), String> { + ctrl_handle.last_fm_scrobble_auth(LAST_FM_API_KEY.to_string(), LAST_FM_API_SECRET.to_string(), LastFMAuth::Session(None)); + Ok(()) +} \ No newline at end of file