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 <noreply@anthropic.com>
This commit is contained in:
parent
f80078d7ed
commit
28a95ee330
5 changed files with 84 additions and 12 deletions
|
|
@ -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 };
|
||||
|
|
|
|||
|
|
@ -2252,10 +2252,13 @@ async function handleImport(e) {
|
|||
if (res.ok) {
|
||||
await loadData();
|
||||
if (data.metadataOnly) {
|
||||
status.textContent = data.metadataFetched
|
||||
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…`;
|
||||
setTimeout(() => { closeModal(importModal); openModelDetail(data.id); }, 1200);
|
||||
// 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);
|
||||
|
|
|
|||
|
|
@ -320,8 +320,10 @@
|
|||
placeholder="Bearer token from your logged-in session" autocomplete="off">
|
||||
<p class="form-hint">
|
||||
Status: <span class="setting-status" data-status="makerworld_token">—</span>.
|
||||
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 <code>token</code> cookie. It expires after a while: paste a new one if
|
||||
MakerWorld downloads stop working.
|
||||
</p>
|
||||
<button type="button" class="btn btn-link clear-setting" data-setting="makerworld_token">Clear token</button>
|
||||
</div>
|
||||
|
|
|
|||
39
server.js
39
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;
|
||||
}
|
||||
|
||||
|
|
|
|||
27
test/makerworld.test.js
Normal file
27
test/makerworld.test.js
Normal file
|
|
@ -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) =>
|
||||
`<html><script id="__NEXT_DATA__" type="application/json">${JSON.stringify({ props: { pageProps: { design } } })}</script></html>`;
|
||||
|
||||
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('<html><title>Just a moment...</title></html>'), []);
|
||||
});
|
||||
|
||||
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');
|
||||
});
|
||||
Loading…
Reference in a new issue