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>
44 lines
1.8 KiB
JavaScript
44 lines
1.8 KiB
JavaScript
// MakerWorld file downloader. MakerWorld is behind Cloudflare and its downloads
|
|
// are auth-gated, so we go through FlareSolverr (solves Cloudflare) carrying the
|
|
// user's `token` cookie (authenticates). The download API returns a short-lived
|
|
// signed CDN URL, which we then fetch directly.
|
|
|
|
const flaresolverr = require('../flaresolverr');
|
|
|
|
// A model page embeds its printable "instances" in __NEXT_DATA__. Each instance
|
|
// id is what the download API takes.
|
|
function parseInstances(html) {
|
|
const m = html.match(/<script id="__NEXT_DATA__"[^>]*>([\s\S]*?)<\/script>/);
|
|
if (!m) return [];
|
|
let data;
|
|
try { data = JSON.parse(m[1]); } catch { return []; }
|
|
|
|
let result = [];
|
|
(function walk(node) {
|
|
if (result.length || !node || typeof node !== 'object') return;
|
|
if (Array.isArray(node.instances) && node.instances.length && typeof node.instances[0].id === 'number') {
|
|
result = node.instances
|
|
.filter((x) => x && typeof x.id === 'number')
|
|
.map((x) => ({ id: x.id, title: x.title || '' }));
|
|
return;
|
|
}
|
|
for (const key of Object.keys(node)) { walk(node[key]); if (result.length) return; }
|
|
})(data);
|
|
return result;
|
|
}
|
|
|
|
// Resolve an instance to { name, url } (a signed, short-lived CDN download URL).
|
|
async function resolveDownload(flaresolverrUrl, instanceId, token) {
|
|
const api = `https://makerworld.com/api/v1/design-service/instance/${instanceId}/f3mf?type=download&fileType=&devModelName=N1`;
|
|
return flaresolverr.solveJson(flaresolverrUrl, api, {
|
|
cookies: [{ name: 'token', value: token, domain: '.makerworld.com' }],
|
|
});
|
|
}
|
|
|
|
// Pull the license string out of the page data (e.g. "MakerWorld Exclusive License").
|
|
function parseLicense(html) {
|
|
const m = html.match(/"license"\s*:\s*"([^"]+)"/);
|
|
return m ? m[1] : null;
|
|
}
|
|
|
|
module.exports = { parseInstances, resolveDownload, parseLicense };
|