import express from 'express'; import db from '../database.js'; import { authenticateToken } from '../middleware/auth.js'; import axios from 'axios'; import * as cheerio from 'cheerio'; import fs from 'fs'; import path from 'path'; import { v4 as uuidv4 } from 'uuid'; import { generateThumbnail } from '../services/thumbnailGenerator.js'; const router = express.Router(); // Import from URL (Thingiverse, Printables, MakerWorld) router.post('/url', authenticateToken, async (req, res) => { const { url } = req.body; if (!url) { return res.status(400).json({ error: 'URL is required' }); } try { let modelData; if (url.includes('thingiverse.com')) { modelData = await scrapeThingiverse(url); } else if (url.includes('printables.com')) { modelData = await scrapePrintables(url); } else if (url.includes('makerworld.com')) { modelData = await scrapeMakerWorld(url); } else { return res.status(400).json({ error: 'Unsupported URL. Only Thingiverse, Printables, and MakerWorld are supported.' }); } if (!modelData) { return res.status(500).json({ error: 'Failed to extract model data from URL' }); } // Download the model file if available let filePath = null; let fileName = null; let fileType = null; let fileSize = null; let thumbnailPath = null; if (modelData.downloadUrl) { const uploadDir = process.env.UPLOAD_DIR || './uploads'; const filesDir = path.join(uploadDir, 'files'); if (!fs.existsSync(filesDir)) { fs.mkdirSync(filesDir, { recursive: true }); } try { const response = await axios.get(modelData.downloadUrl, { responseType: 'arraybuffer', headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' }, maxRedirects: 5 }); fileType = path.extname(modelData.fileName || '.stl').toLowerCase(); fileName = `${uuidv4()}${fileType}`; filePath = path.join(filesDir, fileName); fs.writeFileSync(filePath, response.data); fileSize = fs.statSync(filePath).size; // Generate thumbnail if (['.stl', '.3mf', '.obj'].includes(fileType)) { try { thumbnailPath = await generateThumbnail(filePath, fileType); } catch (error) { console.error('Error generating thumbnail:', error); } } } catch (error) { console.error('Error downloading file:', error); // Continue without file } } // Insert into database db.run( `INSERT INTO models (name, description, file_path, file_name, file_size, file_type, preview_image, creator, source_url, user_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [ modelData.name, modelData.description, filePath, fileName || modelData.fileName, fileSize, fileType, thumbnailPath, modelData.creator, url, req.user.id ], function(err) { if (err) { // Clean up files if database insert fails if (filePath && fs.existsSync(filePath)) { fs.unlinkSync(filePath); } if (thumbnailPath && fs.existsSync(thumbnailPath)) { fs.unlinkSync(thumbnailPath); } return res.status(500).json({ error: err.message }); } res.json({ message: 'Model imported successfully', modelId: this.lastID, hasFile: !!filePath }); } ); } catch (error) { console.error('Import error:', error); res.status(500).json({ error: 'Failed to import model: ' + error.message }); } }); // Scrape Thingiverse async function scrapeThingiverse(url) { try { const response = await axios.get(url, { headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' } }); const $ = cheerio.load(response.data); const name = $('h1[itemprop="name"]').text().trim() || $('h1.thing-name').text().trim(); const description = $('div[itemprop="description"]').text().trim() || $('div.thing-description').text().trim(); const creator = $('a[itemprop="author"] span[itemprop="name"]').text().trim() || $('a.creator-name').text().trim(); // Note: Actual file download from Thingiverse requires authentication // This is a placeholder - in practice, users would need to download manually const downloadUrl = null; const fileName = null; return { name: name || 'Imported from Thingiverse', description: description || 'No description available', creator: creator || 'Unknown', downloadUrl, fileName }; } catch (error) { console.error('Thingiverse scrape error:', error); return null; } } // Scrape Printables async function scrapePrintables(url) { try { const response = await axios.get(url, { headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' } }); const $ = cheerio.load(response.data); const name = $('h1.main-title').text().trim() || $('h1').first().text().trim(); const description = $('div.description').text().trim(); const creator = $('a.user-link').first().text().trim(); // Note: Actual file download requires authentication const downloadUrl = null; const fileName = null; return { name: name || 'Imported from Printables', description: description || 'No description available', creator: creator || 'Unknown', downloadUrl, fileName }; } catch (error) { console.error('Printables scrape error:', error); return null; } } // Scrape MakerWorld async function scrapeMakerWorld(url) { try { const response = await axios.get(url, { headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' } }); const $ = cheerio.load(response.data); // MakerWorld uses React, so scraping is limited // Try to extract from meta tags const name = $('meta[property="og:title"]').attr('content') || $('title').text(); const description = $('meta[property="og:description"]').attr('content'); const creator = 'MakerWorld User'; const downloadUrl = null; const fileName = null; return { name: name || 'Imported from MakerWorld', description: description || 'No description available', creator, downloadUrl, fileName }; } catch (error) { console.error('MakerWorld scrape error:', error); return null; } } export default router;