makerstash/server/routes/import.js

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;