Module split: extract yt-dlp lifecycle helpers to server/ytdlp.js

• 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.
This commit is contained in:
Keysat
2026-05-08 16:54:51 -05:00
parent 4c3cb6a077
commit 9a82fede7a
2 changed files with 109 additions and 85 deletions
+5 -85
View File
@@ -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`);