Implement REPL command to add current item to a m3u8 playlist
This commit is contained in:
parent
99f853e47d
commit
99a1372e47
6 changed files with 106 additions and 18 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -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",
|
||||||
]
|
]
|
||||||
|
|
|
@ -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"] }
|
||||||
|
|
||||||
|
|
|
@ -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.";
|
||||||
|
|
|
@ -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
89
src/playlists.rs
Normal 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(¤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 <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()
|
||||||
|
}
|
27
src/repl.rs
27
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";
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue