Compare commits

...
Sign in to create a new pull request.

8 commits

Author SHA1 Message Date
d98b645377 refactor(player): change to dependency injection
Use this pattern to decouple the dependencies and allow to test te code
and introduce new dependencies in the future
2024-07-26 16:28:09 -04:00
462c480a45 chore: remove cargo warnings 2024-07-26 09:15:53 -04:00
0df5ed34e5 refactor(server): allow to send message between player and server 2024-07-25 16:10:49 -04:00
940093d599 refactor(config): remove global config and pass as referenced 2024-07-19 16:02:56 -04:00
9cec453498 chore: cleanup cargo warnings 2024-07-18 16:06:38 -04:00
2292035b8d refactor(cli): move paths to subcommands 2024-07-18 13:26:27 -04:00
3fefadd5b5 feat(cli): implements subcommands 2024-07-17 14:37:54 -04:00
7382b06bdf feat: implement basic music player and grpc server 2024-07-17 11:21:56 -04:00
11 changed files with 619 additions and 128 deletions

View file

@ -5,9 +5,11 @@ edition = "2021"
[dependencies] [dependencies]
clap = { version = "4.5.4", features = ["derive"] } clap = { version = "4.5.4", features = ["derive"] }
futures = "0.3.30"
ignore = "0.4.22" ignore = "0.4.22"
lazy_static = "1.4.0" lazy_static = "1.4.0"
prost = "0.12.4" prost = "0.12.4"
rodio = "0.17.3"
tokio = { version = "1", features = ["full"] } tokio = { version = "1", features = ["full"] }
tonic = "0.11.0" tonic = "0.11.0"

View file

@ -48,6 +48,24 @@ The project was split into 2 though:
- [Fuuka](https://megamitensei.fandom.com/wiki/Fuuka_Yamagishi), the navi of SEES in Persona 3, you can ask her to change the musing in tartarus. It act as the frontend to interact with the player. - [Fuuka](https://megamitensei.fandom.com/wiki/Fuuka_Yamagishi), the navi of SEES in Persona 3, you can ask her to change the musing in tartarus. It act as the frontend to interact with the player.
- [Juno](https://megamitensei.fandom.com/wiki/Juno), the persona of Fuuka that grants her the ability to communicate telepatically to her teamates. It act as the music player. - [Juno](https://megamitensei.fandom.com/wiki/Juno), the persona of Fuuka that grants her the ability to communicate telepatically to her teamates. It act as the music player.
## Contributing
Aside from rust and cargo, you need the following dependencies:
- alsa-lib devel package
```bash
# fedora
dnf install -y alsa-lib-devel
```
- Tonic protobuf dependencies
```bash
# fedora
dnf install -y protobuf-devel
```
## Similar projects ## Similar projects
- [Navidrome](https://www.navidrome.org) - [Navidrome](https://www.navidrome.org)

View file

@ -2,15 +2,27 @@ syntax = "proto3";
package juno; package juno;
service JunoRequest { service JunoServices {
rpc Ping (PingRequestMessage) returns (PingResponseMessage); rpc Ping (EmptyRequest) returns (PingResponse);
rpc GetFiles (GetFilesRequest) returns (GetFilesResponse); rpc GetFiles (GetFilesRequest) returns (GetFilesResponse);
rpc SkipSong (EmptyRequest) returns (EmptyResponse);
rpc Play (EmptyRequest) returns (EmptyResponse);
rpc Pause (EmptyRequest) returns (EmptyResponse);
rpc PlayPause (EmptyRequest) returns (EmptyResponse);
} }
message PingRequestMessage { enum Status {
SUCCESS = 0;
ERROR = 1;
} }
message PingResponseMessage { message EmptyRequest {
}
message EmptyResponse {
}
message PingResponse {
string message = 1; string message = 1;
} }

View file

@ -1,41 +1,91 @@
use clap::Parser; use clap::{Parser, Subcommand};
use lazy_static::lazy_static; use std::net::SocketAddr;
use std::env;
use std::path::PathBuf; use std::path::PathBuf;
use std::str::FromStr;
lazy_static! { use crate::grpc;
pub static ref CONFIG: Config = Config::new();
#[derive(Debug)]
pub enum ConfigMode {
Server,
Client,
}
#[derive(Subcommand, Debug, Clone)]
pub enum Commands {
/// Start the GRPC server
Start {
#[arg(help = "Directory to scan for files", default_value = ".")]
base_path: PathBuf,
#[arg(
long,
help = "The value 1.0 is the “normal” volume. Any value other than 1.0 will multiply each sample by this value.",
default_value = "1.0"
)]
volume: f32,
},
/// Resume the playback
Play,
/// Pause the playback
Pause,
/// Resume the playback if pause, pause if is playing
PlayPause,
/// Skip the current song
SkipSong,
Set,
/// List the available files
GetFiles {
#[arg(
help = "Directory to scan for files, relative to server base path",
default_value = "."
)]
path: PathBuf,
},
/// Test server connection
Ping,
} }
#[derive(Parser)] #[derive(Parser)]
#[command(version, about, long_about = None)] #[command(version, about, long_about = None)]
struct Args { struct Args {
#[arg(help = "Directory to scan for files")] #[command(subcommand)]
path: Option<PathBuf>, cmd: Commands,
#[arg(short, long, help = "the port to bind to", default_value = "50051")]
port: u16,
} }
#[derive(Debug)] #[derive(Debug)]
pub struct Config { pub struct Config {
pub base_path: PathBuf, pub command: Commands,
pub address: SocketAddr,
pub mode: ConfigMode,
pub volume: f32,
} }
impl Default for Config { impl Default for Config {
fn default() -> Self { fn default() -> Self {
Config { Config {
base_path: env::current_dir().expect("Current directory is not available."), command: Commands::Play,
mode: ConfigMode::Server,
address: SocketAddr::from_str("[::1]:50051").unwrap(),
volume: 1.0,
} }
} }
} }
impl Config { impl Config {
pub fn new() -> Self { pub fn new() -> Self {
let mut config = Self::default();
let cli = Self::get_cli_args(); let cli = Self::get_cli_args();
if let Some(path) = cli.path { let mut config = Self::default();
config.base_path = path; config.address = SocketAddr::from_str(format!("[::1]:{}", cli.port).as_str()).unwrap();
} config.command = cli.cmd;
if grpc::is_socket_in_use(config.address) {
config.mode = ConfigMode::Client;
} else {
config.mode = ConfigMode::Server;
};
config config
} }

View file

@ -1,40 +0,0 @@
use ignore::types::TypesBuilder;
use ignore::WalkBuilder;
use std::path::PathBuf;
use crate::configuration::CONFIG;
pub fn walk_dir(path: &PathBuf) -> Result<Vec<PathBuf>, &str> {
let mut types_builder = TypesBuilder::new();
types_builder.add_defaults();
let accepted_filetypes = ["mp3", "flac"];
for filetype in accepted_filetypes {
let _ = types_builder.add("sound", format!("*.{}", filetype).as_str());
}
types_builder.select("sound");
let search_path = CONFIG.base_path.join(path);
eprintln!(
"DEBUGPRINT[1]: file_explorer.rs:19: search_path={:#?}",
search_path
);
// PathBuf.join() can override the hole path, this ensure we're not accessing files outside
// base_dir
if !search_path.starts_with(&CONFIG.base_path) {
return Err("Tried to access file or directory outside of server `base_dir` config.");
}
let entries: Vec<PathBuf> = WalkBuilder::new(search_path)
.types(types_builder.build().unwrap())
.build()
.filter_map(|entry| entry.ok())
.filter(|entry| !entry.path().is_dir())
.map(|entry| entry.path().to_path_buf())
.collect();
Ok(entries)
}

34
src/file_handler.rs Normal file
View file

@ -0,0 +1,34 @@
use ignore::types::TypesBuilder;
use ignore::WalkBuilder;
use std::path::PathBuf;
pub trait FileExplorer {
fn get_files(path: &PathBuf) -> Vec<PathBuf>;
}
pub struct LocalFileSystem;
impl FileExplorer for LocalFileSystem {
fn get_files(path: &PathBuf) -> Vec<PathBuf> {
let mut types_builder = TypesBuilder::new();
types_builder.add_defaults();
let accepted_filetypes = ["mp3", "flac", "wav"];
for filetype in accepted_filetypes {
let _ = types_builder.add("sound", format!("*.{}", filetype).as_str());
}
types_builder.select("sound");
let entries: Vec<PathBuf> = WalkBuilder::new(path)
.types(types_builder.build().unwrap())
.build()
.filter_map(|entry| entry.ok())
.filter(|entry| !entry.path().is_dir())
.map(|entry| entry.path().to_path_buf())
.collect();
entries
}
}

View file

@ -1,37 +1,19 @@
use std::error::Error;
use std::net::{SocketAddr, TcpListener}; use std::net::{SocketAddr, TcpListener};
use tonic::async_trait; pub use self::client::GRPCClient;
pub use self::server::GRPCServer;
use self::client::GRPCClient;
use self::server::GRPCServer;
mod client; mod client;
mod server; pub mod server;
pub mod grpc_juno { pub mod grpc_juno {
tonic::include_proto!("juno"); tonic::include_proto!("juno");
} }
#[async_trait] /// Return true if the addr is already in use, false otherwise
pub trait Connection { pub fn is_socket_in_use(addr: SocketAddr) -> bool {
async fn connect(&self) -> Result<(), Box<dyn Error>>; match TcpListener::bind(addr) {
} Ok(_) => false,
Err(_) => true,
fn is_socket_in_use(addr: String) -> bool {
let socket: SocketAddr = addr.parse().expect("Failed to create socket");
match TcpListener::bind(socket) {
Ok(_) => true,
Err(_) => false,
}
}
pub fn run() -> Result<Box<dyn Connection>, Box<dyn Error>> {
let addr = "[::1]:50051";
if is_socket_in_use(addr.to_string()) {
Ok(Box::new(GRPCServer::new(addr.to_string())))
} else {
Ok(Box::new(GRPCClient::new(addr.to_string())))
} }
} }

View file

@ -1,34 +1,103 @@
use std::net::SocketAddr;
use std::path::PathBuf;
use crate::grpc::grpc_juno::EmptyRequest;
use super::grpc_juno; use super::grpc_juno;
use grpc_juno::juno_request_client::JunoRequestClient; use grpc_juno::juno_services_client::JunoServicesClient;
use grpc_juno::GetFilesRequest; use grpc_juno::GetFilesRequest;
use tonic::async_trait; use tonic::transport::Channel;
use tonic::Request; use tonic::Request;
#[derive(Debug, Default)] #[derive(Debug)]
pub struct GRPCClient { pub struct GRPCClient {
address: String, address: SocketAddr,
} }
impl GRPCClient { impl GRPCClient {
pub fn new(address: String) -> Self { pub fn new(address: SocketAddr) -> Self {
Self { address } Self { address }
} }
}
#[async_trait] async fn get_client(&self) -> Result<JunoServicesClient<Channel>, Box<dyn std::error::Error>> {
impl super::Connection for GRPCClient { let client =
async fn connect(&self) -> Result<(), Box<dyn std::error::Error>> { JunoServicesClient::connect(format!("http://{}", self.address.to_string())).await?;
let mut client = JunoRequestClient::connect(format!("http://{}", self.address)).await?;
Ok(client)
}
pub async fn ping(&self) -> Result<(), Box<dyn std::error::Error>> {
let mut client = self.get_client().await?;
let request = Request::new(EmptyRequest {});
let response = client.ping(request).await?.into_inner();
println!("RESPONSE={:?}", response);
Ok(())
}
pub async fn play(&self) -> Result<(), Box<dyn std::error::Error>> {
let mut client = self.get_client().await?;
let request = Request::new(EmptyRequest {});
let response = client.play(request).await?.into_inner();
println!("RESPONSE={:?}", response);
Ok(())
}
pub async fn pause(&self) -> Result<(), Box<dyn std::error::Error>> {
let mut client = self.get_client().await?;
let request = Request::new(EmptyRequest {});
let response = client.pause(request).await?.into_inner();
println!("RESPONSE={:?}", response);
Ok(())
}
pub async fn play_pause(&self) -> Result<(), Box<dyn std::error::Error>> {
let mut client = self.get_client().await?;
let request = Request::new(EmptyRequest {});
let response = client.play_pause(request).await?.into_inner();
println!("RESPONSE={:?}", response);
Ok(())
}
pub async fn skip_song(&self) -> Result<(), Box<dyn std::error::Error>> {
let mut client = self.get_client().await?;
let request = Request::new(EmptyRequest {});
let response = client.skip_song(request).await?.into_inner();
println!("RESPONSE={:?}", response);
Ok(())
}
pub async fn get_files(&self, path: &PathBuf) -> Result<(), Box<dyn std::error::Error>> {
let mut client = self.get_client().await?;
let request = Request::new(GetFilesRequest { let request = Request::new(GetFilesRequest {
path: "/home/aleidk/Documents/".to_string(), path: path.display().to_string(),
}); });
let response = client.get_files(request).await?.into_inner(); let response = client.get_files(request).await?.into_inner();
println!("RESPONSE={:?}", response.files); println!("RESPONSE={:?}", response.files);
Ok(()) return Ok(());
} }
} }

View file

@ -1,33 +1,74 @@
use crate::file_explorer;
use super::grpc_juno; use super::grpc_juno;
use grpc_juno::juno_request_server::{JunoRequest, JunoRequestServer}; use grpc_juno::juno_services_server::{JunoServices, JunoServicesServer};
use grpc_juno::{GetFilesRequest, GetFilesResponse, PingRequestMessage, PingResponseMessage}; use grpc_juno::{EmptyRequest, EmptyResponse, GetFilesRequest, GetFilesResponse, PingResponse};
use std::error::Error; use std::error::Error;
use std::net::SocketAddr; use std::net::SocketAddr;
use std::path::PathBuf; use std::path::PathBuf;
use std::str::FromStr; use std::str::FromStr;
use tokio::sync::mpsc::Sender;
use tokio::sync::oneshot;
use tonic::transport::Server; use tonic::transport::Server;
use tonic::{async_trait, Request, Response, Result, Status}; use tonic::{Request, Response, Result, Status};
type Responder<T> = oneshot::Sender<T>;
pub enum GrpcServerMessage {
Play {
resp: Responder<Result<()>>,
},
Pause {
resp: Responder<Result<()>>,
},
PlayPause {
resp: Responder<Result<()>>,
},
SkipSong {
resp: Responder<Result<(), String>>,
},
Set {
resp: Responder<Result<()>>,
},
GetFiles {
path: PathBuf,
resp: Responder<Vec<PathBuf>>,
},
}
#[derive(Debug, Default)] #[derive(Debug, Default)]
pub struct GRPCServer { pub struct GRPCServer {
address: String, transmitter: Option<Sender<GrpcServerMessage>>,
} }
impl GRPCServer { impl GRPCServer {
pub fn new(address: String) -> Self { pub fn new(tx: Sender<GrpcServerMessage>) -> Self {
Self { address } Self {
transmitter: Some(tx),
}
}
pub async fn serve(
address: SocketAddr,
tx: Sender<GrpcServerMessage>,
) -> Result<(), Box<dyn Error>> {
println!("Starting server on: \"{}\"", address.to_string());
Server::builder()
.add_service(JunoServicesServer::new(GRPCServer::new(tx)))
.serve(address)
.await?;
Ok(())
} }
} }
#[tonic::async_trait] #[tonic::async_trait]
impl JunoRequest for GRPCServer { impl JunoServices for GRPCServer {
async fn ping( async fn ping(
&self, &self,
_request: Request<PingRequestMessage>, _request: Request<EmptyRequest>,
) -> Result<Response<PingResponseMessage>, Status> { ) -> Result<Response<PingResponse>, Status> {
let reply = PingResponseMessage { let reply = PingResponse {
message: "pong!".to_string(), message: "pong!".to_string(),
}; };
@ -41,10 +82,24 @@ impl JunoRequest for GRPCServer {
let path = PathBuf::from_str(request.into_inner().path.as_str()) let path = PathBuf::from_str(request.into_inner().path.as_str())
.expect("Failed to create pathbuf"); .expect("Failed to create pathbuf");
let files = match file_explorer::walk_dir(&path) { let mut files: Vec<PathBuf> = vec![];
Ok(files) => files,
Err(err) => return Err(Status::invalid_argument(err)), if let Some(tx) = &self.transmitter {
}; let (resp_tx, resp_rx) = oneshot::channel();
let message = GrpcServerMessage::GetFiles {
resp: resp_tx,
path,
};
if let Err(_err) = tx.send(message).await {
return Err(Status::internal("An internal error has occurred."));
}
files = match resp_rx.await {
Ok(response) => response,
Err(_err) => return Err(Status::internal("An internal error has occurred.")),
};
}
let reply = GetFilesResponse { let reply = GetFilesResponse {
files: files.iter().map(|x| x.display().to_string()).collect(), files: files.iter().map(|x| x.display().to_string()).collect(),
@ -52,20 +107,84 @@ impl JunoRequest for GRPCServer {
Ok(Response::new(reply)) Ok(Response::new(reply))
} }
}
#[async_trait] async fn play(
impl super::Connection for GRPCServer { &self,
async fn connect(&self) -> Result<(), Box<dyn Error>> { _request: Request<EmptyRequest>,
println!("Starting server on: \"{}\"", self.address); ) -> Result<Response<EmptyResponse>, Status> {
if let Some(tx) = &self.transmitter {
let (resp_tx, resp_rx) = oneshot::channel();
let message = GrpcServerMessage::Play { resp: resp_tx };
let socket: SocketAddr = self.address.parse()?; if let Err(_err) = tx.send(message).await {
return Err(Status::internal("An internal error has occurred."));
}
Server::builder() if let Err(_err) = resp_rx.await {
.add_service(JunoRequestServer::new(GRPCServer::default())) return Err(Status::internal("An internal error has occurred."));
.serve(socket) }
.await?; }
Ok(()) Ok(Response::new(EmptyResponse {}))
}
async fn pause(
&self,
_request: Request<EmptyRequest>,
) -> Result<Response<EmptyResponse>, Status> {
if let Some(tx) = &self.transmitter {
let (resp_tx, resp_rx) = oneshot::channel();
let message = GrpcServerMessage::Pause { resp: resp_tx };
if let Err(_err) = tx.send(message).await {
return Err(Status::internal("An internal error has occurred."));
}
if let Err(_err) = resp_rx.await {
return Err(Status::internal("An internal error has occurred."));
}
}
Ok(Response::new(EmptyResponse {}))
}
async fn play_pause(
&self,
_request: Request<EmptyRequest>,
) -> Result<Response<EmptyResponse>, Status> {
if let Some(tx) = &self.transmitter {
let (resp_tx, resp_rx) = oneshot::channel();
let message = GrpcServerMessage::PlayPause { resp: resp_tx };
if let Err(_err) = tx.send(message).await {
return Err(Status::internal("An internal error has occurred."));
}
if let Err(_err) = resp_rx.await {
return Err(Status::internal("An internal error has occurred."));
}
}
Ok(Response::new(EmptyResponse {}))
}
async fn skip_song(
&self,
_request: Request<EmptyRequest>,
) -> Result<Response<EmptyResponse>, Status> {
if let Some(tx) = &self.transmitter {
let (resp_tx, resp_rx) = oneshot::channel();
let message = GrpcServerMessage::SkipSong { resp: resp_tx };
if let Err(_err) = tx.send(message).await {
return Err(Status::internal("An internal error has occurred."));
}
if let Err(_err) = resp_rx.await {
return Err(Status::internal("An internal error has occurred."));
}
}
Ok(Response::new(EmptyResponse {}))
} }
} }

View file

@ -1,14 +1,123 @@
use std::env;
use std::error::Error; use std::error::Error;
use std::time::Duration;
use tokio::time::sleep;
use tokio::sync::mpsc;
use crate::player::Player;
use self::configuration::{Commands, Config, ConfigMode};
use self::file_handler::{FileExplorer, LocalFileSystem};
use self::grpc::server::GrpcServerMessage;
mod configuration; mod configuration;
mod file_explorer; mod file_handler;
mod grpc; mod grpc;
mod player;
#[tokio::main()] async fn handle_message<T: FileExplorer>(player: &mut Player<T>, message: GrpcServerMessage) {
async fn main() -> Result<(), Box<dyn Error>> { match message {
let server = grpc::run()?; GrpcServerMessage::Play { resp } => {
player.play();
let _ = resp.send(Ok(()));
}
GrpcServerMessage::Pause { resp } => {
player.pause();
let _ = resp.send(Ok(()));
}
GrpcServerMessage::PlayPause { resp } => {
player.play_pause();
let _ = resp.send(Ok(()));
}
GrpcServerMessage::SkipSong { resp } => {
let _ = match player.skip_song() {
Ok(_) => resp.send(Ok(())),
Err(err) => resp.send(Err(err.to_string())),
};
}
GrpcServerMessage::Set { resp } => {
let _ = resp.send(Ok(()));
}
GrpcServerMessage::GetFiles { path, resp } => {
let files = player.get_files(&path).unwrap();
let _ = resp.send(files);
}
}
}
server.connect().await?; async fn init_server(config: Config) -> Result<(), Box<dyn Error>> {
let (tx, mut rx) = mpsc::channel::<grpc::server::GrpcServerMessage>(32);
tokio::spawn(async move {
let _ = grpc::GRPCServer::serve(config.address, tx).await;
});
let mut base_path = env::current_dir().expect("Error accesing the enviroment");
let mut volume = 1.0;
if let Commands::Start {
base_path: config_path,
volume: config_volume,
} = config.command
{
base_path = config_path.to_owned();
volume = config_volume;
};
let mut player = Player::new(LocalFileSystem, base_path).expect("Error creating player");
player.set_volume(volume);
println!("Listening for incomming messages...");
// This macro will wait on multiple futures and will return when the first one resolves
// TODO: create a break system for shutdown
loop {
tokio::select! {
Some(msg) = rx.recv() => {
handle_message(&mut player, msg).await;
}
_ = async {
loop {
let _ = player.handle_idle();
sleep(Duration::from_millis(200)).await;
}
} => {}
else => {
println!("player stopped");
}
}
}
}
async fn init_client(config: Config) -> Result<(), Box<dyn Error>> {
let client = grpc::GRPCClient::new(config.address);
match &config.command {
Commands::Play => client.play().await?,
Commands::Pause => client.pause().await?,
Commands::PlayPause => client.play_pause().await?,
Commands::SkipSong => client.skip_song().await?,
Commands::Set => todo!(),
Commands::GetFiles { path } => client.get_files(&path).await?,
Commands::Ping => client.ping().await?,
_ => {
println!("This command doesn't apply to client mode")
}
}
Ok(())
}
#[tokio::main()]
async fn main() -> Result<(), Box<dyn Error>> {
let config = Config::new();
match config.mode {
ConfigMode::Server => init_server(config).await?,
ConfigMode::Client => init_client(config).await?,
};
Ok(()) Ok(())
} }

136
src/player.rs Normal file
View file

@ -0,0 +1,136 @@
use std::collections::VecDeque;
use std::error::Error;
use std::fs::File;
use std::io::BufReader;
use std::path::PathBuf;
use rodio::{OutputStream, Sink};
use crate::file_handler::FileExplorer;
#[allow(dead_code)]
pub struct Player<T: FileExplorer> {
queue: VecDeque<PathBuf>,
sink: Sink,
stream: OutputStream,
base_dir: PathBuf,
explorer: T,
}
impl<T: FileExplorer> std::ops::Deref for Player<T> {
type Target = Sink;
fn deref(&self) -> &Self::Target {
&self.sink
}
}
impl<T: FileExplorer> Player<T> {
pub fn new(explorer: T, base_dir: PathBuf) -> Result<Self, Box<dyn Error>> {
let queue = T::get_files(&base_dir);
// stream needs to exist as long as sink to work
let (stream, stream_handle) = OutputStream::try_default()?;
let sink = Sink::try_new(&stream_handle)?;
Ok(Player {
queue: VecDeque::from(queue),
sink,
stream,
base_dir,
explorer,
})
}
pub fn handle_idle(&mut self) -> Result<(), Box<dyn Error>> {
if self.sink.is_paused() {
return Ok(());
}
if self.queue.len() == 0 {
return Ok(());
}
if self.sink.len() != 0 {
return Ok(());
}
let file_path = self
.queue
.pop_front()
.expect("There was an error with the queue");
self.enqueue_file(file_path)?;
Ok(())
}
pub fn get_files(&mut self, path: &PathBuf) -> Result<Vec<PathBuf>, Box<dyn Error>> {
let search_path = self
.base_dir
.join(path)
.canonicalize()
.expect("Couldn't canonicalizice the path");
// PathBuf.join() can override the hole path, this ensure we're not accessing files outside base_dir
if !search_path.starts_with(&self.base_dir) {
panic!("Tried to access file or directory outside of server `base_path` config.")
}
Ok(T::get_files(&search_path))
}
pub fn play(&mut self) {
self.sink.play();
}
pub fn pause(&mut self) {
self.sink.pause();
}
pub fn play_pause(&self) {
if self.sink.is_paused() {
self.sink.play();
} else {
self.sink.pause();
};
}
pub fn skip_song(&mut self) -> Result<(), Box<dyn Error>> {
println!("Skipping current song...:");
let file_path = self.queue.pop_front().expect("foo");
self.enqueue_file(file_path)?;
self.sink.skip_one();
Ok(())
}
fn enqueue_file(&self, file_path: PathBuf) -> Result<(), Box<dyn Error>> {
println!("Playing file: {}", file_path.display());
let file = File::open(file_path)?;
self.sink.append(rodio::Decoder::new(BufReader::new(file))?);
Ok(())
}
pub fn set_volume(&self, volume: f32) {
self.sink.set_volume(volume);
}
}
#[cfg(test)]
mod tests {
use super::*;
struct MockFileExplorer;
impl FileExplorer for MockFileExplorer {
fn get_files(_: &PathBuf) -> Vec<PathBuf> {
return vec![];
}
}
#[test]
fn player_works() {
let _ = Player::new(MockFileExplorer, PathBuf::from(".")).expect("Error creating player");
}
}