From 9a82fede7a70467300ae5d71c6e13a43b02b431d Mon Sep 17 00:00:00 2001 From: Keysat Date: Fri, 8 May 2026 16:54:51 -0500 Subject: [PATCH] Module split: extract yt-dlp lifecycle helpers to server/ytdlp.js MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit • checkYtdlp() — version + GitHub-latest check (24h-cached) • autoUpdateYtdlp(dataDir) — multi-strategy update (-U, pip, brew, binary) Module-private memoization (ytdlpVersion, ytdlpLastCheck) now stays inside ytdlp.js where it belongs. autoUpdateYtdlp gained an explicit dataDir parameter — strategy 4 (StartOS binary download to /data/bin/) needs it; passing it in keeps the module pure of caller-side state. server/index.js: 2694 → 2614 lines. Smoke tested: server boots; /api/license-status, /api/health respond. No behavior change. --- server/index.js | 90 +++-------------------------------------- server/ytdlp.js | 104 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 109 insertions(+), 85 deletions(-) create mode 100644 server/ytdlp.js diff --git a/server/index.js b/server/index.js index 2ac70e9..89e1559 100644 --- a/server/index.js +++ b/server/index.js @@ -25,6 +25,7 @@ import { splitAudioFile, downloadPodcastAudio, } from "./audio.js"; +import { checkYtdlp, autoUpdateYtdlp } from "./ytdlp.js"; const execFileAsync = promisify(execFile); const app = express(); @@ -351,88 +352,7 @@ async function saveToHistory(videoId, url, title, chunks, entries, logs, uploadD app.use(express.static(path.join(__dirname, "..", "public"))); app.use("/assets", express.static(path.join(__dirname, "..", "assets"))); -// ── yt-dlp auto-update ───────────────────────────────────────────────────── - -let ytdlpVersion = null; -let ytdlpLastCheck = 0; -const UPDATE_CHECK_INTERVAL = 24 * 60 * 60 * 1000; // 24 hours - -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; - } - - // Check for updates at most once per 24h - 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; -} - -async function autoUpdateYtdlp() { - 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 (e1) { - console.log(" … Self-update failed, trying pip..."); - } - - // Strategy 2: pip3 / pip (works in containers and on macOS) - 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(DATA_DIR, "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 (e4) { - console.log(" ✗ All update strategies failed"); - return { success: false, message: "Auto-update failed. Try updating manually from the StartOS Actions menu." }; - } -} +// checkYtdlp + autoUpdateYtdlp moved to ./ytdlp.js // PRICING + calcCost + buildAnalysisPrompt moved to ./gemini-helpers.js // safeText + retryGemini moved to ./util.js @@ -476,7 +396,7 @@ app.post("/api/shutdown", (req, res) => { // ── Manual update endpoint ───────────────────────────────────────────────── app.post("/api/update-ytdlp", async (req, res) => { - const result = await autoUpdateYtdlp(); + const result = await autoUpdateYtdlp(DATA_DIR); const info = await checkYtdlp(); res.json({ ...result, ...info }); }); @@ -2120,7 +2040,7 @@ app.post("/api/process", async (req, res) => { if (blocked && attempt === MAX_RETRIES) { // Last resort: try yt-dlp auto-update in case there's a newer version that handles this log(1, "All retries exhausted — attempting yt-dlp auto-update as last resort..."); - const updateResult = await autoUpdateYtdlp(); + const updateResult = await autoUpdateYtdlp(DATA_DIR); if (updateResult.success) { log(1, "yt-dlp updated! Final retry..."); try { @@ -2646,7 +2566,7 @@ app.listen(PORT, BIND_HOST, async () => { console.log(` ✓ yt-dlp ${info.version} found`); console.log(` ↑ Update available: ${info.latestVersion}`); console.log(` Auto-updating...`); - const result = await autoUpdateYtdlp(); + const result = await autoUpdateYtdlp(DATA_DIR); if (result.success) { const refreshed = await checkYtdlp(); console.log(` ✓ yt-dlp updated to ${refreshed.version}\n`); diff --git a/server/ytdlp.js b/server/ytdlp.js new file mode 100644 index 0000000..0eab9d4 --- /dev/null +++ b/server/ytdlp.js @@ -0,0 +1,104 @@ +// 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." }; + } +}