fix(extension): check a MakerWorld collection against the library in one call

The collection panel sent one /import/status request per model, all at
once. When any failed it counted the model as new, so a collection
already imported could offer to import almost everything again.

Add POST /import/status/batch and use it, falling back to paced
per-model checks on older instances. A failed check now shows an error
instead of guessing.
This commit is contained in:
Tautvydas Derzinskas committed 2026-10-12 03:50:28 +02:00
1 parent 1408b09c59
commit 44c687b3ae
3 files changed
+60 -11

No files matched your search

+16
View File
@@ -16,6 +16,7 @@ import {
importPrintFromUrl,
inspectImportLink,
} from "../services/importService";
import { findLibraryPrints } from "../services/collectionSyncService";
import { resolveMakerworldCookie } from "../services/importResolvers";
import {
extractMakerworldBearerToken,
@@ -190,6 +191,21 @@ router.get(
}),
);
const statusBatchSchema = z.object({
provider: z.enum(["makerworld", "thingiverse", "printables"]),
external_ids: z.array(z.string().max(64)).max(10_000),
});
// One query for a whole collection page; per-model /import/status calls bog down on big ones.
router.post(
"/import/status/batch",
asyncHandler(async (req, res) => {
const body = parseBody(statusBatchSchema, req.body);
const prints = await findLibraryPrints(req.userId!, body.provider, body.external_ids);
res.json({ print_ids: Object.fromEntries(prints) });
}),
);
router.post(
"/import/zip/entries",
asyncHandler(async (req, res) => {
+5
View File
@@ -227,6 +227,11 @@ describe("filling a model's missing details from its source", () => {
.get(`/api/import/status?url=${encodeURIComponent(`https://makerworld.com/en/models/${designId}`)}`)
.set(auth());
expect(status.body.gaps).toEqual([]);
const batch = await request(app)
.post("/api/import/status/batch")
.set(auth())
.send({ provider: "makerworld", external_ids: [designId, "0"] });
expect(batch.body.print_ids).toEqual({ [designId]: print.id });
// Clearing a field the source does have opens a gap again.
await prisma.print.update({ where: { id: print.id }, data: { notes: null } });
@@ -109,6 +109,38 @@ function stepDelayOptions(selectedMs: number): string {
type ModelStatus = { url: string; already_imported: boolean; print_id: string | null };
// Per-model checks, for instances without the batch endpoint, run a few at a time.
const STATUS_CHECK_CONCURRENCY = 6;
/** Throws rather than guessing: an unchecked model counted as new would be re-imported. */
async function checkLibrary(ids: string[]): Promise<ModelStatus[]> {
let printIds: Record<string, string>;
try {
({ print_ids: printIds } = await api<{ print_ids: Record<string, string> }>("POST", "/import/status/batch", {
provider: "makerworld",
external_ids: ids,
}));
} catch {
printIds = {};
const queue = [...ids];
const worker = async () => {
for (let id = queue.shift(); id; id = queue.shift()) {
const status = await api<ImportStatus>(
"GET",
`/import/status?url=${encodeURIComponent(makerworldModelUrl(id))}`,
);
if (status.already_imported && status.print_id) printIds[id] = status.print_id;
}
};
await Promise.all(Array.from({ length: STATUS_CHECK_CONCURRENCY }, worker));
}
return ids.map((id) => ({
url: makerworldModelUrl(id),
already_imported: id in printIds,
print_id: printIds[id] ?? null,
}));
}
export async function loadMakerworldGuidedCollection(): Promise<void> {
const collectionTitle = document.getElementsByTagName("h1")[0]?.innerText?.trim() || null;
@@ -128,17 +160,13 @@ export async function loadMakerworldGuidedCollection(): Promise<void> {
}
renderPanel(statusHtml(`Checking ${ids.length} models against your library…`));
const statuses: ModelStatus[] = await Promise.all(
ids.map(async (id) => {
const url = makerworldModelUrl(id);
try {
const status = await api<ImportStatus>("GET", `/import/status?url=${encodeURIComponent(url)}`);
return { url, already_imported: status.already_imported, print_id: status.print_id ?? null };
} catch {
return { url, already_imported: false, print_id: null };
}
}),
);
let statuses: ModelStatus[];
try {
statuses = await checkLibrary(ids);
} catch (err) {
renderPanel(errorHtml(err));
return;
}
const toImport = statuses.filter((s) => !s.already_imported);
const alreadyImported = statuses.filter((s) => s.already_imported);
const sync = await syncState;