Files
ming-wm/src/bin/audio_player.rs
stjet 724ffbd494 multi-line copy/paste, more copy/paste
fix C and D in nimbus romono
2025-04-26 05:17:06 +00:00

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) = &current.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());
}