From ce2c08aae46186f960b6ed9e1dc0dbd470ea5ad1 Mon Sep 17 00:00:00 2001 From: Fabian Lippold Date: Wed, 15 Nov 2023 08:49:12 +0100 Subject: [PATCH] refactor --- Cargo.toml | 2 +- src/cache.rs | 4 +- src/decoder.rs | 206 ------------------ src/main.rs | 50 +---- src/player.rs | 496 ------------------------------------------ src/player/command.rs | 10 + src/player/facade.rs | 86 ++++++++ src/player/loader.rs | 97 +++++++++ src/player/mod.rs | 381 ++++++++++++++++++++++++++++++++ src/song.rs | 131 ++++++++++- src/tui/fancy.rs | 24 +- src/tui/files.rs | 39 ++-- src/tui/mod.rs | 22 +- src/tui/queue.rs | 17 +- src/tui/search.rs | 16 +- src/tui/status.rs | 33 ++- src/tui/tabs.rs | 2 +- 17 files changed, 798 insertions(+), 818 deletions(-) delete mode 100644 src/decoder.rs delete mode 100644 src/player.rs create mode 100644 src/player/command.rs create mode 100644 src/player/facade.rs create mode 100644 src/player/loader.rs create mode 100644 src/player/mod.rs diff --git a/Cargo.toml b/Cargo.toml index 007750d..3372a40 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,7 @@ edition = "2021" [dependencies] # general -itertools = "0.11.0" +itertools = "0.12.0" log = "0.4.19" simplelog = "0.12.1" serde = { version = "1.0.181", features = ["derive", "rc"] } diff --git a/src/cache.rs b/src/cache.rs index f837813..054f83f 100644 --- a/src/cache.rs +++ b/src/cache.rs @@ -1,4 +1,4 @@ -use crate::{config::Config, decoder, song::Song}; +use crate::{config::Config, song::Song}; use std::{ collections::HashMap, fs::Metadata, @@ -64,7 +64,7 @@ impl Cache { trace!("Found file {}", e.path().display()); }) .filter_map(|(e, m)| { - decoder::song_from_file(e.path()) + Song::load(e.path()) .map(|s| (e.path().to_path_buf(), m, s)) .map_err(|e| { warn!("Failed to read song from {:?}: {}", e, e); diff --git a/src/decoder.rs b/src/decoder.rs deleted file mode 100644 index 8c43e27..0000000 --- a/src/decoder.rs +++ /dev/null @@ -1,206 +0,0 @@ -use std::{collections::HashMap, thread::JoinHandle}; - -use log::{info, warn}; -use replaygain::ReplayGain; -use symphonia::core::{ - audio::{SampleBuffer, SignalSpec}, - codecs::{DecoderOptions, CODEC_TYPE_NULL}, - formats::FormatOptions, - io::{MediaSourceStream, MediaSourceStreamOptions}, - meta::{MetadataOptions, MetadataRevision}, - probe::Hint, -}; - -use crate::song::{Song, StandardTagKey, Value}; - -pub fn read_audio( - path: P, - mut f: F, -) -> anyhow::Result<(Option, SignalSpec, JoinHandle<()>)> -where - P: AsRef, - F: FnMut(&[f32]) -> anyhow::Result<()> + Send + 'static, -{ - let src = std::fs::File::open(path)?; - - let mss = MediaSourceStream::new(Box::new(src), MediaSourceStreamOptions::default()); - let mut probed = symphonia::default::get_probe().format( - &Hint::new(), - mss, - &FormatOptions::default(), - &MetadataOptions::default(), - )?; - - let metadata = { - let mut meta = probed.format.metadata(); - meta.skip_to_latest().cloned() - }; - - let mut format_reader = probed.format; - - let track = format_reader - .tracks() - .into_iter() - .find(|t| t.codec_params.codec != CODEC_TYPE_NULL) - .ok_or(anyhow::anyhow!("No audio tracks found"))?; - - let codec_params = track.codec_params.clone(); - let track_id = track.id; - - let mut decoder = - symphonia::default::get_codecs().make(&track.codec_params, &DecoderOptions::default())?; - - let handle = std::thread::spawn(move || loop { - match format_reader.next_packet() { - Ok(packet) => { - if packet.track_id() == track_id { - let data = match decoder.decode(&packet) { - Ok(d) => d, - Err(e) => { - warn!("Failed to decode packet {:?}", e); - break; - } - }; - - let mut sample_buffer = SampleBuffer::new( - data.capacity() as u64, - SignalSpec::new( - codec_params.sample_rate.unwrap(), - codec_params.channels.unwrap(), - ), - ); - sample_buffer.copy_interleaved_ref(data); - - if f(sample_buffer.samples()).is_err() { - break; - } - } - } - Err(symphonia::core::errors::Error::IoError(e)) => match e.kind() { - std::io::ErrorKind::UnexpectedEof => break, - _ => { - warn!("Failed to read packet {:?}", e); - break; - } - }, - Err(e) => { - warn!("Failed to read packet {:?}", e); - break; - } - }; - }); - - Ok(( - metadata, - SignalSpec::new( - codec_params.sample_rate.unwrap(), - codec_params.channels.unwrap(), - ), - handle, - )) -} - -pub fn song_from_file

(path: P) -> anyhow::Result -where - P: AsRef, -{ - let src = std::fs::File::open(&path)?; - - let mss = MediaSourceStream::new(Box::new(src), MediaSourceStreamOptions::default()); - let mut probed = symphonia::default::get_probe().format( - &Hint::new().with_extension(path.as_ref().extension().unwrap().to_str().unwrap()), - mss, - &FormatOptions { - prebuild_seek_index: false, - seek_index_fill_rate: 0, - enable_gapless: true, - }, - &MetadataOptions::default(), - )?; - - let metadata = { - let mut meta = probed.format.metadata(); - meta.skip_to_latest().cloned() - }; - - let track = probed - .format - .tracks() - .into_iter() - .find(|t| t.codec_params.codec != CODEC_TYPE_NULL) - .ok_or(anyhow::anyhow!("No audio tracks found"))?; - - let duration = track - .codec_params - .time_base - .ok_or(anyhow::anyhow!( - "No time base found for track {:?}", - track.id - ))? - .calc_time(track.codec_params.n_frames.ok_or(anyhow::anyhow!( - "No frame count found for track {:?}", - track.id - ))?); - - let duration = std::time::Duration::from_secs_f64(duration.seconds as f64 + duration.frac); - - let (mut standard_tags, other_tags) = metadata - .map(|m| { - let s = m - .tags() - .into_iter() - .filter_map(|t| t.std_key.map(|k| (k.into(), t.value.clone().into()))) - .collect::>(); - - let o = m - .tags() - .into_iter() - .filter(|t| t.std_key == None) - .map(|t| (t.key.clone(), t.value.clone().into())) - .collect::>(); - - (s, o) - }) - .unwrap_or_default(); - - if !standard_tags.contains_key(&StandardTagKey::ReplayGainTrackGain) { - info!( - "File {} is missing ReplayGain, calculating", - path.as_ref().display() - ); - - let mut rg = ReplayGain::new( - track - .codec_params - .sample_rate - .expect("No sample rate found") as usize, - ) - .expect("Failed to create ReplayGain"); - - let rg_ref = - unsafe { std::mem::transmute::<&'_ mut ReplayGain, &'static mut ReplayGain>(&mut rg) }; - let (_, _, handle) = read_audio(&path, |data| { - rg_ref.process_samples(data); - - Ok(()) - })?; - handle.join().expect("Failed to join thread"); - - let (gain, peak) = rg.finish(); - - standard_tags.insert( - StandardTagKey::ReplayGainTrackGain, - Value::Float(gain as f64), - ); - standard_tags.insert( - StandardTagKey::ReplayGainTrackPeak, - Value::Float(peak as f64), - ); - } - - Ok(Song { - standard_tags, - other_tags, - duration, - }) -} diff --git a/src/main.rs b/src/main.rs index 8f3b312..a26d3b8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,24 +1,19 @@ -use std::{ - fs::File, - io::{Read, Write}, - sync::{atomic::Ordering, Arc}, -}; +use std::{fs::File, sync::Arc}; +use anyhow::Context; use cache::Cache; use log::{info, trace, warn, LevelFilter}; -use player::Player; use simplelog::{CombinedLogger, WriteLogger}; -use crate::{config::Config, tui::tui}; +use crate::{config::Config, player::Player, tui::tui}; mod cache; mod config; -mod decoder; mod player; mod song; mod tui; -fn main() { +fn main() -> anyhow::Result<()> { let config_dir = dirs::config_dir() .expect("Unable to find config directory") .join("ramp"); @@ -42,7 +37,7 @@ fn main() { }), ); - let _logger = CombinedLogger::init(vec![WriteLogger::new( + CombinedLogger::init(vec![WriteLogger::new( #[cfg(debug_assertions)] LevelFilter::Trace, #[cfg(not(debug_assertions))] @@ -55,19 +50,8 @@ fn main() { .build(), File::create(&config.log_path).expect("Failed to create log file"), )]) - .expect("Failed to initialize logger"); - - let quit = Arc::new(std::sync::atomic::AtomicBool::new(false)); - let mut f = File::open(&config.log_path).expect("Failed to open log file"); - let _quit = quit.clone(); - let handle = std::thread::spawn(move || { - while !_quit.load(Ordering::SeqCst) { - let mut buf = Vec::new(); - f.read_to_end(&mut buf).unwrap(); - std::io::stdout().write_all(&buf).unwrap(); - std::thread::sleep(std::time::Duration::from_millis(100)); - } - }); + .context("Failed to initialize logger")?; + info!("Logger initialized"); trace!("loading cache"); let (cache, old_config) = Cache::load(&config).unwrap_or_else(|e| { @@ -99,25 +83,11 @@ fn main() { let cache = Arc::new(cache); trace!("initializing player"); - let player = Player::new(cache.clone()).expect("Failed to initialize player"); - - { - trace!("attaching media controls"); - player - .lock() - .unwrap() - .attach_media_controls() - .unwrap_or_else(|e| { - warn!("Failed to attach media controls: {e:?}"); - }); - } - - quit.store(true, Ordering::SeqCst); - handle.join().expect("Failed to join log thread"); + let (cmd, player) = Player::run(cache.clone()).context("Failed to initialize player")?; trace!("entering tui"); - tui(config.clone(), cache.clone(), player.clone()).expect("Failed to run tui"); + tui(config.clone(), cache.clone(), cmd, player).context("Error in tui")?; trace!("tui exited"); - trace!("quitting"); + Ok(()) } diff --git a/src/player.rs b/src/player.rs deleted file mode 100644 index 6883ceb..0000000 --- a/src/player.rs +++ /dev/null @@ -1,496 +0,0 @@ -use cpal::{ - traits::{DeviceTrait, HostTrait, StreamTrait}, - Device, OutputCallbackInfo, StreamConfig, -}; -use log::{debug, error, info, trace, warn}; -use souvlaki::{MediaControlEvent, MediaControls, MediaMetadata, MediaPlayback, PlatformConfig}; -use symphonia::core::{ - audio::SignalSpec, - meta::{MetadataRevision, StandardVisualKey}, -}; -use tempfile::NamedTempFile; - -use std::{ - collections::VecDeque, - io::Write, - path::{Path, PathBuf}, - sync::{ - mpsc::{Receiver, SyncSender}, - Arc, Mutex, Weak, - }, - time::Duration, -}; - -use crate::{ - cache::Cache, - decoder, - song::{Song, StandardTagKey}, -}; - -enum StreamCommand { - Play, - Pause, -} - -struct QueuedSong { - song: Song, - path: PathBuf, - metadata: Option, - signal_spec: SignalSpec, - receiver: Receiver>, -} - -fn front_cover<'a>(metadata: &'a Option) -> Option<&[u8]> { - metadata - .as_ref() - .and_then(|m| { - m.visuals() - .iter() - .find(|v| v.usage == Some(StandardVisualKey::FrontCover)) - }) - .map(|v| v.data.as_ref()) -} - -struct CurrentSong { - song: Song, - path: PathBuf, - metadata: Option, - stream_command_sender: SyncSender, - elapsed: Duration, - cover_tempfile: NamedTempFile, -} - -pub struct Player { - cache: Arc, - playing: bool, - current: Option, - next: VecDeque, - arc: Weak>, - device: Device, - pub media_controls: MediaControls, - pub tempfile: NamedTempFile, -} - -impl Player { - pub fn new(cache: Arc) -> anyhow::Result>> { - let device = cpal::default_host() - .default_output_device() - .ok_or(anyhow::anyhow!("Failed to get default output device"))?; - - let media_controls = MediaControls::new(PlatformConfig { - display_name: "rcmp", - dbus_name: "rcmp", - hwnd: None, - }) - .map_err(|e| anyhow::anyhow!("Failed to create media controls: {:?}", e))?; - - let player = Player { - cache, - playing: false, - current: None, - next: VecDeque::new(), - device, - media_controls, - tempfile: NamedTempFile::new().expect("Failed to create tempfile"), - arc: Weak::new(), - }; - - let arc = Arc::new(Mutex::new(player)); - - arc.lock().unwrap().arc = Arc::downgrade(&arc); - - Ok(arc) - } - - pub fn update_media_controls( - &mut self, - song: &Song, - cover_tempfile: &NamedTempFile, - ) -> anyhow::Result<()> { - let [title, album, artist] = [ - StandardTagKey::TrackTitle, - StandardTagKey::Album, - StandardTagKey::TrackTitle, - ] - .map(|k| song.standard_tags.get(&k).map(|x| x.to_string())); - - self.media_controls - .set_playback(MediaPlayback::Playing { progress: None }) - .map_err(|e| anyhow::anyhow!("Failed to set playback: {:?}", e))?; - - self.media_controls - .set_metadata(MediaMetadata { - title: title.as_ref().map(|x| x.as_str()), - album: album.as_ref().map(|x| x.as_str()), - artist: artist.as_ref().map(|x| x.as_str()), - cover_url: Some(format!("file://{}", cover_tempfile.path().display()).as_str()), - duration: None, - }) - .map_err(|e| anyhow::anyhow!("Failed to set metadata: {:?}", e))?; - - Ok(()) - } - - pub fn play(&mut self) -> anyhow::Result<()> { - if let Some(CurrentSong { - song, - path, - metadata, - stream_command_sender, - elapsed: duration, - cover_tempfile, - }) = self.current.take() - { - trace!("play: playing current stream"); - - stream_command_sender.send(StreamCommand::Play)?; - trace!("play: sent play command"); - - self.playing = true; - self.update_media_controls(&song, &cover_tempfile)?; - - self.current = Some(CurrentSong { - song, - path, - metadata, - stream_command_sender, - elapsed: duration, - cover_tempfile, - }); - - Ok(()) - } else { - trace!("play: no current stream, trying to get next"); - - if let Some(QueuedSong { - song, - path, - metadata, - signal_spec, - receiver, - }) = self.next.pop_front() - { - let sender = self.spawn_stream_thread(receiver, &signal_spec, &song); - - let mut cover_tempfile = NamedTempFile::new().expect("Failed to create tempfile"); - if let Some(data) = front_cover(&metadata) { - cover_tempfile.write_all(data).unwrap_or_else(|e| { - warn!("Failed to write cover to tempfile: {:?}", e); - }); - } - - self.current = Some(CurrentSong { - song, - path, - metadata, - stream_command_sender: sender, - elapsed: Duration::from_secs(0), - cover_tempfile, - }); - - self.play() - } else { - trace!("play: no next stream"); - Ok(()) - } - } - } - - fn spawn_stream_thread( - &mut self, - receiver: Receiver>, - signal_spec: &SignalSpec, - song: &Song, - ) -> SyncSender { - let gain_factor = song.gain_factor().unwrap_or(1.0); - trace!("play: gain_factor: {}", gain_factor); - - let (command_tx, command_rx) = std::sync::mpsc::sync_channel::(1); - - let buffer_size = signal_spec.rate * 2; - let stream_config = StreamConfig { - channels: signal_spec.channels.count() as u16, - sample_rate: cpal::SampleRate(signal_spec.rate), - buffer_size: cpal::BufferSize::Fixed(buffer_size), - }; - trace!("play: stream_config: {:?}", stream_config); - - let arc = self.arc.upgrade().expect("Failed to upgrade weak player"); - let arc2 = arc.clone(); - let arc3 = arc.clone(); - - let signal_spec = signal_spec.clone(); - let thread = std::thread::spawn(move || { - trace!("locking player"); - let player = arc.lock().expect("Failed to lock player"); - let mut n = 0; - let mut buf = Vec::new(); - - let stream = player - .device - .build_output_stream( - &stream_config, - move |data: &mut [f32], _info: &OutputCallbackInfo| { - let now = std::time::Instant::now(); - while buf.len() < data.len() { - match receiver.recv() { - Ok(s) => buf.extend(s.into_iter().map(|x| x * gain_factor)), - Err(e) => { - debug!("Failed to receive sample, sender disconnected {:?}", e); - - let duration = Duration::from_secs_f32( - buffer_size as f32 - / (signal_spec.channels.bits() * signal_spec.rate) - as f32, - ); - info!("Sleeping for {:?} to finish song", duration); - std::thread::sleep(duration); - - { - trace!("locking player"); - let mut player = - arc2.lock().expect("Failed to lock player"); - player.skip().unwrap_or_else(|e| { - warn!("Failed to skip song: {:?}", e); - }); - } - - return; - } - } - } - trace!( - "receiver.recv() got {} frames, took {:?}", - buf.len(), - now.elapsed() - ); - - { - trace!("locking player"); - let mut player = arc2.lock().expect("Failed to lock player"); - n += data.len(); - if let Some(CurrentSong { - elapsed: ref mut duration, - .. - }) = player.current.as_mut() - { - *duration = Duration::from_secs_f32( - n as f32 - / (signal_spec.channels.bits() * signal_spec.rate) as f32, - ); - } - } - - data.copy_from_slice(buf.drain(0..data.len()).as_slice()); - }, - move |e| match e { - // TODO: figure out why this happens - cpal::StreamError::BackendSpecific { - err: cpal::BackendSpecificError { description }, - } if description == "`alsa::poll()` spuriously returned" => {} - e => { - warn!("Output stream error {:?}", e); - let mut player = arc3.lock().expect("Failed to lock player"); - player.stop().unwrap_or_else(|e| { - warn!("Failed to stop player: {:?}", e); - }); - } - }, - Some(Duration::from_secs_f32(1.0)), - ) - .expect("Failed to build output stream"); - - drop(player); - - loop { - trace!( - "thread {:?} waiting for command", - std::thread::current().id() - ); - match command_rx.recv() { - Ok(s) => match s { - StreamCommand::Play => stream.play().expect("Failed to play output stream"), - StreamCommand::Pause => { - stream.pause().expect("Failed to pause output stream") - } - }, - Err(e) => { - warn!("Failed to receive command, sender disconnected {:?}", e); - break; - } - } - } - - trace!( - "play: stream thread {:?} exiting", - std::thread::current().id() - ); - }); - - trace!("Spawned stream thead {:?}", thread.thread().id()); - - command_tx - } - - pub fn stop(&mut self) -> anyhow::Result<()> { - trace!("stopping player"); - self.current = None; - self.playing = false; - self.media_controls - .set_playback(MediaPlayback::Stopped) - .map_err(|e| anyhow::anyhow!("Failed to set playback: {:?}", e))?; - - Ok(()) - } - - pub fn pause(&mut self) -> anyhow::Result<()> { - trace!("pausing player"); - if let Some(CurrentSong { - stream_command_sender, - .. - }) = self.current.as_ref() - { - trace!("pause: pausing stream"); - stream_command_sender.send(StreamCommand::Pause)?; - self.playing = false; - self.media_controls - .set_playback(MediaPlayback::Paused { progress: None }) - .map_err(|e| anyhow::anyhow!("Failed to set playback: {:?}", e))?; - } else { - trace!("pause: no stream to pause"); - } - - trace!("pause: done"); - - Ok(()) - } - - pub fn queue

(&mut self, path: P) -> anyhow::Result<()> - where - P: AsRef, - { - let song = self - .cache - .get(path.as_ref().clone())? - .ok_or(anyhow::anyhow!( - "Failed to get song with path {:?}", - path.as_ref().display() - ))? - .as_file()?; - - // TODO: adaptively choose buffer size based on signal spec and config - let (tx, rx) = std::sync::mpsc::sync_channel::>(32); - - let mut last = std::time::Instant::now(); - let (metadata, signal_spec, _handle) = decoder::read_audio(&path, move |data| { - trace!( - "read_audio: got {} frames, took {:?}", - data.len(), - last.elapsed() - ); - tx.send(data.to_vec())?; - last = std::time::Instant::now(); - Ok(()) - })?; - - self.next.push_back(QueuedSong { - song: song.clone(), - path: path.as_ref().to_path_buf(), - metadata, - signal_spec, - receiver: rx, - }); - - if self.current.is_none() { - trace!("queue: playing queued song"); - self.play() - } else { - Ok(()) - } - } - - pub fn play_pause(&mut self) -> anyhow::Result<()> { - match self.playing { - true => self.pause(), - false => self.play(), - } - } - - pub fn clear(&mut self) -> anyhow::Result<()> { - trace!("clearing queue"); - self.next.clear(); - self.stop() - } - - pub fn skip(&mut self) -> anyhow::Result<()> { - trace!("skipping song"); - trace!("next len: {:?}", self.next.len()); - self.stop()?; - - trace!("stopped"); - trace!("next len: {:?}", self.next.len()); - self.play()?; - - trace!("played"); - trace!("next len: {:?}", self.next.len()); - - Ok(()) - } - - pub fn current(&self) -> Option<(&Song, &Path)> { - self.current - .as_ref() - .map(|CurrentSong { ref song, path, .. }| (song, path.as_path())) - } - - pub fn current_time(&self) -> Option<&Duration> { - self.current.as_ref().map( - |CurrentSong { - elapsed: ref duration, - .. - }| duration, - ) - } - - pub fn current_cover(&self) -> Option<&[u8]> { - self.current - .as_ref() - .and_then(|cs| front_cover(&cs.metadata)) - } - - pub fn nexts(&self) -> impl Iterator { - self.next.iter().map(|QueuedSong { ref song, .. }| song) - } - - pub fn attach_media_controls(&mut self) -> anyhow::Result<()> { - let weak = self.arc.clone(); - self.media_controls - .attach(move |event: MediaControlEvent| { - trace!("media control event {:?}", event); - - let arc = weak.upgrade().expect("Failed to upgrade weak player"); - let mut player = arc.lock().expect("Failed to lock player"); - - match event { - MediaControlEvent::Play => player.play(), - MediaControlEvent::Pause => player.pause(), - MediaControlEvent::Toggle => player.play_pause(), - MediaControlEvent::Next => player.skip(), - MediaControlEvent::Previous => Ok(()), - MediaControlEvent::Stop => player.stop(), - MediaControlEvent::Seek(_) => todo!(), - MediaControlEvent::SeekBy(_, _) => todo!(), - MediaControlEvent::SetPosition(_) => todo!(), - MediaControlEvent::OpenUri(_) => Ok(()), - MediaControlEvent::Raise => Ok(()), - MediaControlEvent::Quit => Ok(()), - } - .unwrap_or_else(|e| { - error!("Failed to handle media control event: {:?}", e); - }); - }) - .map_err(|e| anyhow::anyhow!("Failed to attach media controls: {:?}", e))?; - - Ok(()) - } -} diff --git a/src/player/command.rs b/src/player/command.rs new file mode 100644 index 0000000..616d138 --- /dev/null +++ b/src/player/command.rs @@ -0,0 +1,10 @@ +pub enum Command { + Play, + Pause, + PlayPause, + Skip, + Stop, + Clear, + Enqueue(Box), + Dequeue(usize), +} diff --git a/src/player/facade.rs b/src/player/facade.rs new file mode 100644 index 0000000..46c9470 --- /dev/null +++ b/src/player/facade.rs @@ -0,0 +1,86 @@ +use std::{ + sync::{atomic::AtomicBool, Arc, RwLock}, + time::Duration, +}; + +use symphonia::core::meta::{MetadataRevision, StandardVisualKey}; + +use crate::song::Song; + +use super::Player; + +#[derive(Default)] +pub enum PlayerStatus { + PlayingOrPaused { + song: Song, + metadata: Option, + playing_duration: Arc>, + paused: Arc, + }, + #[default] + Stopped, +} + +impl PlayerStatus { + fn from_internal(player: &Player) -> PlayerStatus { + match &player.status { + super::InternalPlayerStatus::PlayingOrPaused { + song, + metadata, + playing_duration, + stream_paused, + .. + } => PlayerStatus::PlayingOrPaused { + song: song.clone(), + metadata: metadata.clone(), + playing_duration: playing_duration.clone(), + paused: stream_paused.clone(), + }, + super::InternalPlayerStatus::Stopped => PlayerStatus::Stopped, + } + } +} + +#[derive(Default)] +pub struct PlayerFacade { + pub status: PlayerStatus, + pub queue: Box<[Box]>, +} + +impl PlayerFacade { + pub(super) fn from_player(player: &Player) -> PlayerFacade { + PlayerFacade { + status: PlayerStatus::from_internal(player), + queue: player.queue.clone().into_iter().collect(), + } + } + + pub fn current_song(&self) -> Option<&Song> { + match &self.status { + PlayerStatus::PlayingOrPaused { song, .. } => Some(song), + _ => None, + } + } + + pub fn playing_duration(&self) -> Option { + match &self.status { + PlayerStatus::PlayingOrPaused { + playing_duration, .. + } => Some(*playing_duration.read().unwrap()), + _ => None, + } + } + + pub fn current_cover(&self) -> Option<&[u8]> { + match &self.status { + PlayerStatus::PlayingOrPaused { metadata, .. } => metadata.as_ref(), + PlayerStatus::Stopped => None, + } + .and_then(|m| { + m.visuals() + .iter() + .find(|v| v.usage == Some(StandardVisualKey::FrontCover)) + }) + .map(|v| v.data.as_ref()) + } +} diff --git a/src/player/loader.rs b/src/player/loader.rs new file mode 100644 index 0000000..8d99bd4 --- /dev/null +++ b/src/player/loader.rs @@ -0,0 +1,97 @@ +use anyhow::Context; + +use symphonia::core::{ + audio::{SampleBuffer, SignalSpec}, + codecs::{DecoderOptions, CODEC_TYPE_NULL}, + errors::Error, + formats::FormatOptions, + io::{MediaSourceStream, MediaSourceStreamOptions}, + meta::{MetadataOptions, MetadataRevision}, + probe::Hint, +}; + +use crate::song::Song; + +pub struct LoadedSong { + pub song: Song, + pub metadata: Option, + pub signal_spec: SignalSpec, + pub decoder: Box anyhow::Result<(Option>, bool)> + Send>, +} + +impl LoadedSong { + pub fn load(song: Song) -> anyhow::Result { + let src = std::fs::File::open(song.path.as_ref()).context(format!( + "Failed to open file {}", + song.path.to_string_lossy() + ))?; + + let mss = MediaSourceStream::new(Box::new(src), MediaSourceStreamOptions::default()); + let mut probed = symphonia::default::get_probe().format( + &Hint::new(), + mss, + &FormatOptions::default(), + &MetadataOptions::default(), + )?; + + let metadata = { + let mut meta = probed.format.metadata(); + meta.skip_to_latest().cloned() + }; + + let mut format_reader = probed.format; + + let track = format_reader + .tracks() + .into_iter() + .find(|t| t.codec_params.codec != CODEC_TYPE_NULL) + .ok_or(anyhow::anyhow!("No audio tracks found"))?; + + let codec_params = track.codec_params.clone(); + let track_id = track.id; + + let mut decoder = symphonia::default::get_codecs() + .make(&track.codec_params, &DecoderOptions::default())?; + + let signal_spec = SignalSpec::new( + codec_params + .sample_rate + .ok_or(anyhow::anyhow!("No sample rate"))?, + codec_params + .channels + .ok_or(anyhow::anyhow!("No channels"))?, + ); + + let signal_spec2 = signal_spec.clone(); + let decoder = move || match format_reader.next_packet() { + Ok(packet) => { + if packet.track_id() == track_id { + let data = match decoder.decode(&packet) { + Ok(d) => d, + Err(e) => { + anyhow::bail!("Failed to decode packet {:?}", e); + } + }; + + let mut sample_buffer = SampleBuffer::new(data.capacity() as u64, signal_spec2); + sample_buffer.copy_interleaved_ref(data); + + Ok((Some(sample_buffer), false)) + } else { + Ok((None, false)) + } + } + Err(Error::IoError(e)) if e.to_string() == "end of stream" => Ok((None, true)), + Err(e) => { + anyhow::bail!("Failed to read packet {:?}", e); + } + }; + + Ok(Self { + song, + metadata, + signal_spec, + decoder: Box::new(decoder), + }) + } +} diff --git a/src/player/mod.rs b/src/player/mod.rs new file mode 100644 index 0000000..314d811 --- /dev/null +++ b/src/player/mod.rs @@ -0,0 +1,381 @@ +use crate::{ + cache::Cache, + song::{Song, StandardTagKey}, +}; +use anyhow::Context; +use cpal::{ + traits::{DeviceTrait, HostTrait}, + Stream, StreamConfig, +}; +use log::warn; +use souvlaki::{MediaControls, MediaMetadata, MediaPlayback, MediaPosition, PlatformConfig}; +use std::{ + collections::VecDeque, + io::{Seek, Write}, + sync::{atomic::AtomicBool, mpsc, Arc, RwLock}, + time::Duration, +}; +use symphonia::core::meta::MetadataRevision; +use tempfile::NamedTempFile; + +use self::{command::Command, facade::PlayerFacade, loader::LoadedSong}; + +pub mod command; +pub mod facade; +mod loader; + +enum InternalPlayerStatus { + PlayingOrPaused { + song: Song, + metadata: Option, + playing_duration: Arc>, + stream_paused: Arc, + _stream: Stream, + }, + Stopped, +} + +pub struct Player { + cache: Arc, + status: InternalPlayerStatus, + queue: VecDeque>, + media_controls: MediaControls, + command_tx: mpsc::Sender, +} + +impl Player { + /// command player to continue playing or start playing the next song + fn play(&mut self) -> anyhow::Result<()> { + match &self.status { + InternalPlayerStatus::PlayingOrPaused { + stream_paused: paused, + .. + } => { + if paused.load(std::sync::atomic::Ordering::Relaxed) { + paused.store(false, std::sync::atomic::Ordering::Relaxed); + } + } + InternalPlayerStatus::Stopped => {} + } + + if matches!(self.status, InternalPlayerStatus::Stopped) { + if let Some(path) = self.queue.pop_front() { + let song = self + .cache + .get(path) + .context("Failed to get song from cache")? + .ok_or(anyhow::anyhow!("Song not found in cache"))? + .as_file() + .context("Song is not a file")? + .clone(); + + let loaded_song = LoadedSong::load(song.clone()).context("Failed to load song")?; + + let metadata = loaded_song.metadata.clone(); + let (stream_paused, playing_duration, _stream) = + self.create_playback(loaded_song)?; + + self.status = InternalPlayerStatus::PlayingOrPaused { + song, + metadata, + playing_duration, + stream_paused, + _stream, + } + } + } + + Ok(()) + } + + /// command player to pause + fn pause(&mut self) -> anyhow::Result<()> { + match &self.status { + InternalPlayerStatus::PlayingOrPaused { + stream_paused: paused, + .. + } => { + paused.store(true, std::sync::atomic::Ordering::Relaxed); + } + InternalPlayerStatus::Stopped => {} + } + + Ok(()) + } + + /// command player to play if paused or pause if playing + fn play_pause(&mut self) -> anyhow::Result<()> { + match &self.status { + InternalPlayerStatus::PlayingOrPaused { + stream_paused: paused, + .. + } => { + paused.fetch_xor(true, std::sync::atomic::Ordering::Relaxed); + } + InternalPlayerStatus::Stopped => {} + } + + Ok(()) + } + + /// command player to stop + fn stop(&mut self) -> anyhow::Result<()> { + self.status = InternalPlayerStatus::Stopped; + + Ok(()) + } + + /// command player to skip to next song + fn skip(&mut self) -> anyhow::Result<()> { + self.stop()?; + self.play()?; + + Ok(()) + } + + /// add a song to the queue + /// if the player is stopped, the song will be played + fn enqueue>(&mut self, path: P) -> anyhow::Result<()> { + self.queue.push_back(path.as_ref().into()); + + if matches!(self.status, InternalPlayerStatus::Stopped) { + self.play()?; + } + + Ok(()) + } + + /// remove a song from the queue + fn dequeue(&mut self, index: usize) -> anyhow::Result<()> { + self.queue + .remove(index) + .ok_or(anyhow::anyhow!(format!("No song at index {}", index)))?; + + Ok(()) + } + + /// remove all songs from the queue and stop playing + fn clear(&mut self) -> anyhow::Result<()> { + self.queue.clear(); + self.stop()?; + + Ok(()) + } + + /// create playback stream for song + fn create_playback( + &mut self, + mut song: LoadedSong, + ) -> anyhow::Result<(Arc, Arc>, Stream)> { + let config = StreamConfig { + channels: song.signal_spec.channels.count() as u16, + sample_rate: cpal::SampleRate(song.signal_spec.rate), + buffer_size: cpal::BufferSize::Default, + }; + + let mut buffer = VecDeque::::new(); + + let pause_stream = Arc::new(AtomicBool::new(false)); + let playing_duration = Arc::new(RwLock::new(Duration::from_secs(0))); + + let gain_factor = song.song.gain_factor; + let pause_stream2 = pause_stream.clone(); + let playing_duration2 = playing_duration.clone(); + let command_tx = self.command_tx.clone(); + + let stream = cpal::default_host() + .default_output_device() + .expect("Failed to get default output device") + .build_output_stream::( + &config, + move |dest, _info| { + if pause_stream2.load(std::sync::atomic::Ordering::Relaxed) { + dest.fill(0.0); + return; + } + + let mut duration = playing_duration2.write().unwrap(); + + let mut n = buffer.len().min(dest.len()); + buffer.drain(0..n).enumerate().for_each(|(i, s)| { + dest[i] = s * gain_factor; + }); + + *duration += Duration::from_secs_f64( + n as f64 + / song.signal_spec.rate as f64 + / song.signal_spec.channels.count() as f64, + ); + + let (samples, eof) = (song.decoder)().expect("Failed to decode packet"); + + if let Some(samples) = samples { + buffer.extend(samples.samples()); + + if n < dest.len() { + n = buffer.len().min(dest.len() - n); + buffer.drain(0..n).enumerate().for_each(|(i, s)| { + dest[i] = s * gain_factor; + }); + + *duration += Duration::from_secs_f64( + n as f64 + / song.signal_spec.rate as f64 + / song.signal_spec.channels.count() as f64, + ); + } + } + + if eof && buffer.is_empty() { + command_tx.send(Command::Skip).unwrap(); + } + }, + |e| { + warn!("Error in playback stream: {:?}", e); + }, + None, + ) + .expect("Failed to build output stream"); + + Ok((pause_stream, playing_duration, stream)) + } + + pub fn run( + cache: Arc, + ) -> anyhow::Result<(mpsc::Sender, Arc>)> { + let media_controls = MediaControls::new(PlatformConfig { + display_name: "rcmp", + dbus_name: "rcmp", + hwnd: None, + }) + .map_err(|e| anyhow::anyhow!(format!("{:?}", e))) + .context("Failed to create media controls")?; + + let (tx, rx) = mpsc::channel(); + let facade = Arc::new(RwLock::new(PlayerFacade::default())); + + let tx2 = tx.clone(); + let facade2 = facade.clone(); + std::thread::Builder::new() + .name("player thread".to_string()) + .spawn(move || { + let mut player = Player { + cache, + status: InternalPlayerStatus::Stopped, + queue: VecDeque::new(), + media_controls, + command_tx: tx2.clone(), + }; + + let tx = tx2.clone(); + player + .media_controls + .attach(move |event| match event { + souvlaki::MediaControlEvent::Play => { + tx.send(Command::Play).unwrap(); + } + souvlaki::MediaControlEvent::Pause => { + tx.send(Command::Pause).unwrap(); + } + souvlaki::MediaControlEvent::Toggle => { + tx.send(Command::PlayPause).unwrap(); + } + souvlaki::MediaControlEvent::Next => { + tx.send(Command::Skip).unwrap(); + } + souvlaki::MediaControlEvent::Previous => warn!("Previous not implemented"), + souvlaki::MediaControlEvent::Stop => { + tx.send(Command::Stop).unwrap(); + } + souvlaki::MediaControlEvent::Seek(dir) => { + warn!("Seek {dir:?} not implemented") + } + souvlaki::MediaControlEvent::SeekBy(dir, dur) => { + warn!("SeekBy {dir:?} {dur:?} not implemented") + } + souvlaki::MediaControlEvent::SetPosition(mp) => { + warn!("SetPosition {mp:?} not implemented") + } + souvlaki::MediaControlEvent::OpenUri(uri) => { + warn!("OpenUri {uri:?} not implemented") + } + souvlaki::MediaControlEvent::Raise => {} + souvlaki::MediaControlEvent::Quit => { + warn!("Quit not implemented") + } + }) + .expect("Failed to attach media controls"); + + let mut cover_tempfile; + loop { + match rx.recv().expect("Failed to receive Command") { + Command::Play => player.play().unwrap(), + Command::Pause => player.pause().unwrap(), + Command::PlayPause => player.play_pause().unwrap(), + Command::Skip => player.skip().unwrap(), + Command::Stop => player.stop().unwrap(), + Command::Clear => player.clear().unwrap(), + Command::Enqueue(path) => player.enqueue(path).unwrap(), + Command::Dequeue(index) => player.dequeue(index).unwrap(), + } + + *facade2.write().unwrap() = PlayerFacade::from_player(&player); + + let facade = facade2.read().unwrap(); + + cover_tempfile = NamedTempFile::new().expect("Failed to create tempfile"); + cover_tempfile + .write_all(facade.current_cover().unwrap_or(&[])) + .expect("Failed to write cover to tempfile"); + + player + .media_controls + .set_metadata(MediaMetadata { + title: facade + .current_song() + .and_then(|s| s.tag_string(StandardTagKey::TrackTitle)), + album: facade + .current_song() + .and_then(|s| s.tag_string(StandardTagKey::Album)), + artist: facade + .current_song() + .and_then(|s| s.tag_string(StandardTagKey::Artist)), + cover_url: Some( + format!("file://{}", cover_tempfile.path().display()).as_str(), + ), + duration: facade.current_song().map(|s| s.duration), + }) + .expect("Failed to set metadata"); + + player + .media_controls + .set_playback(match &facade.status { + facade::PlayerStatus::PlayingOrPaused { + playing_duration, + paused, + .. + } => { + if paused.load(std::sync::atomic::Ordering::Relaxed) { + MediaPlayback::Paused { + progress: Some(MediaPosition( + *playing_duration.read().unwrap(), + )), + } + } else { + MediaPlayback::Playing { + progress: Some(MediaPosition( + *playing_duration.read().unwrap(), + )), + } + } + } + facade::PlayerStatus::Stopped => MediaPlayback::Stopped, + }) + .expect("Failed to set playback"); + } + }) + .context("Failed to create player thread")?; + + Ok((tx, facade)) + } +} diff --git a/src/song.rs b/src/song.rs index 50ca6cf..8b37ec3 100644 --- a/src/song.rs +++ b/src/song.rs @@ -1,5 +1,15 @@ use std::{collections::HashMap, fmt::Debug, num::NonZeroU32, time::Duration}; +use anyhow::Context; +use log::warn; +use symphonia::core::{ + codecs, + formats::FormatOptions, + io::{MediaSourceStream, MediaSourceStreamOptions}, + meta::MetadataOptions, + probe::Hint, +}; + #[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] pub enum Value { Binary(Box<[u8]>), @@ -467,18 +477,125 @@ impl From for StandardTagKey { #[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] pub struct Song { + pub path: Box, + pub duration: Duration, + pub gain_factor: f32, pub standard_tags: HashMap, pub other_tags: HashMap, - pub duration: Duration, } impl Song { - pub fn gain_factor(&self) -> Option { - self.standard_tags + pub fn tag_string(&self, key: StandardTagKey) -> Option<&str> { + self.standard_tags.get(&key).and_then(|v| match v { + Value::String(s) => Some(s.as_str()), + _ => None, + }) + } + + pub fn load>(path: P) -> anyhow::Result { + let src = std::fs::File::open(&path) + .context(format!("Failed to open file {}", path.as_ref().display()))?; + + let source = MediaSourceStream::new(Box::new(src), MediaSourceStreamOptions::default()); + + let extension = path + .as_ref() + .extension() + .unwrap() + .to_str() + .ok_or(anyhow::anyhow!( + "Failed to get extension for file {}", + path.as_ref().display() + ))?; + + let mut probed = symphonia::default::get_probe().format( + &Hint::new().with_extension(extension), + source, + &FormatOptions { + prebuild_seek_index: false, + seek_index_fill_rate: 0, + enable_gapless: true, + }, + &MetadataOptions::default(), + )?; + + let mut metadata = probed.format.metadata(); + let metadata = metadata.skip_to_latest().cloned(); + + let track = probed + .format + .tracks() + .into_iter() + .find(|t| t.codec_params.codec != codecs::CODEC_TYPE_NULL) + .ok_or(anyhow::anyhow!("No audio tracks found"))?; + + let duration = track + .codec_params + .time_base + .ok_or(anyhow::anyhow!( + "No time base found for track {:?}", + track.id + ))? + .calc_time(track.codec_params.n_frames.ok_or(anyhow::anyhow!( + "No frame count found for track {:?}", + track.id + ))?); + + let duration = std::time::Duration::from_secs_f64(duration.seconds as f64 + duration.frac); + + let (standard_tags, other_tags) = metadata + .map(|m| { + let s = m + .tags() + .into_iter() + .filter_map(|t| t.std_key.map(|k| (k.into(), t.value.clone().into()))) + .collect::>(); + + let o = m + .tags() + .into_iter() + .filter(|t| t.std_key == None) + .map(|t| (t.key.clone(), t.value.clone().into())) + .collect::>(); + + (s, o) + }) + .unwrap_or_default(); + + let replay_gain = standard_tags .get(&StandardTagKey::ReplayGainTrackGain) - .map(|x| x.to_string()) - .and_then(|x| x.strip_suffix(" dB").map(|x| x.to_string())) - .and_then(|x| x.parse::().ok()) - .map(|x| 10.0f32.powf(x / 20.0)) + .ok_or(anyhow::anyhow!( + "No replay gain found for {}", + path.as_ref().display() + )) + .and_then(|v| match v { + Value::String(s) => { + s.strip_suffix(" dB") + .unwrap_or(s) + .parse::() + .context(format!( + "Failed to parse replay gain for {}", + path.as_ref().display() + )) + } + v => anyhow::bail!("Expected string, got {:?}", v), + }) + .map(|x| 10_f32.powf(x / 20.0)) + .unwrap_or_else(|e| { + warn!( + "Failed to get replay gain for {}: {}", + path.as_ref().display(), + e + ); + 1.0 + }); + + Ok(Song { + path: path.as_ref().into(), + duration, + standard_tags, + other_tags, + gain_factor: replay_gain, + }) } } diff --git a/src/tui/fancy.rs b/src/tui/fancy.rs index fd9f1af..dc3ef05 100644 --- a/src/tui/fancy.rs +++ b/src/tui/fancy.rs @@ -1,4 +1,4 @@ -use std::sync::{Arc, Mutex}; +use std::sync::{Arc, RwLock}; use crossterm::event::Event; use image::imageops::FilterType; @@ -11,16 +11,16 @@ use ratatui::{ Frame, }; -use crate::player::Player; +use crate::player::facade::PlayerFacade; use super::Tui; pub struct Fancy { - player: Arc>, + player: Arc>, } impl Fancy { - pub fn new(player: Arc>) -> Self { + pub fn new(player: Arc>) -> Self { Self { player } } } @@ -28,12 +28,12 @@ impl Fancy { impl Tui for Fancy { fn draw(&self, area: Rect, f: &mut Frame) -> anyhow::Result<()> { trace!("locking player"); - let player = self.player.lock().expect("Failed to lock player"); + let player = self.player.read().expect("Failed to lock player"); let standard_tags = Table::new( player - .current() - .map(|(s, _)| { + .current_song() + .map(|s| { s.standard_tags .iter() .map(|(k, v)| (format!("{:?}", k), v)) @@ -54,10 +54,12 @@ impl Tui for Fancy { .title(format!( " {} ", player - .current() - .map(|(_, p)| { - p.to_str() - .ok_or(anyhow::anyhow!("Failed to convert Path to str: {:?}", p)) + .current_song() + .map(|s| { + s.path.to_str().ok_or(anyhow::anyhow!( + "Failed to convert Path to str: {:?}", + s.path + )) }) .unwrap_or(Ok(""))?, )) diff --git a/src/tui/files.rs b/src/tui/files.rs index 3e990ce..4cc66a8 100644 --- a/src/tui/files.rs +++ b/src/tui/files.rs @@ -1,7 +1,7 @@ use std::{ cmp::Ordering, path::PathBuf, - sync::{Arc, Mutex}, + sync::{mpsc, Arc}, }; use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers}; @@ -17,7 +17,7 @@ use ratatui::{ use crate::{ cache::{Cache, CacheEntry}, - player::Player, + player::command::Command, song::StandardTagKey, tui::song_table, }; @@ -34,12 +34,12 @@ pub struct Files { cache: Arc, path: PathBuf, selected: Vec, - player: Arc>, + player_tx: mpsc::Sender, filter: FilterState, } impl Files { - pub fn new(cache: Arc, player: Arc>) -> Self { + pub fn new(cache: Arc, cmd: mpsc::Sender) -> Self { Self { path: std::path::Path::new("/") .canonicalize() @@ -54,7 +54,7 @@ impl Files { .collect(), selected: vec![0], cache, - player, + player_tx: cmd, filter: FilterState::Disabled, } } @@ -64,9 +64,6 @@ impl Files { let l = self.items()?.count(); - trace!("lock player"); - let mut player = self.player.lock().expect("Failed to lock player"); - if let Event::Key(KeyEvent { code, modifiers, .. }) = event @@ -79,11 +76,25 @@ impl Files { }; } KeyCode::Char(' ') => { - player.play_pause().expect("Failed to play/pause"); + self.player_tx + .send(Command::PlayPause) + .expect("Failed to send play/pause"); + } + KeyCode::Char('n') => { + self.player_tx + .send(Command::Skip) + .expect("Failed to send skip"); + } + KeyCode::Char('s') => { + self.player_tx + .send(Command::Stop) + .expect("Failed to send stop"); + } + KeyCode::Char('c') => { + self.player_tx + .send(Command::Clear) + .expect("Failed to send clear"); } - KeyCode::Char('n') => player.skip().expect("Failed to skip"), - KeyCode::Char('s') => player.stop().expect("Failed to stop"), - KeyCode::Char('c') => player.clear().expect("Failed to clear"), KeyCode::Up => { self.selected .last_mut() @@ -117,7 +128,9 @@ impl Files { match c { CacheEntry::File { .. } => { trace!("queueing song: {:?}", self.path); - player.queue(&self.path.join(f)).expect("Failed to queue"); + self.player_tx + .send(Command::Enqueue(self.path.join(f).as_path().into())) + .unwrap(); } CacheEntry::Directory { .. } => { self.path.push(f.clone()); diff --git a/src/tui/mod.rs b/src/tui/mod.rs index 4d07c9d..b2e9c6b 100644 --- a/src/tui/mod.rs +++ b/src/tui/mod.rs @@ -7,7 +7,7 @@ mod status; mod tabs; use std::{ - sync::{Arc, Mutex}, + sync::{mpsc, Arc, Mutex, RwLock}, time::Duration, }; @@ -22,7 +22,11 @@ use ratatui::{ Frame, Terminal, }; -use crate::{cache::Cache, config::Config, player::Player}; +use crate::{ + cache::Cache, + config::Config, + player::{command::Command, facade::PlayerFacade}, +}; use self::{fancy::Fancy, files::Files, queue::Queue, search::Search, status::Status, tabs::Tabs}; @@ -48,7 +52,8 @@ pub trait Tui { pub fn tui<'a>( _config: Arc, cache: Arc, - player: Arc>, + cmd: mpsc::Sender, + player: Arc>, ) -> anyhow::Result<()> { let stdout = std::io::stdout(); let backend = CrosstermBackend::new(stdout); @@ -62,12 +67,15 @@ pub fn tui<'a>( vec![ ( " Files 🗃️ ", - Box::new(Files::new(cache.clone(), player.clone())), + Box::new(Files::new(cache.clone(), cmd.clone())), + ), + ( + "Queue 🕰️ ", + Box::new(Queue::new(cache.clone(), player.clone())), ), - ("Queue 🕰️ ", Box::new(Queue::new(player.clone()))), ( - "Search 🔎", /* idk, whatever */ - Box::new(Search::new(cache.clone(), player.clone())), + "Search 🔎", + Box::new(Search::new(cache.clone(), cmd.clone())), ), ("Fancy stuff ✨ ", Box::new(Fancy::new(player.clone()))), ], diff --git a/src/tui/queue.rs b/src/tui/queue.rs index c60c5aa..8a83167 100644 --- a/src/tui/queue.rs +++ b/src/tui/queue.rs @@ -1,4 +1,4 @@ -use std::sync::{Arc, Mutex}; +use std::sync::{Arc, RwLock}; use crossterm::event::Event; use log::trace; @@ -8,17 +8,18 @@ use ratatui::{ widgets::Table, }; -use crate::{player::Player, tui::song_table}; +use crate::{cache::Cache, player::facade::PlayerFacade, tui::song_table}; use super::Tui; pub struct Queue { - player: Arc>, + cache: Arc, + player: Arc>, } impl Queue { - pub fn new(player: Arc>) -> Self { - Queue { player } + pub fn new(cache: Arc, player: Arc>) -> Self { + Queue { cache, player } } } @@ -27,10 +28,12 @@ impl Tui for Queue { trace!("drawing queue"); trace!("lock player"); - let player = self.player.lock().unwrap(); + let player = self.player.read().unwrap(); let items = player - .nexts() + .queue + .iter() + .map(|p| self.cache.get(p).unwrap().unwrap().as_file().unwrap()) .map(|s| song_table::song_row(s)) .collect::>(); diff --git a/src/tui/search.rs b/src/tui/search.rs index 11b2ab0..9a74c7a 100644 --- a/src/tui/search.rs +++ b/src/tui/search.rs @@ -1,6 +1,6 @@ use std::{ path::PathBuf, - sync::{Arc, Mutex}, + sync::{mpsc, Arc}, }; use crossterm::event::{Event, KeyCode, KeyEvent}; @@ -17,7 +17,7 @@ use strsim::jaro_winkler; use crate::{ cache::{Cache, CacheEntry}, - player::Player, + player::command::Command, song::{Song, StandardTagKey}, }; @@ -27,17 +27,17 @@ pub struct Search { keyword: String, cache: Arc, selected: usize, - player: Arc>, + cmd: mpsc::Sender, items: Vec<(Song, PathBuf)>, } impl Search { - pub fn new(cache: Arc, player: Arc>) -> Self { + pub fn new(cache: Arc, cmd: mpsc::Sender) -> Self { Self { keyword: String::new(), cache, selected: 0, - player, + cmd, items: vec![], } } @@ -178,11 +178,7 @@ impl Tui for Search { .ok_or(anyhow::anyhow!("Failed to get selected Song"))? .clone(); - self.player - .lock() - .map_err(|e| anyhow::anyhow!("Failed to lock player: {:?}", e))? - .queue(path) - .expect("Failed to queue song"); + self.cmd.send(Command::Enqueue(path.as_path().into()))?; } _ => {} }, diff --git a/src/tui/status.rs b/src/tui/status.rs index c6d0d1b..efddcdb 100644 --- a/src/tui/status.rs +++ b/src/tui/status.rs @@ -1,6 +1,4 @@ -use std::{ - sync::{Arc, Mutex}, -}; +use std::sync::{Arc, RwLock}; use itertools::Itertools; use log::trace; @@ -12,16 +10,16 @@ use ratatui::{ Frame, }; -use crate::{player::Player, song::StandardTagKey, tui::format_duration}; +use crate::{player::facade::PlayerFacade, song::StandardTagKey, tui::format_duration}; use super::{Tui, UNKNOWN_STRING}; pub struct Status { - player: Arc>, + player: Arc>, } impl Status { - pub fn new(player: Arc>) -> Self { + pub fn new(player: Arc>) -> Self { Self { player } } } @@ -38,12 +36,13 @@ impl Tui for Status { trace!("locking player"); let playing = Paragraph::new( - if let Some((song, path)) = self.player.lock().unwrap().current() { + if let Some(song) = self.player.read().unwrap().current_song() { let title = song .standard_tags .get(&StandardTagKey::TrackTitle) .map(|s| s.to_string()) - .or(path + .or(song + .path .components() .last() .map(|s| s.as_os_str().to_string_lossy().to_string())) @@ -84,9 +83,9 @@ impl Tui for Status { .alignment(ratatui::prelude::Alignment::Center); trace!("locking player"); - let player = self.player.lock().unwrap(); - let ratio = if let (Some((song, _)), Some(current_time)) = - (player.current(), player.current_time()) + let player = self.player.read().unwrap(); + let ratio = if let (Some(song), Some(current_time)) = + (player.current_song(), player.playing_duration()) { current_time.as_secs_f64() / song.duration.as_secs_f64() } else { @@ -100,17 +99,17 @@ impl Tui for Status { .label("") .gauge_style(Style::default().fg(Color::LightBlue).bg(Color::DarkGray)); let elapsed = format_duration( - *player - .current_time() - .unwrap_or(&std::time::Duration::from_secs(0)), + player + .playing_duration() + .unwrap_or(std::time::Duration::from_secs(0)), ); let duration = format!( " -{}", format_duration( - if let (Some((current, _)), Some(current_time)) = - (player.current(), player.current_time()) + if let (Some(song), Some(current_time)) = + (player.current_song(), player.playing_duration()) { - current.duration.saturating_sub(*current_time) + song.duration.saturating_sub(current_time) } else { std::time::Duration::from_secs(0) }, diff --git a/src/tui/tabs.rs b/src/tui/tabs.rs index 585c3cd..a2b6531 100644 --- a/src/tui/tabs.rs +++ b/src/tui/tabs.rs @@ -1,4 +1,4 @@ -use std::{sync::Mutex}; +use std::sync::Mutex; use crossterm::event::{Event, KeyCode, KeyEvent}; use log::trace;