221 lines
6.5 KiB
JavaScript
221 lines
6.5 KiB
JavaScript
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;
|