printhive/lib/importers/thingiverse.js
dlawler489 a37db15a3f Initial commit: Printhive 3D-print file library
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>
2026-06-22 13:15:17 +10:00

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 };