1.0.0 rewrite

This commit is contained in:
2025-09-24 22:51:34 +03:00
parent 4f8681a8c6
commit dee908a347
32 changed files with 6973 additions and 7 deletions
+240
View File
@@ -0,0 +1,240 @@
use crate::{
types::pipewire::{AudioDevice, DeviceType, Terminate},
utils::{
daemon::get_daemon_config,
pipewire::{create_link, get_all_devices, get_device},
},
};
use rodio::{Decoder, OutputStream, OutputStreamBuilder, Sink, Source};
use serde::{Deserialize, Serialize};
use std::{
error::Error,
fs,
path::{Path, PathBuf},
time::Duration,
};
#[derive(Debug, Eq, PartialEq, Default, Clone, Serialize, Deserialize)]
pub enum PlayerState {
#[default]
Stopped,
Paused,
Playing,
}
pub struct AudioPlayer {
_stream_handle: OutputStream,
sink: Sink,
input_link_sender: Option<pipewire::channel::Sender<Terminate>>,
pub current_input_device: Option<AudioDevice>,
pub volume: f32,
pub duration: Option<f32>,
pub current_file_path: Option<PathBuf>,
}
impl AudioPlayer {
pub async fn new() -> Result<Self, Box<dyn Error>> {
let daemon_config = get_daemon_config();
let default_volume = daemon_config.default_volume.unwrap_or(1.0);
let mut default_input_device: Option<AudioDevice> = None;
if let Some(id) = daemon_config.default_input_id
&& let Ok(device) = get_device(id).await
&& device.device_type == DeviceType::Input
{
default_input_device = Some(device);
}
let stream_handle = OutputStreamBuilder::open_default_stream()?;
let sink = Sink::connect_new(stream_handle.mixer());
sink.set_volume(default_volume);
let mut audio_player = AudioPlayer {
_stream_handle: stream_handle,
sink,
input_link_sender: None,
current_input_device: default_input_device.clone(),
volume: default_volume,
duration: None,
current_file_path: None,
};
if default_input_device.is_some() {
audio_player.link_devices().await?;
}
Ok(audio_player)
}
fn abort_link_thread(&mut self) {
if let Some(sender) = &self.input_link_sender {
match sender.send(Terminate {}) {
Ok(_) => println!("Sent terminate signal to link thread"),
Err(_) => println!("Failed to send terminate signal to link thread"),
}
}
}
async fn link_devices(&mut self) -> Result<(), Box<dyn Error>> {
self.abort_link_thread();
if self.current_input_device.is_none() {
println!("No input device selected, skipping device linking");
return Ok(());
}
let (input_devices, _) = get_all_devices().await?;
let mut pwsp_daemon_input: Option<AudioDevice> = None;
for input_device in input_devices {
if input_device.name == "pwsp-virtual-mic" {
pwsp_daemon_input = Some(input_device);
break;
}
}
if pwsp_daemon_input.is_none() {
println!("Could not find pwsp-daemon input device, skipping device linking");
return Ok(());
}
let pwsp_daemon_input = pwsp_daemon_input.unwrap();
let current_input_device = self.current_input_device.clone().unwrap();
let output_fl = current_input_device
.clone()
.output_fl
.expect("Failed to get pwsp-daemon output_fl");
let output_fr = current_input_device
.clone()
.output_fr
.expect("Failed to get pwsp-daemon output_fl");
let input_fl = pwsp_daemon_input
.clone()
.input_fl
.expect("Failed to get pwsp-daemon input_fl");
let input_fr = pwsp_daemon_input
.clone()
.input_fr
.expect("Failed to get pwsp-daemon input_fr");
self.input_link_sender = Some(create_link(output_fl, output_fr, input_fl, input_fr)?);
Ok(())
}
pub fn pause(&mut self) {
if self.get_state() == PlayerState::Playing {
self.sink.pause();
}
}
pub fn resume(&mut self) {
if self.get_state() == PlayerState::Paused {
self.sink.play();
}
}
pub fn stop(&mut self) {
self.sink.stop();
}
pub fn is_paused(&self) -> bool {
self.sink.is_paused()
}
pub fn get_state(&mut self) -> PlayerState {
if self.sink.len() == 0 {
return PlayerState::Stopped;
}
if self.sink.is_paused() {
return PlayerState::Paused;
}
PlayerState::Playing
}
pub fn set_volume(&mut self, volume: f32) {
self.volume = volume;
self.sink.set_volume(volume);
}
pub fn get_position(&mut self) -> f32 {
if self.get_state() == PlayerState::Stopped {
return 0.0;
}
self.sink.get_pos().as_secs_f32()
}
pub fn seek(&mut self, position: f32) -> Result<(), Box<dyn Error>> {
match self.sink.try_seek(Duration::from_secs_f32(position)) {
Ok(_) => Ok(()),
Err(err) => Err(err.into()),
}
}
pub fn get_duration(&mut self) -> Result<f32, Box<dyn Error>> {
if self.get_state() == PlayerState::Stopped {
Err("Nothing is playing right now".into())
} else {
match self.duration {
Some(duration) => Ok(duration),
None => Err("Couldn't determine duration for current file".into()),
}
}
}
pub async fn play(&mut self, file_path: &Path) -> Result<(), Box<dyn Error>> {
if !file_path.exists() {
return Err(format!("File does not exist: {}", file_path.display()).into());
}
let file = fs::File::open(file_path)?;
match Decoder::try_from(file) {
Ok(source) => {
self.current_file_path = Some(file_path.to_path_buf());
if let Some(duration) = source.total_duration() {
self.duration = Some(duration.as_secs_f32());
} else {
self.duration = None;
}
self.sink.stop();
self.sink.append(source);
self.sink.play();
self.link_devices().await?;
Ok(())
}
Err(err) => Err(err.into()),
}
}
pub fn get_current_file_path(&mut self) -> &Option<PathBuf> {
if self.get_state() == PlayerState::Stopped {
self.current_file_path = None;
}
&self.current_file_path
}
pub async fn set_current_input_device(&mut self, id: u32) -> Result<(), Box<dyn Error>> {
let input_device = get_device(id).await?;
if input_device.device_type != DeviceType::Input {
return Err("Selected device is not an input device".into());
}
self.current_input_device = Some(input_device);
self.link_devices().await?;
Ok(())
}
}
+234
View File
@@ -0,0 +1,234 @@
use crate::{
types::socket::Response,
utils::{daemon::get_audio_player, pipewire::get_all_devices},
};
use async_trait::async_trait;
use std::path::PathBuf;
#[async_trait]
pub trait Executable {
async fn execute(&self) -> Response;
}
pub struct PingCommand {}
pub struct PauseCommand {}
pub struct ResumeCommand {}
pub struct StopCommand {}
pub struct IsPausedCommand {}
pub struct GetStateCommand {}
pub struct GetVolumeCommand {}
pub struct SetVolumeCommand {
pub volume: Option<f32>,
}
pub struct GetPositionCommand {}
pub struct SeekCommand {
pub position: Option<f32>,
}
pub struct GetDurationCommand {}
pub struct PlayCommand {
pub file_path: Option<PathBuf>,
}
pub struct GetCurrentFilePathCommand {}
pub struct GetCurrentInputCommand {}
pub struct GetAllInputsCommand {}
pub struct SetCurrentInputCommand {
pub id: Option<u32>,
}
#[async_trait]
impl Executable for PingCommand {
async fn execute(&self) -> Response {
Response::new(true, "pong")
}
}
#[async_trait]
impl Executable for PauseCommand {
async fn execute(&self) -> Response {
let mut audio_player = get_audio_player().await.lock().await;
audio_player.pause();
Response::new(true, "Audio was paused")
}
}
#[async_trait]
impl Executable for ResumeCommand {
async fn execute(&self) -> Response {
let mut audio_player = get_audio_player().await.lock().await;
audio_player.resume();
Response::new(true, "Audio was resumed")
}
}
#[async_trait]
impl Executable for StopCommand {
async fn execute(&self) -> Response {
let mut audio_player = get_audio_player().await.lock().await;
audio_player.stop();
Response::new(true, "Audio was stopped")
}
}
#[async_trait]
impl Executable for IsPausedCommand {
async fn execute(&self) -> Response {
let audio_player = get_audio_player().await.lock().await;
let is_paused = audio_player.is_paused().to_string();
Response::new(true, is_paused)
}
}
#[async_trait]
impl Executable for GetStateCommand {
async fn execute(&self) -> Response {
let mut audio_player = get_audio_player().await.lock().await;
let state = audio_player.get_state();
Response::new(true, serde_json::to_string(&state).unwrap())
}
}
#[async_trait]
impl Executable for GetVolumeCommand {
async fn execute(&self) -> Response {
let audio_player = get_audio_player().await.lock().await;
let volume = audio_player.volume;
Response::new(true, volume.to_string())
}
}
#[async_trait]
impl Executable for SetVolumeCommand {
async fn execute(&self) -> Response {
if let Some(volume) = self.volume {
let mut audio_player = get_audio_player().await.lock().await;
audio_player.set_volume(volume);
Response::new(true, format!("Audio volume was set to {}", volume))
} else {
Response::new(false, "Invalid volume value")
}
}
}
#[async_trait]
impl Executable for GetPositionCommand {
async fn execute(&self) -> Response {
let mut audio_player = get_audio_player().await.lock().await;
let position = audio_player.get_position();
Response::new(true, position.to_string())
}
}
#[async_trait]
impl Executable for SeekCommand {
async fn execute(&self) -> Response {
if let Some(position) = self.position {
let mut audio_player = get_audio_player().await.lock().await;
match audio_player.seek(position) {
Ok(_) => Response::new(true, format!("Audio position was set to {}", position)),
Err(err) => Response::new(false, err.to_string()),
}
} else {
Response::new(false, "Invalid position value")
}
}
}
#[async_trait]
impl Executable for GetDurationCommand {
async fn execute(&self) -> Response {
let mut audio_player = get_audio_player().await.lock().await;
match audio_player.get_duration() {
Ok(duration) => Response::new(true, duration.to_string()),
Err(err) => Response::new(false, err.to_string()),
}
}
}
#[async_trait]
impl Executable for PlayCommand {
async fn execute(&self) -> Response {
if let Some(file_path) = &self.file_path {
let mut audio_player = get_audio_player().await.lock().await;
match audio_player.play(file_path).await {
Ok(_) => Response::new(true, format!("Now playing {}", file_path.display())),
Err(err) => Response::new(false, err.to_string()),
}
} else {
Response::new(false, "Invalid file path")
}
}
}
#[async_trait]
impl Executable for GetCurrentFilePathCommand {
async fn execute(&self) -> Response {
let mut audio_player = get_audio_player().await.lock().await;
let current_file_path = audio_player.get_current_file_path();
if let Some(current_file_path) = current_file_path {
Response::new(true, current_file_path.to_str().unwrap())
} else {
Response::new(false, "No file is playing")
}
}
}
#[async_trait]
impl Executable for GetCurrentInputCommand {
async fn execute(&self) -> Response {
let audio_player = get_audio_player().await.lock().await;
if let Some(input_device) = &audio_player.current_input_device {
Response::new(true, format!("{} - {}", input_device.id, input_device.nick))
} else {
Response::new(false, "No input device selected")
}
}
}
#[async_trait]
impl Executable for GetAllInputsCommand {
async fn execute(&self) -> Response {
let (input_devices, _output_devices) = get_all_devices().await.unwrap();
let mut input_devices_strings = vec![];
for device in input_devices {
if device.name == "pwsp-virtual-mic" {
continue;
}
let string = format!("{} - {}", device.id, device.nick);
input_devices_strings.push(string);
}
let response_message = input_devices_strings.join("; ");
Response::new(true, response_message)
}
}
#[async_trait]
impl Executable for SetCurrentInputCommand {
async fn execute(&self) -> Response {
if let Some(id) = self.id {
let mut audio_player = get_audio_player().await.lock().await;
match audio_player.set_current_input_device(id).await {
Ok(_) => Response::new(true, "Input device was set"),
Err(err) => Response::new(false, err.to_string()),
}
} else {
Response::new(false, "Invalid index value")
}
}
}
+81
View File
@@ -0,0 +1,81 @@
use crate::utils::config::get_config_path;
use serde::{Deserialize, Serialize};
use std::{collections::HashSet, error::Error, fs, path::PathBuf};
#[derive(Default, Clone, Serialize, Deserialize)]
pub struct DaemonConfig {
pub default_input_id: Option<u32>,
pub default_volume: Option<f32>,
}
impl DaemonConfig {
pub fn save_to_file(&self) -> Result<(), Box<dyn Error>> {
let config_path = get_config_path()?.join("daemon.json");
let config_dir = config_path.parent().unwrap();
if !config_path.exists() {
fs::create_dir_all(config_dir)?;
}
let config_json = serde_json::to_string_pretty(self)?;
fs::write(config_path, config_json.as_bytes())?;
Ok(())
}
pub fn load_from_file() -> Result<DaemonConfig, Box<dyn Error>> {
let config_path = get_config_path()?.join("daemon.json");
let bytes = fs::read(config_path)?;
Ok(serde_json::from_slice::<DaemonConfig>(&bytes)?)
}
}
#[derive(Clone, Serialize, Deserialize)]
pub struct GuiConfig {
pub scale_factor: f32,
pub save_volume: bool,
pub save_input: bool,
pub save_scale_factor: bool,
pub dirs: HashSet<PathBuf>,
}
impl Default for GuiConfig {
fn default() -> Self {
GuiConfig {
scale_factor: 1.0,
save_volume: false,
save_input: false,
save_scale_factor: false,
dirs: HashSet::default(),
}
}
}
impl GuiConfig {
pub fn save_to_file(&mut self) -> Result<(), Box<dyn Error>> {
let config_path = get_config_path()?.join("gui.json");
let config_dir = config_path.parent().unwrap();
if !config_path.exists() {
fs::create_dir_all(config_dir)?;
}
// Do not save scale factor if user does not want to
if !self.save_scale_factor {
self.scale_factor = 1.0;
}
let config_json = serde_json::to_string_pretty(self)?;
fs::write(config_path, config_json.as_bytes())?;
Ok(())
}
pub fn load_from_file() -> Result<GuiConfig, Box<dyn Error>> {
let config_path = get_config_path()?.join("gui.json");
let bytes = fs::read(config_path)?;
Ok(serde_json::from_slice::<GuiConfig>(&bytes)?)
}
}
+39
View File
@@ -0,0 +1,39 @@
use crate::types::audio_player::PlayerState;
use std::{
collections::{HashMap, HashSet},
path::PathBuf,
};
#[derive(Default, Debug)]
pub struct AppState {
pub search_query: String,
pub position_slider_value: f32,
pub volume_slider_value: f32,
pub position_dragged: bool,
pub volume_dragged: bool,
pub show_settings: bool,
pub current_dir: Option<PathBuf>,
pub dirs: HashSet<PathBuf>,
}
#[derive(Default, Debug, Clone)]
pub struct AudioPlayerState {
pub state: PlayerState,
pub new_state: Option<PlayerState>,
pub current_file_path: PathBuf,
pub is_paused: bool,
pub volume: f32,
pub new_volume: Option<f32>,
pub position: f32,
pub new_position: Option<f32>,
pub duration: f32,
pub current_input: u32,
pub all_inputs: HashMap<u32, String>,
}
+6
View File
@@ -0,0 +1,6 @@
pub mod audio_player;
pub mod commands;
pub mod config;
pub mod gui;
pub mod pipewire;
pub mod socket;
+31
View File
@@ -0,0 +1,31 @@
#[derive(Debug)]
pub struct Terminate {}
#[derive(Debug, Clone, Ord, PartialOrd, Eq, PartialEq)]
pub struct Port {
pub node_id: u32,
pub port_id: u32,
pub name: String,
}
#[derive(Debug, Clone, Ord, PartialOrd, Eq, PartialEq)]
pub enum DeviceType {
Input,
Output,
}
#[derive(Debug, Clone, Ord, PartialOrd, Eq, PartialEq)]
pub struct AudioDevice {
pub id: u32,
pub nick: String,
pub name: String,
pub device_type: DeviceType,
pub input_fl: Option<Port>,
pub input_fr: Option<Port>,
pub output_fl: Option<Port>,
pub output_fr: Option<Port>,
}
+101
View File
@@ -0,0 +1,101 @@
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Default, Debug, Clone, Serialize, Deserialize)]
pub struct Request {
pub name: String,
pub args: HashMap<String, String>,
}
impl Request {
pub fn new<T: AsRef<str>>(function_name: T, data: Vec<(T, T)>) -> Self {
let hashmap_data: HashMap<String, String> = data
.into_iter()
.map(|(key, value)| (key.as_ref().to_string(), value.as_ref().to_string()))
.collect();
Request {
name: function_name.as_ref().to_string(),
args: hashmap_data,
}
}
pub fn ping() -> Self {
Request::new("ping", vec![])
}
pub fn pause() -> Self {
Request::new("pause", vec![])
}
pub fn resume() -> Self {
Request::new("resume", vec![])
}
pub fn stop() -> Self {
Request::new("stop", vec![])
}
pub fn play(file_path: &str) -> Self {
Request::new("play", vec![("file_path", file_path)])
}
pub fn get_is_paused() -> Self {
Request::new("is_paused", vec![])
}
pub fn get_volume() -> Self {
Request::new("get_volume", vec![])
}
pub fn get_position() -> Self {
Request::new("get_position", vec![])
}
pub fn get_duration() -> Self {
Request::new("get_duration", vec![])
}
pub fn get_state() -> Self {
Request::new("get_state", vec![])
}
pub fn get_current_file_path() -> Self {
Request::new("get_current_file_path", vec![])
}
pub fn get_input() -> Self {
Request::new("get_input", vec![])
}
pub fn get_inputs() -> Self {
Request::new("get_inputs", vec![])
}
pub fn set_volume(volume: f32) -> Self {
Request::new("set_volume", vec![("volume", &volume.to_string())])
}
pub fn seek(position: f32) -> Self {
Request::new("seek", vec![("position", &position.to_string())])
}
pub fn set_input(id: u32) -> Self {
Request::new("set_input", vec![("input_id", &id.to_string())])
}
}
#[derive(Default, Debug, Clone, Serialize, Deserialize)]
pub struct Response {
pub status: bool,
pub message: String,
}
impl Response {
pub fn new<T: AsRef<str>>(status: bool, message: T) -> Self {
Response {
status,
message: message.as_ref().to_string(),
}
}
}