diff --git a/src/app.rs b/src/app.rs index f67ca4e..c2ce06c 100755 --- a/src/app.rs +++ b/src/app.rs @@ -170,7 +170,7 @@ impl App { self.selected_tab = match self.selected_tab { SelectedTab::Queue => SelectedTab::DirectoryBrowser, SelectedTab::DirectoryBrowser => SelectedTab::Playlists, - SelectedTab::Playlists => SelectedTab::Queue, + SelectedTab::Playlists => SelectedTab::DirectoryBrowser, }; } diff --git a/src/browser.rs b/src/browser.rs index 6af4f59..73f9cdd 100755 --- a/src/browser.rs +++ b/src/browser.rs @@ -1,3 +1,8 @@ +use std::ffi::OsStr; +use std::path::Path; + +use mpd::Song; + use crate::{app::AppResult, connection::Connection}; #[derive(Debug)] @@ -7,6 +12,24 @@ pub struct FileBrowser { pub prev_selected: usize, pub path: String, pub prev_path: String, + pub songs: Vec, +} + +// https://stackoverflow.com/questions/72392835/check-if-a-file-is-of-a-given-type +pub trait FileExtension { + fn has_extension>(&self, extensions: &[S]) -> bool; +} + +impl> FileExtension for P { + fn has_extension>(&self, extensions: &[S]) -> bool { + if let Some(ref extension) = self.as_ref().extension().and_then(OsStr::to_str) { + return extensions + .iter() + .any(|x| x.as_ref().eq_ignore_ascii_case(extension)); + } + + false + } } impl FileBrowser { @@ -17,6 +40,7 @@ impl FileBrowser { prev_selected: 0, path: ".".to_string(), prev_path: ".".to_string(), + songs: vec![], } } @@ -26,9 +50,41 @@ impl FileBrowser { .conn .listfiles(self.path.as_str())? .into_iter() - .filter(|(f, _)| f == "directory" || f == "file") + .filter(|(f, l)| { + f == "directory" + || f == "file" && Path::new(l).has_extension(&["mp3", "ogg", "flac", "m4a", "wav", "aac" ,"opus", "ape", "wma", "mpc", "aiff", "dff", "mp2", "mka"]) + }) .collect::>(); + self.songs.clear(); + for (t, song) in self.filetree.iter() { + if t == "file" { + let v = conn + .conn + .lsinfo(Song { + file: conn + .get_full_path(song) + .unwrap_or_else(|| "Not a song".to_string()), + ..Default::default() + }) + .unwrap_or_else(|_| { + vec![Song { + file: "Not a song".to_string(), + ..Default::default() + }] + }); + + self.songs.push(v.get(0).unwrap().clone()); + } else { + let v = Song { + file: "".to_string(), + ..Default::default() + }; + + self.songs.push(v); + } + } + Ok(()) } diff --git a/src/connection.rs b/src/connection.rs index 7327bb9..5cba89d 100755 --- a/src/connection.rs +++ b/src/connection.rs @@ -27,14 +27,7 @@ impl Connection { let empty_song = Song { file: "No Song playing or in Queue".to_string(), - artist: None, - title: None, - duration: None, - last_mod: None, - name: None, - place: None, - range: None, - tags: vec![("".to_string(), "".to_string())], + ..Default::default() }; let songs_filenames: Vec = conn @@ -70,7 +63,6 @@ impl Connection { /// Fzf prompt for selecting song pub fn play_fzf(&mut self) -> Result<()> { is_installed("fzf").map_err(|ex| ex)?; - let ss = &self.songs_filenames; let fzf_choice = rust_fzf::select(ss.clone(), Vec::new()).unwrap(); let index = get_choice_index(&self.songs_filenames, fzf_choice.get(0).unwrap()); @@ -174,29 +166,12 @@ impl Connection { pub fn get_song_with_only_filename(&self, filename: &str) -> Song { Song { file: filename.to_string(), - artist: None, - title: None, - duration: None, - last_mod: None, - name: None, - place: None, - range: None, - tags: vec![("".to_string(), "".to_string())], + ..Default::default() } } /// Given a song name from a directory, it returns the full path of the song in the database pub fn get_full_path(&self, short_path: &str) -> Option { - // let list = self - // .songs_filenames - // .iter() - // .map(|f| f.as_str()) - // .collect::>(); - // let (filename, _) = rust_fuzzy_search::fuzzy_search_sorted(&short_path, &list) - // .get(0) - // .unwrap() - // .clone(); - for (i, f) in self.songs_filenames.iter().enumerate() { if f.contains(short_path) { return Some(self.songs_filenames.get(i).unwrap().to_string()); diff --git a/src/handler.rs b/src/handler.rs index c9df412..6d8c32c 100755 --- a/src/handler.rs +++ b/src/handler.rs @@ -62,6 +62,7 @@ pub fn handle_key_events(key_event: KeyEvent, app: &mut App) -> AppResult<()> { } } } + } match key_event.code { @@ -199,6 +200,7 @@ pub fn handle_key_events(key_event: KeyEvent, app: &mut App) -> AppResult<()> { app.conn .load_playlist(app.pl_list.list.get(app.pl_list.index).unwrap())?; } + } app.conn.update_status(); } diff --git a/src/main.rs b/src/main.rs index 9b436a5..467af46 100755 --- a/src/main.rs +++ b/src/main.rs @@ -26,7 +26,7 @@ pub type Error = Box; fn main() -> AppResult<()> { let args = Args::parse(); let env_host = env::var("MPD_HOST").unwrap_or_else(|_| "127.0.0.1".to_string()); - let env_port = env::var("MPD_PORT").unwrap_or_else(|_| "6600".to_string()); + let env_port = env::var("MPD_PORT").unwrap_or_else(|_| "6600".to_string()); let mut app = App::builder(format!("{}:{}", env_host, env_port).as_str())?; if !args.tui { @@ -38,15 +38,7 @@ fn main() -> AppResult<()> { Some(Command::Status) => app.conn.status(), Some(Command::Pause) => app.conn.pause(), Some(Command::Toggle) => app.conn.toggle_pause(), - _ => { - // let mut vec: Vec = Vec::new(); - // for filename in app.conn.songs_filenames { - // let song = RSong::new(&mut app.conn.conn, filename); - // vec.push(song); - // } - // println!("{:#?}", vec); - - } + _ => {} } } Ok(()) diff --git a/src/ui.rs b/src/ui.rs index d23e9ae..93dcd53 100755 --- a/src/ui.rs +++ b/src/ui.rs @@ -1,3 +1,5 @@ +use std::time::Duration; + use crate::app::{App, SelectedTab}; use ratatui::{ prelude::*, @@ -30,7 +32,8 @@ pub fn render(app: &mut App, frame: &mut Frame) { match app.selected_tab { SelectedTab::Queue => draw_queue(frame, app, layout[0]), SelectedTab::Playlists => draw_playlists(frame, app, layout[0]), - SelectedTab::DirectoryBrowser => draw_directory_browser(frame, app, layout[0]), + SelectedTab::DirectoryBrowser => draw_song_browser(frame, app, layout[0]), + // SelectedTab::SongBrowser => draw_song_browser(frame, app, layout[0]), } match app.inputmode { @@ -63,6 +66,7 @@ fn draw_directory_browser(frame: &mut Frame, app: &mut App, size: Rect) { if status { list.push(ListItem::new(s.clone().magenta().bold())); } else { + // list.push(ListItem::new(s.clone())); list.push(ListItem::new(s.clone())); } } else { @@ -75,7 +79,7 @@ fn draw_directory_browser(frame: &mut Frame, app: &mut App, size: Rect) { let list = List::new(list) .block( Block::default() - .title(format!("File Browser: {}", app.browser.path.clone()).bold()) + .title(format!("Directory Browser: {}", app.browser.path.clone()).bold()) .title( Title::from(format!("Total Songs: {}", total_songs).bold().green()) .alignment(Alignment::Center), @@ -101,12 +105,120 @@ fn draw_directory_browser(frame: &mut Frame, app: &mut App, size: Rect) { frame.render_stateful_widget(list, size, &mut song_state); } +/// Draws the song browser +fn draw_song_browser(frame: &mut Frame, app: &mut App, size: Rect) { + let total_songs = app.conn.conn.stats().unwrap().songs.to_string(); + + let rows = app.browser.filetree.iter().enumerate().map(|(i, (t, s))| { + if t == "file" { + let song = app.browser.songs.get(i).unwrap().clone(); + + // metadata + let title = song.clone().title.unwrap_or_else(|| song.clone().file); + let artist = song.clone().artist.unwrap_or_default().cyan(); + let album = song + .tags + .iter() + .filter(|(x, _)| x == "Album") + .map(|(_, l)| l.clone()) + .collect::>() + .join(""); + + let track = song + .tags + .iter() + .filter(|(x, _)| x == "Track") + .map(|(_, l)| l.clone()) + .collect::>() + .join(""); + + let time = humantime::format_duration( + song.clone().duration.unwrap_or_else(|| Duration::new(0, 0)), + ); + + let mut status: bool = false; + for sn in app.queue_list.list.iter() { + if sn.contains(s) { + status = true; + } + } + + if status { + let row = Row::new(vec![ + Cell::from(artist), + Cell::from(track.green()), + Cell::from(title), + Cell::from(album), + Cell::from(time.to_string().red()), + ]); + row.magenta().bold() + } else { + let row = Row::new(vec![ + Cell::from(artist), + Cell::from(track.green()), + Cell::from(title), + Cell::from(album), + Cell::from(time.to_string().red()), + ]); + row + } + } else { + let row = Row::new(vec![Cell::from(format!("[{}]", *s))]); + row + } + }); + + let mut state = TableState::new(); + let header = ["Artist", "Track", "Title", "Album", "Time"] + .into_iter() + .map(Cell::from) + .collect::() + .bold() + .height(1); + let table = Table::new( + rows, + [ + Constraint::Percentage(33), + Constraint::Percentage(3), + Constraint::Percentage(30), + Constraint::Percentage(30), + Constraint::Percentage(3), + ], + ) + .block( + Block::default() + .title(format!("Song Browser: {}", app.browser.path.clone()).bold()) + .title( + Title::from(format!("Total Songs: {}", total_songs).bold().green()) + .alignment(Alignment::Center), + ) + .title( + Title::from(format!("Volume: {}%", app.conn.volume).bold().green()) + .alignment(Alignment::Right), + ) + .borders(Borders::ALL), + ) + .highlight_style( + Style::default() + .add_modifier(Modifier::REVERSED) + .fg(Color::Cyan) + .bg(Color::Black), + ) + .highlight_symbol(">>") + .header(header); + + state.select(Some(app.browser.selected)); + frame.render_stateful_widget(table, size, &mut state); +} + /// draws playing queue fn draw_queue(frame: &mut Frame, app: &mut App, size: Rect) { let mut queue_state = ListState::default(); let title = Block::default() .title(Title::from("Play Queue".green().bold())) - .title(Title::from(format!("({} items)", app.queue_list.list.len()).bold())) + .title(Title::from( + format!("({} items)", app.queue_list.list.len()).bold(), + )) .title( Title::from(format!("Volume: {}%", app.conn.volume).bold().green()) .alignment(Alignment::Right),