rmptui4993431
add metadata in directory tree
+179 −42
6 files changed
- M src/app.rs +1 −1
- M src/browser.rs +57 −1
- M src/connection.rs +2 −27
- M src/handler.rs +2 −0
- M src/main.rs +2 −10
- M src/ui.rs +115 −3
| @@ -170,7 +170,7 @@ impl App { | |||
| 170 | 170 | self.selected_tab = match self.selected_tab { | |
| 171 | 171 | SelectedTab::Queue => SelectedTab::DirectoryBrowser, | |
| 172 | 172 | SelectedTab::DirectoryBrowser => SelectedTab::Playlists, | |
| 173 | − | SelectedTab::Playlists => SelectedTab::Queue, | |
| 173 | + | SelectedTab::Playlists => SelectedTab::DirectoryBrowser, | |
| 174 | 174 | }; | |
| 175 | 175 | } | |
| 176 | 176 | ||
| @@ -1,3 +1,8 @@ | |||
| 1 | + | use std::ffi::OsStr; | |
| 2 | + | use std::path::Path; | |
| 3 | + | ||
| 4 | + | use mpd::Song; | |
| 5 | + | ||
| 1 | 6 | use crate::{app::AppResult, connection::Connection}; | |
| 2 | 7 | ||
| 3 | 8 | #[derive(Debug)] | |
| @@ -7,6 +12,24 @@ pub struct FileBrowser { | |||
| 7 | 12 | pub prev_selected: usize, | |
| 8 | 13 | pub path: String, | |
| 9 | 14 | pub prev_path: String, | |
| 15 | + | pub songs: Vec<Song>, | |
| 16 | + | } | |
| 17 | + | ||
| 18 | + | // https://stackoverflow.com/questions/72392835/check-if-a-file-is-of-a-given-type | |
| 19 | + | pub trait FileExtension { | |
| 20 | + | fn has_extension<S: AsRef<str>>(&self, extensions: &[S]) -> bool; | |
| 21 | + | } | |
| 22 | + | ||
| 23 | + | impl<P: AsRef<Path>> FileExtension for P { | |
| 24 | + | fn has_extension<S: AsRef<str>>(&self, extensions: &[S]) -> bool { | |
| 25 | + | if let Some(ref extension) = self.as_ref().extension().and_then(OsStr::to_str) { | |
| 26 | + | return extensions | |
| 27 | + | .iter() | |
| 28 | + | .any(|x| x.as_ref().eq_ignore_ascii_case(extension)); | |
| 29 | + | } | |
| 30 | + | ||
| 31 | + | false | |
| 32 | + | } | |
| 10 | 33 | } | |
| 11 | 34 | ||
| 12 | 35 | impl FileBrowser { | |
| @@ -17,6 +40,7 @@ impl FileBrowser { | |||
| 17 | 40 | prev_selected: 0, | |
| 18 | 41 | path: ".".to_string(), | |
| 19 | 42 | prev_path: ".".to_string(), | |
| 43 | + | songs: vec![], | |
| 20 | 44 | } | |
| 21 | 45 | } | |
| 22 | 46 | ||
| @@ -26,9 +50,41 @@ impl FileBrowser { | |||
| 26 | 50 | .conn | |
| 27 | 51 | .listfiles(self.path.as_str())? | |
| 28 | 52 | .into_iter() | |
| 29 | − | .filter(|(f, _)| f == "directory" || f == "file") | |
| 53 | + | .filter(|(f, l)| { | |
| 54 | + | f == "directory" | |
| 55 | + | || f == "file" && Path::new(l).has_extension(&["mp3", "ogg", "flac", "m4a", "wav", "aac" ,"opus", "ape", "wma", "mpc", "aiff", "dff", "mp2", "mka"]) | |
| 56 | + | }) | |
| 30 | 57 | .collect::<Vec<(String, String)>>(); | |
| 31 | 58 | ||
| 59 | + | self.songs.clear(); | |
| 60 | + | for (t, song) in self.filetree.iter() { | |
| 61 | + | if t == "file" { | |
| 62 | + | let v = conn | |
| 63 | + | .conn | |
| 64 | + | .lsinfo(Song { | |
| 65 | + | file: conn | |
| 66 | + | .get_full_path(song) | |
| 67 | + | .unwrap_or_else(|| "Not a song".to_string()), | |
| 68 | + | ..Default::default() | |
| 69 | + | }) | |
| 70 | + | .unwrap_or_else(|_| { | |
| 71 | + | vec![Song { | |
| 72 | + | file: "Not a song".to_string(), | |
| 73 | + | ..Default::default() | |
| 74 | + | }] | |
| 75 | + | }); | |
| 76 | + | ||
| 77 | + | self.songs.push(v.get(0).unwrap().clone()); | |
| 78 | + | } else { | |
| 79 | + | let v = Song { | |
| 80 | + | file: "".to_string(), | |
| 81 | + | ..Default::default() | |
| 82 | + | }; | |
| 83 | + | ||
| 84 | + | self.songs.push(v); | |
| 85 | + | } | |
| 86 | + | } | |
| 87 | + | ||
| 32 | 88 | Ok(()) | |
| 33 | 89 | } | |
| 34 | 90 | ||
| @@ -27,14 +27,7 @@ impl Connection { | |||
| 27 | 27 | ||
| 28 | 28 | let empty_song = Song { | |
| 29 | 29 | file: "No Song playing or in Queue".to_string(), | |
| 30 | − | artist: None, | |
| 31 | − | title: None, | |
| 32 | − | duration: None, | |
| 33 | − | last_mod: None, | |
| 34 | − | name: None, | |
| 35 | − | place: None, | |
| 36 | − | range: None, | |
| 37 | − | tags: vec![("".to_string(), "".to_string())], | |
| 30 | + | ..Default::default() | |
| 38 | 31 | }; | |
| 39 | 32 | ||
| 40 | 33 | let songs_filenames: Vec<String> = conn | |
| @@ -70,7 +63,6 @@ impl Connection { | |||
| 70 | 63 | /// Fzf prompt for selecting song | |
| 71 | 64 | pub fn play_fzf(&mut self) -> Result<()> { | |
| 72 | 65 | is_installed("fzf").map_err(|ex| ex)?; | |
| 73 | − | ||
| 74 | 66 | let ss = &self.songs_filenames; | |
| 75 | 67 | let fzf_choice = rust_fzf::select(ss.clone(), Vec::new()).unwrap(); | |
| 76 | 68 | let index = get_choice_index(&self.songs_filenames, fzf_choice.get(0).unwrap()); | |
| @@ -174,29 +166,12 @@ impl Connection { | |||
| 174 | 166 | pub fn get_song_with_only_filename(&self, filename: &str) -> Song { | |
| 175 | 167 | Song { | |
| 176 | 168 | file: filename.to_string(), | |
| 177 | − | artist: None, | |
| 178 | − | title: None, | |
| 179 | − | duration: None, | |
| 180 | − | last_mod: None, | |
| 181 | − | name: None, | |
| 182 | − | place: None, | |
| 183 | − | range: None, | |
| 184 | − | tags: vec![("".to_string(), "".to_string())], | |
| 169 | + | ..Default::default() | |
| 185 | 170 | } | |
| 186 | 171 | } | |
| 187 | 172 | ||
| 188 | 173 | /// Given a song name from a directory, it returns the full path of the song in the database | |
| 189 | 174 | pub fn get_full_path(&self, short_path: &str) -> Option<String> { | |
| 190 | − | // let list = self | |
| 191 | − | // .songs_filenames | |
| 192 | − | // .iter() | |
| 193 | − | // .map(|f| f.as_str()) | |
| 194 | − | // .collect::<Vec<&str>>(); | |
| 195 | − | // let (filename, _) = rust_fuzzy_search::fuzzy_search_sorted(&short_path, &list) | |
| 196 | − | // .get(0) | |
| 197 | − | // .unwrap() | |
| 198 | − | // .clone(); | |
| 199 | − | ||
| 200 | 175 | for (i, f) in self.songs_filenames.iter().enumerate() { | |
| 201 | 176 | if f.contains(short_path) { | |
| 202 | 177 | return Some(self.songs_filenames.get(i).unwrap().to_string()); | |
| @@ -62,6 +62,7 @@ pub fn handle_key_events(key_event: KeyEvent, app: &mut App) -> AppResult<()> { | |||
| 62 | 62 | } | |
| 63 | 63 | } | |
| 64 | 64 | } | |
| 65 | + | ||
| 65 | 66 | } | |
| 66 | 67 | ||
| 67 | 68 | match key_event.code { | |
| @@ -199,6 +200,7 @@ pub fn handle_key_events(key_event: KeyEvent, app: &mut App) -> AppResult<()> { | |||
| 199 | 200 | app.conn | |
| 200 | 201 | .load_playlist(app.pl_list.list.get(app.pl_list.index).unwrap())?; | |
| 201 | 202 | } | |
| 203 | + | ||
| 202 | 204 | } | |
| 203 | 205 | app.conn.update_status(); | |
| 204 | 206 | } | |
| @@ -26,7 +26,7 @@ pub type Error = Box<dyn std::error::Error>; | |||
| 26 | 26 | fn main() -> AppResult<()> { | |
| 27 | 27 | let args = Args::parse(); | |
| 28 | 28 | let env_host = env::var("MPD_HOST").unwrap_or_else(|_| "127.0.0.1".to_string()); | |
| 29 | − | let env_port = env::var("MPD_PORT").unwrap_or_else(|_| "6600".to_string()); | |
| 29 | + | let env_port = env::var("MPD_PORT").unwrap_or_else(|_| "6600".to_string()); | |
| 30 | 30 | let mut app = App::builder(format!("{}:{}", env_host, env_port).as_str())?; | |
| 31 | 31 | ||
| 32 | 32 | if !args.tui { | |
| @@ -38,15 +38,7 @@ fn main() -> AppResult<()> { | |||
| 38 | 38 | Some(Command::Status) => app.conn.status(), | |
| 39 | 39 | Some(Command::Pause) => app.conn.pause(), | |
| 40 | 40 | Some(Command::Toggle) => app.conn.toggle_pause(), | |
| 41 | − | _ => { | |
| 42 | − | // let mut vec: Vec<RSong> = Vec::new(); | |
| 43 | − | // for filename in app.conn.songs_filenames { | |
| 44 | − | // let song = RSong::new(&mut app.conn.conn, filename); | |
| 45 | − | // vec.push(song); | |
| 46 | − | // } | |
| 47 | − | // println!("{:#?}", vec); | |
| 48 | − | ||
| 49 | − | } | |
| 41 | + | _ => {} | |
| 50 | 42 | } | |
| 51 | 43 | } | |
| 52 | 44 | Ok(()) | |
| @@ -1,3 +1,5 @@ | |||
| 1 | + | use std::time::Duration; | |
| 2 | + | ||
| 1 | 3 | use crate::app::{App, SelectedTab}; | |
| 2 | 4 | use ratatui::{ | |
| 3 | 5 | prelude::*, | |
| @@ -30,7 +32,8 @@ pub fn render(app: &mut App, frame: &mut Frame) { | |||
| 30 | 32 | match app.selected_tab { | |
| 31 | 33 | SelectedTab::Queue => draw_queue(frame, app, layout[0]), | |
| 32 | 34 | SelectedTab::Playlists => draw_playlists(frame, app, layout[0]), | |
| 33 | − | SelectedTab::DirectoryBrowser => draw_directory_browser(frame, app, layout[0]), | |
| 35 | + | SelectedTab::DirectoryBrowser => draw_song_browser(frame, app, layout[0]), | |
| 36 | + | // SelectedTab::SongBrowser => draw_song_browser(frame, app, layout[0]), | |
| 34 | 37 | } | |
| 35 | 38 | ||
| 36 | 39 | match app.inputmode { | |
| @@ -63,6 +66,7 @@ fn draw_directory_browser(frame: &mut Frame, app: &mut App, size: Rect) { | |||
| 63 | 66 | if status { | |
| 64 | 67 | list.push(ListItem::new(s.clone().magenta().bold())); | |
| 65 | 68 | } else { | |
| 69 | + | // list.push(ListItem::new(s.clone())); | |
| 66 | 70 | list.push(ListItem::new(s.clone())); | |
| 67 | 71 | } | |
| 68 | 72 | } else { | |
| @@ -75,7 +79,7 @@ fn draw_directory_browser(frame: &mut Frame, app: &mut App, size: Rect) { | |||
| 75 | 79 | let list = List::new(list) | |
| 76 | 80 | .block( | |
| 77 | 81 | Block::default() | |
| 78 | − | .title(format!("File Browser: {}", app.browser.path.clone()).bold()) | |
| 82 | + | .title(format!("Directory Browser: {}", app.browser.path.clone()).bold()) | |
| 79 | 83 | .title( | |
| 80 | 84 | Title::from(format!("Total Songs: {}", total_songs).bold().green()) | |
| 81 | 85 | .alignment(Alignment::Center), | |
| @@ -101,12 +105,120 @@ fn draw_directory_browser(frame: &mut Frame, app: &mut App, size: Rect) { | |||
| 101 | 105 | frame.render_stateful_widget(list, size, &mut song_state); | |
| 102 | 106 | } | |
| 103 | 107 | ||
| 108 | + | /// Draws the song browser | |
| 109 | + | fn draw_song_browser(frame: &mut Frame, app: &mut App, size: Rect) { | |
| 110 | + | let total_songs = app.conn.conn.stats().unwrap().songs.to_string(); | |
| 111 | + | ||
| 112 | + | let rows = app.browser.filetree.iter().enumerate().map(|(i, (t, s))| { | |
| 113 | + | if t == "file" { | |
| 114 | + | let song = app.browser.songs.get(i).unwrap().clone(); | |
| 115 | + | ||
| 116 | + | // metadata | |
| 117 | + | let title = song.clone().title.unwrap_or_else(|| song.clone().file); | |
| 118 | + | let artist = song.clone().artist.unwrap_or_default().cyan(); | |
| 119 | + | let album = song | |
| 120 | + | .tags | |
| 121 | + | .iter() | |
| 122 | + | .filter(|(x, _)| x == "Album") | |
| 123 | + | .map(|(_, l)| l.clone()) | |
| 124 | + | .collect::<Vec<String>>() | |
| 125 | + | .join(""); | |
| 126 | + | ||
| 127 | + | let track = song | |
| 128 | + | .tags | |
| 129 | + | .iter() | |
| 130 | + | .filter(|(x, _)| x == "Track") | |
| 131 | + | .map(|(_, l)| l.clone()) | |
| 132 | + | .collect::<Vec<String>>() | |
| 133 | + | .join(""); | |
| 134 | + | ||
| 135 | + | let time = humantime::format_duration( | |
| 136 | + | song.clone().duration.unwrap_or_else(|| Duration::new(0, 0)), | |
| 137 | + | ); | |
| 138 | + | ||
| 139 | + | let mut status: bool = false; | |
| 140 | + | for sn in app.queue_list.list.iter() { | |
| 141 | + | if sn.contains(s) { | |
| 142 | + | status = true; | |
| 143 | + | } | |
| 144 | + | } | |
| 145 | + | ||
| 146 | + | if status { | |
| 147 | + | let row = Row::new(vec![ | |
| 148 | + | Cell::from(artist), | |
| 149 | + | Cell::from(track.green()), | |
| 150 | + | Cell::from(title), | |
| 151 | + | Cell::from(album), | |
| 152 | + | Cell::from(time.to_string().red()), | |
| 153 | + | ]); | |
| 154 | + | row.magenta().bold() | |
| 155 | + | } else { | |
| 156 | + | let row = Row::new(vec![ | |
| 157 | + | Cell::from(artist), | |
| 158 | + | Cell::from(track.green()), | |
| 159 | + | Cell::from(title), | |
| 160 | + | Cell::from(album), | |
| 161 | + | Cell::from(time.to_string().red()), | |
| 162 | + | ]); | |
| 163 | + | row | |
| 164 | + | } | |
| 165 | + | } else { | |
| 166 | + | let row = Row::new(vec![Cell::from(format!("[{}]", *s))]); | |
| 167 | + | row | |
| 168 | + | } | |
| 169 | + | }); | |
| 170 | + | ||
| 171 | + | let mut state = TableState::new(); | |
| 172 | + | let header = ["Artist", "Track", "Title", "Album", "Time"] | |
| 173 | + | .into_iter() | |
| 174 | + | .map(Cell::from) | |
| 175 | + | .collect::<Row>() | |
| 176 | + | .bold() | |
| 177 | + | .height(1); | |
| 178 | + | let table = Table::new( | |
| 179 | + | rows, | |
| 180 | + | [ | |
| 181 | + | Constraint::Percentage(33), | |
| 182 | + | Constraint::Percentage(3), | |
| 183 | + | Constraint::Percentage(30), | |
| 184 | + | Constraint::Percentage(30), | |
| 185 | + | Constraint::Percentage(3), | |
| 186 | + | ], | |
| 187 | + | ) | |
| 188 | + | .block( | |
| 189 | + | Block::default() | |
| 190 | + | .title(format!("Song Browser: {}", app.browser.path.clone()).bold()) | |
| 191 | + | .title( | |
| 192 | + | Title::from(format!("Total Songs: {}", total_songs).bold().green()) | |
| 193 | + | .alignment(Alignment::Center), | |
| 194 | + | ) | |
| 195 | + | .title( | |
| 196 | + | Title::from(format!("Volume: {}%", app.conn.volume).bold().green()) | |
| 197 | + | .alignment(Alignment::Right), | |
| 198 | + | ) | |
| 199 | + | .borders(Borders::ALL), | |
| 200 | + | ) | |
| 201 | + | .highlight_style( | |
| 202 | + | Style::default() | |
| 203 | + | .add_modifier(Modifier::REVERSED) | |
| 204 | + | .fg(Color::Cyan) | |
| 205 | + | .bg(Color::Black), | |
| 206 | + | ) | |
| 207 | + | .highlight_symbol(">>") | |
| 208 | + | .header(header); | |
| 209 | + | ||
| 210 | + | state.select(Some(app.browser.selected)); | |
| 211 | + | frame.render_stateful_widget(table, size, &mut state); | |
| 212 | + | } | |
| 213 | + | ||
| 104 | 214 | /// draws playing queue | |
| 105 | 215 | fn draw_queue(frame: &mut Frame, app: &mut App, size: Rect) { | |
| 106 | 216 | let mut queue_state = ListState::default(); | |
| 107 | 217 | let title = Block::default() | |
| 108 | 218 | .title(Title::from("Play Queue".green().bold())) | |
| 109 | − | .title(Title::from(format!("({} items)", app.queue_list.list.len()).bold())) | |
| 219 | + | .title(Title::from( | |
| 220 | + | format!("({} items)", app.queue_list.list.len()).bold(), | |
| 221 | + | )) | |
| 110 | 222 | .title( | |
| 111 | 223 | Title::from(format!("Volume: {}%", app.conn.volume).bold().green()) | |
| 112 | 224 | .alignment(Alignment::Right), | |