feat(extension): send MakerWorld page data with imports

Sends the model page's design data (title, tags, description, creator, cover,
gallery, categories) with a MakerWorld import, so the backend doesn't have to
fetch the page again for them -- only when that data is this model's, and
small enough to send.
This commit is contained in:
Tautvydas Derzinskas committed 2026-09-28 08:19:43 +02:00
1 parent 616a8b82c3
commit c3a189ea7f
4 files changed
+45 -8

No files matched your search

+8 -2
View File
@@ -26,9 +26,15 @@ export async function importSingle({ url, entries, collectionId, resolved, title
// backend skip its own resolution, the only two places able to trip MakerWorld's CAPTCHA and its
// 2-hour account-wide lockout. The profile id tells the backend which MakerWorld print profile
// the file is, so importing another profile of a model already in the library adds it as a
// second file instead of being skipped as a duplicate. Omitted when nothing was resolved.
// second file instead of being skipped as a duplicate, and the page's design data spares the
// backend fetching the model page for its details (often Cloudflare-blocked for a server).
// Omitted when nothing was resolved.
const extra = resolved?.downloadUrl
? { resolved_download_url: resolved.downloadUrl, resolved_instance_id: resolved.instanceId || null }
? {
resolved_download_url: resolved.downloadUrl,
resolved_instance_id: resolved.instanceId || null,
makerworld_design: resolved.design ?? null,
}
: null;
let print: Print | null;
@@ -11,7 +11,7 @@
import type { ResolvedDownload } from "../../shared/messages";
import { send } from "../../shared/messages";
import { parseMakerworldModelUrl } from "../../shared/urls";
import { pickMakerworldInstanceId, readMakerworldDesignForPage } from "./pageData";
import { makerworldDesignForImport, pickMakerworldInstanceId, readMakerworldDesignForPage } from "./pageData";
// Mirrors every outbound fetch on the backend side (all built on an AbortController timeout) -- a
// plain fetch() has no timeout at all, so a MakerWorld endpoint that stalls or holds an interactive
@@ -92,12 +92,14 @@ async function resolveFromPageApi(pageUrl: string): Promise<ResolvedDownload | n
const instanceId = pickMakerworldInstanceId(design, page.requestedInstanceId);
if (instanceId) {
const downloadUrl = await fetchInstanceDownloadUrl(instanceId, nonce);
if (downloadUrl) return { downloadUrl, instanceId };
if (downloadUrl) return { downloadUrl, instanceId, design: makerworldDesignForImport(design) };
}
// Model-level, i.e. the default profile's file.
const apiUrl = `https://makerworld.com/api/v1/models/${String(design.id)}/download`;
const downloadUrl = extractDownloadUrl(await fetchMakerworldApiJson(apiUrl, nonce));
return downloadUrl ? { downloadUrl, instanceId: pickMakerworldInstanceId(design, null) } : null;
return downloadUrl
? { downloadUrl, instanceId: pickMakerworldInstanceId(design, null), design: makerworldDesignForImport(design) }
: null;
}
/** The single entry point: tries the real-click capture first (not a guess, so far higher
@@ -110,7 +112,11 @@ export async function resolveMakerworldDownloadUrl(pageUrl: string): Promise<Res
// hash. Without trustworthy page data, the hash is all there is to go on.
const page = readMakerworldDesignForPage(pageUrl);
if (!page) return { downloadUrl: viaClick, instanceId: parseMakerworldModelUrl(pageUrl)?.requestedInstanceId ?? null };
return { downloadUrl: viaClick, instanceId: pickMakerworldInstanceId(page.design, page.requestedInstanceId) };
return {
downloadUrl: viaClick,
instanceId: pickMakerworldInstanceId(page.design, page.requestedInstanceId),
design: makerworldDesignForImport(page.design),
};
}
return resolveFromPageApi(pageUrl).catch(() => null);
}
@@ -122,5 +128,5 @@ export async function resolveMakerworldProfileDownload(pageUrl: string, instance
const page = readMakerworldDesignForPage(pageUrl);
if (!page) return null;
const downloadUrl = await fetchInstanceDownloadUrl(instanceId, page.nonce).catch(() => null);
return downloadUrl ? { downloadUrl, instanceId } : null;
return downloadUrl ? { downloadUrl, instanceId, design: makerworldDesignForImport(page.design) } : null;
}
@@ -112,6 +112,29 @@ export function makerworldProfileIds(design: MakerworldDesign, scope: Makerworld
return [primary, ...wanted.filter((id) => id !== primary)];
}
// A design's details past this size (a very long description, say) aren't sent -- the request
// would outgrow the backend's JSON body limit, and the backend reads the page itself instead.
const MAX_IMPORT_DESIGN_CHARS = 64 * 1024;
/** The parts of the page's design an import uses -- title, tags, description, creator, cover,
* gallery, categories -- sent with the import so the backend doesn't have to fetch this page
* again for them (see the backend's makerworldMetaFromExtension). Null when too big to send. */
export function makerworldDesignForImport(design: MakerworldDesign): Record<string, unknown> | null {
const source = design as Record<string, unknown>;
const out: Record<string, unknown> = {};
for (const key of ["id", "title", "tags", "summary", "coverUrl", "coverPortrait", "coverLandscape", "designCreator"]) {
if (source[key] !== undefined) out[key] = source[key];
}
const pictures = getPath(source, "designExtension", "design_pictures");
if (Array.isArray(pictures)) {
out.designExtension = {
design_pictures: pictures.map((picture) => ({ name: getPath(picture, "name"), url: getPath(picture, "url") })),
};
}
if (Array.isArray(source.categories)) out.categories = source.categories.map((category) => ({ id: getPath(category, "id") }));
return JSON.stringify(out).length <= MAX_IMPORT_DESIGN_CHARS ? out : null;
}
/** The title of the print profile named in the page URL's hash -- null when there's no hash or
* the page data is stale (see readMakerworldDesignForPage). */
export function currentMakerworldProfileTitle(pageUrl: string): string | null {
+3 -1
View File
@@ -18,7 +18,9 @@ export type RecentImport = {
/** A MakerWorld file URL resolved from the live page, with the print profile it belongs to (null
* when unknown). See content/makerworld/downloadResolver.ts. */
export type ResolvedDownload = { downloadUrl: string; instanceId: string | null };
/** `design` is the page's own design data for the import (see makerworldDesignForImport) --
* absent when the page data couldn't be trusted to be this model's. */
export type ResolvedDownload = { downloadUrl: string; instanceId: string | null; design?: Record<string, unknown> | null };
export type ImportSinglePayload = {
url: string;