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
1.9 KiB
JavaScript
55 lines
1.9 KiB
JavaScript
// Thingiverse importer — uses the official REST API (https://www.thingiverse.com/developers).
|
|
// Needs a free app token in THINGIVERSE_TOKEN.
|
|
|
|
const API = 'https://api.thingiverse.com';
|
|
|
|
// Accepts .../thing:12345, .../thing:12345/files, or a bare id. Returns id or null.
|
|
function parse(url) {
|
|
if (/^\d+$/.test(url.trim())) return url.trim();
|
|
const m = String(url).match(/thing:(\d+)/i);
|
|
return m ? m[1] : null;
|
|
}
|
|
|
|
async function api(pathname, token) {
|
|
const res = await fetch(`${API}${pathname}`, {
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
});
|
|
if (!res.ok) {
|
|
throw new Error(`Thingiverse API ${res.status} for ${pathname}`);
|
|
}
|
|
return res.json();
|
|
}
|
|
|
|
// Returns a normalized model description + the list of files to download.
|
|
async function fetchModel(thingId, token) {
|
|
const [thing, files, tags] = await Promise.all([
|
|
api(`/things/${thingId}`, token),
|
|
api(`/things/${thingId}/files`, token),
|
|
api(`/things/${thingId}/tags`, token).catch(() => []),
|
|
]);
|
|
|
|
const creator = thing.creator || {};
|
|
return {
|
|
source_site: 'thingiverse',
|
|
source_id: String(thingId),
|
|
source_url: thing.public_url || `https://www.thingiverse.com/thing:${thingId}`,
|
|
name: thing.name || `Thing ${thingId}`,
|
|
description: stripHtml(thing.description || ''),
|
|
license: thing.license || null,
|
|
designer: creator.name
|
|
? { name: creator.name, source_site: 'thingiverse', profile_url: creator.public_url }
|
|
: null,
|
|
tags: (tags || []).map((t) => t.name).filter(Boolean),
|
|
downloads: (files || []).map((f) => ({
|
|
name: f.name,
|
|
// download_url is an API endpoint that 302s to the real file; needs the token.
|
|
url: `${f.download_url}?access_token=${encodeURIComponent(token)}`,
|
|
})),
|
|
};
|
|
}
|
|
|
|
function stripHtml(html) {
|
|
return String(html).replace(/<[^>]*>/g, '').replace(/\s+\n/g, '\n').trim();
|
|
}
|
|
|
|
module.exports = { parse, fetchModel };
|