// What each model asks for, and what you have on hand. // // A Bambu Studio / OrcaSlicer 3MF names the filaments its plates use. Reading // that means shelling out to Python, so it is done once per model in the // background — like thumbnails and fingerprints — and kept in models.filaments. // With that stored, "what can I print right now" is a plain query rather than // hundreds of subprocesses. const db = require('./db'); const converter = require('./converter'); const BATCH = 25; // Models that have never been looked at, paired with their first loose 3MF. // A model whose only 3MF sits inside an archive is left for a later pass rather // than paying an extraction for each one. const PENDING = ` FROM models m LEFT JOIN LATERAL ( SELECT f.file_path FROM files f WHERE f.model_id = m.id AND f.format = '3mf' ORDER BY f.id LIMIT 1 ) f3 ON true WHERE m.deleted_at IS NULL AND m.filaments_status IS NULL`; // "PLA Basic" and "pla basic" are the same filament; "PLA-CF" is not. const normalise = (s) => String(s || '').trim().toUpperCase().replace(/\s+/g, ' '); // The materials a model needs that nothing on hand provides. Colour is ignored // on purpose: matching a project's exact colours would make almost nothing // printable, and a different colour still prints. function missingMaterials(filaments, spools) { const onHand = new Set(spools .filter((s) => s.remaining_g === null || s.remaining_g === undefined || Number(s.remaining_g) > 0) .map((s) => normalise(s.material))); const needed = [...new Set((filaments || []).map((f) => normalise(f.type)).filter(Boolean))]; return needed.filter((type) => !onHand.has(type)); } // True when every filament the model names is on a spool you still have. // A model we know nothing about is not claimed to be printable. function isPrintable(filaments, spools) { if (!filaments || !filaments.length) return false; return missingMaterials(filaments, spools).length === 0; } // One pass. Returns whether it did any work, so it fits the background worker // loop in fingerprint.js. async function run() { const { rows } = await db.query( `SELECT m.id, f3.file_path ${PENDING} ORDER BY m.id LIMIT ${BATCH}`); if (!rows.length) return false; for (const row of rows) { if (!row.file_path) { await mark(row.id, 'none', null); continue; } try { const filaments = await converter.readFilaments(row.file_path); // A plain 3MF mesh names no filaments — that is an answer, not a failure. await mark(row.id, filaments.length ? 'done' : 'none', filaments.length ? filaments : null); } catch (err) { console.warn(`Could not read filaments for model ${row.id}:`, err.message); await mark(row.id, 'error', null); } } return true; } async function mark(modelId, status, filaments) { await db.query('UPDATE models SET filaments = $2, filaments_status = $3 WHERE id = $1', [modelId, filaments ? JSON.stringify(filaments) : null, status]); } async function pendingCount() { const { rows } = await db.query(`SELECT COUNT(*)::int AS n ${PENDING}`); return rows[0].n; } // Look at every model again — after adding files, say. async function reread() { await db.query('UPDATE models SET filaments_status = NULL'); } module.exports = { run, pendingCount, reread, missingMaterials, isPrintable, normalise };