add metadata in directory tree
This commit is contained in:
parent
51fbf42653
commit
4993431186
|
|
@ -170,7 +170,7 @@ impl App {
|
||||||
self.selected_tab = match self.selected_tab {
|
self.selected_tab = match self.selected_tab {
|
||||||
SelectedTab::Queue => SelectedTab::DirectoryBrowser,
|
SelectedTab::Queue => SelectedTab::DirectoryBrowser,
|
||||||
SelectedTab::DirectoryBrowser => SelectedTab::Playlists,
|
SelectedTab::DirectoryBrowser => SelectedTab::Playlists,
|
||||||
SelectedTab::Playlists => SelectedTab::Queue,
|
SelectedTab::Playlists => SelectedTab::DirectoryBrowser,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,8 @@
|
||||||
|
use std::ffi::OsStr;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
use mpd::Song;
|
||||||
|
|
||||||
use crate::{app::AppResult, connection::Connection};
|
use crate::{app::AppResult, connection::Connection};
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
|
@ -7,6 +12,24 @@ pub struct FileBrowser {
|
||||||
pub prev_selected: usize,
|
pub prev_selected: usize,
|
||||||
pub path: String,
|
pub path: String,
|
||||||
pub prev_path: String,
|
pub prev_path: String,
|
||||||
|
pub songs: Vec<Song>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://stackoverflow.com/questions/72392835/check-if-a-file-is-of-a-given-type
|
||||||
|
pub trait FileExtension {
|
||||||
|
fn has_extension<S: AsRef<str>>(&self, extensions: &[S]) -> bool;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<P: AsRef<Path>> FileExtension for P {
|
||||||
|
fn has_extension<S: AsRef<str>>(&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 {
|
impl FileBrowser {
|
||||||
|
|
@ -17,6 +40,7 @@ impl FileBrowser {
|
||||||
prev_selected: 0,
|
prev_selected: 0,
|
||||||
path: ".".to_string(),
|
path: ".".to_string(),
|
||||||
prev_path: ".".to_string(),
|
prev_path: ".".to_string(),
|
||||||
|
songs: vec![],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -26,9 +50,41 @@ impl FileBrowser {
|
||||||
.conn
|
.conn
|
||||||
.listfiles(self.path.as_str())?
|
.listfiles(self.path.as_str())?
|
||||||
.into_iter()
|
.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::<Vec<(String, String)>>();
|
.collect::<Vec<(String, String)>>();
|
||||||
|
|
||||||
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -27,14 +27,7 @@ impl Connection {
|
||||||
|
|
||||||
let empty_song = Song {
|
let empty_song = Song {
|
||||||
file: "No Song playing or in Queue".to_string(),
|
file: "No Song playing or in Queue".to_string(),
|
||||||
artist: None,
|
..Default::default()
|
||||||
title: None,
|
|
||||||
duration: None,
|
|
||||||
last_mod: None,
|
|
||||||
name: None,
|
|
||||||
place: None,
|
|
||||||
range: None,
|
|
||||||
tags: vec![("".to_string(), "".to_string())],
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let songs_filenames: Vec<String> = conn
|
let songs_filenames: Vec<String> = conn
|
||||||
|
|
@ -70,7 +63,6 @@ impl Connection {
|
||||||
/// Fzf prompt for selecting song
|
/// Fzf prompt for selecting song
|
||||||
pub fn play_fzf(&mut self) -> Result<()> {
|
pub fn play_fzf(&mut self) -> Result<()> {
|
||||||
is_installed("fzf").map_err(|ex| ex)?;
|
is_installed("fzf").map_err(|ex| ex)?;
|
||||||
|
|
||||||
let ss = &self.songs_filenames;
|
let ss = &self.songs_filenames;
|
||||||
let fzf_choice = rust_fzf::select(ss.clone(), Vec::new()).unwrap();
|
let fzf_choice = rust_fzf::select(ss.clone(), Vec::new()).unwrap();
|
||||||
let index = get_choice_index(&self.songs_filenames, fzf_choice.get(0).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 {
|
pub fn get_song_with_only_filename(&self, filename: &str) -> Song {
|
||||||
Song {
|
Song {
|
||||||
file: filename.to_string(),
|
file: filename.to_string(),
|
||||||
artist: None,
|
..Default::default()
|
||||||
title: None,
|
|
||||||
duration: None,
|
|
||||||
last_mod: None,
|
|
||||||
name: None,
|
|
||||||
place: None,
|
|
||||||
range: None,
|
|
||||||
tags: vec![("".to_string(), "".to_string())],
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Given a song name from a directory, it returns the full path of the song in the database
|
/// 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<String> {
|
pub fn get_full_path(&self, short_path: &str) -> Option<String> {
|
||||||
// let list = self
|
|
||||||
// .songs_filenames
|
|
||||||
// .iter()
|
|
||||||
// .map(|f| f.as_str())
|
|
||||||
// .collect::<Vec<&str>>();
|
|
||||||
// let (filename, _) = rust_fuzzy_search::fuzzy_search_sorted(&short_path, &list)
|
|
||||||
// .get(0)
|
|
||||||
// .unwrap()
|
|
||||||
// .clone();
|
|
||||||
|
|
||||||
for (i, f) in self.songs_filenames.iter().enumerate() {
|
for (i, f) in self.songs_filenames.iter().enumerate() {
|
||||||
if f.contains(short_path) {
|
if f.contains(short_path) {
|
||||||
return Some(self.songs_filenames.get(i).unwrap().to_string());
|
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<()> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
match key_event.code {
|
match key_event.code {
|
||||||
|
|
@ -199,6 +200,7 @@ pub fn handle_key_events(key_event: KeyEvent, app: &mut App) -> AppResult<()> {
|
||||||
app.conn
|
app.conn
|
||||||
.load_playlist(app.pl_list.list.get(app.pl_list.index).unwrap())?;
|
.load_playlist(app.pl_list.list.get(app.pl_list.index).unwrap())?;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
app.conn.update_status();
|
app.conn.update_status();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
12
src/main.rs
12
src/main.rs
|
|
@ -26,7 +26,7 @@ pub type Error = Box<dyn std::error::Error>;
|
||||||
fn main() -> AppResult<()> {
|
fn main() -> AppResult<()> {
|
||||||
let args = Args::parse();
|
let args = Args::parse();
|
||||||
let env_host = env::var("MPD_HOST").unwrap_or_else(|_| "127.0.0.1".to_string());
|
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())?;
|
let mut app = App::builder(format!("{}:{}", env_host, env_port).as_str())?;
|
||||||
|
|
||||||
if !args.tui {
|
if !args.tui {
|
||||||
|
|
@ -38,15 +38,7 @@ fn main() -> AppResult<()> {
|
||||||
Some(Command::Status) => app.conn.status(),
|
Some(Command::Status) => app.conn.status(),
|
||||||
Some(Command::Pause) => app.conn.pause(),
|
Some(Command::Pause) => app.conn.pause(),
|
||||||
Some(Command::Toggle) => app.conn.toggle_pause(),
|
Some(Command::Toggle) => app.conn.toggle_pause(),
|
||||||
_ => {
|
_ => {}
|
||||||
// let mut vec: Vec<RSong> = Vec::new();
|
|
||||||
// for filename in app.conn.songs_filenames {
|
|
||||||
// let song = RSong::new(&mut app.conn.conn, filename);
|
|
||||||
// vec.push(song);
|
|
||||||
// }
|
|
||||||
// println!("{:#?}", vec);
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
||||||
118
src/ui.rs
118
src/ui.rs
|
|
@ -1,3 +1,5 @@
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
use crate::app::{App, SelectedTab};
|
use crate::app::{App, SelectedTab};
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
prelude::*,
|
prelude::*,
|
||||||
|
|
@ -30,7 +32,8 @@ pub fn render(app: &mut App, frame: &mut Frame) {
|
||||||
match app.selected_tab {
|
match app.selected_tab {
|
||||||
SelectedTab::Queue => draw_queue(frame, app, layout[0]),
|
SelectedTab::Queue => draw_queue(frame, app, layout[0]),
|
||||||
SelectedTab::Playlists => draw_playlists(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 {
|
match app.inputmode {
|
||||||
|
|
@ -63,6 +66,7 @@ fn draw_directory_browser(frame: &mut Frame, app: &mut App, size: Rect) {
|
||||||
if status {
|
if status {
|
||||||
list.push(ListItem::new(s.clone().magenta().bold()));
|
list.push(ListItem::new(s.clone().magenta().bold()));
|
||||||
} else {
|
} else {
|
||||||
|
// list.push(ListItem::new(s.clone()));
|
||||||
list.push(ListItem::new(s.clone()));
|
list.push(ListItem::new(s.clone()));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -75,7 +79,7 @@ fn draw_directory_browser(frame: &mut Frame, app: &mut App, size: Rect) {
|
||||||
let list = List::new(list)
|
let list = List::new(list)
|
||||||
.block(
|
.block(
|
||||||
Block::default()
|
Block::default()
|
||||||
.title(format!("File Browser: {}", app.browser.path.clone()).bold())
|
.title(format!("Directory Browser: {}", app.browser.path.clone()).bold())
|
||||||
.title(
|
.title(
|
||||||
Title::from(format!("Total Songs: {}", total_songs).bold().green())
|
Title::from(format!("Total Songs: {}", total_songs).bold().green())
|
||||||
.alignment(Alignment::Center),
|
.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);
|
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::<Vec<String>>()
|
||||||
|
.join("");
|
||||||
|
|
||||||
|
let track = song
|
||||||
|
.tags
|
||||||
|
.iter()
|
||||||
|
.filter(|(x, _)| x == "Track")
|
||||||
|
.map(|(_, l)| l.clone())
|
||||||
|
.collect::<Vec<String>>()
|
||||||
|
.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::<Row>()
|
||||||
|
.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
|
/// draws playing queue
|
||||||
fn draw_queue(frame: &mut Frame, app: &mut App, size: Rect) {
|
fn draw_queue(frame: &mut Frame, app: &mut App, size: Rect) {
|
||||||
let mut queue_state = ListState::default();
|
let mut queue_state = ListState::default();
|
||||||
let title = Block::default()
|
let title = Block::default()
|
||||||
.title(Title::from("Play Queue".green().bold()))
|
.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(
|
||||||
Title::from(format!("Volume: {}%", app.conn.volume).bold().green())
|
Title::from(format!("Volume: {}%", app.conn.volume).bold().green())
|
||||||
.alignment(Alignment::Right),
|
.alignment(Alignment::Right),
|
||||||
|
|
|
||||||
Reference in New Issue