Add MPD support to front-end playback
This commit is contained in:
parent
f7e72cd96c
commit
fe7962b229
10 changed files with 94 additions and 15 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -1262,6 +1262,7 @@ version = "0.8.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"fluent-uri",
|
"fluent-uri",
|
||||||
"m3u8-rs",
|
"m3u8-rs",
|
||||||
|
"mpd",
|
||||||
"mpris-player",
|
"mpris-player",
|
||||||
"mps-interpreter",
|
"mps-interpreter",
|
||||||
"rodio",
|
"rodio",
|
||||||
|
|
|
@ -29,7 +29,7 @@ mps-player = { version = "0.8.0", path = "./mps-player", default-features = fals
|
||||||
|
|
||||||
[target.'cfg(target_os = "linux")'.dependencies]
|
[target.'cfg(target_os = "linux")'.dependencies]
|
||||||
# TODO fix need to specify OS-specific dependency of mps-player
|
# 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]
|
[profile.release]
|
||||||
debug = false
|
debug = false
|
||||||
|
|
|
@ -89,7 +89,7 @@ fn song_to_item(song: Song) -> MpsItem {
|
||||||
*/
|
*/
|
||||||
|
|
||||||
for (tag, value) in song.tags {
|
for (tag, value) in song.tags {
|
||||||
item.set_field(&tag, MpsTypePrimitive::parse(value));
|
item.set_field(&tag.to_lowercase(), MpsTypePrimitive::parse(value));
|
||||||
}
|
}
|
||||||
item
|
item
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,7 @@ readme = "README.md"
|
||||||
rodio = { version = "^0.15", features = ["symphonia-all"]}
|
rodio = { version = "^0.15", features = ["symphonia-all"]}
|
||||||
m3u8-rs = { version = "^3.0" }
|
m3u8-rs = { version = "^3.0" }
|
||||||
fluent-uri = { version = "^0.1" }
|
fluent-uri = { version = "^0.1" }
|
||||||
|
mpd = { version = "0.0.12", optional = true }
|
||||||
|
|
||||||
# local
|
# local
|
||||||
mps-interpreter = { path = "../mps-interpreter", version = "0.8.0" }
|
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 }
|
mpris-player = { version = "^0.6", path = "../mpris-player", optional = true }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["os-controls"]
|
default = ["os-controls", "mpd"]
|
||||||
os-controls = []
|
os-controls = []
|
||||||
|
|
||||||
# I wish this worked...
|
# I wish this worked...
|
||||||
|
|
|
@ -5,6 +5,8 @@ use std::convert::Into;
|
||||||
pub enum PlayerError {
|
pub enum PlayerError {
|
||||||
Playback(PlaybackError),
|
Playback(PlaybackError),
|
||||||
Uri(UriError),
|
Uri(UriError),
|
||||||
|
#[cfg(feature = "mpd")]
|
||||||
|
Mpd(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PlayerError {
|
impl PlayerError {
|
||||||
|
@ -15,6 +17,11 @@ impl PlayerError {
|
||||||
/*pub(crate) fn from_err_uri<E: Display>(err: E) -> Self {
|
/*pub(crate) fn from_err_uri<E: Display>(err: E) -> Self {
|
||||||
Self::Uri(UriError::from_err(err))
|
Self::Uri(UriError::from_err(err))
|
||||||
}*/
|
}*/
|
||||||
|
|
||||||
|
#[cfg(feature = "mpd")]
|
||||||
|
pub(crate) fn from_err_mpd<E: Display>(err: E) -> Self {
|
||||||
|
Self::Mpd(format!("{}", err))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for PlayerError {
|
impl Display for PlayerError {
|
||||||
|
@ -22,6 +29,8 @@ impl Display for PlayerError {
|
||||||
match self {
|
match self {
|
||||||
Self::Playback(p) => (p as &dyn Display).fmt(f),
|
Self::Playback(p) => (p as &dyn Display).fmt(f),
|
||||||
Self::Uri(u) => (u 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),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,9 @@ use rodio::{decoder::Decoder, OutputStream, OutputStreamHandle, Sink};
|
||||||
|
|
||||||
use m3u8_rs::{MediaPlaylist, MediaSegment};
|
use m3u8_rs::{MediaPlaylist, MediaSegment};
|
||||||
|
|
||||||
|
#[cfg(feature = "mpd")]
|
||||||
|
use mpd::{Client, Song};
|
||||||
|
|
||||||
use super::uri::Uri;
|
use super::uri::Uri;
|
||||||
|
|
||||||
use mps_interpreter::{tokens::MpsTokenReader, MpsFaye, MpsItem};
|
use mps_interpreter::{tokens::MpsTokenReader, MpsFaye, MpsItem};
|
||||||
|
@ -21,6 +24,8 @@ pub struct MpsPlayer<'a, T: MpsTokenReader + 'a> {
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
output_stream: OutputStream, // this is required for playback, so it must live as long as this struct instance
|
output_stream: OutputStream, // this is required for playback, so it must live as long as this struct instance
|
||||||
output_handle: OutputStreamHandle,
|
output_handle: OutputStreamHandle,
|
||||||
|
#[cfg(feature = "mpd")]
|
||||||
|
mpd_connection: Option<Client<std::net::TcpStream>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, T: MpsTokenReader + 'a> MpsPlayer<'a, T> {
|
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)?,
|
sink: Sink::try_new(&output_handle).map_err(PlayerError::from_err_playback)?,
|
||||||
output_stream: stream,
|
output_stream: stream,
|
||||||
output_handle: output_handle,
|
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> {
|
pub fn play_all(&mut self) -> Result<(), PlayerError> {
|
||||||
while let Some(item) = self.runner.next() {
|
while let Some(item) = self.runner.next() {
|
||||||
self.sink.sleep_until_end();
|
self.sink.sleep_until_end();
|
||||||
|
@ -149,7 +162,7 @@ impl<'a, T: MpsTokenReader + 'a> MpsPlayer<'a, T> {
|
||||||
if let Some(filename) =
|
if let Some(filename) =
|
||||||
music_filename(&music)
|
music_filename(&music)
|
||||||
{
|
{
|
||||||
println!("Adding file `{}` to playlist", filename);
|
//println!("Adding file `{}` to playlist", filename);
|
||||||
playlist.segments.push(MediaSegment {
|
playlist.segments.push(MediaSegment {
|
||||||
uri: filename,
|
uri: filename,
|
||||||
title: music_title(&music),
|
title: music_title(&music),
|
||||||
|
@ -201,7 +214,20 @@ impl<'a, T: MpsTokenReader + 'a> MpsPlayer<'a, T> {
|
||||||
self.sink.append(source);
|
self.sink.append(source);
|
||||||
Ok(())
|
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())
|
scheme => Err(UriError::Unsupported(scheme.to_owned()).into())
|
||||||
},
|
},
|
||||||
None => {
|
None => {
|
||||||
|
|
|
@ -26,4 +26,9 @@ impl<'a> Uri<&'a str> {
|
||||||
None => self.0
|
None => self.0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn uri(&self) -> &'a str {
|
||||||
|
self.0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
11
src/cli.rs
11
src/cli.rs
|
@ -22,8 +22,19 @@ pub struct CliArgs {
|
||||||
/// The volume at which to playback audio, out of 1.0
|
/// The volume at which to playback audio, out of 1.0
|
||||||
#[clap(long)]
|
#[clap(long)]
|
||||||
pub volume: Option<f32>,
|
pub volume: Option<f32>,
|
||||||
|
|
||||||
|
/// MPD server for music playback
|
||||||
|
#[clap(short, long)]
|
||||||
|
pub mpd: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn parse() -> CliArgs {
|
pub fn parse() -> CliArgs {
|
||||||
CliArgs::parse()
|
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(())
|
||||||
|
}
|
||||||
|
|
10
src/main.rs
10
src/main.rs
|
@ -67,6 +67,10 @@ fn play_cursor() -> Result<(), PlayerError> {
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let args = cli::parse();
|
let args = cli::parse();
|
||||||
|
if let Err(e) = cli::validate(&args) {
|
||||||
|
eprintln!("{}", e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(script_file) = &args.file {
|
if let Some(script_file) = &args.file {
|
||||||
// interpret script
|
// interpret script
|
||||||
|
@ -77,6 +81,7 @@ fn main() {
|
||||||
// build playback controller
|
// build playback controller
|
||||||
let script_file2 = script_file.clone();
|
let script_file2 = script_file.clone();
|
||||||
let volume = args.volume.clone();
|
let volume = args.volume.clone();
|
||||||
|
let mpd = args.mpd.clone();
|
||||||
let player_builder = move || {
|
let player_builder = move || {
|
||||||
let script_reader = io::BufReader::new(
|
let script_reader = io::BufReader::new(
|
||||||
std::fs::File::open(&script_file2)
|
std::fs::File::open(&script_file2)
|
||||||
|
@ -84,10 +89,13 @@ fn main() {
|
||||||
);
|
);
|
||||||
let runner = MpsFaye::with_stream(script_reader);
|
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 {
|
if let Some(vol) = volume {
|
||||||
player.set_volume(vol);
|
player.set_volume(vol);
|
||||||
}
|
}
|
||||||
|
if let Some(mpd) = mpd {
|
||||||
|
player.connect_mpd(mpd.parse().unwrap()).unwrap();
|
||||||
|
}
|
||||||
player
|
player
|
||||||
};
|
};
|
||||||
if let Some(playlist_file) = &args.playlist {
|
if let Some(playlist_file) = &args.playlist {
|
||||||
|
|
20
src/repl.rs
20
src/repl.rs
|
@ -47,23 +47,36 @@ pub fn repl(args: CliArgs) {
|
||||||
term.set_title("mps");
|
term.set_title("mps");
|
||||||
let (writer, reader) = channel_io();
|
let (writer, reader) = channel_io();
|
||||||
let volume = args.volume.clone();
|
let volume = args.volume.clone();
|
||||||
|
let mpd = args.mpd.clone();
|
||||||
let player_builder = move || {
|
let player_builder = move || {
|
||||||
let runner = MpsFaye::with_stream(reader);
|
let runner = MpsFaye::with_stream(reader);
|
||||||
|
|
||||||
let player = MpsPlayer::new(runner).unwrap();
|
let mut player = MpsPlayer::new(runner).unwrap();
|
||||||
if let Some(vol) = volume {
|
if let Some(vol) = volume {
|
||||||
player.set_volume(vol);
|
player.set_volume(vol);
|
||||||
}
|
}
|
||||||
|
if let Some(mpd) = mpd {
|
||||||
|
player.connect_mpd(mpd.parse().unwrap()).unwrap();
|
||||||
|
}
|
||||||
player
|
player
|
||||||
};
|
};
|
||||||
let mut state = ReplState::new(writer, term);
|
let mut state = ReplState::new(writer, term);
|
||||||
if let Some(playlist_file) = &args.playlist {
|
if let Some(playlist_file) = &args.playlist {
|
||||||
|
if args.mpd.is_some() {
|
||||||
|
writeln!(
|
||||||
|
state.terminal,
|
||||||
|
"Playlist mode (output: `{}` & MPD)",
|
||||||
|
playlist_file
|
||||||
|
)
|
||||||
|
.expect("Failed to write to terminal output");
|
||||||
|
} else {
|
||||||
writeln!(
|
writeln!(
|
||||||
state.terminal,
|
state.terminal,
|
||||||
"Playlist mode (output: `{}`)",
|
"Playlist mode (output: `{}`)",
|
||||||
playlist_file
|
playlist_file
|
||||||
)
|
)
|
||||||
.expect("Failed to write to terminal output");
|
.expect("Failed to write to terminal output");
|
||||||
|
}
|
||||||
let mut player = player_builder();
|
let mut player = player_builder();
|
||||||
let mut playlist_writer =
|
let mut playlist_writer =
|
||||||
io::BufWriter::new(std::fs::File::create(playlist_file).unwrap_or_else(|_| {
|
io::BufWriter::new(std::fs::File::create(playlist_file).unwrap_or_else(|_| {
|
||||||
|
@ -84,9 +97,14 @@ pub fn repl(args: CliArgs) {
|
||||||
.flush()
|
.flush()
|
||||||
.expect("Failed to flush playlist to file");
|
.expect("Failed to flush playlist to file");
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
if args.mpd.is_some() {
|
||||||
|
writeln!(state.terminal, "Playback mode (output: audio device & MPD)")
|
||||||
|
.expect("Failed to write to terminal output");
|
||||||
} else {
|
} else {
|
||||||
writeln!(state.terminal, "Playback mode (output: audio device)")
|
writeln!(state.terminal, "Playback mode (output: audio device)")
|
||||||
.expect("Failed to write to terminal output");
|
.expect("Failed to write to terminal output");
|
||||||
|
}
|
||||||
let ctrl = MpsController::create_repl(player_builder);
|
let ctrl = MpsController::create_repl(player_builder);
|
||||||
read_loop(&args, &mut state, || {
|
read_loop(&args, &mut state, || {
|
||||||
if args.wait {
|
if args.wait {
|
||||||
|
|
Loading…
Reference in a new issue