mirror of
https://github.com/Dangoware/dango-music-player.git
synced 2025-04-19 10:02:53 -05:00
Implemented ListenBrainz Scrobbling
This commit is contained in:
parent
eed00bc336
commit
64e41af96f
5 changed files with 85 additions and 36 deletions
|
@ -40,3 +40,4 @@ itertools = "0.13.0"
|
||||||
prismriver = { git = "https://github.com/Dangoware/prismriver.git"}
|
prismriver = { git = "https://github.com/Dangoware/prismriver.git"}
|
||||||
parking_lot = "0.12.3"
|
parking_lot = "0.12.3"
|
||||||
discord-presence = { version = "1.4.1", features = ["activity_type"] }
|
discord-presence = { version = "1.4.1", features = ["activity_type"] }
|
||||||
|
listenbrainz = "0.8.1"
|
||||||
|
|
|
@ -1,15 +1,17 @@
|
||||||
#![allow(while_true)]
|
#![allow(while_true)]
|
||||||
use std::{thread::sleep, time::{Duration, SystemTime, UNIX_EPOCH}};
|
use std::{sync::Arc, thread::sleep, time::{Duration, SystemTime, UNIX_EPOCH}};
|
||||||
|
|
||||||
use chrono::TimeDelta;
|
use chrono::TimeDelta;
|
||||||
use crossbeam::{scope, select};
|
use crossbeam::{scope, select};
|
||||||
use crossbeam_channel::{bounded, Receiver};
|
use crossbeam_channel::{bounded, Receiver};
|
||||||
use discord_presence::Client;
|
use discord_presence::Client;
|
||||||
|
use listenbrainz::ListenBrainz;
|
||||||
|
use parking_lot::RwLock;
|
||||||
use prismriver::State as PrismState;
|
use prismriver::State as PrismState;
|
||||||
|
|
||||||
use crate::music_storage::library::{Song, Tag};
|
use crate::{config::Config, music_storage::library::{Song, Tag}};
|
||||||
|
|
||||||
use super::controller::Controller;
|
use super::controller::{Controller, PlaybackInfo};
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub(super) enum ConnectionsNotification {
|
pub(super) enum ConnectionsNotification {
|
||||||
|
@ -19,6 +21,7 @@ pub(super) enum ConnectionsNotification {
|
||||||
},
|
},
|
||||||
StateChange(PrismState),
|
StateChange(PrismState),
|
||||||
SongChange(Song),
|
SongChange(Song),
|
||||||
|
EOS,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
@ -32,7 +35,9 @@ pub(super) struct ControllerConnections {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Controller {
|
impl Controller {
|
||||||
pub(super) fn handle_connections(ControllerConnections {
|
pub(super) fn handle_connections(
|
||||||
|
config: Arc<RwLock<Config>>,
|
||||||
|
ControllerConnections {
|
||||||
notifications_tx,
|
notifications_tx,
|
||||||
inner: ConnectionsInput {
|
inner: ConnectionsInput {
|
||||||
discord_rpc_client_id
|
discord_rpc_client_id
|
||||||
|
@ -41,35 +46,46 @@ impl Controller {
|
||||||
) {
|
) {
|
||||||
let (dc_state_rx, dc_state_tx) = bounded::<PrismState>(1);
|
let (dc_state_rx, dc_state_tx) = bounded::<PrismState>(1);
|
||||||
let (dc_song_rx, dc_song_tx) = bounded::<Song>(1);
|
let (dc_song_rx, dc_song_tx) = bounded::<Song>(1);
|
||||||
|
let (lb_song_rx, lb_song_tx) = bounded::<Song>(1);
|
||||||
|
let (lb_eos_rx, lb_eos_tx) = bounded::<()>(1);
|
||||||
scope(|s| {
|
scope(|s| {
|
||||||
s.builder().name("Notifications Sorter".to_string()).spawn(|_| {
|
s.builder().name("Notifications Sorter".to_string()).spawn(|_| {
|
||||||
use ConnectionsNotification::*;
|
use ConnectionsNotification::*;
|
||||||
while true {
|
while true {
|
||||||
match notifications_tx.recv().unwrap() {
|
match notifications_tx.recv().unwrap() {
|
||||||
Playback { position, duration } => { continue; }
|
Playback { position, duration } => {}
|
||||||
StateChange(state) => {
|
StateChange(state) => {
|
||||||
dc_state_rx.send(state.clone()).unwrap();
|
dc_state_rx.send(state.clone()).unwrap();
|
||||||
}
|
}
|
||||||
SongChange(song) => {
|
SongChange(song) => {
|
||||||
dc_song_rx.send(song).unwrap();
|
dc_song_rx.send(song.clone()).unwrap();
|
||||||
|
lb_song_rx.send(song).unwrap();
|
||||||
|
}
|
||||||
|
EOS => {
|
||||||
|
lb_eos_rx.send(()).unwrap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}).unwrap();
|
}).unwrap();
|
||||||
|
|
||||||
if let Some(client_id) = discord_rpc_client_id {
|
if let Some(client_id) = discord_rpc_client_id {
|
||||||
println!("Discord thingy detected");
|
|
||||||
s.builder().name("Discord RPC Handler".to_string()).spawn(move |_| {
|
s.builder().name("Discord RPC Handler".to_string()).spawn(move |_| {
|
||||||
Controller::discord_rpc(client_id, dc_song_tx, dc_state_tx);
|
Controller::discord_rpc(client_id, dc_song_tx, dc_state_tx);
|
||||||
}).unwrap();
|
}).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_eos_tx);
|
||||||
|
}).unwrap();
|
||||||
|
}
|
||||||
}).unwrap();
|
}).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
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>) {
|
||||||
// TODO: Handle seeking position change
|
// TODO: Handle seeking position change and pause
|
||||||
std::thread::spawn(move || {
|
std::thread::spawn(move || {
|
||||||
let mut client = discord_presence::Client::new(client_id);
|
let mut client = discord_presence::Client::with_error_config(client_id, Duration::from_secs(5), None);
|
||||||
client.start();
|
client.start();
|
||||||
while !Client::is_ready() { sleep(Duration::from_millis(100)); }
|
while !Client::is_ready() { sleep(Duration::from_millis(100)); }
|
||||||
println!("discord connected");
|
println!("discord connected");
|
||||||
|
@ -97,8 +113,8 @@ impl Controller {
|
||||||
*song = Some(song_);
|
*song = Some(song_);
|
||||||
now = SystemTime::now().duration_since(UNIX_EPOCH).expect("Time went backwards?").as_secs();
|
now = SystemTime::now().duration_since(UNIX_EPOCH).expect("Time went backwards?").as_secs();
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
default(Duration::from_millis(4500)) => ()
|
default(Duration::from_millis(99)) => ()
|
||||||
}
|
}
|
||||||
|
|
||||||
client.set_activity(|activity| {
|
client.set_activity(|activity| {
|
||||||
|
@ -131,17 +147,45 @@ impl Controller {
|
||||||
a
|
a
|
||||||
}.assets(|a| {
|
}.assets(|a| {
|
||||||
a.large_text(state.clone())
|
a.large_text(state.clone())
|
||||||
})
|
}).instance(true)
|
||||||
}).unwrap();
|
}).unwrap();
|
||||||
println!("Updated Discord Status");
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn listenbrainz_scrobble(token: &str, song_tx: Receiver<Song>, eos_tx: Receiver<()>) {
|
||||||
|
let mut client = ListenBrainz::new();
|
||||||
|
client.authenticate(token).unwrap();
|
||||||
|
if !client.is_authenticated() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut song: Option<Song> = None;
|
||||||
|
while true {
|
||||||
|
let song = &mut song;
|
||||||
|
let client = &client;
|
||||||
|
select! {
|
||||||
|
recv(song_tx) -> res => {
|
||||||
|
if let Ok(_song) = res {
|
||||||
|
client.playing_now(_song.get_tag(&Tag::Artist).map_or("", |tag| tag.as_str()), _song.get_tag(&Tag::Title).map_or("", |tag| tag.as_str()), None).unwrap();
|
||||||
|
*song = Some(_song);
|
||||||
|
println!("Song Listening")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
recv(eos_tx) -> _ => {
|
||||||
|
if let Some(song) = song {
|
||||||
|
client.listen(song.get_tag(&Tag::Artist).map_or("", |tag| tag.as_str()), song.get_tag(&Tag::Title).map_or("", |tag| tag.as_str()), None).unwrap();
|
||||||
|
println!("Song Scrobbled");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test_super {
|
mod test_super {
|
||||||
use std::thread::sleep;
|
use std::thread::{sleep, spawn};
|
||||||
|
|
||||||
use crossbeam_channel::unbounded;
|
use crossbeam_channel::unbounded;
|
||||||
|
|
||||||
|
@ -150,15 +194,17 @@ mod test_super {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn discord_test() {
|
fn lb_test() {
|
||||||
let client_id = std::env!("DISCORD_CLIENT_ID").parse::<u64>().unwrap();
|
|
||||||
let (song_rx, song_tx) = unbounded();
|
let (song_rx, song_tx) = unbounded();
|
||||||
let (_, state_tx) = unbounded();
|
let (eos_rx, eos_tx) = unbounded();
|
||||||
|
|
||||||
let (_, lib ) = read_config_lib();
|
let (config, lib ) = read_config_lib();
|
||||||
song_rx.send(lib.library[0].clone()).unwrap();
|
song_rx.send(lib.library[0].clone()).unwrap();
|
||||||
|
spawn(|| {
|
||||||
Controller::discord_rpc(client_id, song_tx, state_tx);
|
Controller::listenbrainz_scrobble(config.connections.listenbrainz_token.unwrap().as_str(), song_tx, eos_tx);
|
||||||
sleep(Duration::from_secs(150));
|
});
|
||||||
|
sleep(Duration::from_secs(10));
|
||||||
|
eos_rx.send(()).unwrap();
|
||||||
|
sleep(Duration::from_secs(10));
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -278,6 +278,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();
|
let _notifications_rx = notifications_rx.clone();
|
||||||
|
let _config = config.clone();
|
||||||
move || {
|
move || {
|
||||||
futures::executor::block_on(async {
|
futures::executor::block_on(async {
|
||||||
moro::async_scope!(|scope| {
|
moro::async_scope!(|scope| {
|
||||||
|
@ -303,7 +304,7 @@ impl Controller {
|
||||||
Controller::library_loop(
|
Controller::library_loop(
|
||||||
lib_mail.1,
|
lib_mail.1,
|
||||||
&mut library,
|
&mut library,
|
||||||
config,
|
_config,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
@ -336,9 +337,11 @@ impl Controller {
|
||||||
if let Some(inner) = connections {
|
if let Some(inner) = connections {
|
||||||
dbg!(&inner);
|
dbg!(&inner);
|
||||||
let d = scope.spawn(|| {
|
let d = scope.spawn(|| {
|
||||||
Controller::handle_connections( ControllerConnections {
|
Controller::handle_connections(
|
||||||
notifications_tx,
|
config,
|
||||||
inner,
|
ControllerConnections {
|
||||||
|
notifications_tx,
|
||||||
|
inner,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
--baseColor: #101010;
|
--baseColor: #101010;
|
||||||
--overlayColor: #1e1e1e;
|
--overlayColor: #1e1e1e;
|
||||||
--highlightColor: #1f1f1f;
|
--highlightColor: #1f1f1f;
|
||||||
|
--highlightColor2: #2b2a2c;
|
||||||
--playBarColor: #5c4bb9;
|
--playBarColor: #5c4bb9;
|
||||||
--lightTextColor: #7a7a6f;
|
--lightTextColor: #7a7a6f;
|
||||||
--mediumTextColor: #cacab8;
|
--mediumTextColor: #cacab8;
|
||||||
|
@ -22,6 +23,8 @@ main {
|
||||||
width: 100vw;
|
width: 100vw;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
overflow-y: hidden;
|
||||||
|
overflow-x: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
|
@ -221,6 +224,10 @@ main {
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.queueSong:hover {
|
||||||
|
background-color: var(--highlightColor2);
|
||||||
|
}
|
||||||
|
|
||||||
.queueSongCoverArt {
|
.queueSongCoverArt {
|
||||||
aspect-ratio: 1;
|
aspect-ratio: 1;
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
|
|
14
src/App.tsx
14
src/App.tsx
|
@ -273,14 +273,6 @@ function Song(props: SongProps) {
|
||||||
{ Math.round(+props.duration / 60) }:
|
{ Math.round(+props.duration / 60) }:
|
||||||
{ (+props.duration % 60).toString().padStart(2, "0") }
|
{ (+props.duration % 60).toString().padStart(2, "0") }
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{/*<button onClick={(_) => {
|
|
||||||
invoke('add_song_to_queue', { uuid: props.uuid, location: props.playerLocation }).then(() => {} )
|
|
||||||
}}
|
|
||||||
>Add to Queue</button>
|
|
||||||
<button onClick={() => {
|
|
||||||
invoke("play_now", { uuid: props.uuid, location: props.playerLocation }).then(() => {})
|
|
||||||
}}>Play Now</button>*/}
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -302,8 +294,8 @@ function PlayBar({ playing, setPlaying }: PlayBarProps) {
|
||||||
const pos_ = Array.isArray(info.position) ? info.position![0] : 0;
|
const pos_ = Array.isArray(info.position) ? info.position![0] : 0;
|
||||||
const dur_ = Array.isArray(info.duration) ? info.duration![0] : 0;
|
const dur_ = Array.isArray(info.duration) ? info.duration![0] : 0;
|
||||||
|
|
||||||
setPosition(pos_);
|
setPosition(dur_);
|
||||||
setDuration(dur_);
|
setDuration(pos_);
|
||||||
let progress = ((dur_/pos_) * 100);
|
let progress = ((dur_/pos_) * 100);
|
||||||
setSeekBarSize(progress)
|
setSeekBarSize(progress)
|
||||||
})
|
})
|
||||||
|
@ -401,7 +393,7 @@ function QueueSong({ song, location, index }: QueueSongProps) {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="queueSong" onAuxClick={ removeFromQueue } onClickCapture={ playNow }>
|
<div className="queueSong unselectable" onAuxClickCapture={ removeFromQueue } onDoubleClickCapture={ playNow }>
|
||||||
<img className="queueSongCoverArt" src={ convertFileSrc('abc') + '?' + song.uuid } key={ 'coverArt_' + song.uuid }/>
|
<img className="queueSongCoverArt" src={ convertFileSrc('abc') + '?' + song.uuid } key={ 'coverArt_' + song.uuid }/>
|
||||||
<div className="queueSongTags">
|
<div className="queueSongTags">
|
||||||
<p className="queueSongTitle">{ song.tags.TrackTitle }</p>
|
<p className="queueSongTitle">{ song.tags.TrackTitle }</p>
|
||||||
|
|
Loading…
Reference in a new issue