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
This commit is contained in:
Alexander Navarro 2024-07-26 16:28:09 -04:00
parent 462c480a45
commit d98b645377
4 changed files with 71 additions and 70 deletions

View file

@ -1,45 +0,0 @@
use ignore::types::TypesBuilder;
use ignore::WalkBuilder;
use std::error::Error;
use std::path::PathBuf;
pub fn walk_dir(path: &PathBuf) -> Result<Vec<PathBuf>, Box<dyn Error>> {
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 mut base_path = env::current_dir().expect("Error accesing the enviroment");
//
// match path {
// Some(dir) => {
// search_path = base_path
// .join(dir)
// .canonicalize()
// .expect("Couldn't canonicalizice the path")
// }
// None => search_path = base_path.to_owned(),
// }
//
// // PathBuf.join() can override the hole path, this ensure we're not accessing files outside
// // base_dir
// if !search_path.starts_with(base_path) {
// return Err("Tried to access file or directory outside of server `base_path` config.");
// }
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();
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

@ -8,14 +8,15 @@ use tokio::sync::mpsc;
use crate::player::Player; use crate::player::Player;
use self::configuration::{Commands, Config, ConfigMode}; use self::configuration::{Commands, Config, ConfigMode};
use self::file_handler::{FileExplorer, LocalFileSystem};
use self::grpc::server::GrpcServerMessage; use self::grpc::server::GrpcServerMessage;
mod configuration; mod configuration;
mod file_explorer; mod file_handler;
mod grpc; mod grpc;
mod player; mod player;
async fn handle_message(player: &mut Player, message: GrpcServerMessage) { async fn handle_message<T: FileExplorer>(player: &mut Player<T>, message: GrpcServerMessage) {
match message { match message {
GrpcServerMessage::Play { resp } => { GrpcServerMessage::Play { resp } => {
player.play(); player.play();
@ -64,7 +65,9 @@ async fn init_server(config: Config) -> Result<(), Box<dyn Error>> {
volume = config_volume; volume = config_volume;
}; };
let mut player = Player::new(base_path, volume).expect("Error creating player"); let mut player = Player::new(LocalFileSystem, base_path).expect("Error creating player");
player.set_volume(volume);
println!("Listening for incomming messages..."); println!("Listening for incomming messages...");

View file

@ -6,17 +6,18 @@ use std::path::PathBuf;
use rodio::{OutputStream, Sink}; use rodio::{OutputStream, Sink};
use crate::file_explorer::walk_dir; use crate::file_handler::FileExplorer;
#[allow(dead_code)] #[allow(dead_code)]
pub struct Player { pub struct Player<T: FileExplorer> {
queue: VecDeque<PathBuf>, queue: VecDeque<PathBuf>,
sink: Sink, sink: Sink,
stream: OutputStream, stream: OutputStream,
base_dir: PathBuf, base_dir: PathBuf,
explorer: T,
} }
impl std::ops::Deref for Player { impl<T: FileExplorer> std::ops::Deref for Player<T> {
type Target = Sink; type Target = Sink;
fn deref(&self) -> &Self::Target { fn deref(&self) -> &Self::Target {
@ -24,19 +25,19 @@ impl std::ops::Deref for Player {
} }
} }
impl Player { impl<T: FileExplorer> Player<T> {
pub fn new(base_dir: PathBuf, volume: f32) -> Result<Self, Box<dyn Error>> { pub fn new(explorer: T, base_dir: PathBuf) -> Result<Self, Box<dyn Error>> {
let queue = walk_dir(&base_dir)?; let queue = T::get_files(&base_dir);
// stream needs to exist as long as sink to work // stream needs to exist as long as sink to work
let (stream, stream_handle) = OutputStream::try_default()?; let (stream, stream_handle) = OutputStream::try_default()?;
let sink = Sink::try_new(&stream_handle)?; let sink = Sink::try_new(&stream_handle)?;
sink.set_volume(volume);
Ok(Player { Ok(Player {
queue: VecDeque::from(queue), queue: VecDeque::from(queue),
sink, sink,
stream, stream,
base_dir, base_dir,
explorer,
}) })
} }
@ -64,20 +65,6 @@ impl Player {
} }
pub fn get_files(&mut self, path: &PathBuf) -> Result<Vec<PathBuf>, Box<dyn Error>> { pub fn get_files(&mut self, path: &PathBuf) -> Result<Vec<PathBuf>, Box<dyn Error>> {
// let mut base_path = env::current_dir().expect("Error accesing the enviroment");
//
// match path {
// Some(dir) => {
// }
// None => search_path = base_path.to_owned(),
// }
//
// // PathBuf.join() can override the hole path, this ensure we're not accessing files outside
// // base_dir
// if !search_path.starts_with(base_path) {
// return Err("Tried to access file or directory outside of server `base_path` config.");
// }
let search_path = self let search_path = self
.base_dir .base_dir
.join(path) .join(path)
@ -89,7 +76,7 @@ impl Player {
panic!("Tried to access file or directory outside of server `base_path` config.") panic!("Tried to access file or directory outside of server `base_path` config.")
} }
Ok(walk_dir(&search_path)?) Ok(T::get_files(&search_path))
} }
pub fn play(&mut self) { pub fn play(&mut self) {
@ -124,4 +111,26 @@ impl Player {
self.sink.append(rodio::Decoder::new(BufReader::new(file))?); self.sink.append(rodio::Decoder::new(BufReader::new(file))?);
Ok(()) 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");
}
} }