// Generic page-metadata importer for sites without a usable download API
// (Printables, MakerWorld, or any URL). Reads Open Graph /
tags so we
// can create a model with name, description, and a preview image; the actual
// model files are attached by the user afterwards.
function detectSite(url) {
let host;
try {
const parsed = new URL(url);
if (!/^https?:$/.test(parsed.protocol)) return null;
host = parsed.hostname.replace(/^www\./, '');
} catch {
return null;
}
if (host.includes('thingiverse.com')) return { site: 'thingiverse' };
if (host.includes('printables.com')) return { site: 'printables', id: (url.match(/\/model\/(\d+)/) || [])[1] || null };
if (host.includes('makerworld.com')) return { site: 'makerworld', id: (url.match(/\/models\/(\d+)/) || [])[1] || null };
if (host.includes('cubee3d.com')) {
let slug = null;
try { slug = new URL(url).pathname.split('/').filter(Boolean).pop() || null; } catch { /* ignore */ }
return { site: 'cubee3d', id: slug };
}
return { site: 'web', id: null };
}
function decodeEntities(str) {
return String(str)
.replace(/&/g, '&').replace(/"/g, '"').replace(/'/g, "'")
.replace(/'/g, "'").replace(/</g, '<').replace(/>/g, '>').replace(/ /g, ' ');
}
function metaContent(html, attr, value) {
// Matches in either attribute order.
const a = html.match(new RegExp(`]+${attr}=["']${value}["'][^>]+content=["']([^"']*)["']`, 'i'));
if (a) return decodeEntities(a[1]);
const b = html.match(new RegExp(`]+content=["']([^"']*)["'][^>]+${attr}=["']${value}["']`, 'i'));
return b ? decodeEntities(b[1]) : null;
}
const BROWSER_UA = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0 Safari/537.36';
// A page is a Cloudflare interstitial (not the real content) when it looks like this.
function isCloudflareChallenge(html) {
return /just a moment|cf-browser-verification|challenge-platform|cf_chl_opt/i.test(html || '');
}
async function fetchHtml(url) {
const res = await fetch(url, { headers: { 'User-Agent': BROWSER_UA, Accept: 'text/html' } });
const html = await res.text();
if (!res.ok && !html) throw new Error(`Could not fetch page (HTTP ${res.status}).`);
return { ok: res.ok, status: res.status, html };
}
// Extract Open Graph / title metadata from raw HTML.
function parseMetadata(html, url) {
const titleTag = (html.match(/]*>([^<]*)<\/title>/i) || [])[1];
return {
title: metaContent(html, 'property', 'og:title') || (titleTag ? decodeEntities(titleTag).trim() : null),
description: metaContent(html, 'property', 'og:description') || metaContent(html, 'name', 'description'),
image: metaContent(html, 'property', 'og:image'),
author: metaContent(html, 'name', 'author') || metaContent(html, 'property', 'og:site_name'),
};
}
// Best-effort human name from a URL slug, e.g.
// /models/98765-cool-widget -> "Cool widget". Used when a page can't be read.
function nameFromUrl(url) {
try {
const seg = new URL(url).pathname.split('/').filter(Boolean).pop() || '';
const words = decodeURIComponent(seg).replace(/^\d+-?/, '').replace(/[-_]+/g, ' ').trim();
if (!words) return new URL(url).hostname.replace(/^www\./, '');
return words.charAt(0).toUpperCase() + words.slice(1);
} catch {
return url;
}
}
module.exports = { detectSite, fetchHtml, parseMetadata, isCloudflareChallenge, nameFromUrl };