mirror of
https://github.com/Dangoware/dango-music-player.git
synced 2025-04-19 10:02:53 -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 = [
|
||||
"src-tauri",
|
||||
"dmp-core",
|
||||
"kushi-queue",
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
|
|
|
@ -12,7 +12,6 @@ keywords = []
|
|||
categories = []
|
||||
|
||||
[dependencies]
|
||||
kushi = { path = "../kushi-queue" }
|
||||
file-format = { version = "0.26", features = ["reader"] }
|
||||
lofty = "0.21"
|
||||
serde = { version = "1.0.195", features = ["derive"] }
|
||||
|
@ -41,3 +40,7 @@ prismriver = { git = "https://github.com/Dangoware/prismriver.git" }
|
|||
parking_lot = "0.12.3"
|
||||
discord-presence = { version = "1.4.1", features = ["activity_type"] }
|
||||
listenbrainz = "0.8.1"
|
||||
rustfm-scrobble = "1.1.1"
|
||||
reqwest = { version = "0.12.12", features = ["json"] }
|
||||
tokio = { version = "1.43.0", features = ["macros"] }
|
||||
opener = "0.7.2"
|
||||
|
|
|
@ -94,7 +94,9 @@ impl ConfigLibraries {
|
|||
|
||||
#[derive(Debug, Default, Serialize, Deserialize, Clone)]
|
||||
pub struct ConfigConnections {
|
||||
pub discord_rpc_client_id: Option<u64>,
|
||||
pub listenbrainz_token: Option<String>,
|
||||
pub last_fm_session: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Serialize, Deserialize, Clone)]
|
||||
|
|
|
@ -3,6 +3,7 @@ pub mod music_storage {
|
|||
pub mod library;
|
||||
pub mod music_collection;
|
||||
pub mod playlist;
|
||||
pub mod queue;
|
||||
mod utils;
|
||||
|
||||
#[allow(dead_code)]
|
||||
|
|
|
@ -4,24 +4,24 @@ use std::{
|
|||
Arc,
|
||||
},
|
||||
thread::sleep,
|
||||
time::{Duration, SystemTime, UNIX_EPOCH},
|
||||
time::{Duration, Instant, SystemTime, UNIX_EPOCH},
|
||||
};
|
||||
|
||||
use chrono::TimeDelta;
|
||||
use crossbeam::{scope, select};
|
||||
use crossbeam_channel::{unbounded, Receiver};
|
||||
use crossbeam::select;
|
||||
use crossbeam_channel::{bounded, unbounded, Receiver, Sender};
|
||||
use discord_presence::Client;
|
||||
use listenbrainz::ListenBrainz;
|
||||
use parking_lot::RwLock;
|
||||
use prismriver::State as PrismState;
|
||||
use rustfm_scrobble::{Scrobble, Scrobbler};
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::{
|
||||
config::Config,
|
||||
music_storage::library::{Song, Tag},
|
||||
};
|
||||
|
||||
use super::controller::Controller;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(super) enum ConnectionsNotification {
|
||||
Playback {
|
||||
|
@ -32,96 +32,190 @@ pub(super) enum ConnectionsNotification {
|
|||
SongChange(Song),
|
||||
AboutToFinish,
|
||||
EOS,
|
||||
TryEnableConnection(TryConnectionType),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ConnectionsInput {
|
||||
pub discord_rpc_client_id: Option<u64>,
|
||||
#[non_exhaustive]
|
||||
#[derive(Debug, Clone)]
|
||||
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 notifications_rx: Sender<ConnectionsNotification>,
|
||||
pub notifications_tx: Receiver<ConnectionsNotification>,
|
||||
pub inner: ConnectionsInput,
|
||||
}
|
||||
|
||||
static DC_ACTIVE: AtomicBool = AtomicBool::new(false);
|
||||
static LB_ACTIVE: AtomicBool = AtomicBool::new(false);
|
||||
static LAST_FM_ACTIVE: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
impl Controller {
|
||||
pub(super) fn handle_connections(
|
||||
config: Arc<RwLock<Config>>,
|
||||
ControllerConnections {
|
||||
notifications_rx,
|
||||
notifications_tx,
|
||||
inner: ConnectionsInput {
|
||||
discord_rpc_client_id,
|
||||
},
|
||||
}: ControllerConnections,
|
||||
) {
|
||||
let (dc_state_rx, dc_state_tx) = unbounded::<PrismState>();
|
||||
let (dc_song_rx, dc_song_tx) = unbounded::<Song>();
|
||||
let (lb_song_rx, lb_song_tx) = unbounded::<Song>();
|
||||
let (lb_abt_fin_rx, lb_abt_fn_tx) = unbounded::<()>();
|
||||
let (lb_eos_rx, lb_eos_tx) = unbounded::<()>();
|
||||
let (dc_now_playing_rx, dc_now_playing_tx) = unbounded::<Song>();
|
||||
let (dc_position_rx, dc_position_tx) = bounded::<Option<TimeDelta>>(0);
|
||||
let (lb_now_playing_rx, lb_now_playing_tx) = unbounded::<Song>();
|
||||
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::*;
|
||||
while true {
|
||||
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) => {
|
||||
if DC_ACTIVE.load(Ordering::Relaxed) {
|
||||
dc_state_rx.send(state.clone()).unwrap();
|
||||
}
|
||||
}
|
||||
SongChange(song) => {
|
||||
song_scrobbled = false;
|
||||
if DC_ACTIVE.load(Ordering::Relaxed) {
|
||||
dc_song_rx.send(song.clone()).unwrap();
|
||||
dc_now_playing_rx.send(song.clone()).unwrap();
|
||||
}
|
||||
if LB_ACTIVE.load(Ordering::Relaxed) {
|
||||
lb_song_rx.send(song).unwrap();
|
||||
lb_now_playing_rx.send(song.clone()).unwrap();
|
||||
}
|
||||
if LAST_FM_ACTIVE.load(Ordering::Relaxed) {
|
||||
last_now_playing_rx.send(song.clone()).unwrap();
|
||||
}
|
||||
}
|
||||
EOS => {
|
||||
if LB_ACTIVE.load(Ordering::Relaxed) {
|
||||
lb_eos_rx.send(()).unwrap();
|
||||
}
|
||||
}
|
||||
AboutToFinish => {
|
||||
if LB_ACTIVE.load(Ordering::Relaxed) {
|
||||
lb_abt_fin_rx.send(()).unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
if let Some(client_id) = discord_rpc_client_id {
|
||||
s.builder()
|
||||
EOS => continue,
|
||||
AboutToFinish => continue,
|
||||
TryEnableConnection(c) => {
|
||||
match c {
|
||||
TryConnectionType::Discord(client_id) => {
|
||||
let (dc_song_tx, dc_state_tx, dc_position_tx) = (
|
||||
dc_now_playing_tx.clone(),
|
||||
dc_state_tx.clone(),
|
||||
dc_position_tx.clone(),
|
||||
);
|
||||
std::thread::Builder::new()
|
||||
.name("Discord RPC Handler".to_string())
|
||||
.spawn(move |_| {
|
||||
Controller::discord_rpc(client_id, dc_song_tx, dc_state_tx);
|
||||
.spawn(move || {
|
||||
// TODO: add proper error handling here
|
||||
discord_rpc(client_id, dc_song_tx, dc_state_tx, dc_position_tx);
|
||||
})
|
||||
.unwrap();
|
||||
};
|
||||
|
||||
if let Some(token) = config.read().connections.listenbrainz_token.clone() {
|
||||
s.builder()
|
||||
}
|
||||
TryConnectionType::ListenBrainz(token) => {
|
||||
let (lb_now_playing_tx, lb_scrobble_tx) =
|
||||
(lb_now_playing_tx.clone(), lb_scrobble_tx.clone());
|
||||
std::thread::Builder::new()
|
||||
.name("ListenBrainz Handler".to_string())
|
||||
.spawn(move |_| {
|
||||
Controller::listenbrainz_scrobble(&token, lb_song_tx, lb_abt_fn_tx, lb_eos_tx);
|
||||
.spawn(move || {
|
||||
listenbrainz_scrobble(&token, lb_now_playing_tx, lb_scrobble_tx);
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
TryConnectionType::LastFM {
|
||||
api_key,
|
||||
api_secret,
|
||||
auth,
|
||||
} => {
|
||||
let (config, notifications_rx) = (config.clone(), notifications_rx.clone());
|
||||
let (last_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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn discord_rpc(client_id: u64, song_tx: Receiver<Song>, state_tx: Receiver<PrismState>) {
|
||||
// TODO: Handle seeking position change and pause
|
||||
fn discord_rpc(
|
||||
client_id: u64,
|
||||
song_tx: Receiver<Song>,
|
||||
state_tx: Receiver<PrismState>,
|
||||
position_tx: Receiver<Option<TimeDelta>>,
|
||||
) {
|
||||
let mut client =
|
||||
discord_presence::Client::with_error_config(client_id, Duration::from_secs(5), None);
|
||||
client.start();
|
||||
|
@ -130,26 +224,23 @@ impl Controller {
|
|||
}
|
||||
println!("discord connected");
|
||||
|
||||
let mut state = "Started".to_string();
|
||||
let mut state = None;
|
||||
let mut song: Option<Song> = None;
|
||||
let mut now = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.expect("Time went backwards?")
|
||||
.as_secs();
|
||||
|
||||
DC_ACTIVE.store(true, Ordering::Relaxed);
|
||||
|
||||
while true {
|
||||
let state = &mut state;
|
||||
let song = &mut song;
|
||||
let state: &mut Option<PrismState> = &mut state;
|
||||
let song: &mut Option<Song> = &mut song;
|
||||
|
||||
select! {
|
||||
recv(state_tx) -> res => {
|
||||
if let Ok(state_) = res {
|
||||
*state = match state_ {
|
||||
PrismState::Playing => "Playing",
|
||||
PrismState::Paused => "Paused",
|
||||
PrismState::Stopped => "Stopped",
|
||||
_ => "I'm Scared, Boss"
|
||||
}.to_string();
|
||||
*state = Some(state_);
|
||||
}
|
||||
},
|
||||
recv(song_tx) -> res => {
|
||||
|
@ -158,19 +249,27 @@ impl Controller {
|
|||
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
|
||||
.set_activity(|activity| {
|
||||
let a = activity
|
||||
let activity = activity
|
||||
.state(song.as_ref().map_or(String::new(), |s| {
|
||||
format!(
|
||||
"{}{}{}",
|
||||
s.get_tag(&Tag::Artist)
|
||||
.map_or(String::new(), |album| album.clone()),
|
||||
if s.get_tag(&Tag::Album).is_some()
|
||||
&& s.get_tag(&Tag::Artist).is_some()
|
||||
if s.get_tag(&Tag::Album).is_some() && s.get_tag(&Tag::Artist).is_some()
|
||||
{
|
||||
" - "
|
||||
} else {
|
||||
|
@ -188,17 +287,25 @@ impl Controller {
|
|||
String::new()
|
||||
});
|
||||
if let Some(s) = song {
|
||||
if state.as_str() == "Playing" {
|
||||
a.timestamps(|timestamps| {
|
||||
if *state == Some(PrismState::Playing) {
|
||||
activity.timestamps(|timestamps| {
|
||||
timestamps.start(now).end(now + s.duration.as_secs())
|
||||
})
|
||||
} else {
|
||||
a
|
||||
activity
|
||||
}
|
||||
} 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)
|
||||
})
|
||||
.unwrap();
|
||||
|
@ -206,7 +313,7 @@ impl Controller {
|
|||
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();
|
||||
client.authenticate(token).unwrap();
|
||||
if !client.is_authenticated() {
|
||||
|
@ -214,17 +321,15 @@ impl Controller {
|
|||
}
|
||||
|
||||
let mut song: Option<Song> = None;
|
||||
let mut last_song: Option<Song> = None;
|
||||
LB_ACTIVE.store(true, Ordering::Relaxed);
|
||||
println!("ListenBrainz connected");
|
||||
|
||||
while true {
|
||||
let song = &mut song;
|
||||
let last_song = &mut last_song;
|
||||
let now_playing = &mut song;
|
||||
|
||||
let client = &client;
|
||||
select! {
|
||||
recv(song_tx) -> res => {
|
||||
recv(now_playing_tx) -> res => {
|
||||
if let Ok(_song) = res {
|
||||
let artist = if let Some(tag) = _song.get_tag(&Tag::Artist) {
|
||||
tag.as_str()
|
||||
|
@ -240,15 +345,11 @@ impl Controller {
|
|||
|
||||
client.playing_now(artist, title, release).unwrap();
|
||||
println!("Song Listening = {artist} - {title}");
|
||||
*song = Some(_song);
|
||||
*now_playing = Some(_song);
|
||||
}
|
||||
},
|
||||
recv(abt_fn_tx) -> _ => {
|
||||
*last_song = song.take();
|
||||
println!("song = {:?}", last_song.as_ref().map(|s| s.get_tag(&Tag::Title).map_or("No Title", |t| t.as_str())));
|
||||
},
|
||||
recv(eos_tx) -> _ => {
|
||||
if let Some(song) = last_song {
|
||||
recv(scrobble_tx) -> _ => {
|
||||
if let Some(song) = now_playing.take() {
|
||||
let artist = if let Some(tag) = song.get_tag(&Tag::Artist) {
|
||||
tag.as_str()
|
||||
} else {
|
||||
|
@ -262,11 +363,132 @@ impl Controller {
|
|||
let release = song.get_tag(&Tag::Key(String::from("MusicBrainzReleaseId"))).map(|id| id.as_str());
|
||||
|
||||
client.listen(artist, title, release).unwrap();
|
||||
println!("Song Scrobbled");
|
||||
println!("Song {title} Listened");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
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 crossbeam::atomic::AtomicCell;
|
||||
use crossbeam_channel::{Receiver, Sender};
|
||||
use kushi::Queue;
|
||||
use kushi::{QueueError, QueueItem};
|
||||
use parking_lot::RwLock;
|
||||
use prismriver::{Error as PrismError, Prismriver};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
@ -20,11 +18,13 @@ use thiserror::Error;
|
|||
use uuid::Uuid;
|
||||
|
||||
use crate::config::ConfigError;
|
||||
use crate::music_controller::connections::handle_connections;
|
||||
use crate::music_storage::library::Song;
|
||||
use crate::music_storage::playlist::{ExternalPlaylist, Playlist};
|
||||
use crate::music_storage::queue::{Queue, QueueError, QueueItem};
|
||||
use crate::{config::Config, music_storage::library::MusicLibrary};
|
||||
|
||||
use super::connections::{ConnectionsInput, ConnectionsNotification, ControllerConnections};
|
||||
use super::connections::{ConnectionsNotification, ControllerConnections};
|
||||
use super::controller_handle::{LibraryCommandInput, PlayerCommandInput, QueueCommandInput};
|
||||
use super::queue::{QueueAlbum, QueueSong};
|
||||
|
||||
|
@ -109,6 +109,10 @@ pub enum LibraryCommand {
|
|||
AllSongs,
|
||||
GetLibrary,
|
||||
ExternalPlaylist(Uuid),
|
||||
PlaylistSong{
|
||||
list_uuid: Uuid,
|
||||
item_uuid: Uuid
|
||||
},
|
||||
Playlist(Uuid),
|
||||
ImportM3UPlayList(PathBuf),
|
||||
Save,
|
||||
|
@ -122,6 +126,7 @@ pub enum LibraryResponse {
|
|||
AllSongs(Vec<Song>),
|
||||
Library(MusicLibrary),
|
||||
ExternalPlaylist(ExternalPlaylist),
|
||||
PlaylistSong(Song, usize),
|
||||
Playlist(Playlist),
|
||||
ImportM3UPlayList(Uuid, String),
|
||||
Playlists(Vec<(Uuid, String)>),
|
||||
|
@ -159,24 +164,28 @@ pub struct ControllerInput {
|
|||
async_channel::Sender<QueueCommandInput>,
|
||||
async_channel::Receiver<QueueCommandInput>,
|
||||
),
|
||||
connections_mail: (
|
||||
crossbeam_channel::Sender<ConnectionsNotification>,
|
||||
crossbeam_channel::Receiver<ConnectionsNotification>,
|
||||
),
|
||||
library: MusicLibrary,
|
||||
config: Arc<RwLock<Config>>,
|
||||
playback_info: Arc<AtomicCell<PlaybackInfo>>,
|
||||
notify_next_song: Sender<Song>,
|
||||
connections: Option<ConnectionsInput>,
|
||||
}
|
||||
|
||||
pub struct ControllerHandle {
|
||||
pub(super) lib_mail_rx: async_channel::Sender<LibraryCommandInput>,
|
||||
pub(super) player_mail_rx: async_channel::Sender<PlayerCommandInput>,
|
||||
pub(super) queue_mail_rx: async_channel::Sender<QueueCommandInput>,
|
||||
pub(super) connections_rx: crossbeam_channel::Sender<ConnectionsNotification>,
|
||||
pub config: Arc<RwLock<Config>>,
|
||||
}
|
||||
|
||||
impl ControllerHandle {
|
||||
pub fn new(
|
||||
library: MusicLibrary,
|
||||
config: Arc<RwLock<Config>>,
|
||||
connections: Option<ConnectionsInput>,
|
||||
) -> (
|
||||
Self,
|
||||
ControllerInput,
|
||||
|
@ -186,6 +195,7 @@ impl ControllerHandle {
|
|||
let (lib_mail_rx, lib_mail_tx) = async_channel::unbounded();
|
||||
let (player_mail_rx, player_mail_tx) = async_channel::unbounded();
|
||||
let (queue_mail_rx, queue_mail_tx) = async_channel::unbounded();
|
||||
let (connections_mail_rx, connections_mail_tx) = crossbeam_channel::unbounded();
|
||||
let playback_info = Arc::new(AtomicCell::new(PlaybackInfo::default()));
|
||||
let notify_next_song = crossbeam::channel::unbounded::<Song>();
|
||||
(
|
||||
|
@ -193,16 +203,18 @@ impl ControllerHandle {
|
|||
lib_mail_rx: lib_mail_rx.clone(),
|
||||
player_mail_rx: player_mail_rx.clone(),
|
||||
queue_mail_rx: queue_mail_rx.clone(),
|
||||
connections_rx: connections_mail_rx.clone(),
|
||||
config: config.clone(),
|
||||
},
|
||||
ControllerInput {
|
||||
player_mail: (player_mail_rx, player_mail_tx),
|
||||
lib_mail: (lib_mail_rx, lib_mail_tx),
|
||||
queue_mail: (queue_mail_rx, queue_mail_tx),
|
||||
connections_mail: (connections_mail_rx, connections_mail_tx),
|
||||
library,
|
||||
config,
|
||||
playback_info: Arc::clone(&playback_info),
|
||||
notify_next_song: notify_next_song.0,
|
||||
connections,
|
||||
},
|
||||
playback_info,
|
||||
notify_next_song.1,
|
||||
|
@ -243,18 +255,18 @@ impl ControllerState {
|
|||
}
|
||||
}
|
||||
|
||||
#[allow(unused_variables)]
|
||||
// #[allow(unused_variables)]
|
||||
impl Controller {
|
||||
pub async fn start(
|
||||
ControllerInput {
|
||||
player_mail,
|
||||
lib_mail,
|
||||
queue_mail,
|
||||
connections_mail,
|
||||
mut library,
|
||||
config,
|
||||
playback_info,
|
||||
notify_next_song,
|
||||
connections,
|
||||
}: ControllerInput,
|
||||
) -> Result<(), Box<dyn Error>> {
|
||||
let queue: Queue<QueueSong, QueueAlbum> = Queue {
|
||||
|
@ -279,12 +291,10 @@ impl Controller {
|
|||
let player_timing = player.get_timing_recv();
|
||||
let about_to_finish_tx = player.get_about_to_finish_recv();
|
||||
let finished_tx = player.get_finished_recv();
|
||||
let (notifications_rx, notifications_tx) =
|
||||
crossbeam_channel::unbounded::<ConnectionsNotification>();
|
||||
|
||||
let a = scope.spawn({
|
||||
let queue_mail = queue_mail.clone();
|
||||
let _notifications_rx = notifications_rx.clone();
|
||||
let _notifications_rx = connections_mail.0.clone();
|
||||
let _config = config.clone();
|
||||
move || {
|
||||
futures::executor::block_on(async {
|
||||
|
@ -322,6 +332,7 @@ impl Controller {
|
|||
})
|
||||
});
|
||||
|
||||
let _notifications_rx = connections_mail.0.clone();
|
||||
let c = scope.spawn(|| {
|
||||
Controller::player_monitor_loop(
|
||||
player_state,
|
||||
|
@ -330,27 +341,26 @@ impl Controller {
|
|||
finished_tx,
|
||||
player_mail.0,
|
||||
notify_next_song,
|
||||
notifications_rx,
|
||||
_notifications_rx,
|
||||
playback_info,
|
||||
)
|
||||
.unwrap();
|
||||
});
|
||||
|
||||
if let Some(inner) = connections {
|
||||
dbg!(&inner);
|
||||
let d = scope.spawn(|| {
|
||||
Controller::handle_connections(
|
||||
handle_connections(
|
||||
config,
|
||||
ControllerConnections {
|
||||
notifications_tx,
|
||||
inner,
|
||||
notifications_rx: connections_mail.0,
|
||||
notifications_tx: connections_mail.1,
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
a.join().unwrap();
|
||||
b.join().unwrap();
|
||||
c.join().unwrap();
|
||||
d.join().unwrap();
|
||||
});
|
||||
|
||||
Ok(())
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
use std::path::PathBuf;
|
||||
|
||||
use async_channel::{Receiver, Sender};
|
||||
use kushi::{QueueError, QueueItem};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::music_storage::{library::Song, playlist::ExternalPlaylist};
|
||||
use crate::music_storage::{
|
||||
library::Song,
|
||||
playlist::ExternalPlaylist,
|
||||
queue::{QueueError, QueueItem},
|
||||
};
|
||||
|
||||
use super::{
|
||||
controller::{
|
||||
|
@ -176,6 +179,46 @@ impl ControllerHandle {
|
|||
};
|
||||
res
|
||||
}
|
||||
|
||||
// The Connections Section
|
||||
pub fn discord_rpc(&self, client_id: u64) {
|
||||
self.connections_rx
|
||||
.send(
|
||||
super::connections::ConnectionsNotification::TryEnableConnection(
|
||||
super::connections::TryConnectionType::Discord(client_id),
|
||||
),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
pub fn listenbrainz_scrobble_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 {
|
||||
|
|
|
@ -89,6 +89,15 @@ impl Controller {
|
|||
.await
|
||||
.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!()
|
||||
}
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
use chrono::TimeDelta;
|
||||
use crossbeam_channel::Sender;
|
||||
use kushi::{QueueItem, QueueItemType};
|
||||
use prismriver::{Prismriver, Volume};
|
||||
|
||||
use crate::music_controller::{
|
||||
use crate::{
|
||||
music_controller::{
|
||||
controller::{LibraryCommand, LibraryResponse},
|
||||
queue::QueueSong,
|
||||
},
|
||||
music_storage::queue::{QueueItem, QueueItemType},
|
||||
};
|
||||
|
||||
use super::{
|
||||
|
@ -89,6 +91,8 @@ impl Controller {
|
|||
panic!("This is temporary, handle queueItemTypes at some point")
|
||||
};
|
||||
|
||||
match np_song.location {
|
||||
PlayerLocation::Library => {
|
||||
let (command, tx) =
|
||||
LibraryCommandInput::command(LibraryCommand::AllSongs);
|
||||
// Append next song in library
|
||||
|
@ -97,7 +101,6 @@ impl Controller {
|
|||
else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let (command, tx) = LibraryCommandInput::command(
|
||||
LibraryCommand::Song(np_song.song.uuid),
|
||||
);
|
||||
|
@ -124,7 +127,44 @@ impl Controller {
|
|||
} else {
|
||||
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
|
||||
.send(PlayerResponse::NowPlaying(Ok(np_song.song.clone())))
|
||||
.await
|
||||
|
|
|
@ -34,7 +34,7 @@ impl Controller {
|
|||
move || {
|
||||
println!("playback monitor started");
|
||||
while true {
|
||||
let (position, duration) = playback_time_tx.recv().unwrap();
|
||||
let (duration, position) = playback_time_tx.recv().unwrap();
|
||||
notify_connections
|
||||
.send(ConnectionsNotification::Playback {
|
||||
position: position.clone(),
|
||||
|
@ -52,8 +52,9 @@ impl Controller {
|
|||
futures::executor::block_on(async {
|
||||
while true {
|
||||
_ = about_to_finish_tx.recv();
|
||||
notify_connections.send(ConnectionsNotification::AboutToFinish).unwrap();
|
||||
println!("About to Finish");
|
||||
notify_connections
|
||||
.send(ConnectionsNotification::AboutToFinish)
|
||||
.unwrap();
|
||||
}
|
||||
})
|
||||
});
|
||||
|
@ -81,7 +82,6 @@ impl Controller {
|
|||
notify_connections
|
||||
.send(ConnectionsNotification::EOS)
|
||||
.unwrap();
|
||||
println!("End of song");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use kushi::{Queue, QueueError, QueueItemType};
|
||||
use crate::music_storage::queue::{Queue, QueueError, QueueItemType};
|
||||
|
||||
use super::{
|
||||
controller::{Controller, QueueCommand, QueueResponse},
|
||||
|
|
|
@ -335,6 +335,24 @@ impl Playlist {
|
|||
|
||||
(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 {
|
||||
|
|
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",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@jprochazk/cbor": "github:jprochazk/cbor",
|
||||
"@tauri-apps/api": "^2",
|
||||
"@tauri-apps/plugin-shell": "^2",
|
||||
"cbor": "github:jprochazk/cbor",
|
||||
"cbor-x": "^1.6.0",
|
||||
"node-fetch": "^3.3.2",
|
||||
"path": "^0.12.7",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
"react-dom": "^18.2.0",
|
||||
"url": "^0.11.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tauri-apps/cli": "^2",
|
||||
|
@ -293,78 +292,6 @@
|
|||
"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": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
|
||||
|
@ -733,10 +660,6 @@
|
|||
"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": {
|
||||
"version": "0.3.5",
|
||||
"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_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": {
|
||||
"version": "1.0.30001684",
|
||||
"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": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
|
||||
|
@ -1438,13 +1354,17 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"node_modules/detect-libc": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz",
|
||||
"integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==",
|
||||
"optional": true,
|
||||
"node_modules/dunder-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.1",
|
||||
"es-errors": "^1.3.0",
|
||||
"gopd": "^1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/electron-to-chromium": {
|
||||
|
@ -1453,6 +1373,33 @@
|
|||
"integrity": "sha512-PWVzBjghx7/wop6n22vS2MLU8tKGd4Q91aCEGhG/TYmW6PP5OcSXcdnxTe1NNt0T66N8D6jxh4kC8UsdzOGaIw==",
|
||||
"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": {
|
||||
"version": "0.21.5",
|
||||
"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_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": {
|
||||
"version": "1.0.0-beta.2",
|
||||
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
|
||||
|
@ -1556,6 +1511,41 @@
|
|||
"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": {
|
||||
"version": "11.12.0",
|
||||
"resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
|
||||
|
@ -1565,6 +1555,44 @@
|
|||
"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": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||
|
@ -1614,6 +1642,14 @@
|
|||
"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": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
|
@ -1673,26 +1709,32 @@
|
|||
"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": {
|
||||
"version": "2.0.18",
|
||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz",
|
||||
"integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==",
|
||||
"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": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||
|
@ -1727,6 +1769,33 @@
|
|||
"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": {
|
||||
"version": "18.3.1",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
|
||||
|
@ -1813,6 +1882,74 @@
|
|||
"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": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||
|
@ -1865,6 +2002,26 @@
|
|||
"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": {
|
||||
"version": "5.4.11",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.11.tgz",
|
||||
|
|
|
@ -10,14 +10,13 @@
|
|||
"tauri": "tauri"
|
||||
},
|
||||
"dependencies": {
|
||||
"@jprochazk/cbor": "github:jprochazk/cbor",
|
||||
"@tauri-apps/api": "^2",
|
||||
"@tauri-apps/plugin-shell": "^2",
|
||||
"cbor": "github:jprochazk/cbor",
|
||||
"cbor-x": "^1.6.0",
|
||||
"node-fetch": "^3.3.2",
|
||||
"path": "^0.12.7",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
"react-dom": "^18.2.0",
|
||||
"url": "^0.11.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tauri-apps/cli": "^2",
|
||||
|
|
|
@ -19,7 +19,6 @@ tauri-build = { version = "2", features = [] }
|
|||
|
||||
[dependencies]
|
||||
dmp-core = { path = "../dmp-core" }
|
||||
kushi = { path = "../kushi-queue" }
|
||||
tauri = { version = "2", features = [ "protocol-asset", "unstable"] }
|
||||
tauri-plugin-shell = "2"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
|
|
|
@ -1,15 +1,18 @@
|
|||
use std::{fs::OpenOptions, io::Write};
|
||||
|
||||
use dmp_core::music_controller::{
|
||||
use dmp_core::{
|
||||
music_controller::{
|
||||
connections::LastFMAuth,
|
||||
controller::{ControllerHandle, PlayerLocation},
|
||||
queue::QueueSong,
|
||||
},
|
||||
music_storage::queue::{QueueItem, QueueItemType},
|
||||
};
|
||||
use kushi::QueueItem;
|
||||
use tauri::{AppHandle, Emitter, State, Wry};
|
||||
use tempfile::TempDir;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::wrappers::_Song;
|
||||
use crate::{wrappers::_Song, LAST_FM_API_KEY, LAST_FM_API_SECRET};
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn add_song_to_queue(
|
||||
|
@ -21,7 +24,7 @@ pub async fn add_song_to_queue(
|
|||
dbg!(&location);
|
||||
let (song, _) = ctrl_handle.lib_get_song(uuid).await;
|
||||
match ctrl_handle
|
||||
.queue_append(QueueItem::from_item_type(kushi::QueueItemType::Single(
|
||||
.queue_append(QueueItem::from_item_type(QueueItemType::Single(
|
||||
QueueSong { song, location },
|
||||
)))
|
||||
.await
|
||||
|
@ -80,3 +83,13 @@ pub async fn display_album_art(
|
|||
};
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn last_fm_init_auth(ctrl_handle: State<'_, ControllerHandle>) -> Result<(), String> {
|
||||
ctrl_handle.last_fm_scrobble_auth(
|
||||
LAST_FM_API_KEY.to_string(),
|
||||
LAST_FM_API_SECRET.to_string(),
|
||||
LastFMAuth::Session(None),
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
|
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,
|
||||
};
|
||||
|
||||
use config::{close_window, get_config, open_config_window, save_config};
|
||||
use crossbeam::channel::{bounded, unbounded, Receiver, Sender};
|
||||
use dmp_core::{
|
||||
config::{Config, ConfigLibrary},
|
||||
music_controller::{
|
||||
connections::ConnectionsInput,
|
||||
connections::LastFMAuth,
|
||||
controller::{Controller, ControllerHandle, PlaybackInfo},
|
||||
},
|
||||
music_storage::library::{MusicLibrary, Song},
|
||||
};
|
||||
use futures::channel::oneshot;
|
||||
use parking_lot::RwLock;
|
||||
use tauri::{http::Response, Emitter, Manager, State, Wry};
|
||||
use uuid::Uuid;
|
||||
|
@ -27,23 +27,28 @@ use crate::wrappers::{
|
|||
get_library, get_playlist, get_playlists, get_queue, get_song, import_playlist, next, pause,
|
||||
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 config;
|
||||
pub mod wrappers;
|
||||
|
||||
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)]
|
||||
pub fn run() {
|
||||
let (rx, tx) = unbounded::<Config>();
|
||||
let (config_rx, config_tx) = unbounded::<Config>();
|
||||
let (lib_rx, lib_tx) = unbounded::<Option<PathBuf>>();
|
||||
let (handle_rx, handle_tx) = unbounded::<ControllerHandle>();
|
||||
let (playback_info_rx, playback_info_tx) = bounded(1);
|
||||
let (next_rx, next_tx) = bounded(1);
|
||||
|
||||
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 _temp_config = ConfigLibrary::default();
|
||||
let _lib = config.libraries.get_default().unwrap_or(&_temp_config);
|
||||
|
@ -97,14 +102,25 @@ pub fn run() {
|
|||
|
||||
library.save(save_path).unwrap();
|
||||
|
||||
let (handle, input, playback_info, next_song_notification) = ControllerHandle::new(
|
||||
library,
|
||||
std::sync::Arc::new(RwLock::new(config)),
|
||||
Some(ConnectionsInput {
|
||||
discord_rpc_client_id: std::option_env!("DISCORD_CLIENT_ID")
|
||||
.map(|id| id.parse::<u64>().unwrap()),
|
||||
}),
|
||||
let last_fm_session = config.connections.last_fm_session.clone();
|
||||
let listenbrainz_token = config.connections.listenbrainz_token.clone();
|
||||
|
||||
let (handle, input, playback_info, next_song_notification) =
|
||||
ControllerHandle::new(library, std::sync::Arc::new(RwLock::new(config)));
|
||||
|
||||
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();
|
||||
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 app = tauri::Builder::default()
|
||||
.plugin(tauri_plugin_shell::init())
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
get_config,
|
||||
init_get_config,
|
||||
create_new_library,
|
||||
get_library,
|
||||
play,
|
||||
|
@ -135,8 +152,13 @@ pub fn run() {
|
|||
remove_from_queue,
|
||||
display_album_art,
|
||||
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(HandleTx(handle_tx))
|
||||
.manage(tempfile::TempDir::new().unwrap())
|
||||
|
@ -238,7 +260,7 @@ struct LibRx(Sender<Option<PathBuf>>);
|
|||
struct HandleTx(Receiver<ControllerHandle>);
|
||||
|
||||
#[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") {
|
||||
let path = dir.config_dir();
|
||||
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();
|
||||
|
||||
println!("got config");
|
||||
Ok(config)
|
||||
} else {
|
||||
panic!("No config dir for DMP")
|
||||
|
|
|
@ -4,10 +4,12 @@ use chrono::{serde::ts_milliseconds_option, DateTime, Utc};
|
|||
use crossbeam::channel::Sender;
|
||||
use dmp_core::{
|
||||
music_controller::controller::{ControllerHandle, PlayerLocation},
|
||||
music_storage::library::{Song, Tag, URI},
|
||||
music_storage::{
|
||||
library::{Song, Tag, URI},
|
||||
queue::QueueItemType,
|
||||
},
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use kushi::QueueItemType;
|
||||
use serde::Serialize;
|
||||
use tauri::{AppHandle, Emitter, State, Wry};
|
||||
use uuid::Uuid;
|
||||
|
|
23
src/App.tsx
23
src/App.tsx
|
@ -48,14 +48,14 @@ function App() {
|
|||
const unlisten = appWindow.listen<any>("queue_updated", (_) => {
|
||||
// console.log(event);
|
||||
invoke('get_queue').then((_songs) => {
|
||||
let songs = _songs as any[]
|
||||
let songs = _songs as any[];
|
||||
setQueue(
|
||||
songs.filter((_, i) => i != 0).map((song, i) =>
|
||||
<QueueSong
|
||||
song={ song[0] }
|
||||
location={ song[1] as "Library" | {"Playlist" : string}}
|
||||
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);
|
||||
return (
|
||||
<Song
|
||||
key={ song.uuid }
|
||||
key={ song.uuid + Math.floor(Math.random() * 100_000_000_000) }
|
||||
location={ song.location }
|
||||
playerLocation={ {"Playlist" : item.uuid } }
|
||||
uuid={ song.uuid }
|
||||
|
@ -200,6 +200,7 @@ function PlaylistHead({ playlists, setPlaylists, setViewName, setLibrary }: Play
|
|||
} }>Library</button>
|
||||
{ playlists }
|
||||
<button onClick={ handle_import }>Import .m3u Playlist</button>
|
||||
<button onClick={() => { invoke('open_config_window').then(() => {}) }} style={{marginLeft: "auto", float: "right"}}>Edit DMP</button>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
@ -220,7 +221,7 @@ function MainView({ lib_ref, viewName }: MainViewProps) {
|
|||
|
||||
return (
|
||||
<Song
|
||||
key={ song.uuid }
|
||||
key={ song.uuid + Math.floor(Math.random() * 100_000_000_000) }
|
||||
location={ song.location }
|
||||
playerLocation="Library"
|
||||
uuid={ song.uuid }
|
||||
|
@ -288,6 +289,8 @@ function PlayBar({ playing, setPlaying }: PlayBarProps) {
|
|||
const [seekBarSize, setSeekBarSize] = useState(0);
|
||||
const seekBarRef = React.createRef<HTMLDivElement>();
|
||||
|
||||
const [lastFmLoggedIn, setLastFmLoggedIn] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const unlisten = appWindow.listen<any>("playback_info", ({ payload, }) => {
|
||||
const info = payload as playbackInfo;
|
||||
|
@ -296,7 +299,7 @@ function PlayBar({ playing, setPlaying }: PlayBarProps) {
|
|||
|
||||
setPosition(pos_);
|
||||
setDuration(dur_);
|
||||
let progress = ((dur_/pos_) * 100);
|
||||
let progress = ((pos_/dur_) * 100);
|
||||
setSeekBarSize(progress)
|
||||
})
|
||||
return () => { unlisten.then((f) => f()) }
|
||||
|
@ -305,7 +308,7 @@ function PlayBar({ playing, setPlaying }: PlayBarProps) {
|
|||
const seek = (event: React.MouseEvent<HTMLDivElement>) => {
|
||||
event.stopPropagation();
|
||||
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()
|
||||
};
|
||||
|
@ -332,10 +335,10 @@ function PlayBar({ playing, setPlaying }: PlayBarProps) {
|
|||
invoke('set_volume', { volume: volume.target.value }).then(() => {})
|
||||
}} />
|
||||
<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") }:
|
||||
{ (+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>
|
||||
</div>
|
||||
|
@ -405,7 +408,7 @@ function QueueSong({ song, location, index }: QueueSongProps) {
|
|||
}
|
||||
|
||||
function getConfig(): any {
|
||||
invoke('get_config').then( (_config) => {
|
||||
invoke('init_get_config').then( (_config) => {
|
||||
let config = _config as Config;
|
||||
if (config.libraries.libraries.length == 0) {
|
||||
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>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Library Thing</title>
|
||||
<title>Edit Config</title>
|
||||
</head>
|
||||
<body>
|
||||
<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 {
|
||||
discord_rpc_client_id?: number,
|
||||
last_fm_session?: string,
|
||||
listenbrainz_token?: string
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
import { defineConfig } from "vite";
|
||||
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
|
||||
const host = process.env.TAURI_DEV_HOST;
|
||||
|
@ -12,6 +16,14 @@ export default defineConfig(async () => ({
|
|||
//
|
||||
// 1. prevent vite from obscuring rust errors
|
||||
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
|
||||
server: {
|
||||
port: 1420,
|
||||
|
|
Loading…
Reference in a new issue