Self-hosted Express + Postgres app for organizing STL/3MF/OBJ/etc. with: - Model/file library, tags, collections, print settings - URL import: Thingiverse (API), Printables (GraphQL), MakerWorld (FlareSolverr + token), cubee3d/other (metadata + manual files) - In-browser 3D viewer; 3MF rendered via Python/trimesh GLB conversion - Auto thumbnails, license/commercial-use tracking, in-app editing - Settings (API tokens / FlareSolverr) stored in DB with .env fallback Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
55 lines
2.3 KiB
JavaScript
55 lines
2.3 KiB
JavaScript
// Minimal FlareSolverr client. FlareSolverr runs a headless browser that solves
|
|
// Cloudflare's anti-bot challenge and returns the resolved page HTML.
|
|
// https://github.com/FlareSolverr/FlareSolverr
|
|
|
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
|
|
// opts: { maxTimeout, cookies: [{ name, value, domain }], retries }
|
|
async function solve(flaresolverrUrl, targetUrl, opts = {}) {
|
|
const endpoint = flaresolverrUrl.replace(/\/+$/, '') + '/v1';
|
|
const body = { cmd: 'request.get', url: targetUrl, maxTimeout: opts.maxTimeout || 60000 };
|
|
if (opts.cookies) body.cookies = opts.cookies;
|
|
|
|
// FlareSolverr serializes browser sessions and returns 500 on back-to-back
|
|
// requests, so retry transient failures with a growing backoff.
|
|
const retries = opts.retries ?? 4;
|
|
let lastErr;
|
|
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
if (attempt > 0) await sleep(2000 * attempt);
|
|
try {
|
|
const res = await fetch(endpoint, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(body),
|
|
});
|
|
if (res.status >= 500) { lastErr = new Error(`FlareSolverr HTTP ${res.status}`); continue; }
|
|
if (!res.ok) throw new Error(`FlareSolverr HTTP ${res.status}`);
|
|
const data = await res.json();
|
|
if (data.status !== 'ok' || !data.solution) throw new Error(`FlareSolverr: ${data.message || 'no solution'}`);
|
|
return data.solution.response;
|
|
} catch (err) {
|
|
lastErr = err;
|
|
if (!/HTTP 5\d\d|fetch failed|ECONNRESET|network/i.test(err.message)) throw err;
|
|
}
|
|
}
|
|
throw lastErr;
|
|
}
|
|
|
|
// Like solve(), but for endpoints that return JSON. A real browser wraps JSON
|
|
// in a <pre> tag, so strip tags before parsing.
|
|
async function solveJson(flaresolverrUrl, targetUrl, opts = {}) {
|
|
const html = await solve(flaresolverrUrl, targetUrl, opts);
|
|
// The browser wraps JSON in <pre> and HTML-encodes entities (notably & -> &
|
|
// inside signed URLs), so strip tags and decode before parsing.
|
|
const text = html.replace(/<[^>]*>/g, '')
|
|
.replace(/"/g, '"').replace(/'/g, "'").replace(/</g, '<')
|
|
.replace(/>/g, '>').replace(/&/g, '&')
|
|
.trim();
|
|
try {
|
|
return JSON.parse(text);
|
|
} catch {
|
|
throw new Error('FlareSolverr did not return parseable JSON.');
|
|
}
|
|
}
|
|
|
|
module.exports = { solve, solveJson };
|