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 = [
"src-tauri",
"dmp-core",
"kushi-queue",
]
[workspace.package]

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,4 +1,4 @@
use kushi::{Queue, QueueError, QueueItemType};
use crate::music_storage::queue::{Queue, QueueError, QueueItemType};
use super::{
controller::{Controller, QueueCommand, QueueResponse},

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

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 {
discord_rpc_client_id?: number,
last_fm_session?: string,
listenbrainz_token?: string
}

View file

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