Module split: extract cookies state + helpers + routes to server/cookies.js

• initCookies({ dataDir, envPath })  — boot-time setup; reads .env's
                                         YT_COOKIES_FROM and probes for
                                         cookies.txt
  • ytCookieArgs() / ytExtraArgs()     — yt-dlp arg builders
  • ytCookieMethod()                   — human-readable active method
  • setupCookieRoutes(app)             — registers the four /api/cookies/*
                                         routes (upload / delete / test /
                                         status)

Module owns its private state (browser-name, file-exists, file-path).
Upload and delete routes flip the file-exists flag inside the same
module, so subsequent yt-dlp calls reflect the change immediately
without callers re-reading.

server/index.js: 2614 → 2510 lines.

Smoke tested: server boots; /api/license-status and /api/health
respond. /api/cookies/status returns the existing license-gated 402
for unlicensed callers (unchanged behavior).
This commit is contained in:
Keysat
2026-05-08 16:57:03 -05:00
parent 9a82fede7a
commit 2c655dc9ee
2 changed files with 164 additions and 115 deletions
+11 -115
View File
@@ -26,6 +26,13 @@ import {
downloadPodcastAudio,
} from "./audio.js";
import { checkYtdlp, autoUpdateYtdlp } from "./ytdlp.js";
import {
initCookies,
ytCookieArgs,
ytExtraArgs,
ytCookieMethod,
setupCookieRoutes,
} from "./cookies.js";
const execFileAsync = promisify(execFile);
const app = express();
@@ -94,47 +101,8 @@ setInterval(() => {
refreshServerApiKey("config poll").catch(() => {});
}, CONFIG_POLL_MS);
// ── YouTube cookies (bypass bot detection) ──────────────────────────────
// Priority: 1) cookies.txt file 2) --cookies-from-browser (local dev only) 3) no cookies
// On StartOS: cookies.txt lives in DATA_DIR
// On local dev: cookies.txt in project root, or --cookies-from-browser via YT_COOKIES_FROM
let ytCookiesBrowser = "";
const ytCookiesFilePath = path.join(DATA_DIR, "cookies.txt");
let ytCookiesFileExists = false;
try {
const envContent2 = await fs.readFile(envPath, "utf-8").catch(() => "");
const cm = envContent2.match(/^YT_COOKIES_FROM=(.+)$/m);
if (cm) ytCookiesBrowser = cm[1].trim().replace(/^["']|["']$/g, "").toLowerCase();
} catch {}
try {
await fs.access(ytCookiesFilePath);
ytCookiesFileExists = true;
console.log(" 🍪 Found cookies.txt — will use for YouTube authentication");
} catch {
ytCookiesFileExists = false;
}
function ytCookieArgs() {
// Prefer cookies.txt file (stable, doesn't depend on browser session)
if (ytCookiesFileExists) return ["--cookies", ytCookiesFilePath];
// Fall back to live browser cookies (local dev only, not available on StartOS)
if (ytCookiesBrowser) return ["--cookies-from-browser", ytCookiesBrowser];
return [];
}
// Extra yt-dlp args for robustness (rate limiting)
function ytExtraArgs() {
const args = [];
// Sleep between requests to avoid rate limiting during batch operations
args.push("--sleep-interval", "1", "--max-sleep-interval", "3");
return args;
}
function ytCookieMethod() {
if (ytCookiesFileExists) return "cookies.txt";
if (ytCookiesBrowser) return ytCookiesBrowser;
return "none";
}
// Cookies state + helpers + routes moved to ./cookies.js
await initCookies({ dataDir: DATA_DIR, envPath });
function resolveApiKey(clientKey) {
// Client can send "USE_SERVER_KEY" or empty to use the server's stored key
@@ -403,80 +371,8 @@ app.post("/api/update-ytdlp", async (req, res) => {
// ── Cookie management endpoints ───────────────────────────────────────────
// Upload cookies.txt content
app.post("/api/cookies/upload", express.text({ type: "*/*", limit: "2mb" }), async (req, res) => {
try {
const content = req.body;
if (!content || typeof content !== "string" || content.length < 20) {
return res.status(400).json({ error: "Invalid cookie file content" });
}
// Basic validation: should look like a Netscape cookie file
const firstLine = content.split("\n")[0].trim();
const looksValid = /^#.*cookie|^#.*http|^\./i.test(firstLine) || content.includes("\tTRUE\t") || content.includes("\tFALSE\t");
if (!looksValid) {
return res.status(400).json({ error: "File doesn't look like a valid Netscape cookies.txt file. The first line should start with '# Netscape HTTP Cookie File' or '# HTTP Cookie File'." });
}
await fs.writeFile(ytCookiesFilePath, content, "utf-8");
ytCookiesFileExists = true;
console.log(" 🍪 cookies.txt uploaded via web UI");
res.json({ ok: true, message: "Cookies saved successfully" });
} catch (err) {
res.status(500).json({ error: err.message });
}
});
// Delete cookies.txt
app.post("/api/cookies/delete", async (req, res) => {
try {
await fs.unlink(ytCookiesFilePath).catch(() => {});
ytCookiesFileExists = false;
console.log(" 🍪 cookies.txt deleted via web UI");
res.json({ ok: true });
} catch (err) {
res.status(500).json({ error: err.message });
}
});
// Test if cookies work by trying a lightweight yt-dlp operation
app.post("/api/cookies/test", async (req, res) => {
try {
const cookieArgs = ytCookieArgs();
if (cookieArgs.length === 0) {
return res.json({ ok: false, error: "No cookies configured" });
}
// Try to fetch title of a known video — lightweight, no download
const { stdout } = await execFileAsync("yt-dlp", [
"--print", "%(title)s",
"--no-download",
...cookieArgs,
"https://www.youtube.com/watch?v=dQw4w9WgXcQ", // well-known test video
], { timeout: 20000 });
const title = stdout.trim();
if (title && title.length > 0) {
res.json({ ok: true, message: `Cookies working! Fetched: "${title}"` });
} else {
res.json({ ok: false, error: "yt-dlp returned empty response" });
}
} catch (err) {
const msg = (err.stderr || "") + " " + (err.message || "");
const isAuth = /Sign in|bot|403/i.test(msg);
res.json({ ok: false, error: isAuth ? "Cookies are expired or invalid — YouTube still requires sign-in" : msg.slice(0, 300) });
}
});
// Get cookie status
app.get("/api/cookies/status", async (req, res) => {
const info = { method: ytCookieMethod() };
if (ytCookiesFileExists) {
try {
const stat = await fs.stat(ytCookiesFilePath);
info.fileAgeDays = Math.floor((Date.now() - stat.mtimeMs) / (1000 * 60 * 60 * 24));
info.fileExpiring = info.fileAgeDays > 12;
info.fileSize = stat.size;
} catch {}
}
res.json(info);
});
// /api/cookies/* routes registered via setupCookieRoutes (./cookies.js)
setupCookieRoutes(app);
// ── History endpoints ─────────────────────────────────────────────────────