printhive/lib/importers/printables.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

67 lines
2.7 KiB
JavaScript

// Printables importer. Printables exposes a public GraphQL API at
// api.printables.com/graphql — the file list and per-file download links are
// available anonymously for free models. A bearer token (optional) is sent when
// configured, for anything that needs an account.
const ENDPOINT = 'https://api.printables.com/graphql/';
async function gql(query, variables, token) {
const headers = { 'Content-Type': 'application/json', Origin: 'https://www.printables.com' };
if (token) headers.Authorization = `Bearer ${token}`;
const res = await fetch(ENDPOINT, { method: 'POST', headers, body: JSON.stringify({ query, variables }) });
if (!res.ok) throw new Error(`Printables GraphQL HTTP ${res.status}`);
const json = await res.json();
if (json.errors) throw new Error('Printables GraphQL: ' + json.errors.map((e) => e.message).join('; '));
return json.data;
}
const PRINT_QUERY = `query($id: ID!) {
print(id: $id) {
id name
license { name abbreviation content }
stls { id name fileSize }
slas { id name fileSize }
otherFiles { id name fileSize }
}
}`;
// Returns { name, license, licenseContent, files: [{ id, name, fileType }] }.
// (Commercial use is decided by the caller from licenseContent — the on-page
// excludeCommercialUsage flag is unreliable, e.g. false for the strictly
// non-commercial "Standard Digital File License".)
async function fetchModel(printId, token) {
const data = await gql(PRINT_QUERY, { id: String(printId) }, token);
const p = data.print;
if (!p) throw new Error('Print not found');
const files = [];
(p.stls || []).forEach((f) => files.push({ id: f.id, name: f.name, fileType: 'stl' }));
(p.slas || []).forEach((f) => files.push({ id: f.id, name: f.name, fileType: 'sla' }));
(p.otherFiles || []).forEach((f) => files.push({ id: f.id, name: f.name, fileType: 'other' }));
return {
name: p.name,
license: p.license ? p.license.name : null,
licenseContent: p.license ? p.license.content : null,
files,
};
}
const DOWNLOAD_MUTATION = `mutation($printId: ID!, $files: [DownloadFileInput], $src: DownloadSourceEnum!) {
getDownloadLink(printId: $printId, files: $files, source: $src) {
ok
output { link }
}
}`;
// Resolve one file to a direct CDN download URL.
async function getDownloadLink(printId, fileType, fileId, token) {
const data = await gql(
DOWNLOAD_MUTATION,
{ printId: String(printId), files: [{ fileType, ids: [String(fileId)] }], src: 'model_detail' },
token
);
const r = data.getDownloadLink;
if (!r || !r.ok || !r.output || !r.output.link) throw new Error('No download link returned');
return r.output.link;
}
module.exports = { fetchModel, getDownloadLink };