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>
67 lines
2.7 KiB
JavaScript
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 };
|