more changes to connections

This commit is contained in:
MrDulfin 2025-03-20 18:52:58 -04:00
parent eab4b41210
commit 04da9c88f3
4 changed files with 99 additions and 67 deletions

View file

@ -36,6 +36,7 @@ pub(super) enum ConnectionsNotification {
TryEnableConnection(TryConnectionType) TryEnableConnection(TryConnectionType)
} }
#[non_exhaustive]
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub(super) enum TryConnectionType { pub(super) enum TryConnectionType {
Discord(u64), Discord(u64),
@ -45,11 +46,10 @@ pub(super) enum TryConnectionType {
auth: LastFMAuth auth: LastFMAuth
}, },
ListenBrainz(String), ListenBrainz(String),
Custom(String)
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub(super) enum LastFMAuth { pub enum LastFMAuth {
Session(Option<String>), Session(Option<String>),
UserPass { UserPass {
username: String, username: String,
@ -74,45 +74,59 @@ pub(super) fn handle_connections(
}: ControllerConnections, }: ControllerConnections,
) { ) {
let (dc_state_rx, dc_state_tx) = unbounded::<PrismState>(); let (dc_state_rx, dc_state_tx) = unbounded::<PrismState>();
let (dc_song_rx, dc_song_tx) = unbounded::<Song>(); let (dc_now_playing_rx, dc_now_playing_tx) = unbounded::<Song>();
let (lb_song_rx, lb_song_tx) = unbounded::<Song>(); let (lb_now_playing_rx, lb_now_playing_tx) = unbounded::<Song>();
let (lb_abt_fin_rx, lb_abt_fin_tx) = unbounded::<()>(); let (lb_scrobble_rx, lb_scrobble_tx) = unbounded::<()>();
let (lb_eos_rx, lb_eos_tx) = unbounded::<()>(); let (last_now_playing_rx, last_now_playing_tx) = unbounded::<Song>();
let (last_song_rx, last_song_tx) = unbounded::<Song>(); let (last_scrobble_rx, last_scrobble_tx) = unbounded::<()>();
let (last_abt_fin_rx, last_abt_fin_tx) = unbounded::<()>();
let (last_eos_rx, last_eos_tx) = unbounded::<()>();
let mut song_scrobbled = false;
use ConnectionsNotification::*; use ConnectionsNotification::*;
while true { while true {
match notifications_tx.recv().unwrap() { 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) => { StateChange(state) => {
if DC_ACTIVE.load(Ordering::Relaxed) { if DC_ACTIVE.load(Ordering::Relaxed) {
dc_state_rx.send(state.clone()).unwrap(); dc_state_rx.send(state.clone()).unwrap();
} }
} }
SongChange(song) => { SongChange(song) => {
song_scrobbled = false;
if DC_ACTIVE.load(Ordering::Relaxed) { 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) { if LB_ACTIVE.load(Ordering::Relaxed) {
lb_song_rx.send(song).unwrap(); lb_now_playing_rx.send(song.clone()).unwrap();
} }
} if LAST_FM_ACTIVE.load(Ordering::Relaxed) {
EOS => { last_now_playing_rx.send(song.clone()).unwrap();
if LB_ACTIVE.load(Ordering::Relaxed) {
lb_eos_rx.send(()).unwrap();
}
}
AboutToFinish => {
if LB_ACTIVE.load(Ordering::Relaxed) {
lb_abt_fin_rx.send(()).unwrap();
} }
} }
EOS => { continue }
AboutToFinish => { continue }
TryEnableConnection(c) => { match c { TryEnableConnection(c) => { match c {
TryConnectionType::Discord(client_id) => { 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() std::thread::Builder::new()
.name("Discord RPC Handler".to_string()) .name("Discord RPC Handler".to_string())
.spawn(move || { .spawn(move || {
@ -122,17 +136,17 @@ pub(super) fn handle_connections(
.unwrap(); .unwrap();
}, },
TryConnectionType::ListenBrainz(token) => { 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() std::thread::Builder::new()
.name("ListenBrainz Handler".to_string()) .name("ListenBrainz Handler".to_string())
.spawn(move || { .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(); .unwrap();
} }
TryConnectionType::LastFM { api_key, api_secret, auth } => { TryConnectionType::LastFM { api_key, api_secret, auth } => {
let (config, notifications_rx) = (config.clone(), notifications_rx.clone()); 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() std::thread::Builder::new()
.name("last.fm Handler".to_string()) .name("last.fm Handler".to_string())
.spawn(move || { .spawn(move || {
@ -152,15 +166,13 @@ pub(super) fn handle_connections(
scrobbler 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(); .unwrap();
} }
TryConnectionType::Custom(_) => unimplemented!()
}} }}
} }
} }
} }
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>) {
@ -249,7 +261,7 @@ fn discord_rpc(client_id: u64, song_tx: Receiver<Song>, state_tx: Receiver<Prism
DC_ACTIVE.store(false, Ordering::Relaxed); DC_ACTIVE.store(false, Ordering::Relaxed);
} }
fn listenbrainz_scrobble(token: &str, song_tx: Receiver<Song>, abt_fn_tx: Receiver<()>, eos_tx: Receiver<()>) { fn listenbrainz_scrobble(token: &str, now_playing_tx: Receiver<Song>, scrobble_tx: Receiver<()>) {
let mut client = ListenBrainz::new(); let mut client = ListenBrainz::new();
client.authenticate(token).unwrap(); client.authenticate(token).unwrap();
if !client.is_authenticated() { if !client.is_authenticated() {
@ -257,17 +269,15 @@ fn listenbrainz_scrobble(token: &str, song_tx: Receiver<Song>, abt_fn_tx: Receiv
} }
let mut song: Option<Song> = None; let mut song: Option<Song> = None;
let mut last_song: Option<Song> = None;
LB_ACTIVE.store(true, Ordering::Relaxed); LB_ACTIVE.store(true, Ordering::Relaxed);
println!("ListenBrainz connected"); println!("ListenBrainz connected");
while true { while true {
let song = &mut song; let now_playing = &mut song;
let last_song = &mut last_song;
let client = &client; let client = &client;
select! { select! {
recv(song_tx) -> res => { recv(now_playing_tx) -> res => {
if let Ok(_song) = res { if let Ok(_song) = res {
let artist = if let Some(tag) = _song.get_tag(&Tag::Artist) { let artist = if let Some(tag) = _song.get_tag(&Tag::Artist) {
tag.as_str() tag.as_str()
@ -283,15 +293,11 @@ fn listenbrainz_scrobble(token: &str, song_tx: Receiver<Song>, abt_fn_tx: Receiv
client.playing_now(artist, title, release).unwrap(); client.playing_now(artist, title, release).unwrap();
println!("Song Listening = {artist} - {title}"); println!("Song Listening = {artist} - {title}");
*song = Some(_song); *now_playing = Some(_song);
} }
}, },
recv(abt_fn_tx) -> _ => { recv(scrobble_tx) -> _ => {
*last_song = song.take(); if let Some(song) = now_playing.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) { let artist = if let Some(tag) = song.get_tag(&Tag::Artist) {
tag.as_str() tag.as_str()
} else { } else {
@ -305,7 +311,7 @@ fn listenbrainz_scrobble(token: &str, song_tx: Receiver<Song>, abt_fn_tx: Receiv
let release = song.get_tag(&Tag::Key(String::from("MusicBrainzReleaseId"))).map(|id| id.as_str()); let release = song.get_tag(&Tag::Key(String::from("MusicBrainzReleaseId"))).map(|id| id.as_str());
client.listen(artist, title, release).unwrap(); client.listen(artist, title, release).unwrap();
println!("Song Scrobbled"); println!("Song {title} Listened");
} }
} }
} }
@ -320,6 +326,7 @@ fn last_fm_auth(
) -> Result<Scrobbler, Box<dyn std::error::Error>> { ) -> Result<Scrobbler, Box<dyn std::error::Error>> {
let token = { let token = {
tokio::runtime::Builder::new_current_thread() tokio::runtime::Builder::new_current_thread()
.enable_all()
.build() .build()
.unwrap() .unwrap()
.block_on( .block_on(
@ -345,27 +352,29 @@ fn last_fm_auth(
sleep(Duration::from_millis(1000)); sleep(Duration::from_millis(1000));
}; };
println!("Session: {}", session.key); 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) Ok(scrobbler)
} }
fn last_fm_scrobble(scrobbler: Scrobbler, song_tx: Receiver<Song>, abt_fn_tx: Receiver<()>, eos_tx: Receiver<()>) { fn last_fm_scrobble(scrobbler: Scrobbler, now_playing_tx: Receiver<Song>, scrobble_tx: Receiver<()>) {
// TODO: Add support for scrobble storage for later // TODO: Add support for scrobble storage for later
let mut song: Option<Song> = None; let mut song: Option<Song> = None;
let mut last_song: Option<Song> = None; let mut last_song: Option<Song> = None;
LAST_FM_ACTIVE.store(true, Ordering::Relaxed); LAST_FM_ACTIVE.store(true, Ordering::Relaxed);
println!("ListenBrainz connected"); println!("last.fm connected");
while true { while true {
let song = &mut song; let now_playing = &mut song;
let last_song = &mut last_song;
let scrobbler = &scrobbler; let scrobbler = &scrobbler;
select! { select! {
recv(song_tx) -> res => { recv(now_playing_tx) -> res => {
if let Ok(_song) = res { if let Ok(_song) = res {
let title = if let Some(tag) = _song.get_tag(&Tag::Title) { let title = if let Some(tag) = _song.get_tag(&Tag::Title) {
tag.as_str() tag.as_str()
@ -388,15 +397,11 @@ fn last_fm_scrobble(scrobbler: Scrobbler, song_tx: Receiver<Song>, abt_fn_tx: Re
Err(e) => println!("Error at last.fm now playing:\n{e}") Err(e) => println!("Error at last.fm now playing:\n{e}")
}; };
*song = Some(_song); *now_playing = Some(_song);
} }
}, },
recv(abt_fn_tx) -> _ => { recv(scrobble_tx) -> _ => {
*last_song = song.take(); if let Some(song) = now_playing.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 title = if let Some(tag) = song.get_tag(&Tag::Title) { let title = if let Some(tag) = song.get_tag(&Tag::Title) {
tag.as_str() tag.as_str()
} else { } else {
@ -414,7 +419,7 @@ fn last_fm_scrobble(scrobbler: Scrobbler, song_tx: Receiver<Song>, abt_fn_tx: Re
}; };
match scrobbler.scrobble(&Scrobble::new(artist, title, album)) { 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:?}") Err(e) => println!("Error at last.fm scrobbler:\n{e:?}")
} }
} }

View file

@ -160,6 +160,10 @@ pub struct ControllerInput {
async_channel::Sender<QueueCommandInput>, async_channel::Sender<QueueCommandInput>,
async_channel::Receiver<QueueCommandInput>, async_channel::Receiver<QueueCommandInput>,
), ),
connections_mail: (
crossbeam_channel::Sender<ConnectionsNotification>,
crossbeam_channel::Receiver<ConnectionsNotification>
),
library: MusicLibrary, library: MusicLibrary,
config: Arc<RwLock<Config>>, config: Arc<RwLock<Config>>,
playback_info: Arc<AtomicCell<PlaybackInfo>>, playback_info: Arc<AtomicCell<PlaybackInfo>>,
@ -170,6 +174,7 @@ pub struct ControllerHandle {
pub(super) lib_mail_rx: async_channel::Sender<LibraryCommandInput>, pub(super) lib_mail_rx: async_channel::Sender<LibraryCommandInput>,
pub(super) player_mail_rx: async_channel::Sender<PlayerCommandInput>, pub(super) player_mail_rx: async_channel::Sender<PlayerCommandInput>,
pub(super) queue_mail_rx: async_channel::Sender<QueueCommandInput>, pub(super) queue_mail_rx: async_channel::Sender<QueueCommandInput>,
pub(super) connections_rx: crossbeam_channel::Sender<ConnectionsNotification>,
} }
impl ControllerHandle { impl ControllerHandle {
@ -185,6 +190,7 @@ impl ControllerHandle {
let (lib_mail_rx, lib_mail_tx) = async_channel::unbounded(); let (lib_mail_rx, lib_mail_tx) = async_channel::unbounded();
let (player_mail_rx, player_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 (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 playback_info = Arc::new(AtomicCell::new(PlaybackInfo::default()));
let notify_next_song = crossbeam::channel::unbounded::<Song>(); let notify_next_song = crossbeam::channel::unbounded::<Song>();
( (
@ -192,11 +198,13 @@ impl ControllerHandle {
lib_mail_rx: lib_mail_rx.clone(), lib_mail_rx: lib_mail_rx.clone(),
player_mail_rx: player_mail_rx.clone(), player_mail_rx: player_mail_rx.clone(),
queue_mail_rx: queue_mail_rx.clone(), queue_mail_rx: queue_mail_rx.clone(),
connections_rx: connections_mail_rx.clone(),
}, },
ControllerInput { ControllerInput {
player_mail: (player_mail_rx, player_mail_tx), player_mail: (player_mail_rx, player_mail_tx),
lib_mail: (lib_mail_rx, lib_mail_tx), lib_mail: (lib_mail_rx, lib_mail_tx),
queue_mail: (queue_mail_rx, queue_mail_tx), queue_mail: (queue_mail_rx, queue_mail_tx),
connections_mail: (connections_mail_rx, connections_mail_tx),
library, library,
config, config,
playback_info: Arc::clone(&playback_info), playback_info: Arc::clone(&playback_info),
@ -241,13 +249,14 @@ impl ControllerState {
} }
} }
#[allow(unused_variables)] // #[allow(unused_variables)]
impl Controller { impl Controller {
pub async fn start( pub async fn start(
ControllerInput { ControllerInput {
player_mail, player_mail,
lib_mail, lib_mail,
queue_mail, queue_mail,
connections_mail,
mut library, mut library,
config, config,
playback_info, playback_info,
@ -276,12 +285,10 @@ impl Controller {
let player_timing = player.get_timing_recv(); let player_timing = player.get_timing_recv();
let about_to_finish_tx = player.get_about_to_finish_recv(); let about_to_finish_tx = player.get_about_to_finish_recv();
let finished_tx = player.get_finished_recv(); let finished_tx = player.get_finished_recv();
let (notifications_rx, notifications_tx) =
crossbeam_channel::unbounded::<ConnectionsNotification>();
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(); let _notifications_rx = connections_mail.0.clone();
let _config = config.clone(); let _config = config.clone();
move || { move || {
futures::executor::block_on(async { 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(|| { let c = scope.spawn(|| {
Controller::player_monitor_loop( Controller::player_monitor_loop(
player_state, player_state,
@ -338,8 +345,8 @@ impl Controller {
handle_connections( handle_connections(
config, config,
ControllerConnections { ControllerConnections {
notifications_rx, notifications_rx: connections_mail.0,
notifications_tx, notifications_tx: connections_mail.1,
}, },
); );
}); });

View file

@ -176,6 +176,21 @@ impl ControllerHandle {
}; };
res 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 { pub(super) struct LibraryCommandInput {

View file

@ -1,15 +1,14 @@
use std::{fs::OpenOptions, io::Write}; use std::{fs::OpenOptions, io::Write};
use dmp_core::music_controller::{ use dmp_core::music_controller::{
controller::{ControllerHandle, PlayerLocation}, connections::LastFMAuth, controller::{ControllerHandle, PlayerLocation}, queue::QueueSong
queue::QueueSong,
}; };
use kushi::QueueItem; use kushi::QueueItem;
use tauri::{AppHandle, Emitter, State, Wry}; use tauri::{AppHandle, Emitter, State, Wry};
use tempfile::TempDir; use tempfile::TempDir;
use uuid::Uuid; use uuid::Uuid;
use crate::wrappers::_Song; use crate::{wrappers::_Song, LAST_FM_API_KEY, LAST_FM_API_SECRET};
#[tauri::command] #[tauri::command]
pub async fn add_song_to_queue( pub async fn add_song_to_queue(
@ -80,3 +79,9 @@ pub async fn display_album_art(
}; };
Ok(()) 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(())
}