Implement REPL command to add current item to a m3u8 playlist

This commit is contained in:
NGnius (Graham) 2023-08-22 21:05:41 -04:00
parent 99f853e47d
commit 99a1372e47
6 changed files with 106 additions and 18 deletions

1
Cargo.lock generated
View file

@ -1161,6 +1161,7 @@ dependencies = [
"clap 3.2.25", "clap 3.2.25",
"console", "console",
"lazy_static 1.4.0", "lazy_static 1.4.0",
"m3u8-rs",
"muss-interpreter", "muss-interpreter",
"muss-player", "muss-player",
] ]

View file

@ -25,6 +25,9 @@ clap = { version = "3.0", features = ["derive"] }
console = { version = "0.15" } console = { version = "0.15" }
lazy_static = { version = "1.4" } lazy_static = { version = "1.4" }
# cli add to playlist functionality
m3u8-rs = { version = "^3.0.0" }
[target.'cfg(not(target_os = "linux"))'.dependencies] [target.'cfg(not(target_os = "linux"))'.dependencies]
muss-player = { version = "0.9.0", path = "./player", default-features = false, features = ["mpd"] } muss-player = { version = "0.9.0", path = "./player", default-features = false, features = ["mpd"] }

View file

@ -196,5 +196,8 @@ REPL-specific operations to help with writing Muss scripts: ?command
pause pause
Immediate song control actions to apply to the current item. 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 verbose
Toggle verbose messages, like extra information on items in ?list and other goodies."; Toggle verbose messages, like extra information on items in ?list and other goodies.";

View file

@ -57,6 +57,7 @@ mod channel_io;
mod cli; mod cli;
mod debug_state; mod debug_state;
mod help; mod help;
mod playlists;
mod repl; mod repl;
use std::io; use std::io;

89
src/playlists.rs Normal file
View file

@ -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(&current_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 <playlist file>").expect(TERMINAL_WRITE_ERROR);
}
}
fn open_media_playlist(path: impl AsRef<std::path::Path>) -> Result<MediaPlaylist, String> {
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<std::path::Path>, 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<m3u8_rs::MediaSegment> {
Some(MediaSegment {
uri: music_filename(item)?,
title: music_title(item),
..Default::default()
})
}
fn music_title(item: &Item) -> Option<String> {
item.field("title").and_then(|x| x.to_owned().to_str())
}
fn music_filename(item: &Item) -> Option<String> {
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<muss_interpreter::Item> {
let data = state.controller_debug.read().expect("Failed to get read lock for debug player data");
data.now_playing.clone()
}

View file

@ -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"; const INTERPRETER_WRITE_ERROR: &str = "Failed to write to interpreter";
type DebugItem = Result<Item, String>; type DebugItem = Result<Item, String>;
struct ReplState { pub struct ReplState {
terminal: Term, pub terminal: Term,
line_number: usize, line_number: usize,
statement_buf: Vec<char>, statement_buf: Vec<char>,
writer: ChannelWriter, writer: ChannelWriter,
@ -41,7 +41,7 @@ struct ReplState {
cursor_rightward_position: usize, cursor_rightward_position: usize,
//debug: Arc<RwLock<DebugState>>, //debug: Arc<RwLock<DebugState>>,
list_rx: Receiver<DebugItem>, list_rx: Receiver<DebugItem>,
controller_debug: std::sync::Arc<std::sync::RwLock<crate::debug_state::DebugState>>, pub controller_debug: std::sync::Arc<std::sync::RwLock<crate::debug_state::DebugState>>,
} }
#[derive(Clone)] #[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) writeln!(state.terminal, "{}", super::help::REPL_COMMANDS).expect(TERMINAL_WRITE_ERROR)
} }
"?now" => { "?now" => {
let data = state.controller_debug.read().expect("Failed to get read lock for debug player data"); if let Some(item) = crate::playlists::get_current_item(state) {
if let Some(item) = &data.now_playing {
let verbose = DEBUG_STATE let verbose = DEBUG_STATE
.read() .read()
.expect("Failed to get read lock for debug state") .expect("Failed to get read lock for debug state")
.verbose; .verbose;
pretty_print_item(item, &mut state.terminal, args, verbose); pretty_print_item(&item, &mut state.terminal, args, verbose);
} else { } else {
writeln!(state.terminal, "Nothing playing").expect(TERMINAL_WRITE_ERROR) 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"); .expect("Failed to send control action");
} }
"?volume" => volume_cmd(&words, state), "?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), _ => 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 <playlist file>").expect(TERMINAL_WRITE_ERROR);
}
}
fn volume_cmd(cmd_args: &[&str], state: &mut ReplState) { fn volume_cmd(cmd_args: &[&str], state: &mut ReplState) {
if let Some(volume_arg) = cmd_args.get(1) { if let Some(volume_arg) = cmd_args.get(1) {
let volume_result: Result<f32, _> = volume_arg.parse(); let volume_result: Result<f32, _> = 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") .expect("Failed to get lock for control action sender")
.send(muss_player::ControlAction::SetVolume { ack: false, volume: (vol * 100.0).round() as _ }) .send(muss_player::ControlAction::SetVolume { ack: false, volume: (vol * 100.0).round() as _ })
.expect("Failed to send control action"), .expect("Failed to send control action"),
Err(e) => writeln!(state.terminal, "Error parsing ?volume <float> parameter: {}", e).expect(TERMINAL_WRITE_ERROR) Err(e) => writeln!(state.terminal, "Error parsing ?volume number parameter: {}", e).expect(TERMINAL_WRITE_ERROR)
} }
} else { } else {
writeln!(state.terminal, "Missing ?volume parameter, usage: ?volume <float>").expect(TERMINAL_WRITE_ERROR); writeln!(state.terminal, "Missing ?volume parameter, usage: ?volume number").expect(TERMINAL_WRITE_ERROR);
} }
} }