diff --git a/Cargo.lock b/Cargo.lock index 4658523..c2e54be 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1262,6 +1262,7 @@ version = "0.8.0" dependencies = [ "fluent-uri", "m3u8-rs", + "mpd", "mpris-player", "mps-interpreter", "rodio", diff --git a/Cargo.toml b/Cargo.toml index c7e4fe7..2b2531c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,7 +29,7 @@ mps-player = { version = "0.8.0", path = "./mps-player", default-features = fals [target.'cfg(target_os = "linux")'.dependencies] # TODO fix need to specify OS-specific dependency of mps-player -mps-player = { version = "0.8.0", path = "./mps-player", features = ["mpris-player"] } +mps-player = { version = "0.8.0", path = "./mps-player", features = ["mpris-player", "mpd"] } [profile.release] debug = false diff --git a/mps-interpreter/src/processing/mpd.rs b/mps-interpreter/src/processing/mpd.rs index 1a40e4d..3bc1bfb 100644 --- a/mps-interpreter/src/processing/mpd.rs +++ b/mps-interpreter/src/processing/mpd.rs @@ -89,7 +89,7 @@ fn song_to_item(song: Song) -> MpsItem { */ for (tag, value) in song.tags { - item.set_field(&tag, MpsTypePrimitive::parse(value)); + item.set_field(&tag.to_lowercase(), MpsTypePrimitive::parse(value)); } item } diff --git a/mps-player/Cargo.toml b/mps-player/Cargo.toml index 4d9e42b..1ede60a 100644 --- a/mps-player/Cargo.toml +++ b/mps-player/Cargo.toml @@ -9,6 +9,7 @@ readme = "README.md" rodio = { version = "^0.15", features = ["symphonia-all"]} m3u8-rs = { version = "^3.0" } fluent-uri = { version = "^0.1" } +mpd = { version = "0.0.12", optional = true } # local mps-interpreter = { path = "../mps-interpreter", version = "0.8.0" } @@ -18,7 +19,7 @@ mps-interpreter = { path = "../mps-interpreter", version = "0.8.0" } mpris-player = { version = "^0.6", path = "../mpris-player", optional = true } [features] -default = ["os-controls"] +default = ["os-controls", "mpd"] os-controls = [] # I wish this worked... diff --git a/mps-player/src/errors.rs b/mps-player/src/errors.rs index 920facb..08a29ab 100644 --- a/mps-player/src/errors.rs +++ b/mps-player/src/errors.rs @@ -5,6 +5,8 @@ use std::convert::Into; pub enum PlayerError { Playback(PlaybackError), Uri(UriError), + #[cfg(feature = "mpd")] + Mpd(String), } impl PlayerError { @@ -15,6 +17,11 @@ impl PlayerError { /*pub(crate) fn from_err_uri(err: E) -> Self { Self::Uri(UriError::from_err(err)) }*/ + + #[cfg(feature = "mpd")] + pub(crate) fn from_err_mpd(err: E) -> Self { + Self::Mpd(format!("{}", err)) + } } impl Display for PlayerError { @@ -22,6 +29,8 @@ impl Display for PlayerError { match self { Self::Playback(p) => (p as &dyn Display).fmt(f), Self::Uri(u) => (u as &dyn Display).fmt(f), + #[cfg(feature = "mpd")] + Self::Mpd(m) => (m as &dyn Display).fmt(f), } } } diff --git a/mps-player/src/player.rs b/mps-player/src/player.rs index 2939ec4..5a33f94 100644 --- a/mps-player/src/player.rs +++ b/mps-player/src/player.rs @@ -5,6 +5,9 @@ use rodio::{decoder::Decoder, OutputStream, OutputStreamHandle, Sink}; use m3u8_rs::{MediaPlaylist, MediaSegment}; +#[cfg(feature = "mpd")] +use mpd::{Client, Song}; + use super::uri::Uri; use mps_interpreter::{tokens::MpsTokenReader, MpsFaye, MpsItem}; @@ -21,6 +24,8 @@ pub struct MpsPlayer<'a, T: MpsTokenReader + 'a> { #[allow(dead_code)] output_stream: OutputStream, // this is required for playback, so it must live as long as this struct instance output_handle: OutputStreamHandle, + #[cfg(feature = "mpd")] + mpd_connection: Option>, } impl<'a, T: MpsTokenReader + 'a> MpsPlayer<'a, T> { @@ -32,9 +37,17 @@ impl<'a, T: MpsTokenReader + 'a> MpsPlayer<'a, T> { sink: Sink::try_new(&output_handle).map_err(PlayerError::from_err_playback)?, output_stream: stream, output_handle: output_handle, + #[cfg(feature = "mpd")] + mpd_connection: None, }) } + #[cfg(feature = "mpd")] + pub fn connect_mpd(&mut self, addr: std::net::SocketAddr) -> Result<(), PlayerError> { + self.mpd_connection = Some(Client::connect(addr).map_err(PlayerError::from_err_mpd)?); + Ok(()) + } + pub fn play_all(&mut self) -> Result<(), PlayerError> { while let Some(item) = self.runner.next() { self.sink.sleep_until_end(); @@ -149,7 +162,7 @@ impl<'a, T: MpsTokenReader + 'a> MpsPlayer<'a, T> { if let Some(filename) = music_filename(&music) { - println!("Adding file `{}` to playlist", filename); + //println!("Adding file `{}` to playlist", filename); playlist.segments.push(MediaSegment { uri: filename, title: music_title(&music), @@ -201,7 +214,20 @@ impl<'a, T: MpsTokenReader + 'a> MpsPlayer<'a, T> { self.sink.append(source); Ok(()) }, - //TODO "mpd:" => {}, + #[cfg(feature = "mpd")] + "mpd:" => { + if let Some(mpd_client) = &mut self.mpd_connection { + //println!("Pushing {} into MPD queue", uri.path()); + let song = Song { + file: uri.path().to_owned(), + ..Default::default() + }; + mpd_client.push(song).map_err(PlayerError::from_err_playback)?; + Ok(()) + } else { + Err(PlayerError::from_err_playback("Cannot play MPD song: no MPD client connected")) + } + }, scheme => Err(UriError::Unsupported(scheme.to_owned()).into()) }, None => { diff --git a/mps-player/src/uri.rs b/mps-player/src/uri.rs index 22cf5f1..612de22 100644 --- a/mps-player/src/uri.rs +++ b/mps-player/src/uri.rs @@ -26,4 +26,9 @@ impl<'a> Uri<&'a str> { None => self.0 } } + + #[allow(dead_code)] + pub fn uri(&self) -> &'a str { + self.0 + } } diff --git a/src/cli.rs b/src/cli.rs index ac2d3a3..fd5a701 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -22,8 +22,19 @@ pub struct CliArgs { /// The volume at which to playback audio, out of 1.0 #[clap(long)] pub volume: Option, + + /// MPD server for music playback + #[clap(short, long)] + pub mpd: Option, } pub fn parse() -> CliArgs { CliArgs::parse() } + +pub fn validate(args: &CliArgs) -> Result<(), String> { + if let Some(mpd_addr) = &args.mpd { + let _: std::net::SocketAddr = mpd_addr.parse().map_err(|e| format!("Unrecognized MPS address `{}`: {}", mpd_addr, e))?; + } + Ok(()) +} diff --git a/src/main.rs b/src/main.rs index 1bc3de3..082fd2d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -67,6 +67,10 @@ fn play_cursor() -> Result<(), PlayerError> { fn main() { let args = cli::parse(); + if let Err(e) = cli::validate(&args) { + eprintln!("{}", e); + return; + } if let Some(script_file) = &args.file { // interpret script @@ -77,6 +81,7 @@ fn main() { // build playback controller let script_file2 = script_file.clone(); let volume = args.volume.clone(); + let mpd = args.mpd.clone(); let player_builder = move || { let script_reader = io::BufReader::new( std::fs::File::open(&script_file2) @@ -84,10 +89,13 @@ fn main() { ); let runner = MpsFaye::with_stream(script_reader); - let player = MpsPlayer::new(runner).unwrap(); + let mut player = MpsPlayer::new(runner).unwrap(); if let Some(vol) = volume { player.set_volume(vol); } + if let Some(mpd) = mpd { + player.connect_mpd(mpd.parse().unwrap()).unwrap(); + } player }; if let Some(playlist_file) = &args.playlist { diff --git a/src/repl.rs b/src/repl.rs index 9e6f1af..bc55e89 100644 --- a/src/repl.rs +++ b/src/repl.rs @@ -47,23 +47,36 @@ pub fn repl(args: CliArgs) { term.set_title("mps"); let (writer, reader) = channel_io(); let volume = args.volume.clone(); + let mpd = args.mpd.clone(); let player_builder = move || { let runner = MpsFaye::with_stream(reader); - let player = MpsPlayer::new(runner).unwrap(); + let mut player = MpsPlayer::new(runner).unwrap(); if let Some(vol) = volume { player.set_volume(vol); } + if let Some(mpd) = mpd { + player.connect_mpd(mpd.parse().unwrap()).unwrap(); + } player }; let mut state = ReplState::new(writer, term); if let Some(playlist_file) = &args.playlist { - writeln!( - state.terminal, - "Playlist mode (output: `{}`)", - playlist_file - ) - .expect("Failed to write to terminal output"); + if args.mpd.is_some() { + writeln!( + state.terminal, + "Playlist mode (output: `{}` & MPD)", + playlist_file + ) + .expect("Failed to write to terminal output"); + } else { + writeln!( + state.terminal, + "Playlist mode (output: `{}`)", + playlist_file + ) + .expect("Failed to write to terminal output"); + } let mut player = player_builder(); let mut playlist_writer = io::BufWriter::new(std::fs::File::create(playlist_file).unwrap_or_else(|_| { @@ -85,8 +98,13 @@ pub fn repl(args: CliArgs) { .expect("Failed to flush playlist to file"); }); } else { - writeln!(state.terminal, "Playback mode (output: audio device)") - .expect("Failed to write to terminal output"); + if args.mpd.is_some() { + writeln!(state.terminal, "Playback mode (output: audio device & MPD)") + .expect("Failed to write to terminal output"); + } else { + writeln!(state.terminal, "Playback mode (output: audio device)") + .expect("Failed to write to terminal output"); + } let ctrl = MpsController::create_repl(player_builder); read_loop(&args, &mut state, || { if args.wait {