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

(refactor): migrated tauri project to v2

This commit is contained in:
2025-02-10 09:27:09 +05:30
Verified
parent 5d3a01cc65
commit 8a7c0a1946
20 changed files with 2471 additions and 1798 deletions

2265
src-tauri/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -4,14 +4,15 @@ version = "0.6.0"
description = "PytubePP Helper"
authors = ["neosubhamoy"]
edition = "2021"
license = "MIT"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[build-dependencies]
tauri-build = { version = "1", features = [] }
tauri-build = { version = "2", 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"] }
tauri = { version = "2", features = ["tray-icon"] }
directories = "5.0"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
@@ -19,11 +20,18 @@ tokio = { version = "1.39.2", features = ["full"] }
tokio-tungstenite = "*"
futures-util = "0.3.30"
fix-path-env = { git = "https://github.com/tauri-apps/fix-path-env-rs" }
tauri-plugin-shell = "2"
tauri-plugin-fs = "2"
tauri-plugin-os = "2"
[features]
# This feature is used for production builds or when a dev server is not specified, DO NOT REMOVE!!
custom-protocol = ["tauri/custom-protocol"]
[lib]
name = "pytubepp_helper_lib"
crate-type = ["staticlib", "cdylib", "rlib"]
[workspace]
members = [
".",

View File

@@ -0,0 +1,18 @@
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "main-capability",
"description": "default permissions",
"local": true,
"windows": [
"main"
],
"permissions": [
"core:default",
"shell:default",
"os:default",
"fs:default",
"core:window:allow-hide",
"fs:allow-app-write",
"fs:allow-app-write-recursive"
]
}

View File

@@ -0,0 +1,98 @@
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "shell-scope",
"description": "allowed shell scopes",
"windows": ["main"],
"permissions": [
{
"identifier": "shell:allow-execute",
"allow": [
{
"name": "detect-windows",
"cmd": "systeminfo",
"args": []
},
{
"name": "detect-macos",
"cmd": "sw_vers",
"args": []
},
{
"name": "detect-distro",
"cmd": "grep",
"args": ["^ID=", "/etc/os-release"]
},
{
"name": "detect-pkgmngr",
"cmd": "sh",
"args": ["-c", "command -v apt || command -v dnf || command -v pacman"]
},
{
"name": "is-apt-installed",
"cmd": "apt",
"args": ["--version"]
},
{
"name": "is-dnf-installed",
"cmd": "dnf",
"args": ["--version"]
},
{
"name": "is-python3-installed",
"cmd": "python3",
"args": ["--version"]
},
{
"name": "is-pip3-installed",
"cmd": "pip3",
"args": ["--version"]
},
{
"name": "is-winget-installed",
"cmd": "winget",
"args": ["--version"]
},
{
"name": "is-homebrew-installed",
"cmd": "brew",
"args": ["--version"]
},
{
"name": "is-python-installed",
"cmd": "python",
"args": ["--version"]
},
{
"name": "is-pip-installed",
"cmd": "pip",
"args": ["--version"]
},
{
"name": "is-ffmpeg-installed",
"cmd": "ffmpeg",
"args": ["-version"]
},
{
"name": "is-nodejs-installed",
"cmd": "node",
"args": ["--version"]
},
{
"name": "is-pytubepp-installed",
"cmd": "pytubepp",
"args": ["--version"]
},
{
"name": "fetch-video-info",
"cmd": "pytubepp",
"args": [{ "validator": "\\S+" }, "--raw-info"]
}
]
}
],
"platforms": [
"windows",
"macOS",
"linux"
]
}

View File

@@ -1,6 +1,6 @@
use directories::ProjectDirs;
use serde::{Deserialize, Serialize};
use std::fs;
use directories::ProjectDirs;
use std::path::PathBuf;
#[derive(Debug, Serialize, Deserialize, Clone)]
@@ -10,9 +10,7 @@ pub struct Config {
impl Default for Config {
fn default() -> Self {
Self {
port: 3030,
}
Self { port: 3030 }
}
}
@@ -34,4 +32,4 @@ pub fn load_config() -> Config {
}
}
Config::default()
}
}

View File

@@ -1,18 +1,21 @@
mod config;
use config::load_config;
use serde_json::Value;
use std::io::{self, Read, Write};
use websocket::client::ClientBuilder;
use websocket::OwnedMessage;
use std::thread::sleep;
use std::time::Duration;
use serde_json::Value;
use websocket::client::ClientBuilder;
use websocket::OwnedMessage;
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>> {
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 {
match ClientBuilder::new(url).unwrap().connect_insecure() {
@@ -26,7 +29,10 @@ fn connect_with_retry(url: &str, max_attempts: u32) -> Result<websocket::sync::C
return Err(Box::new(e));
}
let wait_time = Duration::from_secs(2u64.pow(attempts));
eprintln!("Connection attempt {} failed. Retrying in {:?}...", attempts, wait_time);
eprintln!(
"Connection attempt {} failed. Retrying in {:?}...",
attempts, wait_time
);
sleep(wait_time);
}
}
@@ -57,12 +63,12 @@ fn write_stdout_message(message: &str) -> Result<(), Box<dyn std::error::Error>>
fn main() -> Result<(), Box<dyn std::error::Error>> {
eprintln!("Waiting for message from extension...");
let input = match read_stdin_message() {
Ok(msg) => {
eprintln!("Received message: {}", msg);
msg
},
}
Err(e) => {
eprintln!("Error reading message: {:?}", e);
return Err(e);
@@ -70,10 +76,13 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
};
// Send immediate response to the extension
write_stdout_message(&serde_json::json!({
"status": "received",
"message": "Message received by native host"
}).to_string())?;
write_stdout_message(
&serde_json::json!({
"status": "received",
"message": "Message received by native host"
})
.to_string(),
)?;
let parsed: Value = serde_json::from_str(&input)?;
@@ -84,10 +93,13 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
Ok(client) => client,
Err(e) => {
eprintln!("Failed to connect after multiple attempts: {:?}", e);
write_stdout_message(&serde_json::json!({
"status": "error",
"message": "Failed to connect to Tauri app"
}).to_string())?;
write_stdout_message(
&serde_json::json!({
"status": "error",
"message": "Failed to connect to Tauri app"
})
.to_string(),
)?;
return Err(e);
}
};
@@ -97,14 +109,17 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
// Receive response from Tauri app
let message = client.recv_message()?;
// Send Tauri app's response back to browser extension
if let OwnedMessage::Text(text) = message {
write_stdout_message(&serde_json::json!({
"status": "success",
"response": text
}).to_string())?;
write_stdout_message(
&serde_json::json!({
"status": "success",
"response": text
})
.to_string(),
)?;
}
Ok(())
}
}

View File

@@ -1,6 +1,6 @@
use directories::ProjectDirs;
use serde::{Deserialize, Serialize};
use std::fs;
use directories::ProjectDirs;
use std::path::PathBuf;
#[derive(Debug, Serialize, Deserialize, Clone)]
@@ -10,9 +10,7 @@ pub struct Config {
impl Default for Config {
fn default() -> Self {
Self {
port: 3030,
}
Self { port: 3030 }
}
}
@@ -37,18 +35,17 @@ pub fn load_config() -> Config {
}
pub fn save_config(config: &Config) -> Result<(), String> {
let config_dir = get_config_dir()
.ok_or_else(|| "Could not determine config directory".to_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))?;
fs::write(config_path, content).map_err(|e| format!("Failed to write config file: {}", e))?;
Ok(())
}
}

495
src-tauri/src/lib.rs Normal file
View File

@@ -0,0 +1,495 @@
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
mod config;
use config::{get_config_path, load_config, save_config, Config};
use futures_util::{SinkExt, StreamExt};
use serde_json::Value;
use std::{env, process::Command, sync::Arc, time::Duration};
use tauri::{
menu::{Menu, MenuItem},
tray::{TrayIconBuilder, TrayIconEvent, MouseButton, MouseButtonState},
Manager, Emitter
};
use tokio::{
net::{TcpListener, TcpStream},
sync::{oneshot, Mutex},
time::sleep,
};
use tokio_tungstenite::{accept_async, connect_async};
struct ResponseChannel {
sender: Option<oneshot::Sender<String>>,
}
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(port: u16) -> bool {
match connect_async(format!("ws://127.0.0.1:{}", port)).await {
Ok(_) => true,
Err(_) => false,
}
}
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,
state: tauri::State<'_, Arc<Mutex<WebSocketState>>>,
) -> Result<(), String> {
let mut state = state.lock().await;
if let Some(sender) = &mut state.sender {
sender
.send(tokio_tungstenite::tungstenite::Message::Text(message))
.await
.map_err(|e| format!("Failed to send message: {}", e))?;
Ok(())
} else {
Err("No active WebSocket connection".to_string())
}
}
#[tauri::command]
async fn receive_frontend_response(
response: String,
state: tauri::State<'_, Arc<Mutex<WebSocketState>>>,
) -> Result<(), String> {
let mut state = state.lock().await;
if let Some(sender) = state.response_channel.sender.take() {
sender
.send(response)
.map_err(|e| format!("Failed to send response: {:?}", e))?;
}
Ok(())
}
#[tauri::command]
fn fetch_video_info(url: String) {
#[cfg(target_os = "windows")]
{
let command = format!("pytubepp \"{}\" -i", &url);
Command::new("cmd")
.args(["/k", command.as_str()])
.spawn()
.unwrap();
}
#[cfg(target_os = "linux")]
{
let command = format!("pytubepp \"{}\" -i", &url);
Command::new("gnome-terminal")
.args(["--", "bash", "-c", command.as_str()])
.spawn()
.unwrap();
}
#[cfg(target_os = "macos")]
{
let command = format!("pytubepp \"{}\" -i", &url);
let escaped_command = command.replace("\"", "\\\"");
let applescript = format!(
"tell application \"Terminal\"\n\
do script \"{}\"\n\
activate\n\
end tell",
escaped_command
);
Command::new("osascript")
.arg("-e")
.arg(applescript)
.spawn()
.unwrap();
}
}
#[tauri::command]
fn install_program(icommand: String) {
#[cfg(target_os = "windows")]
{
let command = format!("{}", &icommand);
Command::new("cmd")
.args(["/k", command.as_str()])
.spawn()
.unwrap();
}
#[cfg(target_os = "linux")]
{
let command = format!("{}", &icommand);
Command::new("gnome-terminal")
.args(["--", "bash", "-c", command.as_str()])
.spawn()
.unwrap();
}
#[cfg(target_os = "macos")]
{
let command = format!("{}", &icommand);
let escaped_command = command.replace("\"", "\\\"");
let applescript = format!(
"tell application \"Terminal\"\n\
do script \"{}\"\n\
activate\n\
end tell",
escaped_command
);
Command::new("osascript")
.arg("-e")
.arg(applescript)
.spawn()
.unwrap();
}
}
#[tauri::command]
fn download_stream(url: String, stream: String, caption: Option<String>) {
let caption = caption.unwrap_or("none".to_string());
#[cfg(target_os = "windows")]
{
let command = format!("pytubepp \"{}\" -s {} -c {}", &url, &stream, &caption);
Command::new("cmd")
.args(["/k", command.as_str()])
.spawn()
.unwrap();
}
#[cfg(target_os = "linux")]
{
let command = format!("pytubepp \"{}\" -s {} -c {}", &url, &stream, &caption);
Command::new("gnome-terminal")
.args(["--", "bash", "-c", command.as_str()])
.spawn()
.unwrap();
}
#[cfg(target_os = "macos")]
{
let command = format!("pytubepp \"{}\" -s {} -c {}", &url, &stream, &caption);
let escaped_command = command.replace("\"", "\\\"");
let applescript = format!(
"tell application \"Terminal\"\n\
do script \"{}\"\n\
activate\n\
end tell",
escaped_command
);
Command::new("osascript")
.arg("-e")
.arg(applescript)
.spawn()
.unwrap();
}
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub async fn run() {
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(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 port_available = false;
for _ in 0..3 {
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
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());
tauri::Builder::default()
.plugin(tauri_plugin_os::init())
.plugin(tauri_plugin_fs::init())
.plugin(tauri_plugin_shell::init())
.manage(websocket_state.clone())
.setup(move |app| {
// Create menu items
let quit = MenuItem::with_id(app, "quit", "Quit", true, None::<&str>)
.map_err(|e| format!("Failed to create quit menu item: {}", e))?;
let show = MenuItem::with_id(app, "show", "Show", true, None::<&str>)
.map_err(|e| format!("Failed to create show menu item: {}", e))?;
// Create the menu
let menu = Menu::with_items(app, &[&show, &quit])
.map_err(|e| format!("Failed to create menu: {}", e))?;
// Create and store the tray icon
let tray = TrayIconBuilder::with_id("main")
.icon(app.default_window_icon().unwrap().clone())
.menu(&menu)
.tooltip("PytubePP Helper")
.on_menu_event(|app, event| match event.id.as_ref() {
"show" => {
if let Some(window) = app.get_webview_window("main") {
let _ = window.show();
let _ = window.set_focus();
}
}
"quit" => {
app.exit(0);
}
_ => {}
})
.on_tray_icon_event(|tray, event| {
if let TrayIconEvent::Click {
button: MouseButton::Left,
button_state: MouseButtonState::Up,
..
} = event {
let app = tray.app_handle();
if let Some(window) = app.get_webview_window("main") {
let _ = window.show();
let _ = window.set_focus();
}
}
})
.build(app)
.map_err(|e| format!("Failed to create tray: {}", e))?;
// Store the tray handle in the app state
app.manage(tray);
let window = app.get_webview_window("main").unwrap();
if start_hidden {
window.hide().unwrap();
}
// Start the initial WebSocket server
let app_handle = app.handle().clone();
tokio::spawn(async move {
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![
send_to_extension,
fetch_video_info,
install_program,
download_stream,
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) {
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 state = app_handle.state::<Arc<Mutex<WebSocketState>>>();
let mut state = state.lock().await;
state.sender = Some(ws_sender);
}
println!("New WebSocket connection established");
while let Some(msg) = ws_receiver.next().await {
if let Ok(msg) = msg {
if let Ok(text) = msg.to_text() {
println!("Received message: {}", text);
// Parse the JSON message
if let Ok(json_value) = serde_json::from_str::<Value>(text) {
// Create a new channel for this request
let (response_sender, response_receiver) = oneshot::channel();
{
let state = app_handle.state::<Arc<Mutex<WebSocketState>>>();
let mut state = state.lock().await;
state.response_channel.sender = Some(response_sender);
}
// Emit an event to the frontend
app_handle
.emit_to("main", "websocket-message", json_value)
.unwrap();
// Wait for the response from the frontend
let response = response_receiver
.await
.unwrap_or_else(|e| format!("Error receiving response: {:?}", e));
// Send the response back through WebSocket
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;
}
}
}
}
}
println!("WebSocket connection closed");
// Remove the sender from the shared state when the connection closes
let state = app_handle.state::<Arc<Mutex<WebSocketState>>>();
let mut state = state.lock().await;
state.sender = None;
}

View File

@@ -1,454 +1,6 @@
// 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};
use tokio::{net::{TcpListener, TcpStream}, sync::{Mutex, oneshot}, time::sleep};
use tokio_tungstenite::{accept_async, connect_async};
use futures_util::{SinkExt, StreamExt};
struct ResponseChannel {
sender: Option<oneshot::Sender<String>>,
}
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(port: u16) -> bool {
match connect_async(format!("ws://127.0.0.1:{}", port)).await {
Ok(_) => true,
Err(_) => false
}
}
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,
state: tauri::State<'_, Arc<Mutex<WebSocketState>>>,
) -> Result<(), String> {
let mut state = state.lock().await;
if let Some(sender) = &mut state.sender {
sender.send(tokio_tungstenite::tungstenite::Message::Text(message)).await
.map_err(|e| format!("Failed to send message: {}", e))?;
Ok(())
} else {
Err("No active WebSocket connection".to_string())
}
}
#[tauri::command]
async fn receive_frontend_response(
response: String,
state: tauri::State<'_, Arc<Mutex<WebSocketState>>>,
) -> Result<(), String> {
let mut state = state.lock().await;
if let Some(sender) = state.response_channel.sender.take() {
sender.send(response).map_err(|e| format!("Failed to send response: {:?}", e))?;
}
Ok(())
}
#[tauri::command]
fn fetch_video_info(url: String) {
#[cfg(target_os = "windows")]
{
let command = format!("pytubepp \"{}\" -i", &url);
Command::new("cmd")
.args(["/k", command.as_str()])
.spawn()
.unwrap();
}
#[cfg(target_os = "linux")]
{
let command = format!("pytubepp \"{}\" -i", &url);
Command::new("gnome-terminal")
.args(["--", "bash", "-c", command.as_str()])
.spawn()
.unwrap();
}
#[cfg(target_os = "macos")]
{
let command = format!("pytubepp \"{}\" -i", &url);
let escaped_command = command.replace("\"", "\\\"");
let applescript = format!(
"tell application \"Terminal\"\n\
do script \"{}\"\n\
activate\n\
end tell",
escaped_command
);
Command::new("osascript")
.arg("-e")
.arg(applescript)
.spawn()
.unwrap();
}
}
#[tauri::command]
fn install_program(icommand: String) {
#[cfg(target_os = "windows")]
{
let command = format!("{}", &icommand);
Command::new("cmd")
.args(["/k", command.as_str()])
.spawn()
.unwrap();
}
#[cfg(target_os = "linux")]
{
let command = format!("{}", &icommand);
Command::new("gnome-terminal")
.args(["--", "bash", "-c", command.as_str()])
.spawn()
.unwrap();
}
#[cfg(target_os = "macos")]
{
let command = format!("{}", &icommand);
let escaped_command = command.replace("\"", "\\\"");
let applescript = format!(
"tell application \"Terminal\"\n\
do script \"{}\"\n\
activate\n\
end tell",
escaped_command
);
Command::new("osascript")
.arg("-e")
.arg(applescript)
.spawn()
.unwrap();
}
}
#[tauri::command]
fn download_stream(url: String, stream: String, caption: Option<String>) {
let caption = caption.unwrap_or("none".to_string());
#[cfg(target_os = "windows")]
{
let command = format!("pytubepp \"{}\" -s {} -c {}", &url, &stream, &caption);
Command::new("cmd")
.args(["/k", command.as_str()])
.spawn()
.unwrap();
}
#[cfg(target_os = "linux")]
{
let command = format!("pytubepp \"{}\" -s {} -c {}", &url, &stream, &caption);
Command::new("gnome-terminal")
.args(["--", "bash", "-c", command.as_str()])
.spawn()
.unwrap();
}
#[cfg(target_os = "macos")]
{
let command = format!("pytubepp \"{}\" -s {} -c {}", &url, &stream, &caption);
let escaped_command = command.replace("\"", "\\\"");
let applescript = format!(
"tell application \"Terminal\"\n\
do script \"{}\"\n\
activate\n\
end tell",
escaped_command
);
Command::new("osascript")
.arg("-e")
.arg(applescript)
.spawn()
.unwrap();
}
}
#[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(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 port_available = false;
for _ in 0..3 {
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
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 tray_menu = SystemTrayMenu::new()
.add_item(CustomMenuItem::new("show".to_string(), "Show"))
.add_item(CustomMenuItem::new("quit".to_string(), "Quit"));
let system_tray = SystemTray::new().with_menu(tray_menu).with_tooltip("PytubePP Helper");
tauri::Builder::default()
.system_tray(system_tray)
.on_system_tray_event(|app, event| match event {
SystemTrayEvent::LeftClick {
position: _,
size: _,
..
} => {
let window = app.get_window("main").unwrap();
window.show().unwrap();
window.set_focus().unwrap();
}
SystemTrayEvent::MenuItemClick { id, .. } => match id.as_str() {
"show" => {
let window = app.get_window("main").unwrap();
window.show().unwrap();
window.set_focus().unwrap();
}
"quit" => {
app.exit(0);
}
_ => {}
},
_ => {}
})
.manage(websocket_state.clone())
.setup(move |app| {
let window = app.get_window("main").unwrap();
if start_hidden {
window.hide().unwrap();
}
// Start the initial WebSocket server
let app_handle = app.handle();
tokio::spawn(async move {
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![
send_to_extension,
fetch_video_info,
install_program,
download_stream,
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) {
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 state = app_handle.state::<Arc<Mutex<WebSocketState>>>();
let mut state = state.lock().await;
state.sender = Some(ws_sender);
}
println!("New WebSocket connection established");
while let Some(msg) = ws_receiver.next().await {
if let Ok(msg) = msg {
if let Ok(text) = msg.to_text() {
println!("Received message: {}", text);
// Parse the JSON message
if let Ok(json_value) = serde_json::from_str::<Value>(text) {
// Create a new channel for this request
let (response_sender, response_receiver) = oneshot::channel();
{
let state = app_handle.state::<Arc<Mutex<WebSocketState>>>();
let mut state = state.lock().await;
state.response_channel.sender = Some(response_sender);
}
// Emit an event to the frontend
app_handle.emit_all("websocket-message", json_value).unwrap();
// Wait for the response from the frontend
let response = response_receiver.await
.unwrap_or_else(|e| format!("Error receiving response: {:?}", e));
// Send the response back through WebSocket
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;
}
}
}
}
}
println!("WebSocket connection closed");
// Remove the sender from the shared state when the connection closes
let state = app_handle.state::<Arc<Mutex<WebSocketState>>>();
let mut state = state.lock().await;
state.sender = None;
pytubepp_helper_lib::run().await;
}

View File

@@ -1,39 +1,38 @@
{
"$schema": "https://schema.tauri.app/config/2",
"build": {
"beforeDevCommand": "npm run dev",
"beforeBuildCommand": "npm run build",
"devPath": "http://localhost:1422",
"distDir": "../dist"
"frontendDist": "../dist",
"devUrl": "http://localhost:1422"
},
"package": {
"productName": "pytubepp-helper",
"version": "0.6.0"
"bundle": {
"active": true,
"targets": "all",
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
]
},
"tauri": {
"allowlist": {
"all": false
"productName": "pytubepp-helper",
"mainBinaryName": "pytubepp-helper",
"version": "0.6.0",
"identifier": "com.neosubhamoy.pytubepp.helper",
"plugins": {},
"app": {
"security": {
"csp": null
},
"windows": [
{
"title": "PytubePP Helper",
"width": 500,
"height": 320
"width": 510,
"height": 345,
"useHttpsScheme": true
}
],
"security": {
"csp": null
},
"bundle": {
"active": true,
"targets": "all",
"identifier": "com.neosubhamoy.pytubepp.helper",
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
]
}
]
}
}
}

View File

@@ -2,141 +2,40 @@
"build": {
"beforeDevCommand": "npm run dev && cargo build --manifest-path=./src-tauri/msghost/Cargo.toml",
"beforeBuildCommand": "npm run build && cargo build --release --manifest-path=./src-tauri/msghost/Cargo.toml",
"devPath": "http://localhost:1422",
"distDir": "../dist"
"devUrl": "http://localhost:1422",
"frontendDist": "../dist"
},
"tauri": {
"allowlist": {
"all": false,
"os": {
"all": true
},
"shell": {
"all": true,
"execute": true,
"sidecar": true,
"open": true,
"scope": [
{
"name": "detect-windows",
"cmd": "systeminfo",
"args": []
},
{
"name": "detect-macos",
"cmd": "sw_vers",
"args": []
},
{
"name": "detect-distro",
"cmd": "grep",
"args": ["^ID=", "/etc/os-release"]
},
{
"name": "detect-pkgmngr",
"cmd": "sh",
"args": ["-c", "command -v apt || command -v dnf || command -v pacman"]
},
{
"name": "is-apt-installed",
"cmd": "apt",
"args": ["--version"]
},
{
"name": "is-dnf-installed",
"cmd": "dnf",
"args": ["--version"]
},
{
"name": "is-python3-installed",
"cmd": "python3",
"args": ["--version"]
},
{
"name": "is-pip3-installed",
"cmd": "pip3",
"args": ["--version"]
},
{
"name": "is-winget-installed",
"cmd": "winget",
"args": ["--version"]
},
{
"name": "is-homebrew-installed",
"cmd": "brew",
"args": ["--version"]
},
{
"name": "is-python-installed",
"cmd": "python",
"args": ["--version"]
},
{
"name": "is-pip-installed",
"cmd": "pip",
"args": ["--version"]
},
{
"name": "is-ffmpeg-installed",
"cmd": "ffmpeg",
"args": ["-version"]
},
{
"name": "is-nodejs-installed",
"cmd": "node",
"args": ["--version"]
},
{
"name": "is-pytubepp-installed",
"cmd": "pytubepp",
"args": ["--version"]
},
{
"name": "fetch-video-info",
"cmd": "pytubepp",
"args": [{ "validator": "\\S+" }, "--raw-info"]
}
]
},
"window": {
"all": false,
"close": true,
"hide": true,
"show": true,
"maximize": true,
"minimize": true,
"unmaximize": true,
"unminimize": true,
"startDragging": true
},
"process": {
"all": false,
"exit": true,
"relaunch": true
}
},
"identifier": "com.neosubhamoy.pytubepp.helper",
"plugins": {},
"app": {
"windows": [
{
"title": "PytubePP Helper",
"width": 510,
"height": 345
"height": 345,
"useHttpsScheme": true
}
],
"security": {
"csp": null
},
"bundle": {
"active": true,
"targets": ["deb", "rpm"],
"identifier": "com.neosubhamoy.pytubepp.helper",
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
],
"csp": null,
"capabilities": [
"main-capability",
"shell-scope"
]
}
},
"bundle": {
"active": true,
"targets": ["deb", "rpm"],
"licenseFile": "../LICENSE",
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
],
"linux": {
"deb": {
"depends": ["python3-pip", "ffmpeg", "gnome-terminal"],
"files": {
@@ -150,7 +49,6 @@
"rpm": {
"epoch": 0,
"release": "1",
"license": "MIT",
"depends": ["python3-pip", "ffmpeg", "gnome-terminal"],
"files": {
"/etc/opt/chrome/native-messaging-hosts/com.neosubhamoy.pytubepp.helper.json": "./msghost-manifest/linux/chrome/com.neosubhamoy.pytubepp.helper.json",
@@ -160,10 +58,6 @@
"/etc/xdg/autostart/pytubepp-helper-autostart.desktop": "./autostart/pytubepp-helper-autostart.desktop"
}
}
},
"systemTray": {
"iconPath": "icons/32x32.png",
"iconAsTemplate": true
}
}
}
}

View File

@@ -2,175 +2,71 @@
"build": {
"beforeDevCommand": "[[ -n \"$TARGET_ARCH\" ]] && ARCH=\"$TARGET_ARCH\" || ARCH=\"$(uname -m | sed 's/^arm64$/aarch64/')-apple-darwin\" && cargo build --target=$ARCH --manifest-path=./src-tauri/msghost/Cargo.toml && npm run dev",
"beforeBuildCommand": "[[ -n \"$TARGET_ARCH\" ]] && ARCH=\"$TARGET_ARCH\" || ARCH=\"$(uname -m | sed 's/^arm64$/aarch64/')-apple-darwin\" && cargo build --release --target=$ARCH --manifest-path=./src-tauri/msghost/Cargo.toml && node copyFiles.${ARCH}.js && npm run build",
"devPath": "http://localhost:1422",
"distDir": "../dist"
"devUrl": "http://localhost:1422",
"frontendDist": "../dist"
},
"tauri": {
"allowlist": {
"all": false,
"os": {
"all": true
},
"shell": {
"all": true,
"execute": true,
"sidecar": true,
"open": true,
"scope": [
{
"name": "detect-windows",
"cmd": "systeminfo",
"args": []
},
{
"name": "detect-macos",
"cmd": "sw_vers",
"args": []
},
{
"name": "detect-distro",
"cmd": "grep",
"args": ["^ID=", "/etc/os-release"]
},
{
"name": "detect-pkgmngr",
"cmd": "sh",
"args": ["-c", "command -v apt || command -v dnf || command -v pacman"]
},
{
"name": "is-apt-installed",
"cmd": "apt",
"args": ["--version"]
},
{
"name": "is-dnf-installed",
"cmd": "dnf",
"args": ["--version"]
},
{
"name": "is-python3-installed",
"cmd": "python3",
"args": ["--version"]
},
{
"name": "is-pip3-installed",
"cmd": "pip3",
"args": ["--version"]
},
{
"name": "is-winget-installed",
"cmd": "winget",
"args": ["--version"]
},
{
"name": "is-homebrew-installed",
"cmd": "brew",
"args": ["--version"]
},
{
"name": "is-python-installed",
"cmd": "python",
"args": ["--version"]
},
{
"name": "is-pip-installed",
"cmd": "pip",
"args": ["--version"]
},
{
"name": "is-ffmpeg-installed",
"cmd": "ffmpeg",
"args": ["-version"]
},
{
"name": "is-nodejs-installed",
"cmd": "node",
"args": ["--version"]
},
{
"name": "is-pytubepp-installed",
"cmd": "pytubepp",
"args": ["--version"]
},
{
"name": "fetch-video-info",
"cmd": "pytubepp",
"args": [{ "validator": "\\S+" }, "--raw-info"]
}
]
},
"fs": {
"all": true,
"copyFile": true,
"exists": true,
"createDir": true,
"scope": [
"$HOME/Library/LaunchAgents/",
"$HOME/Library/Application Support/Google/Chrome/NativeMessagingHosts/",
"$HOME/Library/Application Support/Chromium/NativeMessagingHosts/",
"$HOME/Library/Application Support/Mozilla/NativeMessagingHosts/",
"$RESOURCE/pytubepp-helper-msghost.json",
"$RESOURCE/pytubepp-helper-msghost-moz.json",
"$RESOURCE/pytubepp-helper-msghost",
"$RESOURCE/pytubepp-helper-autostart.plist"
]
},
"path": {
"all": true
},
"window": {
"all": false,
"close": true,
"hide": true,
"show": true,
"maximize": true,
"minimize": true,
"unmaximize": true,
"unminimize": true,
"startDragging": true
},
"process": {
"all": false,
"exit": true,
"relaunch": true
}
},
"identifier": "com.neosubhamoy.pytubepp.helper",
"plugins": {},
"app": {
"windows": [
{
"title": "PytubePP Helper",
"width": 515,
"height": 365
"height": 365,
"useHttpsScheme": true
}
],
"security": {
"csp": null
},
"bundle": {
"active": true,
"targets": ["app", "dmg"],
"identifier": "com.neosubhamoy.pytubepp.helper",
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
],
"macOS": {
"minimumSystemVersion": "10.13",
"license": "../LICENSE",
"providerShortName": "neosubhamoy"
},
"resources": [
"pytubepp-helper-msghost.json",
"pytubepp-helper-msghost-moz.json",
"pytubepp-helper-msghost",
"pytubepp-helper-autostart.plist"
"csp": null,
"capabilities": [
"main-capability",
"shell-scope",
{
"identifier": "fs-scope",
"description": "allowed file system scopes",
"permissions": [
{
"identifier": "fs:scope",
"allow": [
{ "path": "$HOME/Library/LaunchAgents/" },
{ "path": "$HOME/Library/Application Support/Google/Chrome/NativeMessagingHosts/" },
{ "path": "$HOME/Library/Application Support/Chromium/NativeMessagingHosts/" },
{ "path": "$HOME/Library/Application Support/Mozilla/NativeMessagingHosts/" },
{ "path": "$HOME/Library/LaunchAgents/*" },
{ "path": "$HOME/Library/Application Support/Google/Chrome/NativeMessagingHosts/*" },
{ "path": "$HOME/Library/Application Support/Chromium/NativeMessagingHosts/*" },
{ "path": "$HOME/Library/Application Support/Mozilla/NativeMessagingHosts/*" },
{ "path": "$RESOURCE/pytubepp-helper-msghost.json" },
{ "path": "$RESOURCE/pytubepp-helper-msghost-moz.json" },
{ "path": "$RESOURCE/pytubepp-helper-msghost" },
{ "path": "$RESOURCE/pytubepp-helper-autostart.plist" }
]
}
]
}
]
},
"systemTray": {
"iconPath": "icons/32x32.png",
"iconAsTemplate": true
}
},
"bundle": {
"active": true,
"targets": ["app", "dmg"],
"licenseFile": "../LICENSE",
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
],
"macOS": {
"minimumSystemVersion": "10.13",
"providerShortName": "neosubhamoy"
},
"resources": [
"pytubepp-helper-msghost.json",
"pytubepp-helper-msghost-moz.json",
"pytubepp-helper-msghost",
"pytubepp-helper-autostart.plist"
]
}
}

View File

@@ -2,167 +2,66 @@
"build": {
"beforeDevCommand": "cargo build --manifest-path=./src-tauri/msghost/Cargo.toml && npm run dev",
"beforeBuildCommand": "cargo build --release --manifest-path=./src-tauri/msghost/Cargo.toml && node copyFiles.js && npm run build",
"devPath": "http://localhost:1422",
"distDir": "../dist"
"devUrl": "http://localhost:1422",
"frontendDist": "../dist"
},
"tauri": {
"allowlist": {
"all": false,
"os": {
"all": true
},
"shell": {
"all": true,
"execute": true,
"sidecar": true,
"open": true,
"scope": [
{
"name": "detect-windows",
"cmd": "systeminfo",
"args": []
},
{
"name": "detect-macos",
"cmd": "sw_vers",
"args": []
},
{
"name": "detect-distro",
"cmd": "grep",
"args": ["^ID=", "/etc/os-release"]
},
{
"name": "detect-pkgmngr",
"cmd": "sh",
"args": ["-c", "command -v apt || command -v dnf || command -v pacman"]
},
{
"name": "is-apt-installed",
"cmd": "apt",
"args": ["--version"]
},
{
"name": "is-dnf-installed",
"cmd": "dnf",
"args": ["--version"]
},
{
"name": "is-python3-installed",
"cmd": "python3",
"args": ["--version"]
},
{
"name": "is-pip3-installed",
"cmd": "pip3",
"args": ["--version"]
},
{
"name": "is-winget-installed",
"cmd": "winget",
"args": ["--version"]
},
{
"name": "is-homebrew-installed",
"cmd": "brew",
"args": ["--version"]
},
{
"name": "is-python-installed",
"cmd": "python",
"args": ["--version"]
},
{
"name": "is-pip-installed",
"cmd": "pip",
"args": ["--version"]
},
{
"name": "is-ffmpeg-installed",
"cmd": "ffmpeg",
"args": ["-version"]
},
{
"name": "is-nodejs-installed",
"cmd": "node",
"args": ["--version"]
},
{
"name": "is-pytubepp-installed",
"cmd": "pytubepp",
"args": ["--version"]
},
{
"name": "fetch-video-info",
"cmd": "pytubepp",
"args": [{ "validator": "\\S+" }, "--raw-info"]
}
]
},
"fs": {
"scope": [
"$RESOURCE/pytubepp-helper-msghost.json",
"$RESOURCE/pytubepp-helper-msghost-moz.json",
"$RESOURCE/pytubepp-helper-msghost.exe"
]
},
"window": {
"all": false,
"close": true,
"hide": true,
"show": true,
"maximize": true,
"minimize": true,
"unmaximize": true,
"unminimize": true,
"startDragging": true
},
"process": {
"all": false,
"exit": true,
"relaunch": true
}
},
"identifier": "com.neosubhamoy.pytubepp.helper",
"plugins": {},
"app": {
"windows": [
{
"title": "PytubePP Helper",
"width": 510,
"height": 345
"height": 345,
"useHttpsScheme": true
}
],
"security": {
"csp": null
},
"bundle": {
"active": true,
"targets": ["msi", "nsis"],
"identifier": "com.neosubhamoy.pytubepp.helper",
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
],
"windows": {
"wix": {
"fragmentPaths": ["installer/windows/wix-fragment-registry.wxs"],
"componentRefs": ["PytubeppHelperFragmentRegistryEntries"],
"license": "../LICENSE"
},
"nsis": {
"template": "installer/windows/nsis-template.nsi"
"csp": null,
"capabilities": [
"main-capability",
"shell-scope",
{
"identifier": "fs-scope",
"description": "allowed file system scopes",
"permissions": [
{
"identifier": "fs:scope",
"allow": [
{ "path": "$RESOURCE/pytubepp-helper-msghost.json" },
{ "path": "$RESOURCE/pytubepp-helper-msghost-moz.json" },
{ "path": "$RESOURCE/pytubepp-helper-msghost.exe" }
]
}
]
}
},
"resources": [
"pytubepp-helper-msghost.json",
"pytubepp-helper-msghost-moz.json",
"pytubepp-helper-msghost.exe"
]
},
"systemTray": {
"iconPath": "icons/icon.ico",
"iconAsTemplate": true
}
},
"bundle": {
"active": true,
"targets": ["msi", "nsis"],
"licenseFile": "../LICENSE",
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
],
"windows": {
"wix": {
"fragmentPaths": ["installer/windows/wix-fragment-registry.wxs"],
"componentRefs": ["PytubeppHelperFragmentRegistryEntries"]
},
"nsis": {
"template": "installer/windows/nsis-template.nsi"
}
},
"resources": [
"pytubepp-helper-msghost.json",
"pytubepp-helper-msghost-moz.json",
"pytubepp-helper-msghost.exe"
]
}
}