mirror of
https://github.com/TautvydasDerzinskas/Thingport.git
synced 2026-10-21 14:12:25 -05:00
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:
1 parent
616a8b82c3
commit
c3a189ea7f
4 files changed
+45
-8
No files matched your search
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user