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",
"console",
"lazy_static 1.4.0",
"m3u8-rs",
"muss-interpreter",
"muss-player",
]

View file

@ -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"] }

View file

@ -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.";

View file

@ -57,6 +57,7 @@ mod channel_io;
mod cli;
mod debug_state;
mod help;
mod playlists;
mod repl;
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";
type DebugItem = Result<Item, String>;
struct ReplState {
terminal: Term,
pub struct ReplState {
pub terminal: Term,
line_number: usize,
statement_buf: Vec<char>,
writer: ChannelWriter,
@ -41,7 +41,7 @@ struct ReplState {
cursor_rightward_position: usize,
//debug: Arc<RwLock<DebugState>>,
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)]
@ -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 <playlist file>").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<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")
.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 <float> 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 <float>").expect(TERMINAL_WRITE_ERROR);
writeln!(state.terminal, "Missing ?volume parameter, usage: ?volume number").expect(TERMINAL_WRITE_ERROR);
}
}