From 28a95ee3303915c255e28632fe4c66460d9a356b Mon Sep 17 00:00:00 2001 From: dlawler489 <104159223@student.swin.edu.au> Date: Tue, 15 Sep 2026 18:17:46 +1000 Subject: [PATCH] Say why MakerWorld files didn't download instead of importing silently without them MakerWorld answers an expired or missing auth token with {"error": "Please log in to download models."} and no download link, which was skipped without a word, so imports and "Download files from source" just ended with no files. Now the reason comes back: missing settings, a refused token (probably expired), no printable files on the page, or a failed download. The import screen shows it, and the Settings hint explains where the token comes from. Co-Authored-By: Claude Opus 5 --- lib/importers/makerworld.js | 13 ++++++++++++- public/app.js | 11 +++++++---- public/index.html | 6 ++++-- server.js | 39 ++++++++++++++++++++++++++++++++----- test/makerworld.test.js | 27 +++++++++++++++++++++++++ 5 files changed, 84 insertions(+), 12 deletions(-) create mode 100644 test/makerworld.test.js 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'); +});