diff --git a/dmp-core/Cargo.toml b/dmp-core/Cargo.toml index afbcf56..e7521ad 100644 --- a/dmp-core/Cargo.toml +++ b/dmp-core/Cargo.toml @@ -37,4 +37,5 @@ futures = "0.3.30" async-channel = "2.3.1" ciborium = "0.2.2" itertools = "0.13.0" -prismriver = { git = "https://github.com/Dangoware/prismriver.git" } +prismriver = { git = "https://github.com/Dangoware/prismriver.git"} +parking_lot = "0.12.3" diff --git a/dmp-core/src/music_controller/controller.rs b/dmp-core/src/music_controller/controller.rs index 251b249..c19081c 100644 --- a/dmp-core/src/music_controller/controller.rs +++ b/dmp-core/src/music_controller/controller.rs @@ -3,8 +3,10 @@ //! other functions #![allow(while_true)] +use async_channel::unbounded; use chrono::TimeDelta; use crossbeam::atomic::AtomicCell; +use crossbeam_channel::{Receiver, Sender}; use kushi::{Queue, QueueItemType}; use kushi::{QueueError, QueueItem}; use prismriver::{Prismriver, Volume, Error as PrismError}; @@ -82,6 +84,7 @@ pub enum PlayerCommand { PrevSong, Pause, Play, + Seek(i64), Enqueue(usize), SetVolume(f32), PlayNow(Uuid, PlayerLocation), @@ -161,6 +164,7 @@ pub struct ControllerInput { library: MusicLibrary, config: Arc>, playback_info: Arc>, + notify_next_song: Sender, } pub struct ControllerHandle { @@ -170,11 +174,12 @@ pub struct ControllerHandle { } impl ControllerHandle { - pub fn new(library: MusicLibrary, config: Arc>) -> (Self, ControllerInput, Arc>) { + pub fn new(library: MusicLibrary, config: Arc>) -> (Self, ControllerInput, Arc>, Receiver) { let lib_mail = MailMan::double(); let player_mail = MailMan::double(); let queue_mail = MailMan::double(); let playback_info = Arc::new(AtomicCell::new(PlaybackInfo::default())); + let notify_next_song = crossbeam::channel::unbounded::(); ( ControllerHandle { lib_mail: lib_mail.0.clone(), @@ -188,8 +193,10 @@ impl ControllerHandle { library, config, playback_info: Arc::clone(&playback_info), + notify_next_song: notify_next_song.0, }, playback_info, + notify_next_song.1 ) } } @@ -237,6 +244,7 @@ impl Controller { mut library, config, playback_info, + notify_next_song, }: ControllerInput ) -> Result<(), Box> { let queue: Queue = Queue { @@ -309,6 +317,7 @@ impl Controller { player_mail.0, queue_mail.0, playback_info, + notify_next_song, ).unwrap(); }); @@ -328,7 +337,6 @@ impl Controller { mut state: ControllerState, ) -> Result<(), ()> { player.write().unwrap().set_volume(Volume::new(state.volume)); - println!("volume set to {}", state.volume); 'outer: while true { let _mail = player_mail.recv().await; if let Ok(mail) = _mail { @@ -343,9 +351,13 @@ impl Controller { player_mail.send(PlayerResponse::Empty(Ok(()))).await.unwrap(); } + PlayerCommand::Seek(time) => { + let res = player.write().unwrap().seek_to(TimeDelta::milliseconds(time)); + player_mail.send(PlayerResponse::Empty(res.map_err(|e| e.into()))).await.unwrap(); + } + PlayerCommand::SetVolume(volume) => { player.write().unwrap().set_volume(Volume::new(volume)); - println!("volume set to {volume}"); player_mail.send(PlayerResponse::Empty(Ok(()))).await.unwrap(); state.volume = volume; @@ -403,6 +415,9 @@ impl Controller { } player_mail.send(PlayerResponse::NowPlaying(Ok(np_song.song.clone()))).await.unwrap(); + + state.now_playing = np_song.song.uuid; + _ = state.write_file(); } QueueResponse::Item(Err(e)) => { player_mail.send(PlayerResponse::NowPlaying(Err(e.into()))).await.unwrap(); } @@ -425,6 +440,9 @@ impl Controller { let QueueItemType::Single(np_song) = item.item else { panic!("This is temporary, handle queueItemTypes at some point")}; player_mail.send(PlayerResponse::NowPlaying(Ok(np_song.song.clone()))).await.unwrap(); + + state.now_playing = np_song.song.uuid; + _ = state.write_file(); } QueueResponse::Item(Err(e)) => { player_mail.send(PlayerResponse::NowPlaying(Err(e.into()))).await.unwrap(); @@ -543,6 +561,7 @@ impl Controller { player_mail: MailMan, queue_mail: MailMan, player_info: Arc>, + notify_next_song: Sender, ) -> Result<(), ()> { let finished_recv = player.read().unwrap().get_finished_recv(); @@ -550,7 +569,7 @@ impl Controller { std::thread::scope(|s| { // Thread for timing and metadata s.spawn({ - let player = Arc::clone(&player); + // let player = Arc::clone(&player); move || { while true { let player = player.read().unwrap(); @@ -567,13 +586,21 @@ impl Controller { }); // Thread for End of Track - s.spawn(move || { + s.spawn(move || { futures::executor::block_on(async { while true { let _ = finished_recv.recv(); + println!("End of song"); - std::thread::sleep(Duration::from_millis(100)); + player_mail.send(PlayerCommand::NextSong).await.unwrap(); + let PlayerResponse::NowPlaying(res) = player_mail.recv().await.unwrap() else { + unreachable!() + }; + if let Ok(song) = res { + notify_next_song.send(song).unwrap(); + } } - }); + std::thread::sleep(Duration::from_millis(100)); + });}); }); // Check for duration and spit it out @@ -681,7 +708,7 @@ impl Controller { } } -#[derive(Debug, Default)] +#[derive(Debug, Default, Serialize, Clone)] pub struct PlaybackInfo { pub duration: Option, pub position: Option, diff --git a/dmp-core/src/music_controller/controller_handle.rs b/dmp-core/src/music_controller/controller_handle.rs index 7752fa3..1b9a0f0 100644 --- a/dmp-core/src/music_controller/controller_handle.rs +++ b/dmp-core/src/music_controller/controller_handle.rs @@ -106,6 +106,14 @@ impl ControllerHandle { res } + pub async fn seek(&self, time: i64) -> Result<(), PlayerError> { + self.player_mail.send(PlayerCommand::Seek(time)).await.unwrap(); + let PlayerResponse::Empty(res) = self.player_mail.recv().await.unwrap() else { + unreachable!() + }; + res + } + pub async fn set_volume(&self, volume: f32) -> () { self.player_mail.send(PlayerCommand::SetVolume(volume)).await.unwrap(); let PlayerResponse::Empty(Ok(())) = self.player_mail.recv().await.unwrap() else { diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 3616309..d39ce6f 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -36,6 +36,8 @@ rfd = "0.15.1" colog = "1.3.0" tempfile = "3.14.0" opener = "0.7.2" +discord-presence = { version = "1.4.1", features = ["activity_type"] } +parking_lot = "0.12.3" [features] default = [ "custom-protocol" ] diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index c29d545..d535c83 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,12 +1,18 @@ -use std::{fs, path::PathBuf, str::FromStr, thread::{scope, spawn}}; +#![allow(while_true)] -use crossbeam::channel::{unbounded, Receiver, Sender}; -use dmp_core::{config::{Config, ConfigLibrary}, music_controller::controller::{Controller, ControllerHandle, LibraryResponse, PlaybackInfo}, music_storage::library::MusicLibrary}; +use std::{borrow::BorrowMut, fs, ops::Deref, path::PathBuf, sync::{atomic::Ordering, Arc}, thread::{scope, spawn}, time::Duration}; + +use crossbeam::channel::{bounded, unbounded, Receiver, Sender}; +use discord_presence::{models::{Activity, ActivityButton, ActivityTimestamps, ActivityType}, Event}; +use dmp_core::{config::{Config, ConfigLibrary}, music_controller::controller::{Controller, ControllerHandle, LibraryResponse, PlaybackInfo}, music_storage::library::{MusicLibrary, Song}}; +use futures::channel::oneshot; +use parking_lot::lock_api::{RawRwLock, RwLock}; use rfd::FileHandle; use tauri::{http::Response, Emitter, Manager, State, WebviewWindowBuilder, Wry}; use uuid::Uuid; +use wrappers::_Song; -use crate::wrappers::{get_library, play, pause, prev, set_volume, get_song, next, get_queue, import_playlist, get_playlist, get_playlists, remove_from_queue}; +use crate::wrappers::{get_library, play, pause, prev, set_volume, get_song, next, get_queue, import_playlist, get_playlist, get_playlists, remove_from_queue, seek}; use commands::{add_song_to_queue, play_now, display_album_art}; @@ -20,15 +26,25 @@ pub fn run() { let (rx, tx) = unbounded::(); let (lib_rx, lib_tx) = unbounded::>(); let (handle_rx, handle_tx) = unbounded::(); + let (playback_info_rx, playback_info_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 scan_path = { lib_tx.recv().unwrap() }; let _temp_config = ConfigLibrary::default(); let _lib = config.libraries.get_default().unwrap_or(&_temp_config); let save_path = if _lib.path == PathBuf::default() { - scan_path.as_ref().unwrap().clone().canonicalize().unwrap().join("library.dlib") + let p = scan_path.as_ref().unwrap().clone().canonicalize().unwrap(); + + if cfg!(windows) { + p.join("library_windows.dlib") + } else if cfg!(unix) { + p.join("library_unix.dlib") + } else { + p.join("library.dlib") + } } else { _lib.path.clone() }; @@ -54,21 +70,22 @@ pub fn run() { library.save(save_path).unwrap(); - let (handle, input, playback_info) = ControllerHandle::new( + let ( + handle, + input, + playback_info, + next_song_notification, + ) = ControllerHandle::new( library, std::sync::Arc::new(std::sync::RwLock::new(config)) ); handle_rx.send(handle).unwrap(); + playback_info_rx.send(playback_info).unwrap(); + next_rx.send(next_song_notification).unwrap(); - scope(|s| { - s.spawn(|| { - let _controller = futures::executor::block_on(Controller::start(input)).unwrap(); - }); - s.spawn(|| { + let _controller = futures::executor::block_on(Controller::start(input)).unwrap(); - }); - }) }); @@ -93,10 +110,85 @@ pub fn run() { get_playlists, remove_from_queue, display_album_art, + seek, ]).manage(ConfigRx(rx)) .manage(LibRx(lib_rx)) .manage(HandleTx(handle_tx)) .manage(tempfile::TempDir::new().unwrap()) + .setup(|app| { + let _app = app.handle().clone(); + let app = _app.clone(); + + std::thread::Builder::new() + .name("PlaybackInfo handler".to_string()) + .spawn(move || { + let mut _info = Arc::new(RwLock::new(PlaybackInfo::default())); + let mut _now_playing: Arc>> = Arc::new(RwLock::new(None)); + + scope(|s| { + let info = _info.clone(); + s.spawn(|| { + let info = info; + let playback_info = playback_info_tx.recv().unwrap(); + while true { + let i = playback_info.take(); + app.emit("playback_info", i.clone()).unwrap(); + *info.write() = i; + std::thread::sleep(Duration::from_millis(100)); + } + }); + + let now_playing = _now_playing.clone(); + s.spawn(|| { + let now_playing = now_playing; + let next_song_notification = next_tx.recv().unwrap(); + while true { + let song = next_song_notification.recv().unwrap(); + app.emit("now_playing_change", _Song::from(&song)).unwrap(); + app.emit("queue_updated", ()).unwrap(); + app.emit("playing", ()).unwrap(); + now_playing.write().insert(song); + } + }); + + let info = _info.clone(); + let now_playing = _now_playing.clone(); + s.spawn(|| { + let info = info; + let now_playing = now_playing; + let mut rpc_client = discord_presence::Client::new(std::env!("DISCORD_SECRET").parse::().unwrap()); + rpc_client.start(); + rpc_client.block_until_event(Event::Connected).unwrap(); + rpc_client.set_activity(|_| { + Activity { + state: Some("Idle".to_string()), + _type: Some(ActivityType::Listening), + buttons: vec![ActivityButton { + label: Some("Try the Player!(beta)".to_string()), + url: Some("https://github.com/Dangoware/dango-music-player".to_string()) + }], + ..Default::default() + } + }).unwrap(); + + while true { + rpc_client.set_activity(|mut a| { + if let Some(song) = now_playing.read().clone() { + + a.timestamps = info.read().duration.map(|dur| ActivityTimestamps::new().end(dur.num_milliseconds() as u64) ); + a.details = Some(format!("{} 🍡 {}" song.tags. )) + } + a + }); + } + }); + + + }); + }).unwrap(); + + Ok(()) + }) .register_asynchronous_uri_scheme_protocol("asset", move |ctx, req, res| { let query = req .clone() diff --git a/src-tauri/src/wrappers.rs b/src-tauri/src/wrappers.rs index d5fc41a..ab877f4 100644 --- a/src-tauri/src/wrappers.rs +++ b/src-tauri/src/wrappers.rs @@ -193,3 +193,8 @@ pub async fn get_song(ctrl_handle: State<'_, ControllerHandle>, uuid: Uuid) -> R println!("got song {}", &song.tags.get(&Tag::Title).unwrap_or(&String::new())); Ok(_Song::from(&song)) } + +#[tauri::command] +pub async fn seek(ctrl_handle: State<'_, ControllerHandle>, time: i64) -> Result<(), String> { + ctrl_handle.seek(time).await.map_err(|e| e.to_string()) +} diff --git a/src/App.css b/src/App.css index e392bd6..09b8865 100644 --- a/src/App.css +++ b/src/App.css @@ -239,3 +239,8 @@ main { margin: 0; color: var(--mediumTextColor); } + +.unselectable { + -webkit-user-select: none; + user-select: none; +} \ No newline at end of file diff --git a/src/App.tsx b/src/App.tsx index 1117ce2..4e2ec53 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,7 +1,7 @@ -import { useEffect, useState } from "react"; +import React, { createRef, useEffect, useRef, useState } from "react"; import { convertFileSrc, invoke } from "@tauri-apps/api/core"; import "./App.css"; -import { Config } from "./types"; +import { Config, playbackInfo } from "./types"; // import { EventEmitter } from "@tauri-apps/plugin-shell"; // import { listen } from "@tauri-apps/api/event"; // import { fetch } from "@tauri-apps/plugin-http"; @@ -263,13 +263,13 @@ function Song(props: SongProps) { // console.log(props.tags); return( -
{ +
{ invoke("play_now", { uuid: props.uuid, location: props.playerLocation }).then(() => {}) }} className="song"> -

{ props.tags.TrackArtist }

-

{ props.tags.TrackTitle }

-

{ props.tags.AlbumTitle }

-

+

{ props.tags.TrackArtist }

+

{ props.tags.TrackTitle }

+

{ props.tags.AlbumTitle }

+

{ Math.round(+props.duration / 60) }: { (+props.duration % 60).toString().padStart(2, "0") }

@@ -291,10 +291,38 @@ interface PlayBarProps { } function PlayBar({ playing, setPlaying }: PlayBarProps) { + const [position, setPosition] = useState(0); + const [duration, setDuration] = useState(0); + const [seekBarSize, setSeekBarSize] = useState(0); + const seekBarRef = React.createRef(); + + useEffect(() => { + const unlisten = appWindow.listen("playback_info", ({ payload, }) => { + const info = payload as playbackInfo; + const _pos = Array.isArray(info.position) ? info.position![0] : 0; + const _dur = Array.isArray(info.duration) ? info.duration![0] : 0; + + setPosition(_pos); + setDuration(_dur); + let progress = (Math.floor((_pos/_dur)*100)); + console.log(progress + '%'); + setSeekBarSize(progress) + }) + return () => { unlisten.then((f) => f()) } + }, []); + + const seek = (event: React.MouseEvent) => { + event.stopPropagation(); + let rect = seekBarRef.current!.getBoundingClientRect(); + let val = ((event.clientX-rect.left) / (rect.width))*duration; + + invoke('seek', { time: Math.round(val * 1000) }).then() + }; + return ( -
-
-
+
+
+
@@ -312,7 +340,12 @@ function PlayBar({ playing, setPlaying }: PlayBarProps) { { invoke('set_volume', { volume: volume.target.value }).then(() => {}) }} /> -

+

+ { Math.round(+position / 60) }: + { (+position % 60).toString().padStart(2, "0") } / + { Math.round(+duration / 60) }: + { (+duration % 60).toString().padStart(2, "0") } +

@@ -329,7 +362,7 @@ interface NowPlayingProps { function NowPlaying({ title, artist, album, artwork }: NowPlayingProps) { return (
-
+
{ artwork }

{ title }

@@ -358,7 +391,7 @@ interface QueueSongProps { } function QueueSong({ song, location, index }: QueueSongProps) { - console.log(song.tags); + // console.log(song.tags); let removeFromQueue = () => { invoke('remove_from_queue', { index: index }).then(() => {}) diff --git a/src/types.ts b/src/types.ts index 9b58d6f..eace57d 100644 --- a/src/types.ts +++ b/src/types.ts @@ -60,4 +60,10 @@ export enum URI { export enum BannedType { +} + +export interface playbackInfo { + duration?: number[], + position?: [number, number], + metadata: Map } \ No newline at end of file