1
1
mirror of https://github.com/neosubhamoy/pytubepp-helper.git synced 2026-02-04 11:22:22 +05:30

(feat): added dynamic port config for msghost, new settings page and ui improvements

This commit is contained in:
2025-02-05 20:15:19 +05:30
Verified
parent 279f651b1e
commit d8a191e408
24 changed files with 2236 additions and 449 deletions

30
src-tauri/Cargo.lock generated
View File

@@ -667,6 +667,15 @@ dependencies = [
"crypto-common",
]
[[package]]
name = "directories"
version = "5.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a49173b84e034382284f27f1af4dcbbd231ffa358c0fe316541a7337f376a35"
dependencies = [
"dirs-sys",
]
[[package]]
name = "dirs-next"
version = "2.0.0"
@@ -677,6 +686,18 @@ dependencies = [
"dirs-sys-next",
]
[[package]]
name = "dirs-sys"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c"
dependencies = [
"libc",
"option-ext",
"redox_users",
"windows-sys 0.48.0",
]
[[package]]
name = "dirs-sys-next"
version = "0.1.2"
@@ -2104,6 +2125,12 @@ dependencies = [
"vcpkg",
]
[[package]]
name = "option-ext"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
[[package]]
name = "os_info"
version = "3.8.2"
@@ -2475,6 +2502,7 @@ dependencies = [
name = "pytubepp-helper"
version = "0.6.0"
dependencies = [
"directories",
"fix-path-env",
"futures-util",
"serde",
@@ -2489,6 +2517,8 @@ dependencies = [
name = "pytubepp-helper-msghost"
version = "0.1.0"
dependencies = [
"directories",
"serde",
"serde_json",
"websocket",
]

View File

@@ -12,6 +12,7 @@ tauri-build = { version = "1", features = [] }
[dependencies]
tauri = { version = "1", features = [ "os-all", "process-relaunch", "window-start-dragging", "window-close", "window-unmaximize", "process-exit", "window-show", "window-unminimize", "window-hide", "window-minimize", "window-maximize", "system-tray", "shell-all"] }
directories = "5.0"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tokio = { version = "1.39.2", features = ["full"] }

View File

@@ -9,4 +9,6 @@ edition = "2021"
[dependencies]
websocket = "0.27.1"
directories = "5.0"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"

View File

@@ -0,0 +1,37 @@
use serde::{Deserialize, Serialize};
use std::fs;
use directories::ProjectDirs;
use std::path::PathBuf;
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Config {
pub port: u16,
}
impl Default for Config {
fn default() -> Self {
Self {
port: 3030,
}
}
}
pub fn get_config_dir() -> Option<PathBuf> {
ProjectDirs::from("com", "neosubhamoy", "pytubepp-helper")
.map(|proj_dirs| proj_dirs.config_dir().to_path_buf())
}
pub fn get_config_path() -> Option<PathBuf> {
get_config_dir().map(|dir| dir.join("config.json"))
}
pub fn load_config() -> Config {
if let Some(config_path) = get_config_path() {
if let Ok(content) = fs::read_to_string(config_path) {
if let Ok(config) = serde_json::from_str(&content) {
return config;
}
}
}
Config::default()
}

View File

@@ -1,3 +1,5 @@
mod config;
use config::load_config;
use std::io::{self, Read, Write};
use websocket::client::ClientBuilder;
use websocket::OwnedMessage;
@@ -5,6 +7,11 @@ use std::thread::sleep;
use std::time::Duration;
use serde_json::Value;
fn get_websocket_url() -> String {
let config = load_config();
format!("ws://localhost:{}", config.port)
}
fn connect_with_retry(url: &str, max_attempts: u32) -> Result<websocket::sync::Client<std::net::TcpStream>, Box<dyn std::error::Error>> {
let mut attempts = 0;
loop {
@@ -70,10 +77,10 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
let parsed: Value = serde_json::from_str(&input)?;
let websocket_url = "ws://localhost:3030";
let websocket_url = get_websocket_url();
eprintln!("Attempting to connect to {}", websocket_url);
let mut client = match connect_with_retry(websocket_url, 2) {
let mut client = match connect_with_retry(&websocket_url, 2) {
Ok(client) => client,
Err(e) => {
eprintln!("Failed to connect after multiple attempts: {:?}", e);

54
src-tauri/src/config.rs Normal file
View File

@@ -0,0 +1,54 @@
use serde::{Deserialize, Serialize};
use std::fs;
use directories::ProjectDirs;
use std::path::PathBuf;
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Config {
pub port: u16,
}
impl Default for Config {
fn default() -> Self {
Self {
port: 3030,
}
}
}
pub fn get_config_dir() -> Option<PathBuf> {
ProjectDirs::from("com", "neosubhamoy", "pytubepp-helper")
.map(|proj_dirs| proj_dirs.config_dir().to_path_buf())
}
pub fn get_config_path() -> Option<PathBuf> {
get_config_dir().map(|dir| dir.join("config.json"))
}
pub fn load_config() -> Config {
if let Some(config_path) = get_config_path() {
if let Ok(content) = fs::read_to_string(config_path) {
if let Ok(config) = serde_json::from_str(&content) {
return config;
}
}
}
Config::default()
}
pub fn save_config(config: &Config) -> Result<(), String> {
let config_dir = get_config_dir()
.ok_or_else(|| "Could not determine config directory".to_string())?;
fs::create_dir_all(&config_dir)
.map_err(|e| format!("Failed to create config directory: {}", e))?;
let config_path = config_dir.join("config.json");
let content = serde_json::to_string_pretty(config)
.map_err(|e| format!("Failed to serialize config: {}", e))?;
fs::write(config_path, content)
.map_err(|e| format!("Failed to write config file: {}", e))?;
Ok(())
}

View File

@@ -1,6 +1,8 @@
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
mod config;
use config::{Config, load_config, save_config, get_config_path};
use std::{process::Command, sync::Arc, env, time::Duration};
use serde_json::Value;
use tauri::{CustomMenuItem, Manager, SystemTray, SystemTrayEvent, SystemTrayMenu};
@@ -15,22 +17,134 @@ struct ResponseChannel {
struct WebSocketState {
sender: Option<futures_util::stream::SplitSink<tokio_tungstenite::WebSocketStream<TcpStream>, tokio_tungstenite::tungstenite::Message>>,
response_channel: ResponseChannel,
server_abort: Option<tokio::sync::oneshot::Sender<()>>,
config: Config,
}
async fn is_another_instance_running() -> bool {
match connect_async("ws://127.0.0.1:3030").await {
async fn is_another_instance_running(port: u16) -> bool {
match connect_async(format!("ws://127.0.0.1:{}", port)).await {
Ok(_) => true,
Err(_) => false
}
}
async fn try_bind_ws_port() -> Option<TcpListener> {
match TcpListener::bind("127.0.0.1:3030").await {
async fn try_bind_ws_port(port: u16) -> Option<TcpListener> {
match TcpListener::bind(format!("127.0.0.1:{}", port)).await {
Ok(listener) => Some(listener),
Err(_) => None
}
}
async fn start_websocket_server(app_handle: tauri::AppHandle, port: u16) -> Result<(), String> {
let addr = format!("127.0.0.1:{}", port);
// First ensure any existing server is stopped
{
let state = app_handle.state::<Arc<Mutex<WebSocketState>>>();
let mut state = state.lock().await;
if let Some(old_abort) = state.server_abort.take() {
let _ = old_abort.send(());
// Give it a moment to shut down
sleep(Duration::from_millis(200)).await;
}
}
// Now try to bind to the port
let listener = TcpListener::bind(&addr).await
.map_err(|e| format!("Failed to bind to port {}: {}", port, e))?;
let (abort_sender, mut abort_receiver) = tokio::sync::oneshot::channel();
// Store the new abort sender
{
let state = app_handle.state::<Arc<Mutex<WebSocketState>>>();
let mut state = state.lock().await;
state.server_abort = Some(abort_sender);
}
// Spawn the server task
tokio::spawn(async move {
println!("Starting WebSocket server on port {}", port);
loop {
tokio::select! {
accept_result = listener.accept() => {
match accept_result {
Ok((stream, _)) => {
let app_handle = app_handle.clone();
tokio::spawn(handle_connection(stream, app_handle));
}
Err(e) => {
println!("Error accepting connection: {}", e);
break;
}
}
}
_ = &mut abort_receiver => {
println!("WebSocket server shutting down on port {}...", port);
break;
}
}
}
});
// Wait a moment to ensure the server has started
sleep(Duration::from_millis(100)).await;
Ok(())
}
#[tauri::command]
async fn get_config(state: tauri::State<'_, Arc<Mutex<WebSocketState>>>) -> Result<Config, String> {
let state = state.lock().await;
Ok(state.config.clone())
}
#[tauri::command]
fn get_config_file_path() -> Result<String, String> {
match get_config_path() {
Some(path) => Ok(path.to_string_lossy().into_owned()),
None => Err("Could not determine config path".to_string()),
}
}
#[tauri::command]
async fn update_config(
new_config: Config,
state: tauri::State<'_, Arc<Mutex<WebSocketState>>>,
app_handle: tauri::AppHandle,
) -> Result<Config, String> {
// Save the new config first
save_config(&new_config)?;
// Update the state with new config
{
let mut state = state.lock().await;
state.config = new_config.clone();
}
// Start the new server (this will also handle stopping the old one)
start_websocket_server(app_handle, new_config.port).await?;
Ok(new_config)
}
#[tauri::command]
async fn reset_config(
state: tauri::State<'_, Arc<Mutex<WebSocketState>>>,
app_handle: tauri::AppHandle,
) -> Result<Config, String> {
let config = Config::default();
save_config(&config)?;
{
let mut state = state.lock().await;
state.config = config.clone();
}
start_websocket_server(app_handle, config.port).await?;
Ok(config)
}
#[tauri::command]
async fn send_to_extension(
message: String,
@@ -184,39 +298,41 @@ fn download_stream(url: String, stream: String) {
#[tokio::main]
async fn main() {
let _ = fix_path_env::fix();
let config = load_config();
let port = config.port;
let websocket_state = Arc::new(Mutex::new(WebSocketState {
sender: None,
response_channel: ResponseChannel { sender: None },
server_abort: None,
config,
}));
// Check if another instance is running
if is_another_instance_running().await {
if is_another_instance_running(port).await {
println!("Another instance is already running. Exiting...");
std::process::exit(0);
}
// Try to bind to the WebSocket port with a few retries
let mut listener = None;
let mut port_available = false;
for _ in 0..3 {
if let Some(l) = try_bind_ws_port().await {
listener = Some(l);
if let Some(_) = try_bind_ws_port(port).await {
port_available = true;
break;
}
sleep(Duration::from_millis(100)).await;
}
// If we couldn't bind to the port after retries, assume another instance is running
let listener = match listener {
Some(l) => l,
None => {
println!("Could not bind to WebSocket port. Another instance might be running. Exiting...");
std::process::exit(0);
}
};
if !port_available {
println!("Could not bind to WebSocket port. Another instance might be running. Exiting...");
std::process::exit(0);
}
let args: Vec<String> = env::args().collect();
let start_hidden = args.contains(&"--hidden".to_string());
let websocket_state = Arc::new(Mutex::new(WebSocketState {
sender: None,
response_channel: ResponseChannel { sender: None },
}));
let tray_menu = SystemTrayMenu::new()
.add_item(CustomMenuItem::new("show".to_string(), "Show"))
@@ -257,17 +373,14 @@ async fn main() {
window.hide().unwrap();
}
// Start the initial WebSocket server
let app_handle = app.handle();
let ws_state = websocket_state.clone();
tokio::spawn(async move {
println!("WebSocket server listening on ws://127.0.0.1:3030");
while let Ok((stream, _)) = listener.accept().await {
let app_handle = app_handle.clone();
let ws_state = ws_state.clone();
tokio::spawn(handle_connection(stream, app_handle, ws_state));
if let Err(e) = start_websocket_server(app_handle, port).await {
println!("Failed to start initial WebSocket server: {}", e);
}
});
Ok(())
})
.invoke_handler(tauri::generate_handler![
@@ -275,19 +388,24 @@ async fn main() {
fetch_video_info,
install_program,
download_stream,
receive_frontend_response
receive_frontend_response,
get_config,
update_config,
reset_config,
get_config_file_path
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
async fn handle_connection(stream: TcpStream, app_handle: tauri::AppHandle, ws_state: Arc<Mutex<WebSocketState>>) {
async fn handle_connection(stream: TcpStream, app_handle: tauri::AppHandle) {
let ws_stream = accept_async(stream).await.unwrap();
let (ws_sender, mut ws_receiver) = ws_stream.split();
// Store the sender in the shared state
{
let mut state = ws_state.lock().await;
let state = app_handle.state::<Arc<Mutex<WebSocketState>>>();
let mut state = state.lock().await;
state.sender = Some(ws_sender);
}
@@ -303,7 +421,8 @@ async fn handle_connection(stream: TcpStream, app_handle: tauri::AppHandle, ws_s
// Create a new channel for this request
let (response_sender, response_receiver) = oneshot::channel();
{
let mut state = ws_state.lock().await;
let state = app_handle.state::<Arc<Mutex<WebSocketState>>>();
let mut state = state.lock().await;
state.response_channel.sender = Some(response_sender);
}
@@ -315,7 +434,8 @@ async fn handle_connection(stream: TcpStream, app_handle: tauri::AppHandle, ws_s
.unwrap_or_else(|e| format!("Error receiving response: {:?}", e));
// Send the response back through WebSocket
let mut state = ws_state.lock().await;
let state = app_handle.state::<Arc<Mutex<WebSocketState>>>();
let mut state = state.lock().await;
if let Some(sender) = &mut state.sender {
let _ = sender.send(tokio_tungstenite::tungstenite::Message::Text(response)).await;
}
@@ -327,6 +447,7 @@ async fn handle_connection(stream: TcpStream, app_handle: tauri::AppHandle, ws_s
println!("WebSocket connection closed");
// Remove the sender from the shared state when the connection closes
let mut state = ws_state.lock().await;
let state = app_handle.state::<Arc<Mutex<WebSocketState>>>();
let mut state = state.lock().await;
state.sender = None;
}