v0.1.1: fix .app bundle PATH for pyenv/nvm, detect yt-dlp pot-provider plugin

- fix_path_env() now adds ~/.pyenv/shims (with PYENV_ROOT) before
  Homebrew paths so pyenv-managed yt-dlp (with pip-installed plugins)
  takes priority over Homebrew's bare yt-dlp binary
- Also handles ~/.nvm, ~/.deno/bin, ~/.cargo/bin for broader coverage
- check_pot_plugin uses lightweight 'yt-dlp -v' instead of network
  simulation for reliable plugin detection
- Bump version to 0.1.1

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-22 14:09:23 -04:00
parent 1ea7f19668
commit d46973a719
10 changed files with 1001 additions and 17 deletions

2
src-tauri/Cargo.lock generated
View File

@@ -1281,7 +1281,7 @@ dependencies = [
[[package]]
name = "gui-video-clipper"
version = "0.1.0"
version = "0.1.1"
dependencies = [
"axum",
"dirs",

View File

@@ -1,6 +1,6 @@
[package]
name = "gui-video-clipper"
version = "0.1.0"
version = "0.1.1"
description = "A macOS GUI app for clipping online videos"
authors = ["cottongin"]
edition = "2021"

View File

@@ -2,11 +2,89 @@ mod commands;
mod models;
mod services;
use std::collections::HashSet;
use std::path::PathBuf;
use commands::{dependencies, export, media_analysis, video};
use tauri::Manager;
struct MediaServerPort(u16);
/// Prepend well-known macOS tool directories to PATH so that Homebrew-installed
/// binaries (ffmpeg, yt-dlp, etc.) are discoverable when the app is launched as
/// a standalone .app bundle (which inherits only a minimal system PATH).
///
/// Also sets PYENV_ROOT when pyenv is present so that pyenv shims can resolve
/// to the correct Python (and its pip-installed yt-dlp plugins).
fn fix_path_env() {
let home = std::env::var("HOME").unwrap_or_default();
let mut extra_dirs: Vec<PathBuf> = Vec::new();
if !home.is_empty() {
let home_path = PathBuf::from(&home);
// pyenv shims — must come before /opt/homebrew/bin so the pyenv-managed
// yt-dlp (which knows about pip-installed plugins) wins over Homebrew's.
let pyenv_root = home_path.join(".pyenv");
if pyenv_root.is_dir() {
extra_dirs.push(pyenv_root.join("shims"));
if std::env::var("PYENV_ROOT").is_err() {
std::env::set_var("PYENV_ROOT", &pyenv_root);
}
}
// nvm — node/deno installed via nvm live under a versioned directory
let nvm_dir = home_path.join(".nvm/versions/node");
if nvm_dir.is_dir() {
if let Ok(entries) = std::fs::read_dir(&nvm_dir) {
// Pick the lexicographically last (highest) version
let mut versions: Vec<PathBuf> = entries
.filter_map(|e| e.ok())
.map(|e| e.path().join("bin"))
.filter(|p| p.is_dir())
.collect();
versions.sort();
if let Some(latest) = versions.pop() {
extra_dirs.push(latest);
}
}
}
// User-local installs
for rel in [".local/bin", ".cargo/bin", ".deno/bin"] {
extra_dirs.push(home_path.join(rel));
}
}
// System-wide Homebrew / MacPorts paths
for dir in [
"/opt/homebrew/bin",
"/opt/homebrew/sbin",
"/usr/local/bin",
"/usr/local/sbin",
] {
extra_dirs.push(PathBuf::from(dir));
}
// Filter to directories that actually exist
let extra_dirs: Vec<PathBuf> = extra_dirs.into_iter().filter(|p| p.is_dir()).collect();
let current = std::env::var("PATH").unwrap_or_default();
let current_dirs: HashSet<&str> = current.split(':').collect();
let mut new_path = String::new();
for dir in &extra_dirs {
let s = dir.to_string_lossy();
if !current_dirs.contains(s.as_ref()) {
new_path.push_str(&s);
new_path.push(':');
}
}
new_path.push_str(&current);
std::env::set_var("PATH", &new_path);
}
#[tauri::command]
fn get_media_server_port(state: tauri::State<'_, MediaServerPort>) -> u16 {
state.0
@@ -20,6 +98,8 @@ pub fn run() {
.plugin(tauri_plugin_store::Builder::new().build())
.plugin(tauri_plugin_process::init())
.setup(|app| {
fix_path_env();
// Bind synchronously (no async needed), then spawn on Tauri's runtime
let (port, listener) = services::media_server::bind_media_server()
.expect("Failed to bind media server");

View File

@@ -54,17 +54,11 @@ pub fn check_js_runtime() -> Option<String> {
check_tool_exists("deno").or_else(|| check_tool_exists("node"))
}
pub fn check_pot_plugin(cookie_source: &CookieSource) -> bool {
let mut command = Command::new("yt-dlp");
command
.arg("-v")
.arg("--simulate")
.arg("--remote-components")
.arg("ejs:github")
.args(cookie_source.to_ytdlp_args())
.arg("https://www.youtube.com/watch?v=dQw4w9WgXcQ");
let output = command.output();
pub fn check_pot_plugin(_cookie_source: &CookieSource) -> bool {
// Use `yt-dlp -v` (no URL) which lists loaded plugins without network access.
// The old approach ran a full YouTube simulation (~9s, network-dependent) which
// failed silently in the .app bundle context.
let output = Command::new("yt-dlp").arg("-v").output();
match output {
Ok(out) => {
let stderr = String::from_utf8_lossy(&out.stderr);

View File

@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "GUI Video Clipper",
"version": "0.1.0",
"version": "0.1.1",
"identifier": "xyz.cottongin.gui-video-clipper",
"build": {
"beforeDevCommand": "npm run dev",
@@ -34,7 +34,10 @@
},
"bundle": {
"active": true,
"targets": ["app", "dmg"],
"targets": [
"app",
"dmg"
],
"icon": [
"icons/32x32.png",
"icons/128x128.png",