Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2688,6 +2688,12 @@ async fn ensure_ytdlp(app: tauri::AppHandle) {
ytdlp::ensure(app).await;
}

/// Throttled update check for a process that remains alive in the tray.
#[tauri::command]
async fn check_ytdlp_update(app: tauri::AppHandle) {
ytdlp::check_update(app).await;
}

/// Run yt-dlp to resolve a videoId into metadata JSON.
#[tauri::command]
fn resolve_stream_ytdlp(app: tauri::AppHandle, video_id: String) -> Result<String, String> {
Expand Down Expand Up @@ -3715,6 +3721,7 @@ pub fn run() {
.manage(lastfm::LastfmState::default())
.invoke_handler(tauri::generate_handler![
ensure_ytdlp,
check_ytdlp_update,
resolve_stream_ytdlp,
get_stream_base_url,
start_login,
Expand Down
17 changes: 15 additions & 2 deletions src-tauri/src/ytdlp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,10 @@ fn emit_state(app: &tauri::AppHandle, phase: &str, message: Option<String>) {
);
}

// Setup, manual retries and the long-session timer can overlap. One lock keeps
// them from updating or replacing the managed binary concurrently.
static ENSURE_LOCK: tokio::sync::Mutex<()> = tokio::sync::Mutex::const_new(());

/// Idempotent "make yt-dlp available" entry point. Called from the
/// frontend on every launch (so the webview's event listener is
/// guaranteed to be mounted before any state event fires) and safe to
Expand All @@ -128,8 +132,7 @@ fn emit_state(app: &tauri::AppHandle, phase: &str, message: Option<String>) {
/// Emits `ytdlp-state` events: `downloading` → `ready` | `error`.
pub async fn ensure(app: tauri::AppHandle) {
// Serialize concurrent calls (StrictMode double-mount, retry spam).
static LOCK: tokio::sync::Mutex<()> = tokio::sync::Mutex::const_new(());
let _guard = LOCK.lock().await;
let _guard = ENSURE_LOCK.lock().await;

let managed = managed_path(&app);

Expand Down Expand Up @@ -176,6 +179,16 @@ pub async fn ensure(app: tauri::AppHandle) {
}
}

/// Check an existing managed copy for an update without repeating first-run
/// setup, readiness events, or macOS prewarming and legacy cleanup.
pub async fn check_update(app: tauri::AppHandle) {
let _guard = ENSURE_LOCK.lock().await;
let managed = managed_path(&app);
if managed.exists() {
maybe_self_update(&managed).await;
}
}

/// True when a bare `yt-dlp --version` spawn succeeds (PATH install).
async fn probe_path_install() -> bool {
let mut cmd = tokio::process::Command::new("yt-dlp");
Expand Down
27 changes: 23 additions & 4 deletions src/lib/ytdlp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ type YtdlpState = {
};

const TOAST_ID = "ytdlp-setup";
// `ensure_ytdlp` does no network work until its 72-hour stamp expires. Polling
// hourly keeps a tray-resident process close to that cadence without turning
// a launch-time check into a second scheduler on the Rust side.
const UPDATE_POLL_INTERVAL_MS = 60 * 60 * 1000;

/**
* Mount once in AppShell. Kicks off `ensure_ytdlp` on the Rust side
Expand All @@ -28,6 +32,15 @@ export function useYtdlpSetup(): void {
useEffect(() => {
let cancelled = false;
let dispose: (() => void) | undefined;
let updateTimer: number | undefined;

const runYtdlpCommand = (
command: "ensure_ytdlp" | "check_ytdlp_update",
) => {
void invoke(command).catch((err) => {
console.error(`[ytdlp] ${command} failed:`, err);
});
};

void listen<YtdlpState>("ytdlp-state", (e) => {
const { phase, message } = e.payload;
Expand All @@ -51,7 +64,7 @@ export function useYtdlpSetup(): void {
action: {
label: "Retry",
onClick: () => {
void invoke("ensure_ytdlp");
runYtdlpCommand("ensure_ytdlp");
},
},
});
Expand All @@ -63,14 +76,20 @@ export function useYtdlpSetup(): void {
}
dispose = un;
// Listener is live — safe to start the Rust side now.
void invoke("ensure_ytdlp").catch((err) => {
console.error("[ytdlp] ensure_ytdlp failed:", err);
});
runYtdlpCommand("ensure_ytdlp");
// Closing the main window hides this process to the tray by default,
// so it may not launch again for weeks. Keep checking while it lives;
// the Rust-side stamp makes every early poll a local no-op.
updateTimer = window.setInterval(
() => runYtdlpCommand("check_ytdlp_update"),
UPDATE_POLL_INTERVAL_MS,
);
});

return () => {
cancelled = true;
dispose?.();
if (updateTimer !== undefined) window.clearInterval(updateTimer);
};
}, []);
}
Loading