//! Melody - Music Player // IDEA: Iterm display for album cover? (probably not) // TODO: Media Controls #[macro_use] extern crate clap; extern crate directories; #[macro_use] extern crate human_panic; extern crate indicatif; extern crate melody; extern crate rodio; use std::{thread, time::Duration}; use clap::{App, Arg}; use indicatif::{ProgressBar, ProgressStyle}; use melody::*; mod config; mod input_handler; use crate::config::Settings; #[derive(Debug)] pub enum Errors { FailedToGetAppDirectory, FailedToGetUserDir, FailedToGetAudioDir, FailedToGetConfig, FailedToCreatePlaylist, FailedToStartMusicPlayer, } fn generate_progress_bar(s: Song) -> ProgressBar { let pb = ProgressBar::new(s.duration.as_secs()); pb.set_style( ProgressStyle::default_bar() .template( "{spinner:.green} {msg} [{elapsed_precise}] [{bar:40.cyan/blue}] ({eta_precise})", ) .progress_chars("#>-"), ); pb.set_message(&format!( "{} - {} - {}", s.artist.unwrap_or_else(|| String::from("Unknown Artist")), s.album.unwrap_or_else(|| String::from("Unknown Album")), s.title.unwrap_or_else(|| String::from("Unknown Title")) )); pb } fn create_filter(settings: Settings) -> impl Fn(Song) -> Option { move |song| { let ig_all = settings.ignore_all_unknowns; if (settings.ignore_unknown_album || ig_all) & song.album.is_none() { return None; } if (settings.ignore_unknown_title || ig_all) & song.title.is_none() { return None; } if (settings.ignore_unknown_artist || ig_all) & song.artist.is_none() { return None; } Some(song) } } fn main() { setup_panic!(); let matches = App::new("melody") .version(crate_version!()) .author(crate_authors!()) .about("Terminal Music player, written in rust.") .args(&[ Arg::with_name("minimal") .short("min") .long("minimal") .case_insensitive(true) .help("Runs Melody in minimal mode. Disables Queue preview and shuffle."), Arg::with_name("unknown-artist") .long("ignore-unknown-artist") .help("Ignores unknown artists"), Arg::with_name("unknown-album") .long("ignore-unknown-album") .help("Ignores Unknown albums"), Arg::with_name("unknown-title") .long("ignore-unknown-title") .help("Ignores Unknown title"), Arg::with_name("unknown") .long("ignore-unknown") .help("Ignore all unknowns"), Arg::with_name("volume") .long("volume") .short("vol") .takes_value(true) .help("Sets volume 0.5 = 50%"), Arg::with_name("path") .long("path") .short("p") .takes_value(true) .help("Music directory you wish to listen from."), ]) .get_matches(); let mut config = Settings::new().expect("Failed to get config"); if matches.is_present("unknown") { config.ignore_all_unknowns = true; } else { if matches.is_present("unknown-artist") { config.ignore_unknown_artist = true; } if matches.is_present("unknown-album") { config.ignore_unknown_album = true; } if matches.is_present("unknown-title") { config.ignore_unknown_title = true; } }; if let Some(v) = matches.value_of("volume") { match v.parse() { Ok(v) => { config.volume = v; } Err(_) => return eprintln!("Not a valid value for Volume"), } } if let Some(v) = matches.value_of("path") { config.music = std::path::PathBuf::from(v); }; let _raw = ::crossterm::RawScreen::into_raw_mode(); let crossterm = ::crossterm::Crossterm::new(); let _ = crossterm.cursor().hide().is_ok(); if matches.is_present("minimal") { minimal_player(config); } else { let _ = crossterm .terminal() .clear(::crossterm::ClearType::All) .is_ok(); standard_player(config).expect("Music player threw error") } } fn standard_player(config: Settings) -> Result<(), Errors> { let mut stdin = ::crossterm::input().read_async(); let mut playlist = if config.prioritize_cwd { ::std::env::current_dir() .ok() .and_then(Playlist::from_dir) .and_then(|pl| if pl.is_empty() { None } else { Some(pl) }) .or_else(|| Playlist::from_dir(config.music.clone())) .ok_or(Errors::FailedToCreatePlaylist) } else { Playlist::from_dir(config.music.clone()) .and_then(|pl| if pl.is_empty() { None } else { Some(pl) }) .or_else(|| ::std::env::current_dir().ok().and_then(Playlist::from_dir)) .ok_or(Errors::FailedToCreatePlaylist) }?; let vol = config.volume; playlist.filter_map(create_filter(config)); let mut mp = MusicPlayer::new(playlist); mp.set_volume(vol); mp.shuffle(); let mut pb = ProgressBar::hidden(); let mut paused = false; loop { if let Some(action) = input_handler::read_async(&mut stdin) { match action { self::input_handler::Action::Quit => { mp.quit(); pb.finish_and_clear(); break; } self::input_handler::Action::PlayPause => { if paused { mp.resume(); paused = false; } else { mp.pause(); paused = true } } self::input_handler::Action::Skip => { mp.stop(); } self::input_handler::Action::VolumeUp => { mp.set_volume(mp.volume() + 0.01); } self::input_handler::Action::VolumeDown => { let vol = mp.volume(); if vol > 0.0 { mp.set_volume(vol - 0.01); } } self::input_handler::Action::Shuffle => { mp.shuffle(); draw(gen_text(mp.to_string())); } _ => (), //self::input_handler::Action::Rewind } } match mp.status() { MusicPlayerStatus::NowPlaying(song) => { pb.set_position(song.elapsed().as_secs()); } MusicPlayerStatus::Stopped(_) => { if mp.queue().is_empty() { break; } else { mp.play_next(); draw(gen_text(mp.to_string())); pb = match mp.status() { MusicPlayerStatus::NowPlaying(song) => generate_progress_bar(song), _ => break, }; } } _ => (), } thread::sleep(Duration::from_millis(250)) } println!("\r\nGoodbye!"); Ok(()) } fn playlist(path: &std::path::Path) -> impl Iterator { utils::list_supported_files(path) .map(Song::load) .filter_map(::std::result::Result::ok) } fn minimal_player(config: Settings) { let mut stdin = ::crossterm::input().read_async(); let path = config.music.as_path().to_path_buf(); let vol = config.volume; let mut mp = melody::MinimalMusicPlayer::consume(playlist(&path).filter_map(create_filter(config))); mp.set_volume(vol); loop { let pb = if let Some(song) = mp.next() { generate_progress_bar(song.clone()) } else { break; }; pb.enable_steady_tick(250); let mut paused = false; while mp.is_playing() { if let Some(action) = input_handler::read_async(&mut stdin) { match action { input_handler::Action::Quit => { mp.stop(); return; } input_handler::Action::PlayPause => { if paused { mp.resume(); pb.enable_steady_tick(250); paused = false; } else { mp.pause(); pb.disable_steady_tick(); paused = true; } } input_handler::Action::VolumeDown => { let volume = mp.volume(); if volume > 0.0 { mp.set_volume(volume) } } input_handler::Action::BiggerVolumeDown => { let volume = mp.volume(); if volume > 0.0 { if volume <= 0.05 { mp.set_volume(0.0) } else { mp.set_volume(volume - 0.05); } } } input_handler::Action::VolumeUp => mp.set_volume(mp.volume() + 0.01), input_handler::Action::BiggerVolumeUp => mp.set_volume(mp.volume() + 0.05), input_handler::Action::Skip => { //println!("\r\nSkipping"); //::std::thread::sleep(::std::time::Duration::from_secs(10)); mp.stop(); mp.sleep_until_end() } _ => (), } } } } } fn gen_text(playlist_str: String) -> String { let terminal = ::crossterm::terminal(); let lines = playlist_str.lines().skip(1); let line_count = lines.count(); let lines = playlist_str .lines() .skip(1) .map(|l| l.trim().to_string() + "\n"); let height = (terminal.terminal_size().1 as usize) - 4usize; if line_count == height { let txt: String = lines.collect(); txt } else if line_count < height { let mut txt: String = lines.collect(); for _ in 0..(height - line_count) { txt += "\n"; } txt } else { let remianing = (line_count - 1) - height; let text: String = lines.take(height).collect(); format!( "\r{}\n[Pause: p Skip: n Prev: b VolUp: > VolDown: < Quit: q]\n(... {} others ...)\n\n", text.trim(), remianing ) } } fn draw(text: String) { let term = ::crossterm::terminal(); for line in text.lines() { let _ = term.write(String::from("\r\n") + line).is_ok(); } let _ = term.write("\r\n").is_ok(); }