use std::fs::{File}; use std::io::BufReader; use std::collections::{VecDeque}; use crate::event::{AppEvent, Event, EventHandler}; use crate::library::{Library, Song}; use crossterm::event::MediaKeyCode::{self, PlayPause}; use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; use discord_rich_presence::activity::{Assets, Timestamps}; use ratatui::DefaultTerminal; use ratatui::widgets::{List, ListItem, ListState}; use rodio::{Decoder}; use audiotags::{MimeType, Tag}; use discord_rich_presence::{activity, DiscordIpc, DiscordIpcClient}; use std::time::{Duration, SystemTime, UNIX_EPOCH}; #[derive(Debug, Default, PartialEq, Eq)] pub enum Focus { #[default] Artists, Albums, Songs, Queue, } /// Application. //#[derive(Debug)] pub struct App { /// Is the application running? pub running: bool, pub library: Library, pub queue: VecDeque, pub sink_handle: rodio::MixerDeviceSink, pub player: rodio::Player, pub artist_state: ListState, pub album_state: ListState, pub song_state: ListState, pub queue_state: ListState, pub focus: Focus, pub current_song: Option, pub discord_client: Option, /// Event handler. pub events: EventHandler, } impl App { /// Constructs a new instance of [`App`]. pub fn new() -> Self { let sink_handle = rodio::DeviceSinkBuilder::open_default_sink() .expect("open default audio stream"); let player = rodio::Player::connect_new(sink_handle.mixer()); player.set_volume(1.0); let discord_client = { let mut client = DiscordIpcClient::new("1525954755292299426"); match client.connect() { Ok(_) => Some(client), Err(e) => { eprintln!("Discord RPC unavailable: {e}"); None } } }; Self { running: true, library: Library::new(), //songs: get_songs(), queue: vec![].into(), artist_state: ListState::default().with_selected(Some(0)), album_state: ListState::default().with_selected(Some(0)), song_state: ListState::default().with_selected(Some(0)), queue_state: ListState::default().with_selected(Some(0)), focus: Focus::default(), events: EventHandler::new(), current_song: None, sink_handle, player, discord_client, } } /// Run the application's main loop. pub async fn run(mut self, mut terminal: DefaultTerminal) -> color_eyre::Result<()> { while self.running { terminal.draw(|frame| frame.render_widget(&mut self, frame.area()))?; match self.events.next().await? { Event::Tick => self.tick(), Event::Crossterm(event) => match event { crossterm::event::Event::Key(key_event) if key_event.kind == crossterm::event::KeyEventKind::Press => { self.handle_key_events(key_event)? } _ => {} }, Event::App(app_event) => match app_event { AppEvent::Play => match self.focus { Focus::Artists => (), Focus::Albums => self.enqueue_album(), Focus::Songs => self.play_song(self.song_state.selected().expect("None")), _ => (), }, AppEvent::Skip => self.skip(), AppEvent::PlayPause => self.play_pause(), AppEvent::IncVolume => self.inc_volume(), AppEvent::DecVolume => self.dec_volume(), AppEvent::MoveRight => self.move_right(), AppEvent::MoveLeft => self.move_left(), AppEvent::RemoveFromQueue => self.remove_from_queue(), AppEvent::Quit => self.quit(), }, } } drop(self.discord_client); Ok(()) } /// Handles the key events and updates the state of [`App`]. pub fn handle_key_events(&mut self, key_event: KeyEvent) -> color_eyre::Result<()> { match key_event.code { KeyCode::Esc | KeyCode::Char('q') => self.events.send(AppEvent::Quit), KeyCode::Char('c' | 'C') if key_event.modifiers == KeyModifiers::CONTROL => { self.events.send(AppEvent::Quit) }, KeyCode::Char('R') => { self.library = Library::scan(); self.library.save(); }, KeyCode::Char('l') | KeyCode::Right | KeyCode::Tab => self.events.send(AppEvent::MoveRight), KeyCode::Char('h') | KeyCode::Left => self.events.send(AppEvent::MoveLeft), KeyCode::Char('j') | KeyCode::Down => match self.focus { Focus::Artists => self.artist_state.select_next(), Focus::Albums => self.album_state.select_next(), Focus::Songs => self.song_state.select_next(), Focus::Queue => self.queue_state.select_next(), }, KeyCode::Char('k') | KeyCode::Up => match self.focus { Focus::Artists => self.artist_state.select_previous(), Focus::Albums => self.album_state.select_previous(), Focus::Songs => self.song_state.select_previous(), Focus::Queue => self.queue_state.select_previous(), }, KeyCode::Char('g') => match self.focus { Focus::Artists => self.artist_state.select_first(), Focus::Albums => self.album_state.select_first(), Focus::Songs => self.song_state.select_first(), Focus::Queue => self.queue_state.select_first(), }, KeyCode::Char('G') => match self.focus { Focus::Artists => self.artist_state.select_last(), Focus::Albums => self.album_state.select_last(), Focus::Songs => self.song_state.select_last(), Focus::Queue => self.queue_state.select_last(), }, KeyCode::Enter => self.events.send(AppEvent::Play), KeyCode::Char('x') => self.stop_playback(), KeyCode::Char(' ') | KeyCode::Media(MediaKeyCode::PlayPause) => self.events.send(AppEvent::PlayPause), KeyCode::Char('+') | KeyCode::Char('=') => self.events.send(AppEvent::IncVolume), KeyCode::Char('-') | KeyCode::Char('_') => self.events.send(AppEvent::DecVolume), KeyCode::Delete => self.events.send(AppEvent::RemoveFromQueue), KeyCode::Char('>') => self.events.send(AppEvent::Skip), // Other handlers you could add here. _ => {} } Ok(()) } /* pub fn play(&mut self) { let file = File::open(self.songs[self.list_state.selected().expect("None")].clone()).unwrap(); let source = Decoder::new(BufReader::new(file)).unwrap(); if self.player.empty() { self.player.append(source); } else { self.player.stop(); self.player.append(source); } } */ pub fn play(&mut self) { if !self.queue.is_empty() { for _i in 0..self.queue.len() { let file = File::open(self.library.songs[*self.queue.front().expect("None")].path.clone()).unwrap(); let source = Decoder::new(BufReader::new(file)).unwrap(); self.queue.pop_front(); self.player.append(source); } } else { let file = File::open(self.library.songs[self.artist_state.selected().expect("None")].path.clone()).unwrap(); let source = Decoder::new(BufReader::new(file)).unwrap(); if self.player.empty() { self.player.append(source); } else { self.player.stop(); self.player.append(source); } } } pub fn play_song(&mut self, index: usize) { let song_indexes = self.current_selected_song(); let song_index = song_indexes[index]; self.queue.push_back(song_index); } pub fn enqueue_album(&mut self) { let song_indexes = self.current_selected_song(); for &song in &song_indexes { self.queue.push_back(song); } let song = &self.library.songs[song_indexes[0]]; // We only need to read the tags if we might have to save the artwork. let tag = Tag::default() .read_from_path(&song.path) .expect("Error"); if let Some(cover) = tag.album_cover() { let extension = match cover.mime_type { MimeType::Png => "png", MimeType::Jpeg => "jpg", _ => "bin", }; let filename = format!("{} - {}.{}", song.artist, song.album, extension); let path = dirs::cache_dir() .unwrap() .join("ncmprs") .join("art") .join(filename); if !path.exists() { std::fs::create_dir_all(path.parent().unwrap()).unwrap(); std::fs::write(&path, cover.data).unwrap(); } } } pub fn current_selected_song(&mut self) -> Vec { let selected_artist = &self.library.artists[self.artist_state.selected().unwrap()]; let albums = self.library.albums_for_artist(selected_artist); let selected_album = &albums[self.album_state.selected().unwrap()]; let song_indexes = self.library.songs_for_album(selected_artist, selected_album); song_indexes.to_owned() } pub fn play_pause(&mut self) { if !self.player.is_paused() { self.player.pause(); } else if self.player.is_paused() { self.player.play(); } } pub fn inc_volume(&mut self) { let vol = self.player.volume(); //self.player.set_volume(self.player.volume() + 0.1); if vol >= 1.0 { self.player.set_volume(1.0); } else { self.player.set_volume(vol + 0.05); } } pub fn dec_volume(&mut self) { let vol = self.player.volume(); if vol <= 0.01 { self.player.set_volume(0.0); } else { self.player.set_volume(vol - 0.05); } } pub fn skip(&mut self) { self.player.skip_one(); if self.queue.is_empty() { self.current_song = None; if let Some(client) = self.discord_client.as_mut() { let _ = client.clear_activity(); } } } pub fn remove_from_queue(&mut self) { if self.queue_state.selected().is_some() { self.queue.remove(self.queue_state.selected().unwrap()); } } pub fn stop_playback(&mut self) { self.queue.clear(); self.player.stop(); if let Some(client) = self.discord_client.as_mut() { let _ = client.clear_activity(); } self.current_song = None; } pub fn move_right(&mut self) { match self.focus { Focus::Artists => self.focus = Focus::Albums, Focus::Albums => self.focus = Focus::Songs, Focus::Songs => self.focus = Focus::Queue, Focus::Queue => self.focus = Focus::Artists, } } pub fn move_left(&mut self) { match self.focus { Focus::Artists => self.focus = Focus::Queue, Focus::Albums => self.focus = Focus::Artists, Focus::Songs => self.focus = Focus::Albums, Focus::Queue => self.focus = Focus::Songs, } } pub fn current_song(&self) -> Option<&Song> { self.current_song.map(|i| &self.library.songs[i]) } pub fn current_song_title(&self) -> &str { self.current_song() .map(|s| s.title.as_str()) .unwrap_or("") } pub fn update_rpc(&mut self, index: usize) { if let Some(client) = self.discord_client.as_mut() { let now = SystemTime::now() .duration_since(UNIX_EPOCH) .unwrap() .as_secs() as i64; let song = &self.library.songs[index]; let duration = song.duration.unwrap_or(1.0) as i64; let artist = &song.artist; let timestamps = Timestamps::new() .start(now) .end(now + duration); let asset = Assets::new(); let payload = activity::Activity::new() .name(song.title.clone()) .state(artist) .activity_type(activity::ActivityType::Listening) .timestamps(timestamps) .assets(asset.large_image("https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/png/navidrome.png")); let _ = client.set_activity(payload); } } /// Handles the tick event of the terminal. /// /// The tick event is where you can update the state of your application with any logic that /// needs to be updated at a fixed frame rate. E.g. polling a server, updating an animation. pub fn tick(&mut self) { if self.player.empty() && !self.queue.is_empty() { let file = File::open(&self.library.songs[*self.queue.front().expect("Queue is empty")].path).unwrap(); let source = Decoder::new(BufReader::new(file)).unwrap(); self.player.append(source); self.current_song = Some(self.queue.front().unwrap().to_owned()); self.update_rpc(self.queue.front().unwrap().to_owned()); self.queue.pop_front(); } } /// Set running to false to quit the application. pub fn quit(&mut self) { self.running = false; } }