// yt-dlp lifecycle helpers: version check, GitHub-latest comparison, and // multi-strategy auto-update. Self-contained — module-private memoization // for the GitHub check (capped to once per 24h to avoid hammering the // API). import { execFile } from "child_process"; import { promisify } from "util"; import path from "path"; import fs from "fs/promises"; const execFileAsync = promisify(execFile); let ytdlpVersion = null; let ytdlpLastCheck = 0; const UPDATE_CHECK_INTERVAL = 24 * 60 * 60 * 1000; // 24 hours // ── Version + update-availability check ───────────────────────────────────── // Returns `{ installed, version, updateAvailable, latestVersion }`. // Suppresses errors — a missing yt-dlp returns `installed: false`, a // failed GitHub call just leaves `latestVersion` as null. The GitHub // fetch is rate-limited to once per 24h. export async function checkYtdlp() { const info = { installed: false, version: null, updateAvailable: false, latestVersion: null }; try { const { stdout } = await execFileAsync("yt-dlp", ["--version"]); info.installed = true; info.version = stdout.trim(); ytdlpVersion = info.version; } catch { return info; } const now = Date.now(); if (now - ytdlpLastCheck > UPDATE_CHECK_INTERVAL) { ytdlpLastCheck = now; try { const resp = await fetch("https://api.github.com/repos/yt-dlp/yt-dlp/releases/latest", { headers: { "Accept": "application/vnd.github.v3+json" }, signal: AbortSignal.timeout(5000), }); if (resp.ok) { const data = await resp.json(); info.latestVersion = data.tag_name?.replace(/^v/, "") || null; if (info.latestVersion && info.version !== info.latestVersion) { info.updateAvailable = true; } } } catch {} // Network errors are fine, just skip the check } return info; } // ── Multi-strategy auto-update ────────────────────────────────────────────── // Tries strategies in order until one works: // 1. yt-dlp -U (built-in self-update, when permitted) // 2. pip3/pip install -U (containers, macOS) // 3. brew upgrade yt-dlp (macOS local dev) // 4. Direct binary download (StartOS fallback to /data/bin/) // // `dataDir` is needed only for strategy 4. Returns `{ success, message }`. export async function autoUpdateYtdlp(dataDir) { console.log(" ↻ Updating yt-dlp..."); // Strategy 1: yt-dlp's built-in self-update try { const { stdout } = await execFileAsync("yt-dlp", ["-U"], { timeout: 60000 }); console.log(` ${stdout.trim()}`); return { success: true, message: stdout.trim() }; } catch { console.log(" … Self-update failed, trying pip..."); } // Strategy 2: pip3 / pip for (const pip of ["pip3", "pip"]) { try { await execFileAsync(pip, ["install", "-U", "yt-dlp"], { timeout: 120000 }); console.log(` ✓ Updated via ${pip}`); return { success: true, message: `Updated via ${pip}` }; } catch {} } // Strategy 3: Homebrew (macOS local dev only) try { await execFileAsync("brew", ["upgrade", "yt-dlp"], { timeout: 120000 }); console.log(" ✓ Updated via Homebrew"); return { success: true, message: "Updated via Homebrew" }; } catch {} // Strategy 4: Direct binary download to persistent storage (StartOS fallback) try { const binDir = path.join(dataDir, "bin"); await fs.mkdir(binDir, { recursive: true }); const binPath = path.join(binDir, "yt-dlp"); console.log(" … Trying direct binary download..."); const { stdout } = await execFileAsync("sh", ["-c", `curl -L -o "${binPath}" "https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp" && chmod +x "${binPath}" && "${binPath}" --version` ], { timeout: 120000 }); console.log(` ✓ Downloaded yt-dlp binary: ${stdout.trim()}`); return { success: true, message: `Downloaded binary: ${stdout.trim()}` }; } catch { console.log(" ✗ All update strategies failed"); return { success: false, message: "Auto-update failed. Try updating manually from the StartOS Actions menu." }; } }