Repository navigation
Expand file tree
/
Copy pathadd-model.js
More file actions
134 lines (111 loc) · 5.5 KB
/
Copy pathadd-model.js
File metadata and controls
134 lines (111 loc) · 5.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
#!/usr/bin/env node
// Run: node add-model.js
import { readFileSync, writeFileSync } from 'fs';
import { createInterface } from 'readline';
import { execSync } from 'child_process';
const rl = createInterface({ input: process.stdin, output: process.stdout });
const ask = (q) => new Promise(res => rl.question(q, res));
const prompt = async (label, def) => {
const val = await ask(def ? `${label} [${def}]: ` : `${label}: `);
return val.trim() || def || '';
};
async function fetchModelData(url) {
process.stdout.write(' Fetching Creality Cloud page... ');
const res = await fetch(url, {
headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36' }
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const html = await res.text();
const ogTitle =
html.match(/property="og:title"\s+content="([^"]+)"/)?.[1] ||
html.match(/content="([^"]+)"\s+property="og:title"/)?.[1];
const ogImage =
html.match(/property="og:image"\s+content="([^"]+)"/)?.[1] ||
html.match(/content="([^"]+)"\s+property="og:image"/)?.[1];
const titleTag = html.match(/<title>([^<]+)<\/title>/)?.[1]
?.replace(/\s*[-|].*?(Creality|Cloud).*$/i, '').trim();
const cleanTitle = ogTitle
? ogTitle.split('|').map(s => s.trim()).find(s => s && !/^(3d printer|3mf file|creality)/i.test(s)) || ogTitle
: null;
const ogDesc =
html.match(/property="og:description"\s+content="([^"]+)"/)?.[1] ||
html.match(/content="([^"]+)"\s+property="og:description"/)?.[1] ||
html.match(/name="description"\s+content="([^"]+)"/)?.[1] ||
html.match(/content="([^"]+)"\s+name="description"/)?.[1] || '';
// Decode HTML entities and take the first sentence
const decoded = ogDesc
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
.replace(/"/g, '"').replace(/'/g, "'").replace(/ /g, ' ');
const firstSentence = decoded.match(/^[^.!?]+[.!?]/)?.[0].trim() || decoded.slice(0, 120).trim();
console.log('done.');
return { name: cleanTitle || titleTag || '', thumb: ogImage || null, blurb: firstSentence };
}
function toFilename(name) {
return name.toLowerCase().replace(/[^a-z0-9]+/g, '_').replace(/^_|_$/, '') + '.3mf';
}
function formatEntry(e) {
const lines = [
` {`,
` name: ${JSON.stringify(e.name)},`,
` file: ${JSON.stringify(e.file)},`,
` diff: '${e.diff}',`,
` category: ${JSON.stringify(e.category)},`,
` nozzle: ${JSON.stringify(e.nozzle)}, layer: ${JSON.stringify(e.layer)}, infill: ${JSON.stringify(e.infill)}, material: ${JSON.stringify(e.material)}, supports: ${JSON.stringify(e.supports)},`,
` blurb: ${JSON.stringify(e.blurb)},`,
` thumb: ${e.thumb ? JSON.stringify(e.thumb) : 'null'},`,
` url: ${JSON.stringify(e.url)},`,
];
if (e.github) lines.push(` github: ${JSON.stringify(e.github)},`);
lines.push(` },`);
return lines.join('\n');
}
console.log('\n── Add a new model ─────────────────────────────\n');
// 1. Creality URL
const url = await prompt('Creality Cloud URL');
if (!url.startsWith('http')) { console.error('Invalid URL.'); process.exit(1); }
// 2. Fetch name, thumbnail, blurb
const { name: fetchedName, thumb, blurb: fetchedBlurb } = await fetchModelData(url);
// 3. Confirm / edit name
const name = await prompt('Model name', fetchedName);
// 4. Difficulty
let diff = '';
while (!['easy','medium','hard','expert'].includes(diff)) {
diff = (await prompt('Difficulty (easy / medium / hard / expert)', 'easy')).toLowerCase();
}
// 5. Category
const category = await prompt('Category (e.g. Desk, Electronics, Decor, Workspace)', 'TBC');
// 6. Blurb
const blurb = await prompt('Short description (blurb)', fetchedBlurb);
// 7. Print settings
console.log('\n Print settings — press Enter to keep the default:\n');
const nozzle = await prompt(' Nozzle', '0.4mm');
const layer = await prompt(' Layer', '0.2mm');
const infill = await prompt(' Infill', '15% grid');
const material = await prompt(' Material', 'PLA');
const supInput = (await prompt(' Supports (yes / no)', 'no')).toLowerCase();
const supports = supInput.startsWith('y') ? 'Yes' : 'No';
// 8. GitHub (optional)
const githubRaw = await prompt('\nGitHub URL (press Enter to skip)');
const github = githubRaw.startsWith('http') ? githubRaw : null;
rl.close();
// Build entry
const file = toFilename(name);
const entry = { name, file, diff, category, nozzle, layer, infill, material, supports, blurb, thumb, url, github };
// Insert into Files.jsx at the sentinel comment
const filePath = 'site/Files.jsx';
let src = readFileSync(filePath, 'utf8');
const sentinel = '// ADD_MODELS_HERE';
const insertAt = src.indexOf(sentinel);
if (insertAt === -1) { console.error('Could not find // ADD_MODELS_HERE in Files.jsx'); process.exit(1); }
src = src.slice(0, insertAt) + formatEntry(entry) + '\n' + src.slice(insertAt);
writeFileSync(filePath, src);
// Git commit and push
console.log('\n── Pushing to GitHub ────────────────────────────\n');
try {
execSync(`git add site/Files.jsx`, { stdio: 'inherit' });
execSync(`git commit -m "Add model: ${name.replace(/"/g, "'")}"`, { stdio: 'inherit' });
execSync(`git push`, { stdio: 'inherit' });
console.log(`\n✓ Done! "${name}" is live — Cloudflare will deploy in ~30 seconds.`);
} catch {
console.error('\nGit step failed. Your entry was saved to Files.jsx — run git add/commit/push manually.');
}