diff --git a/Cargo.lock b/Cargo.lock index ab543dd..ad3c188 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1161,6 +1161,7 @@ dependencies = [ "clap 3.2.25", "console", "lazy_static 1.4.0", + "m3u8-rs", "muss-interpreter", "muss-player", ] diff --git a/Cargo.toml b/Cargo.toml index 1189c9f..1f763c7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,6 +25,9 @@ clap = { version = "3.0", features = ["derive"] } console = { version = "0.15" } lazy_static = { version = "1.4" } +# cli add to playlist functionality +m3u8-rs = { version = "^3.0.0" } + [target.'cfg(not(target_os = "linux"))'.dependencies] muss-player = { version = "0.9.0", path = "./player", default-features = false, features = ["mpd"] } diff --git a/src/help.rs b/src/help.rs index de88e79..0bb897b 100644 --- a/src/help.rs +++ b/src/help.rs @@ -196,5 +196,8 @@ REPL-specific operations to help with writing Muss scripts: ?command pause Immediate song control actions to apply to the current item. + volume number + Set playback volume to number, in percent (starts at 100% when the cli parameter is not used). + verbose Toggle verbose messages, like extra information on items in ?list and other goodies."; diff --git a/src/main.rs b/src/main.rs index 4107492..9619450 100644 --- a/src/main.rs +++ b/src/main.rs @@ -57,6 +57,7 @@ mod channel_io; mod cli; mod debug_state; mod help; +mod playlists; mod repl; use std::io; diff --git a/src/playlists.rs b/src/playlists.rs new file mode 100644 index 0000000..41e0f88 --- /dev/null +++ b/src/playlists.rs @@ -0,0 +1,89 @@ +use std::io::Write; +use std::path::Path; + +use m3u8_rs::{MediaSegment, MediaPlaylist}; + +use muss_interpreter::Item; + +use crate::repl::{TERMINAL_WRITE_ERROR, ReplState}; + +/// Add the currently-playing item to a m3u8 playlist, printing any error to the terminal +pub fn add_to_playlist_cmd(cmd_args: &[&str], state: &mut ReplState) { + if let Some(&file_arg) = cmd_args.get(1) { + let mut playlist = match open_media_playlist(file_arg) { + Ok(playlist) => playlist, + Err(e) => { + writeln!(state.terminal, "Failed to open playlist {}: {}", file_arg, e).expect(TERMINAL_WRITE_ERROR); + return; + } + }; + let current_item = match get_current_item(state) { + Some(item) => item, + None => { + writeln!(state.terminal, "Nothing playing").expect(TERMINAL_WRITE_ERROR); + return; + } + }; + match item_to_media_segment(¤t_item) { + Some(segment) => playlist.segments.push(segment), + None => { + writeln!(state.terminal, "Failed to convert item into playlist media segment").expect(TERMINAL_WRITE_ERROR); + return; + } + } + match save_media_playlist(file_arg, playlist) { + Ok(_) => {}, + Err(e) => { + writeln!(state.terminal, "Failed to save playlist {}: {}", file_arg, e).expect(TERMINAL_WRITE_ERROR); + return; + } + } + } else { + writeln!(state.terminal, "Missing ?add-to parameter, usage: ?add-to ").expect(TERMINAL_WRITE_ERROR); + } +} + +fn open_media_playlist(path: impl AsRef) -> Result { + if path.as_ref().exists() { + let playlist_bytes = std::fs::read(path).map_err(|e| e.to_string())?; + m3u8_rs::parse_media_playlist_res(&playlist_bytes).map_err(|e| e.to_string()) + } else { + Ok(MediaPlaylist::default()) + } +} + +fn save_media_playlist(path: impl AsRef, playlist: MediaPlaylist) -> std::io::Result<()> { + let mut playlist_file = std::io::BufWriter::new(std::fs::File::create(path)?); + playlist.write_to(&mut playlist_file) +} + +fn item_to_media_segment(item: &Item) -> Option { + Some(MediaSegment { + uri: music_filename(item)?, + title: music_title(item), + ..Default::default() + }) +} + +fn music_title(item: &Item) -> Option { + item.field("title").and_then(|x| x.to_owned().to_str()) +} + +fn music_filename(item: &Item) -> Option { + if let Some(filename) = item.field("filename") { + let relative_path = if let Ok(cwd) = std::env::current_dir() { + let cwd: &Path = &cwd; + filename.as_str().replace(cwd.to_str().unwrap_or(""), "./") + } else { + filename.to_string() + }; + Some(relative_path) + } else { + None + } +} + +pub fn get_current_item(state: &mut ReplState) -> Option { + let data = state.controller_debug.read().expect("Failed to get read lock for debug player data"); + data.now_playing.clone() +} diff --git a/src/repl.rs b/src/repl.rs index 3aa1cf0..a42df6f 100644 --- a/src/repl.rs +++ b/src/repl.rs @@ -22,13 +22,13 @@ lazy_static! { }); } -const TERMINAL_WRITE_ERROR: &str = "Failed to write to terminal output"; +pub const TERMINAL_WRITE_ERROR: &str = "Failed to write to terminal output"; const INTERPRETER_WRITE_ERROR: &str = "Failed to write to interpreter"; type DebugItem = Result; -struct ReplState { - terminal: Term, +pub struct ReplState { + pub terminal: Term, line_number: usize, statement_buf: Vec, writer: ChannelWriter, @@ -41,7 +41,7 @@ struct ReplState { cursor_rightward_position: usize, //debug: Arc>, list_rx: Receiver, - controller_debug: std::sync::Arc>, + pub controller_debug: std::sync::Arc>, } #[derive(Clone)] @@ -753,13 +753,12 @@ fn repl_commands(command_str: &str, state: &mut ReplState, args: &CliArgs) { writeln!(state.terminal, "{}", super::help::REPL_COMMANDS).expect(TERMINAL_WRITE_ERROR) } "?now" => { - let data = state.controller_debug.read().expect("Failed to get read lock for debug player data"); - if let Some(item) = &data.now_playing { + if let Some(item) = crate::playlists::get_current_item(state) { let verbose = DEBUG_STATE .read() .expect("Failed to get read lock for debug state") .verbose; - pretty_print_item(item, &mut state.terminal, args, verbose); + pretty_print_item(&item, &mut state.terminal, args, verbose); } else { writeln!(state.terminal, "Nothing playing").expect(TERMINAL_WRITE_ERROR) } @@ -809,19 +808,11 @@ fn repl_commands(command_str: &str, state: &mut ReplState, args: &CliArgs) { .expect("Failed to send control action"); } "?volume" => volume_cmd(&words, state), - "?add-to" => add_to_playlist_cmd(&words, state), + "?add-to" => crate::playlists::add_to_playlist_cmd(&words, state), _ => writeln!(state.terminal, "Unknown command, try ?help").expect(TERMINAL_WRITE_ERROR), } } -fn add_to_playlist_cmd(cmd_args: &[&str], state: &mut ReplState) { - if let Some(file_arg) = cmd_args.get(1) { - writeln!(state.terminal, "Got ?add-to {}, doing nothing (this command is not implemented)", file_arg).expect(TERMINAL_WRITE_ERROR); - } else { - writeln!(state.terminal, "Missing ?add-to parameter, usage: ?add-to ").expect(TERMINAL_WRITE_ERROR); - } -} - fn volume_cmd(cmd_args: &[&str], state: &mut ReplState) { if let Some(volume_arg) = cmd_args.get(1) { let volume_result: Result = volume_arg.parse(); @@ -835,9 +826,9 @@ fn volume_cmd(cmd_args: &[&str], state: &mut ReplState) { .expect("Failed to get lock for control action sender") .send(muss_player::ControlAction::SetVolume { ack: false, volume: (vol * 100.0).round() as _ }) .expect("Failed to send control action"), - Err(e) => writeln!(state.terminal, "Error parsing ?volume parameter: {}", e).expect(TERMINAL_WRITE_ERROR) + Err(e) => writeln!(state.terminal, "Error parsing ?volume number parameter: {}", e).expect(TERMINAL_WRITE_ERROR) } } else { - writeln!(state.terminal, "Missing ?volume parameter, usage: ?volume ").expect(TERMINAL_WRITE_ERROR); + writeln!(state.terminal, "Missing ?volume parameter, usage: ?volume number").expect(TERMINAL_WRITE_ERROR); } }