Compare commits

..

12 commits
0.1.0 ... main

30 changed files with 1146 additions and 575 deletions

View file

@ -3,7 +3,6 @@ resolver = "2"
members = [ members = [
"src-tauri", "src-tauri",
"dmp-core", "dmp-core",
"kushi-queue",
] ]
[workspace.package] [workspace.package]

View file

@ -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"

View file

@ -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)]

View file

@ -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)]

View file

@ -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,
} }

View file

@ -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(())

View file

@ -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 {

View file

@ -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!()
} }

View file

@ -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

View file

@ -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");
} }
}); });
}); });

View file

@ -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},

View file

@ -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 {

View file

@ -1 +0,0 @@
/target

65
kushi-queue/Cargo.lock generated
View file

@ -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"

View file

@ -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"

View file

@ -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
View file

@ -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",

View file

@ -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",

View file

@ -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"] }

View file

@ -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
View 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(())
}

View file

@ -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")

View file

@ -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;

View file

@ -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
View 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} />
</>
)
}

View file

@ -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>

View file

@ -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>
</>
)
}

View file

@ -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
} }

View file

@ -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,