diff --git a/lib/importers/makerworld.js b/lib/importers/makerworld.js
index d056310..a81eed2 100644
--- a/lib/importers/makerworld.js
+++ b/lib/importers/makerworld.js
@@ -35,10 +35,21 @@ async function resolveDownload(flaresolverrUrl, instanceId, token) {
});
}
+// Why a download-link answer came back without a link, in words you can act on;
+// null when it has one. Not logged in: {"code":1,"error":"Please log in to download models."}
+function downloadProblem(answer) {
+ if (answer && answer.url) return null;
+ const detail = String((answer && (answer.error || answer.message || answer.msg)) || '').trim();
+ if (/log ?in|sign ?in|unauthori[sz]ed|token|expired/i.test(detail)) {
+ return 'MakerWorld didn’t accept your auth token (it has probably expired). Paste a fresh one in Settings → MakerWorld auth token, then use “Download files from source”.';
+ }
+ return `MakerWorld didn’t give a download link${detail ? `: ${detail}` : ''}`;
+}
+
// 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 };
+module.exports = { parseInstances, resolveDownload, downloadProblem, parseLicense };
diff --git a/public/app.js b/public/app.js
index 56fd91a..72da659 100644
--- a/public/app.js
+++ b/public/app.js
@@ -2252,10 +2252,13 @@ async function handleImport(e) {
if (res.ok) {
await loadData();
if (data.metadataOnly) {
- status.textContent = data.metadataFetched
- ? `Imported "${data.name}". Opening it so you can add the downloaded files…`
- : `Couldn't read that page (the site blocks automated access), so I created a stub named "${data.name}". Opening it so you can add files…`;
- setTimeout(() => { closeModal(importModal); openModelDetail(data.id); }, 1200);
+ status.textContent = data.filesError
+ ? `Imported "${data.name}", but its files couldn’t be downloaded: ${data.filesError}`
+ : data.metadataFetched
+ ? `Imported "${data.name}". Opening it so you can add the downloaded files…`
+ : `Couldn't read that page (the site blocks automated access), so I created a stub named "${data.name}". Opening it so you can add files…`;
+ // Give a download problem time to be read before moving on.
+ setTimeout(() => { closeModal(importModal); openModelDetail(data.id); }, data.filesError ? 6000 : 1200);
} else {
status.textContent = `Imported "${data.name}" with ${data.files.length} file(s).`;
setTimeout(() => closeModal(importModal), 900);
diff --git a/public/index.html b/public/index.html
index 8487601..23dc9ce 100644
--- a/public/index.html
+++ b/public/index.html
@@ -320,8 +320,10 @@
placeholder="Bearer token from your logged-in session" autocomplete="off">
Status: —.
- Lets Printhive download files from MakerWorld using your account. Paste the bearer
- token from your logged-in browser session (see instructions in chat).
+ Lets Printhive download files from MakerWorld using your account. In a browser signed in to
+ makerworld.com, open the developer tools (Application / Storage → Cookies → makerworld.com)
+ and copy the value of the token cookie. It expires after a while: paste a new one if
+ MakerWorld downloads stop working.
diff --git a/server.js b/server.js
index eb18843..ea69ea8 100644
--- a/server.js
+++ b/server.js
@@ -444,6 +444,7 @@ app.post('/api/models/:id/download-files', async (req, res, next) => {
// original name is already present.
async function fetchSourceFiles(model, skipNames) {
if (model.source_site === 'makerworld') {
+ await requireMakerworldSettings(); // before the page fetch, which fails without FlareSolverr
const html = await fetchPageHtml(model.source_url);
return downloadMakerworldFiles(html, skipNames);
}
@@ -828,11 +829,13 @@ app.post('/api/import', async (req, res, next) => {
// Download the actual files where we can (MakerWorld via FlareSolverr+token,
// Printables via its public GraphQL API), and capture the license.
let files = [];
+ let filesError = null; // why no files could be downloaded, shown after the import
let license = meta.license || null;
let commercial = null;
if (detected.site === 'makerworld' && html) {
files = await downloadMakerworldFiles(html).catch((err) => {
console.warn('MakerWorld file download failed:', err.message);
+ filesError = err.message;
return [];
});
license = makerworld.parseLicense(html) || license;
@@ -840,6 +843,7 @@ app.post('/api/import', async (req, res, next) => {
} else if (detected.site === 'printables' && detected.id) {
const result = await downloadPrintablesFiles(detected.id).catch((err) => {
console.warn('Printables file download failed:', err.message);
+ filesError = err.message;
return { files: [] };
});
files = result.files;
@@ -869,7 +873,7 @@ app.post('/api/import', async (req, res, next) => {
files,
});
fingerprint.kick();
- res.status(201).json({ ...publicModel(model), metadataOnly: files.length === 0, metadataFetched });
+ res.status(201).json({ ...publicModel(model), metadataOnly: files.length === 0, metadataFetched, filesError });
} catch (e) { next(e); }
});
@@ -918,14 +922,28 @@ async function resolveCommercial(licenseName, fallback) {
// Download every printable instance of a MakerWorld model (needs token + FlareSolverr).
// skipNames: original filenames already present (so a re-download only adds new ones).
-async function downloadMakerworldFiles(html, skipNames = new Set()) {
+// MakerWorld downloads need both a FlareSolverr address and your auth token.
+async function requireMakerworldSettings() {
const { value: token } = await resolveSetting('makerworld_token');
const { value: flareUrl } = await resolveSetting('flaresolverr_url');
- if (!token || !flareUrl) return [];
+ if (!token || !flareUrl) {
+ throw Object.assign(new Error('Add your MakerWorld auth token and FlareSolverr URL in Settings to download MakerWorld files.'), { status: 400 });
+ }
+ return { token, flareUrl };
+}
+
+// Downloads a MakerWorld model's files. When nothing could be downloaded it throws
+// with the reason (missing settings, an expired token, …) so the import can say why.
+async function downloadMakerworldFiles(html, skipNames = new Set()) {
+ const { token, flareUrl } = await requireMakerworldSettings();
const instances = makerworld.parseInstances(html);
+ if (!instances.length) {
+ throw Object.assign(new Error('Couldn’t find any printable files on the MakerWorld page.'), { status: 502 });
+ }
ensureUploadDir();
const files = [];
+ const problems = [];
for (let i = 0; i < instances.length; i++) {
if (i > 0) await sleep(1200); // give FlareSolverr's browser room between calls
const inst = instances[i];
@@ -934,18 +952,29 @@ async function downloadMakerworldFiles(html, skipNames = new Set()) {
dl = await makerworld.resolveDownload(flareUrl, inst.id, token);
} catch (err) {
console.warn(`MakerWorld instance ${inst.id} resolve failed:`, err.message);
+ problems.push(`Couldn’t ask MakerWorld for a download link: ${err.message}`);
+ continue;
+ }
+ const problem = makerworld.downloadProblem(dl);
+ if (problem) {
+ console.warn(`MakerWorld instance ${inst.id}: ${problem}`);
+ problems.push(problem);
continue;
}
- if (!dl || !dl.url) continue;
const original = dl.name || `instance-${inst.id}.3mf`;
if (skipNames.has(original)) continue;
const resp = await fetch(dl.url);
- if (!resp.ok) { console.warn(`MakerWorld CDN download ${resp.status} for ${original}`); continue; }
+ if (!resp.ok) {
+ console.warn(`MakerWorld CDN download ${resp.status} for ${original}`);
+ problems.push(`Downloading ${original} from MakerWorld failed (HTTP ${resp.status})`);
+ continue;
+ }
const fileName = uniqueName(original);
const filePath = path.join(UPLOAD_DIR, fileName);
fs.writeFileSync(filePath, Buffer.from(await resp.arrayBuffer()));
files.push(describeFile(filePath, fileName, original));
}
+ if (!files.length && problems.length) throw Object.assign(new Error(problems[0]), { status: 502 });
return files;
}
diff --git a/test/makerworld.test.js b/test/makerworld.test.js
new file mode 100644
index 0000000..7795a7c
--- /dev/null
+++ b/test/makerworld.test.js
@@ -0,0 +1,27 @@
+// MakerWorld importer helpers (lib/importers/makerworld.js).
+const test = require('node:test');
+const assert = require('node:assert/strict');
+const makerworld = require('../lib/importers/makerworld');
+
+const page = (design) =>
+ ``;
+
+test('printable instances and the license are read from the page data', () => {
+ const html = page({
+ license: 'Standard Digital File License',
+ instances: [{ id: 650088, title: 'AMS - 0.4 Nozzle - 0.08 Layer' }, { id: 650089 }],
+ });
+ assert.deepEqual(makerworld.parseInstances(html), [
+ { id: 650088, title: 'AMS - 0.4 Nozzle - 0.08 Layer' },
+ { id: 650089, title: '' },
+ ]);
+ assert.equal(makerworld.parseLicense(html), 'Standard Digital File License');
+ assert.deepEqual(makerworld.parseInstances('Just a moment...'), []);
+});
+
+test('a download answer without a link says why', () => {
+ assert.equal(makerworld.downloadProblem({ name: 'bookmarks.3mf', url: 'https://cdn.example/bookmarks.3mf' }), null);
+ assert.match(makerworld.downloadProblem({ code: 1, error: 'Please log in to download models.' }), /token \(it has probably expired\)/);
+ assert.equal(makerworld.downloadProblem({ code: 7, error: 'Model not found' }), 'MakerWorld didn’t give a download link: Model not found');
+ assert.equal(makerworld.downloadProblem(null), 'MakerWorld didn’t give a download link');
+});