Compare commits
8 commits
main
...
feature/mu
| Author | SHA1 | Date | |
|---|---|---|---|
| d98b645377 | |||
| 462c480a45 | |||
| 0df5ed34e5 | |||
| 940093d599 | |||
| 9cec453498 | |||
| 2292035b8d | |||
| 3fefadd5b5 | |||
| 7382b06bdf |
11 changed files with 619 additions and 128 deletions
|
|
@ -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"
|
||||||
|
|
||||||
|
|
|
||||||
18
README.md
18
README.md
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
34
src/file_handler.rs
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
34
src/grpc.rs
34
src/grpc.rs
|
|
@ -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())))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 fn get_client(&self) -> Result<JunoServicesClient<Channel>, Box<dyn std::error::Error>> {
|
||||||
|
let client =
|
||||||
|
JunoServicesClient::connect(format!("http://{}", self.address.to_string())).await?;
|
||||||
|
|
||||||
|
Ok(client)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
pub async fn ping(&self) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
impl super::Connection for GRPCClient {
|
let mut client = self.get_client().await?;
|
||||||
async fn connect(&self) -> Result<(), Box<dyn std::error::Error>> {
|
|
||||||
let mut client = JunoRequestClient::connect(format!("http://{}", self.address)).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(());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,31 +82,109 @@ 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(),
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(Response::new(reply))
|
Ok(Response::new(reply))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn play(
|
||||||
|
&self,
|
||||||
|
_request: Request<EmptyRequest>,
|
||||||
|
) -> Result<Response<EmptyResponse>, Status> {
|
||||||
|
if let Some(tx) = &self.transmitter {
|
||||||
|
let (resp_tx, resp_rx) = oneshot::channel();
|
||||||
|
let message = GrpcServerMessage::Play { resp: resp_tx };
|
||||||
|
|
||||||
|
if let Err(_err) = tx.send(message).await {
|
||||||
|
return Err(Status::internal("An internal error has occurred."));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
if let Err(_err) = resp_rx.await {
|
||||||
impl super::Connection for GRPCServer {
|
return Err(Status::internal("An internal error has occurred."));
|
||||||
async fn connect(&self) -> Result<(), Box<dyn Error>> {
|
}
|
||||||
println!("Starting server on: \"{}\"", self.address);
|
}
|
||||||
|
|
||||||
let socket: SocketAddr = self.address.parse()?;
|
Ok(Response::new(EmptyResponse {}))
|
||||||
|
}
|
||||||
Server::builder()
|
|
||||||
.add_service(JunoRequestServer::new(GRPCServer::default()))
|
async fn pause(
|
||||||
.serve(socket)
|
&self,
|
||||||
.await?;
|
_request: Request<EmptyRequest>,
|
||||||
|
) -> Result<Response<EmptyResponse>, Status> {
|
||||||
Ok(())
|
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 {}))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
119
src/main.rs
119
src/main.rs
|
|
@ -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
136
src/player.rs
Normal 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");
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue