236f36aae6
- JWT-based app authentication with user roles, folder/route access control - Dashboard with storage stats, health checks, and recent activity - Auto-download/scrape scheduler (12h interval) with per-user and per-job configs - Video upload, tagging, HLS transcoding, and detail pages - New scrapers: LeakGallery, Mega (megajs), yt-dlp - FlareSolverr integration for Cloudflare-protected sites - Gallery: advanced filtering (date, size, search), sort modes, equal-mix shuffle - Forum sites management with stored cookies/auth - GridWall/GridCell components for responsive media grid - Media API with folder-access permissions Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
129 lines
4.4 KiB
JavaScript
129 lines
4.4 KiB
JavaScript
import { Router } from 'express';
|
|
import { exec } from 'child_process';
|
|
import { promisify } from 'util';
|
|
import path from 'path';
|
|
import { fileURLToPath } from 'url';
|
|
import { getForumSiteById, updateForumSite } from './db.js';
|
|
|
|
const execAsync = promisify(exec);
|
|
const router = Router();
|
|
const FLARESOLVERR_URL = process.env.FLARESOLVERR_URL || 'http://localhost:8191';
|
|
const CHROMIUM_PATH = process.env.CHROMIUM_PATH || '/usr/bin/chromium-browser';
|
|
|
|
const __filename = fileURLToPath(import.meta.url);
|
|
const __dirname = path.dirname(__filename);
|
|
|
|
/**
|
|
* Refresh forum cookies using undetected_chromedriver (Python).
|
|
* Runs login_helper.py via xvfb-run so Chrome runs in headed mode
|
|
* with a virtual display — this is what lets Turnstile auto-solve.
|
|
*/
|
|
export async function refreshForumCookies(siteId) {
|
|
const site = getForumSiteById(siteId);
|
|
if (!site) throw new Error(`Forum site ${siteId} not found`);
|
|
if (!site.username || !site.password) {
|
|
throw new Error('Forum site has no saved credentials — set username and password first');
|
|
}
|
|
|
|
const baseUrl = site.base_url || 'https://simpcity.su';
|
|
const loginUrl = `${baseUrl}/login/`;
|
|
const helperPath = path.join(__dirname, 'login_helper.py');
|
|
|
|
console.log(`[flaresolverr] Refreshing cookies for site ${siteId} (${site.name})`);
|
|
console.log(`[flaresolverr] Login URL: ${loginUrl}`);
|
|
|
|
// Run the Python helper with xvfb-run for virtual display
|
|
// Escape arguments for shell safety
|
|
const escapedUrl = loginUrl.replace(/'/g, "'\\''");
|
|
const escapedUser = site.username.replace(/'/g, "'\\''");
|
|
const escapedPass = site.password.replace(/'/g, "'\\''");
|
|
|
|
const cmd = `xvfb-run --auto-servernum --server-args='-screen 0 1920x1080x24' python3 '${helperPath}' '${escapedUrl}' '${escapedUser}' '${escapedPass}'`;
|
|
|
|
try {
|
|
const { stdout, stderr } = await execAsync(cmd, {
|
|
timeout: 120000, // 2 minutes
|
|
maxBuffer: 10 * 1024 * 1024,
|
|
env: { ...process.env, CHROMIUM_PATH },
|
|
});
|
|
|
|
// Log stderr (debug output from login_helper.py)
|
|
if (stderr) {
|
|
for (const line of stderr.split('\n').filter(Boolean)) {
|
|
console.log(`[flaresolverr] ${line}`);
|
|
}
|
|
}
|
|
|
|
// Parse JSON from stdout
|
|
const result = JSON.parse(stdout.trim());
|
|
|
|
if (!result.ok) {
|
|
throw new Error(result.error || 'Login failed');
|
|
}
|
|
|
|
// Update DB with new cookies
|
|
const expiresAt = new Date(Date.now() + 25 * 24 * 60 * 60 * 1000).toISOString();
|
|
updateForumSite(siteId, {
|
|
cookies: result.cookies,
|
|
cookie_expires_at: expiresAt,
|
|
});
|
|
|
|
console.log(`[flaresolverr] Cookie refresh successful for site ${siteId}`);
|
|
return result.cookies;
|
|
} catch (err) {
|
|
// If execAsync fails, the error might have stderr info
|
|
if (err.stderr) {
|
|
for (const line of err.stderr.split('\n').filter(Boolean)) {
|
|
console.error(`[flaresolverr] ${line}`);
|
|
}
|
|
}
|
|
// Try to parse stdout for a structured error
|
|
if (err.stdout) {
|
|
try {
|
|
const result = JSON.parse(err.stdout.trim());
|
|
if (result.error) throw new Error(result.error);
|
|
} catch (parseErr) {
|
|
// Not JSON, use original error
|
|
}
|
|
}
|
|
throw new Error(`Cookie refresh failed: ${err.message}`);
|
|
}
|
|
}
|
|
|
|
// --- API Endpoints ---
|
|
|
|
// Manual cookie refresh
|
|
router.post('/api/flaresolverr/refresh/:siteId', async (req, res) => {
|
|
const siteId = parseInt(req.params.siteId, 10);
|
|
try {
|
|
const cookieStr = await refreshForumCookies(siteId);
|
|
res.json({ ok: true, cookies: cookieStr });
|
|
} catch (err) {
|
|
console.error(`[flaresolverr] Refresh failed for site ${siteId}:`, err.message);
|
|
res.status(500).json({ error: err.message });
|
|
}
|
|
});
|
|
|
|
// Check if cookie refresh is available (Chromium + xvfb-run installed)
|
|
router.get('/api/flaresolverr/status', async (_req, res) => {
|
|
try {
|
|
// Check for xvfb-run and chromium
|
|
await execAsync('which xvfb-run && which chromium-browser || which chromium', { timeout: 5000 });
|
|
// Check for undetected_chromedriver python package
|
|
await execAsync('python3 -c "import undetected_chromedriver"', { timeout: 5000 });
|
|
res.json({ available: true });
|
|
} catch {
|
|
// Fallback: check FlareSolverr service
|
|
try {
|
|
const resp = await fetch(`${FLARESOLVERR_URL}/health`, {
|
|
signal: AbortSignal.timeout(5000),
|
|
});
|
|
res.json({ available: resp.ok });
|
|
} catch {
|
|
res.json({ available: false, error: 'Neither undetected_chromedriver nor FlareSolverr available' });
|
|
}
|
|
}
|
|
});
|
|
|
|
export default router;
|