mirror of
https://github.com/Dangoware/dango-music-player.git
synced 2025-04-19 18:12:54 -05:00
Compare commits
12 commits
Author | SHA1 | Date | |
---|---|---|---|
|
359990b36a | ||
|
fce2927986 | ||
|
8f30c38c4c | ||
|
d88c6e07d5 | ||
|
41c7820dfb | ||
|
7a9ee5c22a | ||
|
7b5d811102 | ||
|
6f90aa09b9 | ||
|
04da9c88f3 | ||
|
eab4b41210 | ||
|
9586c126d0 | ||
|
43a98d1151 |
30 changed files with 1146 additions and 575 deletions
|
@ -3,7 +3,6 @@ resolver = "2"
|
||||||
members = [
|
members = [
|
||||||
"src-tauri",
|
"src-tauri",
|
||||||
"dmp-core",
|
"dmp-core",
|
||||||
"kushi-queue",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
|
|
|
@ -12,7 +12,6 @@ keywords = []
|
||||||
categories = []
|
categories = []
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
kushi = { path = "../kushi-queue" }
|
|
||||||
file-format = { version = "0.26", features = ["reader"] }
|
file-format = { version = "0.26", features = ["reader"] }
|
||||||
lofty = "0.21"
|
lofty = "0.21"
|
||||||
serde = { version = "1.0.195", features = ["derive"] }
|
serde = { version = "1.0.195", features = ["derive"] }
|
||||||
|
@ -41,3 +40,7 @@ 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"
|
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"
|
||||||
|
|
|
@ -94,7 +94,9 @@ impl ConfigLibraries {
|
||||||
|
|
||||||
#[derive(Debug, Default, Serialize, Deserialize, Clone)]
|
#[derive(Debug, Default, Serialize, Deserialize, Clone)]
|
||||||
pub struct ConfigConnections {
|
pub struct ConfigConnections {
|
||||||
|
pub discord_rpc_client_id: Option<u64>,
|
||||||
pub listenbrainz_token: Option<String>,
|
pub listenbrainz_token: Option<String>,
|
||||||
|
pub last_fm_session: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default, Serialize, Deserialize, Clone)]
|
#[derive(Debug, Default, Serialize, Deserialize, Clone)]
|
||||||
|
|
|
@ -3,6 +3,7 @@ pub mod music_storage {
|
||||||
pub mod library;
|
pub mod library;
|
||||||
pub mod music_collection;
|
pub mod music_collection;
|
||||||
pub mod playlist;
|
pub mod playlist;
|
||||||
|
pub mod queue;
|
||||||
mod utils;
|
mod utils;
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
|
|
|
@ -4,24 +4,24 @@ use std::{
|
||||||
Arc,
|
Arc,
|
||||||
},
|
},
|
||||||
thread::sleep,
|
thread::sleep,
|
||||||
time::{Duration, SystemTime, UNIX_EPOCH},
|
time::{Duration, Instant, SystemTime, UNIX_EPOCH},
|
||||||
};
|
};
|
||||||
|
|
||||||
use chrono::TimeDelta;
|
use chrono::TimeDelta;
|
||||||
use crossbeam::{scope, select};
|
use crossbeam::select;
|
||||||
use crossbeam_channel::{unbounded, Receiver};
|
use crossbeam_channel::{bounded, unbounded, Receiver, Sender};
|
||||||
use discord_presence::Client;
|
use discord_presence::Client;
|
||||||
use listenbrainz::ListenBrainz;
|
use listenbrainz::ListenBrainz;
|
||||||
use parking_lot::RwLock;
|
use parking_lot::RwLock;
|
||||||
use prismriver::State as PrismState;
|
use prismriver::State as PrismState;
|
||||||
|
use rustfm_scrobble::{Scrobble, Scrobbler};
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
config::Config,
|
config::Config,
|
||||||
music_storage::library::{Song, Tag},
|
music_storage::library::{Song, Tag},
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::controller::Controller;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub(super) enum ConnectionsNotification {
|
pub(super) enum ConnectionsNotification {
|
||||||
Playback {
|
Playback {
|
||||||
|
@ -32,96 +32,190 @@ pub(super) enum ConnectionsNotification {
|
||||||
SongChange(Song),
|
SongChange(Song),
|
||||||
AboutToFinish,
|
AboutToFinish,
|
||||||
EOS,
|
EOS,
|
||||||
|
TryEnableConnection(TryConnectionType),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[non_exhaustive]
|
||||||
pub struct ConnectionsInput {
|
#[derive(Debug, Clone)]
|
||||||
pub discord_rpc_client_id: Option<u64>,
|
pub(super) enum TryConnectionType {
|
||||||
|
Discord(u64),
|
||||||
|
LastFM {
|
||||||
|
api_key: String,
|
||||||
|
api_secret: String,
|
||||||
|
auth: LastFMAuth,
|
||||||
|
},
|
||||||
|
ListenBrainz(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum LastFMAuth {
|
||||||
|
Session(Option<String>),
|
||||||
|
UserPass { username: String, password: String },
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) struct ControllerConnections {
|
pub(super) struct ControllerConnections {
|
||||||
|
pub notifications_rx: Sender<ConnectionsNotification>,
|
||||||
pub notifications_tx: Receiver<ConnectionsNotification>,
|
pub notifications_tx: Receiver<ConnectionsNotification>,
|
||||||
pub inner: ConnectionsInput,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static DC_ACTIVE: AtomicBool = AtomicBool::new(false);
|
static DC_ACTIVE: AtomicBool = AtomicBool::new(false);
|
||||||
static LB_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(
|
||||||
pub(super) fn handle_connections(
|
|
||||||
config: Arc<RwLock<Config>>,
|
config: Arc<RwLock<Config>>,
|
||||||
ControllerConnections {
|
ControllerConnections {
|
||||||
|
notifications_rx,
|
||||||
notifications_tx,
|
notifications_tx,
|
||||||
inner: ConnectionsInput {
|
|
||||||
discord_rpc_client_id,
|
|
||||||
},
|
|
||||||
}: 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 (dc_position_rx, dc_position_tx) = bounded::<Option<TimeDelta>>(0);
|
||||||
let (lb_abt_fin_rx, lb_abt_fn_tx) = unbounded::<()>();
|
let (lb_now_playing_rx, lb_now_playing_tx) = unbounded::<Song>();
|
||||||
let (lb_eos_rx, lb_eos_tx) = unbounded::<()>();
|
let (lb_scrobble_rx, lb_scrobble_tx) = unbounded::<()>();
|
||||||
|
let (last_now_playing_rx, last_now_playing_tx) = unbounded::<Song>();
|
||||||
|
let (last_scrobble_rx, last_scrobble_tx) = unbounded::<()>();
|
||||||
|
|
||||||
|
let mut song_scrobbled = false;
|
||||||
|
|
||||||
|
//TODO: update scrobble position on seek
|
||||||
|
// /// The position at which you can scrobble the song. changes on seek
|
||||||
|
// struct ScrobblePosition {
|
||||||
|
// percent: f32,
|
||||||
|
// position: i32
|
||||||
|
// }
|
||||||
|
// let mut scrobble_position = ScrobblePosition { percent: f32::MAX, position: i32::MAX };
|
||||||
|
|
||||||
scope(|s| {
|
|
||||||
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 { .. } => {}
|
Playback {
|
||||||
|
position: _position,
|
||||||
|
duration: _duration,
|
||||||
|
} => {
|
||||||
|
_ = dc_position_rx.send_timeout(_position.clone(), Duration::from_millis(0));
|
||||||
|
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) {
|
||||||
|
last_now_playing_rx.send(song.clone()).unwrap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
EOS => {
|
EOS => continue,
|
||||||
if LB_ACTIVE.load(Ordering::Relaxed) {
|
AboutToFinish => continue,
|
||||||
lb_eos_rx.send(()).unwrap();
|
TryEnableConnection(c) => {
|
||||||
}
|
match c {
|
||||||
}
|
TryConnectionType::Discord(client_id) => {
|
||||||
AboutToFinish => {
|
let (dc_song_tx, dc_state_tx, dc_position_tx) = (
|
||||||
if LB_ACTIVE.load(Ordering::Relaxed) {
|
dc_now_playing_tx.clone(),
|
||||||
lb_abt_fin_rx.send(()).unwrap();
|
dc_state_tx.clone(),
|
||||||
}
|
dc_position_tx.clone(),
|
||||||
}
|
);
|
||||||
}
|
std::thread::Builder::new()
|
||||||
}
|
|
||||||
})
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
if let Some(client_id) = discord_rpc_client_id {
|
|
||||||
s.builder()
|
|
||||||
.name("Discord RPC Handler".to_string())
|
.name("Discord RPC Handler".to_string())
|
||||||
.spawn(move |_| {
|
.spawn(move || {
|
||||||
Controller::discord_rpc(client_id, dc_song_tx, dc_state_tx);
|
// TODO: add proper error handling here
|
||||||
|
discord_rpc(client_id, dc_song_tx, dc_state_tx, dc_position_tx);
|
||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
};
|
}
|
||||||
|
TryConnectionType::ListenBrainz(token) => {
|
||||||
if let Some(token) = config.read().connections.listenbrainz_token.clone() {
|
let (lb_now_playing_tx, lb_scrobble_tx) =
|
||||||
s.builder()
|
(lb_now_playing_tx.clone(), lb_scrobble_tx.clone());
|
||||||
|
std::thread::Builder::new()
|
||||||
.name("ListenBrainz Handler".to_string())
|
.name("ListenBrainz Handler".to_string())
|
||||||
.spawn(move |_| {
|
.spawn(move || {
|
||||||
Controller::listenbrainz_scrobble(&token, lb_song_tx, lb_abt_fn_tx, lb_eos_tx);
|
listenbrainz_scrobble(&token, lb_now_playing_tx, lb_scrobble_tx);
|
||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
TryConnectionType::LastFM {
|
||||||
|
api_key,
|
||||||
|
api_secret,
|
||||||
|
auth,
|
||||||
|
} => {
|
||||||
|
let (config, notifications_rx) = (config.clone(), notifications_rx.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 || {
|
||||||
|
let scrobbler = match auth {
|
||||||
|
LastFMAuth::Session(key) => if let Some(session) = key {
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.unwrap(),
|
||||||
|
LastFMAuth::UserPass { username, password } => {
|
||||||
|
let mut scrobbler = Scrobbler::new(&api_key, &api_secret);
|
||||||
|
scrobbler
|
||||||
|
.authenticate_with_password(&username, &password)
|
||||||
|
.unwrap();
|
||||||
|
scrobbler
|
||||||
|
}
|
||||||
|
};
|
||||||
|
last_fm_scrobble(scrobbler, last_now_playing_tx, last_scrobble_tx);
|
||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn discord_rpc(client_id: u64, song_tx: Receiver<Song>, state_tx: Receiver<PrismState>) {
|
fn discord_rpc(
|
||||||
// TODO: Handle seeking position change and pause
|
client_id: u64,
|
||||||
|
song_tx: Receiver<Song>,
|
||||||
|
state_tx: Receiver<PrismState>,
|
||||||
|
position_tx: Receiver<Option<TimeDelta>>,
|
||||||
|
) {
|
||||||
let mut client =
|
let mut client =
|
||||||
discord_presence::Client::with_error_config(client_id, Duration::from_secs(5), None);
|
discord_presence::Client::with_error_config(client_id, Duration::from_secs(5), None);
|
||||||
client.start();
|
client.start();
|
||||||
|
@ -130,26 +224,23 @@ impl Controller {
|
||||||
}
|
}
|
||||||
println!("discord connected");
|
println!("discord connected");
|
||||||
|
|
||||||
let mut state = "Started".to_string();
|
let mut state = None;
|
||||||
let mut song: Option<Song> = None;
|
let mut song: Option<Song> = None;
|
||||||
let mut now = SystemTime::now()
|
let mut now = SystemTime::now()
|
||||||
.duration_since(UNIX_EPOCH)
|
.duration_since(UNIX_EPOCH)
|
||||||
.expect("Time went backwards?")
|
.expect("Time went backwards?")
|
||||||
.as_secs();
|
.as_secs();
|
||||||
|
|
||||||
DC_ACTIVE.store(true, Ordering::Relaxed);
|
DC_ACTIVE.store(true, Ordering::Relaxed);
|
||||||
|
|
||||||
while true {
|
while true {
|
||||||
let state = &mut state;
|
let state: &mut Option<PrismState> = &mut state;
|
||||||
let song = &mut song;
|
let song: &mut Option<Song> = &mut song;
|
||||||
|
|
||||||
select! {
|
select! {
|
||||||
recv(state_tx) -> res => {
|
recv(state_tx) -> res => {
|
||||||
if let Ok(state_) = res {
|
if let Ok(state_) = res {
|
||||||
*state = match state_ {
|
*state = Some(state_);
|
||||||
PrismState::Playing => "Playing",
|
|
||||||
PrismState::Paused => "Paused",
|
|
||||||
PrismState::Stopped => "Stopped",
|
|
||||||
_ => "I'm Scared, Boss"
|
|
||||||
}.to_string();
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
recv(song_tx) -> res => {
|
recv(song_tx) -> res => {
|
||||||
|
@ -158,19 +249,27 @@ impl Controller {
|
||||||
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(99)) => ()
|
default(Duration::from_millis(1000)) => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Ok(Some(pos)) = position_tx.recv_timeout(Duration::from_millis(100)) {
|
||||||
|
// set back the start position to where it would be if it hadn't been paused / seeked
|
||||||
|
now = SystemTime::now()
|
||||||
|
.duration_since(UNIX_EPOCH)
|
||||||
|
.expect("Time went backwards?")
|
||||||
|
.as_secs()
|
||||||
|
- u64::try_from(pos.num_seconds()).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
client
|
client
|
||||||
.set_activity(|activity| {
|
.set_activity(|activity| {
|
||||||
let a = activity
|
let activity = activity
|
||||||
.state(song.as_ref().map_or(String::new(), |s| {
|
.state(song.as_ref().map_or(String::new(), |s| {
|
||||||
format!(
|
format!(
|
||||||
"{}{}{}",
|
"{}{}{}",
|
||||||
s.get_tag(&Tag::Artist)
|
s.get_tag(&Tag::Artist)
|
||||||
.map_or(String::new(), |album| album.clone()),
|
.map_or(String::new(), |album| album.clone()),
|
||||||
if s.get_tag(&Tag::Album).is_some()
|
if s.get_tag(&Tag::Album).is_some() && s.get_tag(&Tag::Artist).is_some()
|
||||||
&& s.get_tag(&Tag::Artist).is_some()
|
|
||||||
{
|
{
|
||||||
" - "
|
" - "
|
||||||
} else {
|
} else {
|
||||||
|
@ -188,25 +287,33 @@ impl Controller {
|
||||||
String::new()
|
String::new()
|
||||||
});
|
});
|
||||||
if let Some(s) = song {
|
if let Some(s) = song {
|
||||||
if state.as_str() == "Playing" {
|
if *state == Some(PrismState::Playing) {
|
||||||
a.timestamps(|timestamps| {
|
activity.timestamps(|timestamps| {
|
||||||
timestamps.start(now).end(now + s.duration.as_secs())
|
timestamps.start(now).end(now + s.duration.as_secs())
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
a
|
activity
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
a
|
activity
|
||||||
}
|
}
|
||||||
.assets(|a| a.large_text(state.clone()))
|
.assets(|a| {
|
||||||
|
a.large_text(match state {
|
||||||
|
Some(PrismState::Playing) => "Playing",
|
||||||
|
Some(PrismState::Paused) => "Paused",
|
||||||
|
Some(PrismState::Stopped) => "Stopped",
|
||||||
|
None => "Started",
|
||||||
|
_ => "I'm Scared, Boss",
|
||||||
|
})
|
||||||
|
})
|
||||||
.instance(true)
|
.instance(true)
|
||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
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() {
|
||||||
|
@ -214,17 +321,15 @@ impl Controller {
|
||||||
}
|
}
|
||||||
|
|
||||||
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()
|
||||||
|
@ -240,15 +345,11 @@ impl Controller {
|
||||||
|
|
||||||
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 {
|
||||||
|
@ -262,11 +363,132 @@ impl Controller {
|
||||||
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");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
LB_ACTIVE.store(false, Ordering::Relaxed);
|
LB_ACTIVE.store(false, Ordering::Relaxed);
|
||||||
}
|
}
|
||||||
|
fn last_fm_auth(
|
||||||
|
config: Arc<RwLock<Config>>,
|
||||||
|
notifications_rx: Sender<ConnectionsNotification>,
|
||||||
|
api_key: &str,
|
||||||
|
api_secret: &str,
|
||||||
|
) -> Result<Scrobbler, Box<dyn std::error::Error>> {
|
||||||
|
let token = {
|
||||||
|
tokio::runtime::Builder::new_current_thread()
|
||||||
|
.enable_all()
|
||||||
|
.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::<Token>()
|
||||||
|
.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);
|
||||||
|
{
|
||||||
|
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,
|
||||||
|
now_playing_tx: Receiver<Song>,
|
||||||
|
scrobble_tx: Receiver<()>,
|
||||||
|
) {
|
||||||
|
// TODO: Add support for scrobble storage for later
|
||||||
|
|
||||||
|
let mut song: Option<Song> = None;
|
||||||
|
LAST_FM_ACTIVE.store(true, Ordering::Relaxed);
|
||||||
|
println!("last.fm connected");
|
||||||
|
|
||||||
|
while true {
|
||||||
|
let now_playing = &mut song;
|
||||||
|
|
||||||
|
let scrobbler = &scrobbler;
|
||||||
|
|
||||||
|
select! {
|
||||||
|
recv(now_playing_tx) -> res => {
|
||||||
|
if let Ok(_song) = res {
|
||||||
|
let title = if let Some(tag) = _song.get_tag(&Tag::Title) {
|
||||||
|
tag.as_str()
|
||||||
|
} else {
|
||||||
|
continue
|
||||||
|
};
|
||||||
|
let artist = if let Some(tag) = _song.get_tag(&Tag::Artist) {
|
||||||
|
tag.as_str()
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
};
|
||||||
|
let album = if let Some(tag) = _song.get_tag(&Tag::Album) {
|
||||||
|
tag.as_str()
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
};
|
||||||
|
|
||||||
|
match scrobbler.now_playing(&Scrobble::new(artist, title, album)) {
|
||||||
|
Ok(_) => println!("Song Scrobbling = {artist} - {title} - {album}"),
|
||||||
|
Err(e) => println!("Error at last.fm now playing:\n{e}")
|
||||||
|
};
|
||||||
|
|
||||||
|
*now_playing = Some(_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 {
|
||||||
|
continue
|
||||||
|
};
|
||||||
|
let artist = if let Some(tag) = song.get_tag(&Tag::Artist) {
|
||||||
|
tag.as_str()
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
};
|
||||||
|
let album = if let Some(tag) = song.get_tag(&Tag::Album) {
|
||||||
|
tag.as_str()
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
};
|
||||||
|
|
||||||
|
match scrobbler.scrobble(&Scrobble::new(artist, title, album)) {
|
||||||
|
Ok(_) => println!("Song {title} Scrobbled"),
|
||||||
|
Err(e) => println!("Error at last.fm scrobbler:\n{e:?}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LAST_FM_ACTIVE.store(false, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct Token {
|
||||||
|
token: String,
|
||||||
}
|
}
|
|
@ -5,8 +5,6 @@
|
||||||
use chrono::TimeDelta;
|
use chrono::TimeDelta;
|
||||||
use crossbeam::atomic::AtomicCell;
|
use crossbeam::atomic::AtomicCell;
|
||||||
use crossbeam_channel::{Receiver, Sender};
|
use crossbeam_channel::{Receiver, Sender};
|
||||||
use kushi::Queue;
|
|
||||||
use kushi::{QueueError, QueueItem};
|
|
||||||
use parking_lot::RwLock;
|
use parking_lot::RwLock;
|
||||||
use prismriver::{Error as PrismError, Prismriver};
|
use prismriver::{Error as PrismError, Prismriver};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
@ -20,11 +18,13 @@ use thiserror::Error;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::config::ConfigError;
|
use crate::config::ConfigError;
|
||||||
|
use crate::music_controller::connections::handle_connections;
|
||||||
use crate::music_storage::library::Song;
|
use crate::music_storage::library::Song;
|
||||||
use crate::music_storage::playlist::{ExternalPlaylist, Playlist};
|
use crate::music_storage::playlist::{ExternalPlaylist, Playlist};
|
||||||
|
use crate::music_storage::queue::{Queue, QueueError, QueueItem};
|
||||||
use crate::{config::Config, music_storage::library::MusicLibrary};
|
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::controller_handle::{LibraryCommandInput, PlayerCommandInput, QueueCommandInput};
|
||||||
use super::queue::{QueueAlbum, QueueSong};
|
use super::queue::{QueueAlbum, QueueSong};
|
||||||
|
|
||||||
|
@ -109,6 +109,10 @@ pub enum LibraryCommand {
|
||||||
AllSongs,
|
AllSongs,
|
||||||
GetLibrary,
|
GetLibrary,
|
||||||
ExternalPlaylist(Uuid),
|
ExternalPlaylist(Uuid),
|
||||||
|
PlaylistSong{
|
||||||
|
list_uuid: Uuid,
|
||||||
|
item_uuid: Uuid
|
||||||
|
},
|
||||||
Playlist(Uuid),
|
Playlist(Uuid),
|
||||||
ImportM3UPlayList(PathBuf),
|
ImportM3UPlayList(PathBuf),
|
||||||
Save,
|
Save,
|
||||||
|
@ -122,6 +126,7 @@ pub enum LibraryResponse {
|
||||||
AllSongs(Vec<Song>),
|
AllSongs(Vec<Song>),
|
||||||
Library(MusicLibrary),
|
Library(MusicLibrary),
|
||||||
ExternalPlaylist(ExternalPlaylist),
|
ExternalPlaylist(ExternalPlaylist),
|
||||||
|
PlaylistSong(Song, usize),
|
||||||
Playlist(Playlist),
|
Playlist(Playlist),
|
||||||
ImportM3UPlayList(Uuid, String),
|
ImportM3UPlayList(Uuid, String),
|
||||||
Playlists(Vec<(Uuid, String)>),
|
Playlists(Vec<(Uuid, String)>),
|
||||||
|
@ -159,24 +164,28 @@ 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>>,
|
||||||
notify_next_song: Sender<Song>,
|
notify_next_song: Sender<Song>,
|
||||||
connections: Option<ConnectionsInput>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct ControllerHandle {
|
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>,
|
||||||
|
pub config: Arc<RwLock<Config>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ControllerHandle {
|
impl ControllerHandle {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
library: MusicLibrary,
|
library: MusicLibrary,
|
||||||
config: Arc<RwLock<Config>>,
|
config: Arc<RwLock<Config>>,
|
||||||
connections: Option<ConnectionsInput>,
|
|
||||||
) -> (
|
) -> (
|
||||||
Self,
|
Self,
|
||||||
ControllerInput,
|
ControllerInput,
|
||||||
|
@ -186,6 +195,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>();
|
||||||
(
|
(
|
||||||
|
@ -193,16 +203,18 @@ 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(),
|
||||||
|
config: config.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),
|
||||||
notify_next_song: notify_next_song.0,
|
notify_next_song: notify_next_song.0,
|
||||||
connections,
|
|
||||||
},
|
},
|
||||||
playback_info,
|
playback_info,
|
||||||
notify_next_song.1,
|
notify_next_song.1,
|
||||||
|
@ -243,18 +255,18 @@ 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,
|
||||||
notify_next_song,
|
notify_next_song,
|
||||||
connections,
|
|
||||||
}: ControllerInput,
|
}: ControllerInput,
|
||||||
) -> Result<(), Box<dyn Error>> {
|
) -> Result<(), Box<dyn Error>> {
|
||||||
let queue: Queue<QueueSong, QueueAlbum> = Queue {
|
let queue: Queue<QueueSong, QueueAlbum> = Queue {
|
||||||
|
@ -279,12 +291,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 {
|
||||||
|
@ -322,6 +332,7 @@ impl Controller {
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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,
|
||||||
|
@ -330,27 +341,26 @@ impl Controller {
|
||||||
finished_tx,
|
finished_tx,
|
||||||
player_mail.0,
|
player_mail.0,
|
||||||
notify_next_song,
|
notify_next_song,
|
||||||
notifications_rx,
|
_notifications_rx,
|
||||||
playback_info,
|
playback_info,
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
});
|
});
|
||||||
|
|
||||||
if let Some(inner) = connections {
|
|
||||||
dbg!(&inner);
|
|
||||||
let d = scope.spawn(|| {
|
let d = scope.spawn(|| {
|
||||||
Controller::handle_connections(
|
handle_connections(
|
||||||
config,
|
config,
|
||||||
ControllerConnections {
|
ControllerConnections {
|
||||||
notifications_tx,
|
notifications_rx: connections_mail.0,
|
||||||
inner,
|
notifications_tx: connections_mail.1,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
|
||||||
a.join().unwrap();
|
a.join().unwrap();
|
||||||
b.join().unwrap();
|
b.join().unwrap();
|
||||||
c.join().unwrap();
|
c.join().unwrap();
|
||||||
|
d.join().unwrap();
|
||||||
});
|
});
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
@ -1,10 +1,13 @@
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use async_channel::{Receiver, Sender};
|
use async_channel::{Receiver, Sender};
|
||||||
use kushi::{QueueError, QueueItem};
|
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::music_storage::{library::Song, playlist::ExternalPlaylist};
|
use crate::music_storage::{
|
||||||
|
library::Song,
|
||||||
|
playlist::ExternalPlaylist,
|
||||||
|
queue::{QueueError, QueueItem},
|
||||||
|
};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
controller::{
|
controller::{
|
||||||
|
@ -176,6 +179,46 @@ 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_auth(&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 {
|
||||||
|
|
|
@ -89,6 +89,15 @@ impl Controller {
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
LibraryCommand::PlaylistSong { list_uuid, item_uuid } => {
|
||||||
|
let playlist= library.playlists.query_uuid(&list_uuid).unwrap();
|
||||||
|
let Some((uuid, index)) = playlist.query_uuid(&item_uuid) else { todo!() };
|
||||||
|
let Some((song, _)) = library.query_uuid(uuid) else { todo!() };
|
||||||
|
res_rx
|
||||||
|
.send(LibraryResponse::PlaylistSong(song.clone(), index))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
_ => {
|
_ => {
|
||||||
todo!()
|
todo!()
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,13 @@
|
||||||
use chrono::TimeDelta;
|
use chrono::TimeDelta;
|
||||||
use crossbeam_channel::Sender;
|
use crossbeam_channel::Sender;
|
||||||
use kushi::{QueueItem, QueueItemType};
|
|
||||||
use prismriver::{Prismriver, Volume};
|
use prismriver::{Prismriver, Volume};
|
||||||
|
|
||||||
use crate::music_controller::{
|
use crate::{
|
||||||
|
music_controller::{
|
||||||
controller::{LibraryCommand, LibraryResponse},
|
controller::{LibraryCommand, LibraryResponse},
|
||||||
queue::QueueSong,
|
queue::QueueSong,
|
||||||
|
},
|
||||||
|
music_storage::queue::{QueueItem, QueueItemType},
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
|
@ -89,6 +91,8 @@ impl Controller {
|
||||||
panic!("This is temporary, handle queueItemTypes at some point")
|
panic!("This is temporary, handle queueItemTypes at some point")
|
||||||
};
|
};
|
||||||
|
|
||||||
|
match np_song.location {
|
||||||
|
PlayerLocation::Library => {
|
||||||
let (command, tx) =
|
let (command, tx) =
|
||||||
LibraryCommandInput::command(LibraryCommand::AllSongs);
|
LibraryCommandInput::command(LibraryCommand::AllSongs);
|
||||||
// Append next song in library
|
// Append next song in library
|
||||||
|
@ -97,7 +101,6 @@ impl Controller {
|
||||||
else {
|
else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
||||||
let (command, tx) = LibraryCommandInput::command(
|
let (command, tx) = LibraryCommandInput::command(
|
||||||
LibraryCommand::Song(np_song.song.uuid),
|
LibraryCommand::Song(np_song.song.uuid),
|
||||||
);
|
);
|
||||||
|
@ -124,7 +127,44 @@ impl Controller {
|
||||||
} else {
|
} else {
|
||||||
println!("Library Empty");
|
println!("Library Empty");
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
PlayerLocation::Playlist(uuid) => {
|
||||||
|
let (command, tx) = LibraryCommandInput::command(
|
||||||
|
LibraryCommand::ExternalPlaylist(uuid),
|
||||||
|
);
|
||||||
|
lib_mail.send(command).await.unwrap();
|
||||||
|
let LibraryResponse::ExternalPlaylist(playlist) = tx.recv().await.unwrap() else {
|
||||||
|
unreachable!()
|
||||||
|
};
|
||||||
|
let (command, tx) = LibraryCommandInput::command(
|
||||||
|
LibraryCommand::PlaylistSong { list_uuid: playlist.uuid, item_uuid: np_song.song.uuid }
|
||||||
|
);
|
||||||
|
lib_mail.send(command).await.unwrap();
|
||||||
|
let LibraryResponse::PlaylistSong(_, i) = tx.recv().await.unwrap() else {
|
||||||
|
unreachable!()
|
||||||
|
};
|
||||||
|
if let Some(song) = playlist.tracks.get(i + 49) {
|
||||||
|
let (command, tx) =
|
||||||
|
QueueCommandInput::command(QueueCommand::Append(
|
||||||
|
QueueItem::from_item_type(QueueItemType::Single(
|
||||||
|
QueueSong {
|
||||||
|
song: song.clone(),
|
||||||
|
location: np_song.location,
|
||||||
|
},
|
||||||
|
)),
|
||||||
|
false,
|
||||||
|
));
|
||||||
|
queue_mail.send(command).await.unwrap();
|
||||||
|
let QueueResponse::Empty(Ok(())) = tx.recv().await.unwrap()
|
||||||
|
else {
|
||||||
|
unreachable!()
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
println!("Playlist Empty");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => todo!()
|
||||||
|
}
|
||||||
res_rx
|
res_rx
|
||||||
.send(PlayerResponse::NowPlaying(Ok(np_song.song.clone())))
|
.send(PlayerResponse::NowPlaying(Ok(np_song.song.clone())))
|
||||||
.await
|
.await
|
||||||
|
|
|
@ -34,7 +34,7 @@ impl Controller {
|
||||||
move || {
|
move || {
|
||||||
println!("playback monitor started");
|
println!("playback monitor started");
|
||||||
while true {
|
while true {
|
||||||
let (position, duration) = playback_time_tx.recv().unwrap();
|
let (duration, position) = playback_time_tx.recv().unwrap();
|
||||||
notify_connections
|
notify_connections
|
||||||
.send(ConnectionsNotification::Playback {
|
.send(ConnectionsNotification::Playback {
|
||||||
position: position.clone(),
|
position: position.clone(),
|
||||||
|
@ -52,8 +52,9 @@ impl Controller {
|
||||||
futures::executor::block_on(async {
|
futures::executor::block_on(async {
|
||||||
while true {
|
while true {
|
||||||
_ = about_to_finish_tx.recv();
|
_ = about_to_finish_tx.recv();
|
||||||
notify_connections.send(ConnectionsNotification::AboutToFinish).unwrap();
|
notify_connections
|
||||||
println!("About to Finish");
|
.send(ConnectionsNotification::AboutToFinish)
|
||||||
|
.unwrap();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
@ -81,7 +82,6 @@ impl Controller {
|
||||||
notify_connections
|
notify_connections
|
||||||
.send(ConnectionsNotification::EOS)
|
.send(ConnectionsNotification::EOS)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
println!("End of song");
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use kushi::{Queue, QueueError, QueueItemType};
|
use crate::music_storage::queue::{Queue, QueueError, QueueItemType};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
controller::{Controller, QueueCommand, QueueResponse},
|
controller::{Controller, QueueCommand, QueueResponse},
|
||||||
|
|
|
@ -335,6 +335,24 @@ impl Playlist {
|
||||||
|
|
||||||
(songs, invalid_uuids)
|
(songs, invalid_uuids)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn query_uuid(&self, uuid: &Uuid) -> Option<(&Uuid, usize)> {
|
||||||
|
let result = self
|
||||||
|
.tracks
|
||||||
|
.par_iter()
|
||||||
|
.enumerate()
|
||||||
|
.try_for_each(|(i, track)| {
|
||||||
|
if uuid == track {
|
||||||
|
return std::ops::ControlFlow::Break((track, i));
|
||||||
|
}
|
||||||
|
std::ops::ControlFlow::Continue(())
|
||||||
|
});
|
||||||
|
|
||||||
|
match result {
|
||||||
|
std::ops::ControlFlow::Break(song) => Some(song),
|
||||||
|
std::ops::ControlFlow::Continue(_) => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Playlist {
|
impl Default for Playlist {
|
||||||
|
|
1
kushi-queue/.gitignore
vendored
1
kushi-queue/.gitignore
vendored
|
@ -1 +0,0 @@
|
||||||
/target
|
|
65
kushi-queue/Cargo.lock
generated
65
kushi-queue/Cargo.lock
generated
|
@ -1,65 +0,0 @@
|
||||||
# This file is automatically @generated by Cargo.
|
|
||||||
# It is not intended for manual editing.
|
|
||||||
version = 3
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "kushi"
|
|
||||||
version = "0.1.2"
|
|
||||||
dependencies = [
|
|
||||||
"thiserror",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "proc-macro2"
|
|
||||||
version = "1.0.85"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "22244ce15aa966053a896d1accb3a6e68469b97c7f33f284b99f0d576879fc23"
|
|
||||||
dependencies = [
|
|
||||||
"unicode-ident",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "quote"
|
|
||||||
version = "1.0.36"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro2",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "syn"
|
|
||||||
version = "2.0.66"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "c42f3f41a2de00b01c0aaad383c5a45241efc8b2d1eda5661812fda5f3cdcff5"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"unicode-ident",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "thiserror"
|
|
||||||
version = "1.0.61"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709"
|
|
||||||
dependencies = [
|
|
||||||
"thiserror-impl",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "thiserror-impl"
|
|
||||||
version = "1.0.61"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "unicode-ident"
|
|
||||||
version = "1.0.12"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
|
|
|
@ -1,16 +0,0 @@
|
||||||
[package]
|
|
||||||
name = "kushi"
|
|
||||||
version = "0.1.3"
|
|
||||||
license = "MIT OR Apache-2.0"
|
|
||||||
description = "A queue built for the Dango Music Player and Oden Music Bot"
|
|
||||||
homepage = "https://github.com/Dangoware/kushi-queue"
|
|
||||||
edition = "2021"
|
|
||||||
readme = "README.md"
|
|
||||||
repository = "https://github.com/Dangoware/kushi-queue"
|
|
||||||
keywords = ["queue","music","vec", "dmp"]
|
|
||||||
categories = ["data-structures"]
|
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
thiserror = "1.0.61"
|
|
|
@ -1 +0,0 @@
|
||||||
a queue built for the [Dango Music Player](https://github.com/Dangoware/dango-music-player) and [Oden Music Bot](https://github.com/Dangoware/oden-music-bot)
|
|
425
package-lock.json
generated
425
package-lock.json
generated
|
@ -8,14 +8,13 @@
|
||||||
"name": "dango-music-player",
|
"name": "dango-music-player",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jprochazk/cbor": "github:jprochazk/cbor",
|
|
||||||
"@tauri-apps/api": "^2",
|
"@tauri-apps/api": "^2",
|
||||||
"@tauri-apps/plugin-shell": "^2",
|
"@tauri-apps/plugin-shell": "^2",
|
||||||
"cbor": "github:jprochazk/cbor",
|
|
||||||
"cbor-x": "^1.6.0",
|
|
||||||
"node-fetch": "^3.3.2",
|
"node-fetch": "^3.3.2",
|
||||||
|
"path": "^0.12.7",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0"
|
"react-dom": "^18.2.0",
|
||||||
|
"url": "^0.11.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tauri-apps/cli": "^2",
|
"@tauri-apps/cli": "^2",
|
||||||
|
@ -293,78 +292,6 @@
|
||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@cbor-extract/cbor-extract-darwin-arm64": {
|
|
||||||
"version": "2.2.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-darwin-arm64/-/cbor-extract-darwin-arm64-2.2.0.tgz",
|
|
||||||
"integrity": "sha512-P7swiOAdF7aSi0H+tHtHtr6zrpF3aAq/W9FXx5HektRvLTM2O89xCyXF3pk7pLc7QpaY7AoaE8UowVf9QBdh3w==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"darwin"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"node_modules/@cbor-extract/cbor-extract-darwin-x64": {
|
|
||||||
"version": "2.2.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-darwin-x64/-/cbor-extract-darwin-x64-2.2.0.tgz",
|
|
||||||
"integrity": "sha512-1liF6fgowph0JxBbYnAS7ZlqNYLf000Qnj4KjqPNW4GViKrEql2MgZnAsExhY9LSy8dnvA4C0qHEBgPrll0z0w==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"darwin"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"node_modules/@cbor-extract/cbor-extract-linux-arm": {
|
|
||||||
"version": "2.2.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-linux-arm/-/cbor-extract-linux-arm-2.2.0.tgz",
|
|
||||||
"integrity": "sha512-QeBcBXk964zOytiedMPQNZr7sg0TNavZeuUCD6ON4vEOU/25+pLhNN6EDIKJ9VLTKaZ7K7EaAriyYQ1NQ05s/Q==",
|
|
||||||
"cpu": [
|
|
||||||
"arm"
|
|
||||||
],
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"node_modules/@cbor-extract/cbor-extract-linux-arm64": {
|
|
||||||
"version": "2.2.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-linux-arm64/-/cbor-extract-linux-arm64-2.2.0.tgz",
|
|
||||||
"integrity": "sha512-rQvhNmDuhjTVXSPFLolmQ47/ydGOFXtbR7+wgkSY0bdOxCFept1hvg59uiLPT2fVDuJFuEy16EImo5tE2x3RsQ==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"node_modules/@cbor-extract/cbor-extract-linux-x64": {
|
|
||||||
"version": "2.2.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-linux-x64/-/cbor-extract-linux-x64-2.2.0.tgz",
|
|
||||||
"integrity": "sha512-cWLAWtT3kNLHSvP4RKDzSTX9o0wvQEEAj4SKvhWuOVZxiDAeQazr9A+PSiRILK1VYMLeDml89ohxCnUNQNQNCw==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"node_modules/@cbor-extract/cbor-extract-win32-x64": {
|
|
||||||
"version": "2.2.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-win32-x64/-/cbor-extract-win32-x64-2.2.0.tgz",
|
|
||||||
"integrity": "sha512-l2M+Z8DO2vbvADOBNLbbh9y5ST1RY5sqkWOg/58GkUPBYou/cuNZ68SGQ644f1CvZ8kcOxyZtw06+dxWHIoN/w==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"win32"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/aix-ppc64": {
|
"node_modules/@esbuild/aix-ppc64": {
|
||||||
"version": "0.21.5",
|
"version": "0.21.5",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
|
||||||
|
@ -733,10 +660,6 @@
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@jprochazk/cbor": {
|
|
||||||
"version": "0.5.0",
|
|
||||||
"resolved": "git+ssh://git@github.com/jprochazk/cbor.git#4824b43c60f8a1c38fd8ef3d51d1f24e30a55743"
|
|
||||||
},
|
|
||||||
"node_modules/@jridgewell/gen-mapping": {
|
"node_modules/@jridgewell/gen-mapping": {
|
||||||
"version": "0.3.5",
|
"version": "0.3.5",
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz",
|
||||||
|
@ -1347,6 +1270,33 @@
|
||||||
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
|
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/call-bind-apply-helpers": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"function-bind": "^1.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/call-bound": {
|
||||||
|
"version": "1.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
|
||||||
|
"integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bind-apply-helpers": "^1.0.2",
|
||||||
|
"get-intrinsic": "^1.3.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/caniuse-lite": {
|
"node_modules/caniuse-lite": {
|
||||||
"version": "1.0.30001684",
|
"version": "1.0.30001684",
|
||||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001684.tgz",
|
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001684.tgz",
|
||||||
|
@ -1367,40 +1317,6 @@
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/cbor": {
|
|
||||||
"name": "@jprochazk/cbor",
|
|
||||||
"version": "0.5.0",
|
|
||||||
"resolved": "git+ssh://git@github.com/jprochazk/cbor.git#4824b43c60f8a1c38fd8ef3d51d1f24e30a55743"
|
|
||||||
},
|
|
||||||
"node_modules/cbor-extract": {
|
|
||||||
"version": "2.2.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/cbor-extract/-/cbor-extract-2.2.0.tgz",
|
|
||||||
"integrity": "sha512-Ig1zM66BjLfTXpNgKpvBePq271BPOvu8MR0Jl080yG7Jsl+wAZunfrwiwA+9ruzm/WEdIV5QF/bjDZTqyAIVHA==",
|
|
||||||
"hasInstallScript": true,
|
|
||||||
"optional": true,
|
|
||||||
"dependencies": {
|
|
||||||
"node-gyp-build-optional-packages": "5.1.1"
|
|
||||||
},
|
|
||||||
"bin": {
|
|
||||||
"download-cbor-prebuilds": "bin/download-prebuilds.js"
|
|
||||||
},
|
|
||||||
"optionalDependencies": {
|
|
||||||
"@cbor-extract/cbor-extract-darwin-arm64": "2.2.0",
|
|
||||||
"@cbor-extract/cbor-extract-darwin-x64": "2.2.0",
|
|
||||||
"@cbor-extract/cbor-extract-linux-arm": "2.2.0",
|
|
||||||
"@cbor-extract/cbor-extract-linux-arm64": "2.2.0",
|
|
||||||
"@cbor-extract/cbor-extract-linux-x64": "2.2.0",
|
|
||||||
"@cbor-extract/cbor-extract-win32-x64": "2.2.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/cbor-x": {
|
|
||||||
"version": "1.6.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/cbor-x/-/cbor-x-1.6.0.tgz",
|
|
||||||
"integrity": "sha512-0kareyRwHSkL6ws5VXHEf8uY1liitysCVJjlmhaLG+IXLqhSaOO+t63coaso7yjwEzWZzLy8fJo06gZDVQM9Qg==",
|
|
||||||
"optionalDependencies": {
|
|
||||||
"cbor-extract": "^2.2.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/convert-source-map": {
|
"node_modules/convert-source-map": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
|
||||||
|
@ -1438,13 +1354,17 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/detect-libc": {
|
"node_modules/dunder-proto": {
|
||||||
"version": "2.0.3",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||||
"integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==",
|
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
||||||
"optional": true,
|
"dependencies": {
|
||||||
|
"call-bind-apply-helpers": "^1.0.1",
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"gopd": "^1.2.0"
|
||||||
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/electron-to-chromium": {
|
"node_modules/electron-to-chromium": {
|
||||||
|
@ -1453,6 +1373,33 @@
|
||||||
"integrity": "sha512-PWVzBjghx7/wop6n22vS2MLU8tKGd4Q91aCEGhG/TYmW6PP5OcSXcdnxTe1NNt0T66N8D6jxh4kC8UsdzOGaIw==",
|
"integrity": "sha512-PWVzBjghx7/wop6n22vS2MLU8tKGd4Q91aCEGhG/TYmW6PP5OcSXcdnxTe1NNt0T66N8D6jxh4kC8UsdzOGaIw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/es-define-property": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/es-errors": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
||||||
|
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/es-object-atoms": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
|
||||||
|
"dependencies": {
|
||||||
|
"es-errors": "^1.3.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/esbuild": {
|
"node_modules/esbuild": {
|
||||||
"version": "0.21.5",
|
"version": "0.21.5",
|
||||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
|
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
|
||||||
|
@ -1547,6 +1494,14 @@
|
||||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/function-bind": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/gensync": {
|
"node_modules/gensync": {
|
||||||
"version": "1.0.0-beta.2",
|
"version": "1.0.0-beta.2",
|
||||||
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
|
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
|
||||||
|
@ -1556,6 +1511,41 @@
|
||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/get-intrinsic": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
||||||
|
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bind-apply-helpers": "^1.0.2",
|
||||||
|
"es-define-property": "^1.0.1",
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"es-object-atoms": "^1.1.1",
|
||||||
|
"function-bind": "^1.1.2",
|
||||||
|
"get-proto": "^1.0.1",
|
||||||
|
"gopd": "^1.2.0",
|
||||||
|
"has-symbols": "^1.1.0",
|
||||||
|
"hasown": "^2.0.2",
|
||||||
|
"math-intrinsics": "^1.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/get-proto": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
|
||||||
|
"dependencies": {
|
||||||
|
"dunder-proto": "^1.0.1",
|
||||||
|
"es-object-atoms": "^1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/globals": {
|
"node_modules/globals": {
|
||||||
"version": "11.12.0",
|
"version": "11.12.0",
|
||||||
"resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
|
"resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
|
||||||
|
@ -1565,6 +1555,44 @@
|
||||||
"node": ">=4"
|
"node": ">=4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/gopd": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/has-symbols": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/hasown": {
|
||||||
|
"version": "2.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
||||||
|
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"function-bind": "^1.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/inherits": {
|
||||||
|
"version": "2.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
|
||||||
|
"integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw=="
|
||||||
|
},
|
||||||
"node_modules/js-tokens": {
|
"node_modules/js-tokens": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||||
|
@ -1614,6 +1642,14 @@
|
||||||
"yallist": "^3.0.2"
|
"yallist": "^3.0.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/math-intrinsics": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ms": {
|
"node_modules/ms": {
|
||||||
"version": "2.1.3",
|
"version": "2.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||||
|
@ -1673,26 +1709,32 @@
|
||||||
"url": "https://opencollective.com/node-fetch"
|
"url": "https://opencollective.com/node-fetch"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/node-gyp-build-optional-packages": {
|
|
||||||
"version": "5.1.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.1.1.tgz",
|
|
||||||
"integrity": "sha512-+P72GAjVAbTxjjwUmwjVrqrdZROD4nf8KgpBoDxqXXTiYZZt/ud60dE5yvCSr9lRO8e8yv6kgJIC0K0PfZFVQw==",
|
|
||||||
"optional": true,
|
|
||||||
"dependencies": {
|
|
||||||
"detect-libc": "^2.0.1"
|
|
||||||
},
|
|
||||||
"bin": {
|
|
||||||
"node-gyp-build-optional-packages": "bin.js",
|
|
||||||
"node-gyp-build-optional-packages-optional": "optional.js",
|
|
||||||
"node-gyp-build-optional-packages-test": "build-test.js"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/node-releases": {
|
"node_modules/node-releases": {
|
||||||
"version": "2.0.18",
|
"version": "2.0.18",
|
||||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz",
|
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz",
|
||||||
"integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==",
|
"integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/object-inspect": {
|
||||||
|
"version": "1.13.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
|
||||||
|
"integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/path": {
|
||||||
|
"version": "0.12.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/path/-/path-0.12.7.tgz",
|
||||||
|
"integrity": "sha512-aXXC6s+1w7otVF9UletFkFcDsJeO7lSZBPUQhtb5O0xJe8LtYhj/GxldoL09bBj9+ZmE2hNoHqQSFMN5fikh4Q==",
|
||||||
|
"dependencies": {
|
||||||
|
"process": "^0.11.1",
|
||||||
|
"util": "^0.10.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/picocolors": {
|
"node_modules/picocolors": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||||
|
@ -1727,6 +1769,33 @@
|
||||||
"node": "^10 || ^12 || >=14"
|
"node": "^10 || ^12 || >=14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/process": {
|
||||||
|
"version": "0.11.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
|
||||||
|
"integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/punycode": {
|
||||||
|
"version": "1.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz",
|
||||||
|
"integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ=="
|
||||||
|
},
|
||||||
|
"node_modules/qs": {
|
||||||
|
"version": "6.14.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz",
|
||||||
|
"integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==",
|
||||||
|
"dependencies": {
|
||||||
|
"side-channel": "^1.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.6"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react": {
|
"node_modules/react": {
|
||||||
"version": "18.3.1",
|
"version": "18.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
|
||||||
|
@ -1813,6 +1882,74 @@
|
||||||
"semver": "bin/semver.js"
|
"semver": "bin/semver.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/side-channel": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
|
||||||
|
"dependencies": {
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"object-inspect": "^1.13.3",
|
||||||
|
"side-channel-list": "^1.0.0",
|
||||||
|
"side-channel-map": "^1.0.1",
|
||||||
|
"side-channel-weakmap": "^1.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/side-channel-list": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
|
||||||
|
"dependencies": {
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"object-inspect": "^1.13.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/side-channel-map": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bound": "^1.0.2",
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"get-intrinsic": "^1.2.5",
|
||||||
|
"object-inspect": "^1.13.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/side-channel-weakmap": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bound": "^1.0.2",
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"get-intrinsic": "^1.2.5",
|
||||||
|
"object-inspect": "^1.13.3",
|
||||||
|
"side-channel-map": "^1.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/source-map-js": {
|
"node_modules/source-map-js": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||||
|
@ -1865,6 +2002,26 @@
|
||||||
"browserslist": ">= 4.21.0"
|
"browserslist": ">= 4.21.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/url": {
|
||||||
|
"version": "0.11.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/url/-/url-0.11.4.tgz",
|
||||||
|
"integrity": "sha512-oCwdVC7mTuWiPyjLUz/COz5TLk6wgp0RCsN+wHZ2Ekneac9w8uuV0njcbbie2ME+Vs+d6duwmYuR3HgQXs1fOg==",
|
||||||
|
"dependencies": {
|
||||||
|
"punycode": "^1.4.1",
|
||||||
|
"qs": "^6.12.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/util": {
|
||||||
|
"version": "0.10.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/util/-/util-0.10.4.tgz",
|
||||||
|
"integrity": "sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==",
|
||||||
|
"dependencies": {
|
||||||
|
"inherits": "2.0.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/vite": {
|
"node_modules/vite": {
|
||||||
"version": "5.4.11",
|
"version": "5.4.11",
|
||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.11.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.11.tgz",
|
||||||
|
|
|
@ -10,14 +10,13 @@
|
||||||
"tauri": "tauri"
|
"tauri": "tauri"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jprochazk/cbor": "github:jprochazk/cbor",
|
|
||||||
"@tauri-apps/api": "^2",
|
"@tauri-apps/api": "^2",
|
||||||
"@tauri-apps/plugin-shell": "^2",
|
"@tauri-apps/plugin-shell": "^2",
|
||||||
"cbor": "github:jprochazk/cbor",
|
|
||||||
"cbor-x": "^1.6.0",
|
|
||||||
"node-fetch": "^3.3.2",
|
"node-fetch": "^3.3.2",
|
||||||
|
"path": "^0.12.7",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0"
|
"react-dom": "^18.2.0",
|
||||||
|
"url": "^0.11.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tauri-apps/cli": "^2",
|
"@tauri-apps/cli": "^2",
|
||||||
|
|
|
@ -19,7 +19,6 @@ tauri-build = { version = "2", features = [] }
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
dmp-core = { path = "../dmp-core" }
|
dmp-core = { path = "../dmp-core" }
|
||||||
kushi = { path = "../kushi-queue" }
|
|
||||||
tauri = { version = "2", features = [ "protocol-asset", "unstable"] }
|
tauri = { version = "2", features = [ "protocol-asset", "unstable"] }
|
||||||
tauri-plugin-shell = "2"
|
tauri-plugin-shell = "2"
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
|
|
@ -1,15 +1,18 @@
|
||||||
use std::{fs::OpenOptions, io::Write};
|
use std::{fs::OpenOptions, io::Write};
|
||||||
|
|
||||||
use dmp_core::music_controller::{
|
use dmp_core::{
|
||||||
|
music_controller::{
|
||||||
|
connections::LastFMAuth,
|
||||||
controller::{ControllerHandle, PlayerLocation},
|
controller::{ControllerHandle, PlayerLocation},
|
||||||
queue::QueueSong,
|
queue::QueueSong,
|
||||||
|
},
|
||||||
|
music_storage::queue::{QueueItem, QueueItemType},
|
||||||
};
|
};
|
||||||
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(
|
||||||
|
@ -21,7 +24,7 @@ pub async fn add_song_to_queue(
|
||||||
dbg!(&location);
|
dbg!(&location);
|
||||||
let (song, _) = ctrl_handle.lib_get_song(uuid).await;
|
let (song, _) = ctrl_handle.lib_get_song(uuid).await;
|
||||||
match ctrl_handle
|
match ctrl_handle
|
||||||
.queue_append(QueueItem::from_item_type(kushi::QueueItemType::Single(
|
.queue_append(QueueItem::from_item_type(QueueItemType::Single(
|
||||||
QueueSong { song, location },
|
QueueSong { song, location },
|
||||||
)))
|
)))
|
||||||
.await
|
.await
|
||||||
|
@ -80,3 +83,13 @@ 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(())
|
||||||
|
}
|
||||||
|
|
50
src-tauri/src/config.rs
Normal file
50
src-tauri/src/config.rs
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use dmp_core::{config::Config, music_controller::controller::ControllerHandle};
|
||||||
|
use tauri::{State, WebviewWindowBuilder, Window, Wry};
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn open_config_window(app: tauri::AppHandle<Wry>) -> Result<(), String> {
|
||||||
|
WebviewWindowBuilder::new(
|
||||||
|
&app,
|
||||||
|
"config",
|
||||||
|
tauri::WebviewUrl::App(PathBuf::from("src/config/index.html")),
|
||||||
|
)
|
||||||
|
.title("Edit Dango Music Player")
|
||||||
|
.build()
|
||||||
|
.unwrap();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn get_config(ctrl_handle: State<'_, ControllerHandle>) -> Result<Config, String> {
|
||||||
|
Ok(ctrl_handle.config.read().clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn save_config(
|
||||||
|
ctrl_handle: State<'_, ControllerHandle>,
|
||||||
|
config: Config,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
let config_original = ctrl_handle.config.read().clone();
|
||||||
|
|
||||||
|
if config
|
||||||
|
.connections
|
||||||
|
.listenbrainz_token
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|t| Some(t) != config_original.connections.listenbrainz_token.as_ref())
|
||||||
|
{
|
||||||
|
let token = config.connections.listenbrainz_token.clone().unwrap();
|
||||||
|
ctrl_handle.listenbrainz_scrobble_auth(dbg!(token));
|
||||||
|
}
|
||||||
|
|
||||||
|
*ctrl_handle.config.write() = config;
|
||||||
|
ctrl_handle.config.read().write_file().unwrap();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn close_window(window: Window<Wry>) -> Result<(), String> {
|
||||||
|
window.close().unwrap();
|
||||||
|
Ok(())
|
||||||
|
}
|
|
@ -8,16 +8,16 @@ use std::{
|
||||||
time::Duration,
|
time::Duration,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use config::{close_window, get_config, open_config_window, save_config};
|
||||||
use crossbeam::channel::{bounded, unbounded, Receiver, Sender};
|
use crossbeam::channel::{bounded, unbounded, Receiver, Sender};
|
||||||
use dmp_core::{
|
use dmp_core::{
|
||||||
config::{Config, ConfigLibrary},
|
config::{Config, ConfigLibrary},
|
||||||
music_controller::{
|
music_controller::{
|
||||||
connections::ConnectionsInput,
|
connections::LastFMAuth,
|
||||||
controller::{Controller, ControllerHandle, PlaybackInfo},
|
controller::{Controller, ControllerHandle, PlaybackInfo},
|
||||||
},
|
},
|
||||||
music_storage::library::{MusicLibrary, Song},
|
music_storage::library::{MusicLibrary, Song},
|
||||||
};
|
};
|
||||||
use futures::channel::oneshot;
|
|
||||||
use parking_lot::RwLock;
|
use parking_lot::RwLock;
|
||||||
use tauri::{http::Response, Emitter, Manager, State, Wry};
|
use tauri::{http::Response, Emitter, Manager, State, Wry};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
@ -27,23 +27,28 @@ use crate::wrappers::{
|
||||||
get_library, get_playlist, get_playlists, get_queue, get_song, import_playlist, next, pause,
|
get_library, get_playlist, get_playlists, get_queue, get_song, import_playlist, next, pause,
|
||||||
play, prev, remove_from_queue, seek, set_volume,
|
play, prev, remove_from_queue, seek, set_volume,
|
||||||
};
|
};
|
||||||
use commands::{add_song_to_queue, display_album_art, play_now};
|
use commands::{add_song_to_queue, display_album_art, last_fm_init_auth, play_now};
|
||||||
|
|
||||||
pub mod commands;
|
pub mod commands;
|
||||||
|
pub mod config;
|
||||||
pub mod wrappers;
|
pub mod wrappers;
|
||||||
|
|
||||||
const DEFAULT_IMAGE: &[u8] = include_bytes!("../icons/icon.png");
|
const DEFAULT_IMAGE: &[u8] = include_bytes!("../icons/icon.png");
|
||||||
|
|
||||||
|
const DISCORD_CLIENT_ID: u64 = 1198868728243290152;
|
||||||
|
const LAST_FM_API_KEY: &str = env!("LAST_FM_API_KEY", "None");
|
||||||
|
const LAST_FM_API_SECRET: &str = env!("LAST_FM_API_SECRET", "None");
|
||||||
|
|
||||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||||
pub fn run() {
|
pub fn run() {
|
||||||
let (rx, tx) = unbounded::<Config>();
|
let (config_rx, config_tx) = unbounded::<Config>();
|
||||||
let (lib_rx, lib_tx) = unbounded::<Option<PathBuf>>();
|
let (lib_rx, lib_tx) = unbounded::<Option<PathBuf>>();
|
||||||
let (handle_rx, handle_tx) = unbounded::<ControllerHandle>();
|
let (handle_rx, handle_tx) = unbounded::<ControllerHandle>();
|
||||||
let (playback_info_rx, playback_info_tx) = bounded(1);
|
let (playback_info_rx, playback_info_tx) = bounded(1);
|
||||||
let (next_rx, next_tx) = bounded(1);
|
let (next_rx, next_tx) = bounded(1);
|
||||||
|
|
||||||
let _controller_thread = spawn(move || {
|
let _controller_thread = spawn(move || {
|
||||||
let mut config = { tx.recv().unwrap() };
|
let mut config = { config_tx.recv().unwrap() };
|
||||||
let scan_path = { lib_tx.recv().unwrap() };
|
let scan_path = { lib_tx.recv().unwrap() };
|
||||||
let _temp_config = ConfigLibrary::default();
|
let _temp_config = ConfigLibrary::default();
|
||||||
let _lib = config.libraries.get_default().unwrap_or(&_temp_config);
|
let _lib = config.libraries.get_default().unwrap_or(&_temp_config);
|
||||||
|
@ -97,14 +102,25 @@ pub fn run() {
|
||||||
|
|
||||||
library.save(save_path).unwrap();
|
library.save(save_path).unwrap();
|
||||||
|
|
||||||
let (handle, input, playback_info, next_song_notification) = ControllerHandle::new(
|
let last_fm_session = config.connections.last_fm_session.clone();
|
||||||
library,
|
let listenbrainz_token = config.connections.listenbrainz_token.clone();
|
||||||
std::sync::Arc::new(RwLock::new(config)),
|
|
||||||
Some(ConnectionsInput {
|
let (handle, input, playback_info, next_song_notification) =
|
||||||
discord_rpc_client_id: std::option_env!("DISCORD_CLIENT_ID")
|
ControllerHandle::new(library, std::sync::Arc::new(RwLock::new(config)));
|
||||||
.map(|id| id.parse::<u64>().unwrap()),
|
|
||||||
}),
|
handle.discord_rpc(DISCORD_CLIENT_ID);
|
||||||
|
if let Some(token) = listenbrainz_token {
|
||||||
|
handle.listenbrainz_scrobble_auth(token);
|
||||||
|
} else {
|
||||||
|
println!("No ListenBrainz token found");
|
||||||
|
}
|
||||||
|
if let Some(session) = last_fm_session {
|
||||||
|
handle.last_fm_scrobble_auth(
|
||||||
|
LAST_FM_API_KEY.to_string(),
|
||||||
|
LAST_FM_API_SECRET.to_string(),
|
||||||
|
LastFMAuth::Session(Some(session)),
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
handle_rx.send(handle).unwrap();
|
handle_rx.send(handle).unwrap();
|
||||||
playback_info_rx.send(playback_info).unwrap();
|
playback_info_rx.send(playback_info).unwrap();
|
||||||
|
@ -112,10 +128,11 @@ pub fn run() {
|
||||||
|
|
||||||
let _controller = futures::executor::block_on(Controller::start(input)).unwrap();
|
let _controller = futures::executor::block_on(Controller::start(input)).unwrap();
|
||||||
});
|
});
|
||||||
|
|
||||||
let app = tauri::Builder::default()
|
let app = tauri::Builder::default()
|
||||||
.plugin(tauri_plugin_shell::init())
|
.plugin(tauri_plugin_shell::init())
|
||||||
.invoke_handler(tauri::generate_handler![
|
.invoke_handler(tauri::generate_handler![
|
||||||
get_config,
|
init_get_config,
|
||||||
create_new_library,
|
create_new_library,
|
||||||
get_library,
|
get_library,
|
||||||
play,
|
play,
|
||||||
|
@ -135,8 +152,13 @@ pub fn run() {
|
||||||
remove_from_queue,
|
remove_from_queue,
|
||||||
display_album_art,
|
display_album_art,
|
||||||
seek,
|
seek,
|
||||||
|
last_fm_init_auth,
|
||||||
|
open_config_window,
|
||||||
|
get_config,
|
||||||
|
save_config,
|
||||||
|
close_window,
|
||||||
])
|
])
|
||||||
.manage(ConfigRx(rx))
|
.manage(ConfigRx(config_rx))
|
||||||
.manage(LibRx(lib_rx))
|
.manage(LibRx(lib_rx))
|
||||||
.manage(HandleTx(handle_tx))
|
.manage(HandleTx(handle_tx))
|
||||||
.manage(tempfile::TempDir::new().unwrap())
|
.manage(tempfile::TempDir::new().unwrap())
|
||||||
|
@ -238,7 +260,7 @@ struct LibRx(Sender<Option<PathBuf>>);
|
||||||
struct HandleTx(Receiver<ControllerHandle>);
|
struct HandleTx(Receiver<ControllerHandle>);
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn get_config(state: State<'_, ConfigRx>) -> Result<Config, String> {
|
async fn init_get_config(state: State<'_, ConfigRx>) -> Result<Config, String> {
|
||||||
if let Some(dir) = directories::ProjectDirs::from("", "Dangoware", "dmp") {
|
if let Some(dir) = directories::ProjectDirs::from("", "Dangoware", "dmp") {
|
||||||
let path = dir.config_dir();
|
let path = dir.config_dir();
|
||||||
fs::create_dir_all(path)
|
fs::create_dir_all(path)
|
||||||
|
@ -268,6 +290,7 @@ async fn get_config(state: State<'_, ConfigRx>) -> Result<Config, String> {
|
||||||
|
|
||||||
state.inner().0.send(config.clone()).unwrap();
|
state.inner().0.send(config.clone()).unwrap();
|
||||||
|
|
||||||
|
println!("got config");
|
||||||
Ok(config)
|
Ok(config)
|
||||||
} else {
|
} else {
|
||||||
panic!("No config dir for DMP")
|
panic!("No config dir for DMP")
|
||||||
|
|
|
@ -4,10 +4,12 @@ use chrono::{serde::ts_milliseconds_option, DateTime, Utc};
|
||||||
use crossbeam::channel::Sender;
|
use crossbeam::channel::Sender;
|
||||||
use dmp_core::{
|
use dmp_core::{
|
||||||
music_controller::controller::{ControllerHandle, PlayerLocation},
|
music_controller::controller::{ControllerHandle, PlayerLocation},
|
||||||
music_storage::library::{Song, Tag, URI},
|
music_storage::{
|
||||||
|
library::{Song, Tag, URI},
|
||||||
|
queue::QueueItemType,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use kushi::QueueItemType;
|
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use tauri::{AppHandle, Emitter, State, Wry};
|
use tauri::{AppHandle, Emitter, State, Wry};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
23
src/App.tsx
23
src/App.tsx
|
@ -48,14 +48,14 @@ function App() {
|
||||||
const unlisten = appWindow.listen<any>("queue_updated", (_) => {
|
const unlisten = appWindow.listen<any>("queue_updated", (_) => {
|
||||||
// console.log(event);
|
// console.log(event);
|
||||||
invoke('get_queue').then((_songs) => {
|
invoke('get_queue').then((_songs) => {
|
||||||
let songs = _songs as any[]
|
let songs = _songs as any[];
|
||||||
setQueue(
|
setQueue(
|
||||||
songs.filter((_, i) => i != 0).map((song, i) =>
|
songs.filter((_, i) => i != 0).map((song, i) =>
|
||||||
<QueueSong
|
<QueueSong
|
||||||
song={ song[0] }
|
song={ song[0] }
|
||||||
location={ song[1] as "Library" | {"Playlist" : string}}
|
location={ song[1] as "Library" | {"Playlist" : string}}
|
||||||
index={i+1}
|
index={i+1}
|
||||||
key={ song.uuid + '_' + Math.floor((Math.random() * 100_000) + 1) + '_' + Date.now() }
|
key={ Math.floor((Math.random() * 100_000_000_000) + 1) + '_' + Date.now() }
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -126,7 +126,7 @@ function PlaylistHead({ playlists, setPlaylists, setViewName, setLibrary }: Play
|
||||||
// console.log(song);
|
// console.log(song);
|
||||||
return (
|
return (
|
||||||
<Song
|
<Song
|
||||||
key={ song.uuid }
|
key={ song.uuid + Math.floor(Math.random() * 100_000_000_000) }
|
||||||
location={ song.location }
|
location={ song.location }
|
||||||
playerLocation={ {"Playlist" : item.uuid } }
|
playerLocation={ {"Playlist" : item.uuid } }
|
||||||
uuid={ song.uuid }
|
uuid={ song.uuid }
|
||||||
|
@ -200,6 +200,7 @@ function PlaylistHead({ playlists, setPlaylists, setViewName, setLibrary }: Play
|
||||||
} }>Library</button>
|
} }>Library</button>
|
||||||
{ playlists }
|
{ playlists }
|
||||||
<button onClick={ handle_import }>Import .m3u Playlist</button>
|
<button onClick={ handle_import }>Import .m3u Playlist</button>
|
||||||
|
<button onClick={() => { invoke('open_config_window').then(() => {}) }} style={{marginLeft: "auto", float: "right"}}>Edit DMP</button>
|
||||||
</section>
|
</section>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -220,7 +221,7 @@ function MainView({ lib_ref, viewName }: MainViewProps) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Song
|
<Song
|
||||||
key={ song.uuid }
|
key={ song.uuid + Math.floor(Math.random() * 100_000_000_000) }
|
||||||
location={ song.location }
|
location={ song.location }
|
||||||
playerLocation="Library"
|
playerLocation="Library"
|
||||||
uuid={ song.uuid }
|
uuid={ song.uuid }
|
||||||
|
@ -288,6 +289,8 @@ function PlayBar({ playing, setPlaying }: PlayBarProps) {
|
||||||
const [seekBarSize, setSeekBarSize] = useState(0);
|
const [seekBarSize, setSeekBarSize] = useState(0);
|
||||||
const seekBarRef = React.createRef<HTMLDivElement>();
|
const seekBarRef = React.createRef<HTMLDivElement>();
|
||||||
|
|
||||||
|
const [lastFmLoggedIn, setLastFmLoggedIn] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const unlisten = appWindow.listen<any>("playback_info", ({ payload, }) => {
|
const unlisten = appWindow.listen<any>("playback_info", ({ payload, }) => {
|
||||||
const info = payload as playbackInfo;
|
const info = payload as playbackInfo;
|
||||||
|
@ -296,7 +299,7 @@ function PlayBar({ playing, setPlaying }: PlayBarProps) {
|
||||||
|
|
||||||
setPosition(pos_);
|
setPosition(pos_);
|
||||||
setDuration(dur_);
|
setDuration(dur_);
|
||||||
let progress = ((dur_/pos_) * 100);
|
let progress = ((pos_/dur_) * 100);
|
||||||
setSeekBarSize(progress)
|
setSeekBarSize(progress)
|
||||||
})
|
})
|
||||||
return () => { unlisten.then((f) => f()) }
|
return () => { unlisten.then((f) => f()) }
|
||||||
|
@ -305,7 +308,7 @@ function PlayBar({ playing, setPlaying }: PlayBarProps) {
|
||||||
const seek = (event: React.MouseEvent<HTMLDivElement>) => {
|
const seek = (event: React.MouseEvent<HTMLDivElement>) => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
let rect = seekBarRef.current!.getBoundingClientRect();
|
let rect = seekBarRef.current!.getBoundingClientRect();
|
||||||
let val = ((event.clientX-rect.left) / (rect.width))*position;
|
let val = ((event.clientX-rect.left) / (rect.width))*duration;
|
||||||
|
|
||||||
invoke('seek', { time: Math.round(val * 1000) }).then()
|
invoke('seek', { time: Math.round(val * 1000) }).then()
|
||||||
};
|
};
|
||||||
|
@ -332,10 +335,10 @@ function PlayBar({ playing, setPlaying }: PlayBarProps) {
|
||||||
invoke('set_volume', { volume: volume.target.value }).then(() => {})
|
invoke('set_volume', { volume: volume.target.value }).then(() => {})
|
||||||
}} />
|
}} />
|
||||||
<p id="timeDisplay">
|
<p id="timeDisplay">
|
||||||
{ Math.floor(+duration / 60).toString().padStart(2, "0") }:
|
|
||||||
{ (+duration % 60).toString().padStart(2, "0") }/
|
|
||||||
{ Math.floor(+position / 60).toString().padStart(2, "0") }:
|
{ Math.floor(+position / 60).toString().padStart(2, "0") }:
|
||||||
{ (+position % 60).toString().padStart(2, "0") }
|
{ (+position % 60).toString().padStart(2, "0") }/
|
||||||
|
{ Math.floor(+duration / 60).toString().padStart(2, "0") }:
|
||||||
|
{ (+duration % 60).toString().padStart(2, "0") }
|
||||||
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
@ -405,7 +408,7 @@ function QueueSong({ song, location, index }: QueueSongProps) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function getConfig(): any {
|
function getConfig(): any {
|
||||||
invoke('get_config').then( (_config) => {
|
invoke('init_get_config').then( (_config) => {
|
||||||
let config = _config as Config;
|
let config = _config as Config;
|
||||||
if (config.libraries.libraries.length == 0) {
|
if (config.libraries.libraries.length == 0) {
|
||||||
invoke('create_new_library').then(() => {})
|
invoke('create_new_library').then(() => {})
|
||||||
|
|
71
src/config/code.tsx
Normal file
71
src/config/code.tsx
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
|
import { ChangeEvent, useEffect, useRef, useState } from "react";
|
||||||
|
import ReactDOM from "react-dom/client";
|
||||||
|
import { Config, ConfigConnections } from "../types";
|
||||||
|
import { TauriEvent } from "@tauri-apps/api/event";
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
|
||||||
|
<>
|
||||||
|
<App />
|
||||||
|
</>,
|
||||||
|
);
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
let [config, setConfig] = useState<Config>();
|
||||||
|
useEffect(() => {
|
||||||
|
invoke('get_config').then((_config) => {
|
||||||
|
let config = _config as Config;
|
||||||
|
console.log(config);
|
||||||
|
|
||||||
|
setConfig(config);
|
||||||
|
});
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const last_fm_login = () => {
|
||||||
|
invoke('last_fm_init_auth').then(() => {})
|
||||||
|
}
|
||||||
|
const save_config = () => {
|
||||||
|
invoke('save_config', { config: config }).then(() => {
|
||||||
|
// invoke('close_window').then(() => {})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h1>Config</h1>
|
||||||
|
<label>last.fm:</label>
|
||||||
|
{ config?.connections.last_fm_session ? (" already signed in") : (<button onClick={last_fm_login}>sign into last.fm</button>) }
|
||||||
|
<br/>
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
<ListenBrainz config={ config } setConfig={ setConfig } />
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
<button onClick={ save_config }>save</button>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ListenBrainzProps {
|
||||||
|
config: Config | undefined,
|
||||||
|
setConfig: React.Dispatch<React.SetStateAction<Config | undefined>>,
|
||||||
|
}
|
||||||
|
function ListenBrainz({ config, setConfig }: ListenBrainzProps) {
|
||||||
|
const [token, setToken] = useState("");
|
||||||
|
|
||||||
|
useEffect( () => {
|
||||||
|
console.log("Token: " + token);
|
||||||
|
|
||||||
|
config? setConfig((prev) => ({...prev!, connections: {...config.connections, listenbrainz_token: token}})) : {}
|
||||||
|
}, [token])
|
||||||
|
|
||||||
|
const updateToken = (e: ChangeEvent<HTMLInputElement>)=> {
|
||||||
|
setToken(e.currentTarget.value);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<label>{ "Listenbrainz Token" }</label>
|
||||||
|
<input type="text" value={ config?.connections.listenbrainz_token } onChange={updateToken} />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
|
@ -3,7 +3,7 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Library Thing</title>
|
<title>Edit Config</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
|
@ -1,24 +0,0 @@
|
||||||
import { invoke } from "@tauri-apps/api/core";
|
|
||||||
import { useRef } from "react";
|
|
||||||
import ReactDOM from "react-dom/client";
|
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
|
|
||||||
<App />,
|
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
function App() {
|
|
||||||
let x = useRef('')
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<h2>Insert your music folder path here</h2>
|
|
||||||
<form>
|
|
||||||
<input type="text" name="libinput" id="libinput" onChange={ (event) => x.current = event.target.value as string } />
|
|
||||||
<input type="submit" value="sumbit" onClick={(event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
invoke('create_library', { path: x.current }).then(() => {})
|
|
||||||
}} />
|
|
||||||
</form>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -20,6 +20,8 @@ export interface Config {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ConfigConnections {
|
export interface ConfigConnections {
|
||||||
|
discord_rpc_client_id?: number,
|
||||||
|
last_fm_session?: string,
|
||||||
listenbrainz_token?: string
|
listenbrainz_token?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,9 @@
|
||||||
import { defineConfig } from "vite";
|
import { defineConfig } from "vite";
|
||||||
import react from "@vitejs/plugin-react";
|
import react from "@vitejs/plugin-react";
|
||||||
|
import { dirname, resolve} from 'path'
|
||||||
|
import { fileURLToPath } from 'url'
|
||||||
|
|
||||||
|
const __dirname = dirname(fileURLToPath(import.meta.url))
|
||||||
|
|
||||||
// @ts-expect-error process is a nodejs global
|
// @ts-expect-error process is a nodejs global
|
||||||
const host = process.env.TAURI_DEV_HOST;
|
const host = process.env.TAURI_DEV_HOST;
|
||||||
|
@ -12,6 +16,14 @@ export default defineConfig(async () => ({
|
||||||
//
|
//
|
||||||
// 1. prevent vite from obscuring rust errors
|
// 1. prevent vite from obscuring rust errors
|
||||||
clearScreen: false,
|
clearScreen: false,
|
||||||
|
build: {
|
||||||
|
rollupOptions: {
|
||||||
|
input: {
|
||||||
|
index: resolve(__dirname, "index.html"),
|
||||||
|
config: resolve(__dirname, "src/config/index.html")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
// 2. tauri expects a fixed port, fail if that port is not available
|
// 2. tauri expects a fixed port, fail if that port is not available
|
||||||
server: {
|
server: {
|
||||||
port: 1420,
|
port: 1420,
|
||||||
|
|
Loading…
Reference in a new issue