296 lines
11 KiB
Rust
296 lines
11 KiB
Rust
use std::vec::Vec;
|
|
use std::vec;
|
|
use std::io::BufReader;
|
|
use std::path::PathBuf;
|
|
use std::fs::{ read_to_string, File };
|
|
use std::time::{ Duration, SystemTime, UNIX_EPOCH };
|
|
use std::thread;
|
|
use std::sync::{ Arc, Mutex };
|
|
|
|
use rodio::{ Decoder, OutputStream, Sink, Source };
|
|
use rand::{ SeedableRng, prelude::SliceRandom, rngs::SmallRng };
|
|
use id3::TagLike;
|
|
use mp4ameta;
|
|
use metaflac;
|
|
|
|
use ming_wm_lib::window_manager_types::{ DrawInstructions, WindowLike, WindowLikeType };
|
|
use ming_wm_lib::messages::{ WindowMessage, WindowMessageResponse, WindowManagerRequest, ShortcutType };
|
|
use ming_wm_lib::framebuffer_types::Dimensions;
|
|
use ming_wm_lib::themes::ThemeInfo;
|
|
use ming_wm_lib::utils::{ concat_paths, get_all_files, path_autocomplete, format_seconds, Substring };
|
|
use ming_wm_lib::dirs::home;
|
|
use ming_wm_lib::ipc::listen;
|
|
|
|
fn get_artist(path: &PathBuf) -> Option<String> {
|
|
let ext = path.extension().unwrap();
|
|
if ext == "mp4" {
|
|
let tag = mp4ameta::Tag::read_from_path(path).unwrap();
|
|
tag.artist().map(|s| s.to_string())
|
|
} else if ext == "flac" {
|
|
let tag = metaflac::Tag::read_from_path(path).unwrap();
|
|
let x = if let Some(mut artists) = tag.get_vorbis("Artist") {
|
|
Some(artists.next().unwrap().to_string()) //get the first one
|
|
} else {
|
|
None
|
|
};
|
|
x
|
|
} else if ext == "mp3" {
|
|
let tag = id3::Tag::read_from_path(path).unwrap();
|
|
tag.artist().map(|s| s.to_string())
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
const MONO_WIDTH: u8 = 10;
|
|
const LINE_HEIGHT: usize = 18;
|
|
|
|
type QueueItem = (PathBuf, u64, Option<String>);
|
|
|
|
struct InternalPlayer {
|
|
pub queue: Vec<QueueItem>,
|
|
pub sink: Sink,
|
|
}
|
|
|
|
impl InternalPlayer {
|
|
fn add(internal: Arc<Mutex<InternalPlayer>>, queue: Vec<PathBuf>) {
|
|
thread::spawn(move || {
|
|
for item in queue {
|
|
let file = BufReader::new(File::open(&item).unwrap());
|
|
//slightly faster for mp3s? since it doesn't need to check if it is .wav, etc. but maybe not
|
|
let decoded = if item.ends_with(".mp3") { Decoder::new_mp3(file) } else { Decoder::new(file) }.unwrap();
|
|
let mut internal_locked = internal.lock().unwrap();
|
|
(*internal_locked).queue.push((item.clone(), decoded.total_duration().unwrap().as_secs(), get_artist(&item)));
|
|
(*internal_locked).sink.append(decoded);
|
|
(*internal_locked).sink.play();
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
pub struct AudioPlayer {
|
|
dimensions: Dimensions,
|
|
base_directory: String,
|
|
_stream: Box<OutputStream>,
|
|
internal: Arc<Mutex<InternalPlayer>>,
|
|
command: String,
|
|
response: String,
|
|
}
|
|
|
|
impl WindowLike for AudioPlayer {
|
|
fn handle_message(&mut self, message: WindowMessage) -> WindowMessageResponse {
|
|
match message {
|
|
WindowMessage::Init(dimensions) => {
|
|
self.dimensions = dimensions;
|
|
WindowMessageResponse::JustRedraw
|
|
},
|
|
WindowMessage::ChangeDimensions(dimensions) => {
|
|
self.dimensions = dimensions;
|
|
WindowMessageResponse::JustRedraw
|
|
},
|
|
WindowMessage::KeyPress(key_press) => {
|
|
if key_press.key == '𐘂' { //the enter key
|
|
self.response = self.process_command();
|
|
self.command = String::new();
|
|
} else if key_press.key == '𐘁' { //backspace
|
|
if self.command.len() > 0 {
|
|
self.command = self.command.remove_last();
|
|
}
|
|
} else if key_press.key == '\t' { //tab
|
|
let mut parts = self.command.split(" ");
|
|
let parts_len = parts.clone().count();
|
|
if parts_len == 2 {
|
|
if let Some(add) = path_autocomplete(&self.base_directory, parts.nth(1).unwrap()) {
|
|
self.command += &add;
|
|
} else {
|
|
return WindowMessageResponse::DoNothing;
|
|
}
|
|
} else {
|
|
return WindowMessageResponse::DoNothing;
|
|
}
|
|
} else if key_press.is_regular() {
|
|
self.command += &key_press.key.to_string();
|
|
} else {
|
|
return WindowMessageResponse::DoNothing
|
|
}
|
|
WindowMessageResponse::JustRedraw
|
|
},
|
|
WindowMessage::Shortcut(shortcut) => {
|
|
match shortcut {
|
|
ShortcutType::ClipboardPaste(paste_string) => {
|
|
self.command += &paste_string.replace("\n", "");
|
|
WindowMessageResponse::JustRedraw
|
|
},
|
|
ShortcutType::ClipboardCopy => {
|
|
let internal_locked = self.internal.lock().unwrap();
|
|
let sink_len = internal_locked.sink.len();
|
|
if sink_len > 0 {
|
|
let queue = &internal_locked.queue;
|
|
let current = &queue[queue.len() - sink_len];
|
|
let current_name = current.0.file_name().unwrap().to_string_lossy().into_owned();
|
|
WindowMessageResponse::Request(WindowManagerRequest::ClipboardCopy(current_name))
|
|
} else {
|
|
WindowMessageResponse::DoNothing
|
|
}
|
|
},
|
|
_ => WindowMessageResponse::DoNothing,
|
|
}
|
|
},
|
|
_ => {
|
|
WindowMessageResponse::DoNothing
|
|
},
|
|
}
|
|
}
|
|
|
|
fn draw(&self, theme_info: &ThemeInfo) -> Vec<DrawInstructions> {
|
|
let mut instructions = vec![DrawInstructions::Text([2, self.dimensions[1] - LINE_HEIGHT], vec!["nimbus-roman".to_string()], if self.command.len() > 0 { self.command.clone() } else { self.response.clone() }, theme_info.text, theme_info.background, None, None)];
|
|
let internal_locked = self.internal.lock().unwrap();
|
|
let sink_len = internal_locked.sink.len();
|
|
if sink_len > 0 {
|
|
let queue = &internal_locked.queue;
|
|
let current = &queue[queue.len() - sink_len];
|
|
let current_name = current.0.file_name().unwrap().to_string_lossy().into_owned();
|
|
instructions.push(DrawInstructions::Text([self.dimensions[0] / 2 - current_name.len() * MONO_WIDTH as usize / 2, 2], vec!["nimbus-romono".to_string(), "shippori-mincho".to_string()], current_name.clone(), theme_info.text, theme_info.background, Some(0), Some(MONO_WIDTH)));
|
|
if let Some(artist) = ¤t.2 {
|
|
let artist_string = "by ".to_string() + &artist;
|
|
instructions.push(DrawInstructions::Text([self.dimensions[0] / 2 - artist_string.len() * MONO_WIDTH as usize / 2, LINE_HEIGHT + 2], vec!["nimbus-romono".to_string()], artist_string, theme_info.text, theme_info.background, Some(0), Some(MONO_WIDTH)));
|
|
}
|
|
let time_string = format!("{}/{}", format_seconds(internal_locked.sink.get_pos().as_secs()), format_seconds(current.1));
|
|
instructions.push(DrawInstructions::Text([self.dimensions[0] / 2 - time_string.len() * MONO_WIDTH as usize / 2, LINE_HEIGHT * 2 + 2], vec!["nimbus-romono".to_string()], time_string, theme_info.text, theme_info.background, Some(0), Some(MONO_WIDTH)));
|
|
} else {
|
|
instructions.push(DrawInstructions::Text([2, 2], vec!["nimbus-roman".to_string()], "type to write commands, enter to execute.".to_string(), theme_info.text, theme_info.background, None, None));
|
|
instructions.push(DrawInstructions::Text([2, 2 + LINE_HEIGHT], vec!["nimbus-roman".to_string()], "See help in start menu for commands.".to_string(), theme_info.text, theme_info.background, None, None));
|
|
}
|
|
//
|
|
instructions
|
|
}
|
|
|
|
//properties
|
|
|
|
fn title(&self) -> String {
|
|
"Audio Player".to_string()
|
|
}
|
|
|
|
fn subtype(&self) -> WindowLikeType {
|
|
WindowLikeType::Window
|
|
}
|
|
|
|
fn ideal_dimensions(&self, _dimensions: Dimensions) -> Dimensions {
|
|
[500, 200]
|
|
}
|
|
|
|
fn resizable(&self) -> bool {
|
|
true
|
|
}
|
|
}
|
|
|
|
impl AudioPlayer {
|
|
pub fn new() -> Self {
|
|
let (stream, stream_handle) = OutputStream::try_default().unwrap();
|
|
Self {
|
|
dimensions: Default::default(),
|
|
base_directory: home().unwrap_or(PathBuf::from("/")).to_string_lossy().to_string(),
|
|
_stream: Box::new(stream),
|
|
internal: Arc::new(Mutex::new(InternalPlayer {
|
|
queue: Vec::new(),
|
|
sink: Sink::try_new(&stream_handle).unwrap(),
|
|
})),
|
|
command: Default::default(),
|
|
response: Default::default(),
|
|
}
|
|
}
|
|
|
|
//t: toggle pause/play
|
|
//l: next/skip
|
|
//j: volume down
|
|
//k: volume up
|
|
//b <dir>: set base directory
|
|
//p <dir>/<playlist file>: play directory or playlist in random order
|
|
//a <dir>/<playlist file>: same as p but appends to queue instead of clearing
|
|
//just hit enter or any key to refresh
|
|
fn process_command(&mut self) -> String {
|
|
if self.command.len() == 1 {
|
|
let sink = &(*self.internal.lock().unwrap()).sink;
|
|
if self.command == "t" {
|
|
if sink.is_paused() {
|
|
sink.play();
|
|
return "Resumed".to_string();
|
|
} else {
|
|
sink.pause();
|
|
return "Paused".to_string();
|
|
}
|
|
} else if self.command == "l" {
|
|
sink.skip_one();
|
|
return "Skipped".to_string();
|
|
} else if self.command == "j" {
|
|
sink.set_volume(sink.volume() - 0.1);
|
|
return "Volume decreased".to_string();
|
|
} else if self.command == "k" {
|
|
sink.set_volume(sink.volume() + 0.1);
|
|
return "Volume increased".to_string();
|
|
}
|
|
} else {
|
|
let parts: Vec<&str> = self.command.split(" ").collect();
|
|
if self.command.starts_with("p ") || self.command.starts_with("a ") {
|
|
if parts.len() == 2 {
|
|
if let Ok(new_path) = concat_paths(&self.base_directory, parts[1]) {
|
|
if new_path.exists() {
|
|
let mut queue = if parts[1].ends_with(".playlist") {
|
|
let mut queue = Vec::new();
|
|
let contents = read_to_string(new_path).unwrap();
|
|
for line in contents.split("\n") {
|
|
//todo: handle more edge cases later
|
|
if line.ends_with("/*") {
|
|
queue.extend(get_all_files(concat_paths(&self.base_directory, &line[..line.len() - 2]).unwrap()));
|
|
} else if line.len() > 0 {
|
|
//if no file ext, assumes mp3
|
|
queue.push(concat_paths(&self.base_directory, &(line.to_owned() + if line.contains(".") { "" } else { ".mp3" })).unwrap());
|
|
}
|
|
}
|
|
queue
|
|
} else if parts[1].ends_with(".mp3") {
|
|
vec![concat_paths(&self.base_directory, parts[1]).unwrap()]
|
|
} else {
|
|
get_all_files(PathBuf::from(new_path))
|
|
};
|
|
let mut rng = SmallRng::seed_from_u64(SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs());
|
|
queue.shuffle(&mut rng);
|
|
if self.command.starts_with("p ") {
|
|
let mut locked_internal = self.internal.lock().unwrap();
|
|
(*locked_internal).sink.clear();
|
|
(*locked_internal).queue = Vec::new();
|
|
}
|
|
InternalPlayer::add(Arc::clone(&self.internal), queue);
|
|
//to hopefully allow the first file to be loaded so info displays
|
|
thread::sleep(Duration::from_millis(10));
|
|
return if self.command.starts_with("p ") {
|
|
"Playing".to_string()
|
|
} else {
|
|
"Appended".to_string()
|
|
};
|
|
}
|
|
}
|
|
}
|
|
} else if self.command.starts_with("b ") {
|
|
if parts.len() == 2 {
|
|
let new_path = if parts[1].starts_with("/") { Ok(PathBuf::from(parts[1])) } else { concat_paths(&self.base_directory, parts[1]) };
|
|
if let Ok(new_path) = new_path {
|
|
if new_path.exists() {
|
|
self.base_directory = new_path.to_str().unwrap().to_string();
|
|
return "Set new base directory".to_string();
|
|
} else {
|
|
return "Failed to set new base directory".to_string();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
String::new()
|
|
}
|
|
}
|
|
|
|
pub fn main() {
|
|
listen(AudioPlayer::new());
|
|
}
|
|
|