// server.js const express = require('express'); const axios = require('axios'); const cheerio = require('cheerio'); const path = require('path'); const app = express(); const port = process.env.PORT || 2106; // Servir les fichiers statiques app.use(express.static('public')); // Route pour vérifier que le serveur fonctionne app.get('/api/health', (req, res) => { res.json({ status: 'ok', time: new Date().toISOString() }); }); // Route pour récupérer les contributeurs d'une campagne app.get('/api/ulule/:slug/contributors', async (req, res) => { try { const { slug } = req.params; console.log(`Récupération des contributeurs pour la campagne: ${slug}`); // Configuration de la requête const config = { headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8', 'Accept-Language': 'fr,fr-FR;q=0.8,en-US;q=0.5,en;q=0.3' }, timeout: 10000 }; // Récupérer la page des contributeurs const response = await axios.get(`https://fr.ulule.com/${slug}/supporters/`, config); const html = response.data; // Analyser le HTML avec Cheerio const $ = cheerio.load(html); const contributors = []; // Chercher les contributeurs dans la page $('.supporters-list__item').each((index, element) => { try { const name = $(element).find('.supporters-list__name').text().trim(); const reward = $(element).find('.supporters-list__reward').text().trim(); let amount = $(element).find('.supporters-list__amount').text().trim(); // Convertir le montant en nombre si présent if (amount) { // Extraire seulement les chiffres et la virgule/point const amountMatch = amount.match(/(\d[\d\s.,]*)/); if (amountMatch) { amount = parseFloat(amountMatch[1].replace(/\s/g, '').replace(',', '.')); } else { amount = null; } } else { amount = null; } contributors.push({ name, reward, amount }); } catch (itemError) { console.error("Erreur lors de l'extraction d'un contributeur:", itemError); } }); console.log(`${contributors.length} contributeurs trouvés`); // Si aucun contributeur trouvé, essayer une autre structure HTML if (contributors.length === 0) { console.log("Tentative avec une structure HTML alternative"); $('.supporter-item').each((index, element) => { try { const name = $(element).find('.supporter-name').text().trim() || $(element).find('.supporter__name').text().trim(); const reward = $(element).find('.supporter-reward').text().trim() || $(element).find('.supporter__reward').text().trim(); let amount = $(element).find('.supporter-amount').text().trim() || $(element).find('.supporter__amount').text().trim(); if (amount) { const amountMatch = amount.match(/(\d[\d\s.,]*)/); if (amountMatch) { amount = parseFloat(amountMatch[1].replace(/\s/g, '').replace(',', '.')); } else { amount = null; } } else { amount = null; } contributors.push({ name, reward, amount }); } catch (itemError) { console.error("Erreur lors de l'extraction alternative d'un contributeur:", itemError); } }); console.log(`${contributors.length} contributeurs trouvés avec la méthode alternative`); } // Renvoyer la liste des contributeurs return res.json({ contributors }); } catch (error) { console.error('Erreur lors de la récupération des contributeurs:', error); return res.json({ contributors: [] }); } }); // Route principale pour extraire les données d'une campagne Ulule app.get('/api/ulule/:slug', async (req, res) => { try { const { slug } = req.params; console.log(`Récupération des données pour la campagne: ${slug}`); // Configuration de la requête avec les entêtes appropriés const config = { headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8', 'Accept-Language': 'fr,fr-FR;q=0.8,en-US;q=0.5,en;q=0.3' }, timeout: 10000 }; // Essayer d'abord l'API publique d'Ulule try { const apiResponse = await axios.get(`https://api.ulule.com/v1/projects/${slug}`, config); if (apiResponse.status === 200 && apiResponse.data) { console.log("Données récupérées depuis l'API publique d'Ulule"); // Formater les données de l'API const apiData = apiResponse.data; const formattedData = { name: apiData.name ? apiData.name.fr || apiData.name.en || Object.values(apiData.name)[0] : slug, amount_raised: apiData.amount_raised || 0, goal: apiData.goal || 0, currency: apiData.currency || "EUR", supporters_count: apiData.supporters_count || 0, days_left: 0, // Sera calculé plus bas percent: apiData.percent || 0, success: true, source: 'api' }; // Calculer les jours restants if (apiData.date_end) { const endDate = new Date(apiData.date_end); const now = new Date(); const diffTime = endDate - now; formattedData.days_left = Math.ceil(diffTime / (1000 * 60 * 60 * 24)); } return res.json(formattedData); } } catch (apiError) { console.log("Échec de l'API publique, tentative avec le scraping de la page HTML..."); } // Si l'API échoue, essayer de récupérer la page HTML const response = await axios.get(`https://fr.ulule.com/${slug}/`, config); const html = response.data; // Utiliser Cheerio pour analyser le HTML const $ = cheerio.load(html); // Essayer d'extraire les données depuis les scripts JS injectés dans la page let projectData = null; $('script').each((i, script) => { const scriptContent = $(script).html(); // Chercher les données de projet dans window.UFE.data.project if (scriptContent && scriptContent.includes('window.UFE')) { try { const match = scriptContent.match(/window\.UFE\s*=\s*(\{[\s\S]*?\});/); if (match && match[1]) { const ufeData = eval(`(${match[1]})`); if (ufeData && ufeData.data && ufeData.data.project) { projectData = ufeData.data.project; console.log("Données extraites depuis le script UFE"); } } } catch (error) { console.log("Erreur lors de l'extraction des données UFE:", error.message); } } }); // Si nous avons trouvé les données dans les scripts if (projectData) { const formattedData = { name: projectData.name ? projectData.name.fr || projectData.name.en || Object.values(projectData.name)[0] : slug, amount_raised: projectData.amount_raised || 0, goal: projectData.goal || 0, currency: projectData.currency || "EUR", supporters_count: projectData.supporters_count || 0, days_left: 0, percent: projectData.percent || 0, success: true, source: 'script' }; // Calculer les jours restants if (projectData.date_end) { const endDate = new Date(projectData.date_end); const now = new Date(); const diffTime = endDate - now; formattedData.days_left = Math.ceil(diffTime / (1000 * 60 * 60 * 24)); } return res.json(formattedData); } // Méthode de secours: analyser le HTML directement console.log("Tentative d'extraction depuis le HTML avec analyse manuelle"); // Extraire le titre de la campagne const title = $('title').text().trim().split(' - ')[0] || slug; // Extraire les données numériques par analyse du HTML const fullText = $('body').text(); // Analyser le texte complet pour trouver des patterns de montants let amountRaised = 0; let goal = 0; let percent = 0; let supportersCount = 0; let daysLeft = 0; // Rechercher les montants avec différents patterns const numberPatterns = { // Format: 12 345 €, 12.345 €, 12,345 € amountPattern: /(\d[\d\s.,]*)\s*€\s*(?:collectés|recueillis|sur)/i, goalPattern: /sur\s*(\d[\d\s.,]*)\s*€|objectif\s*:\s*(\d[\d\s.,]*)\s*€/i, percentPattern: /(\d+)\s*%/, supportersPattern: /(\d+)\s*(?:contributeurs|participants)/i, daysPattern: /(\d+)\s*jours?\s*(?:restants?)?/i }; // Extraire les montants const amountMatch = fullText.match(numberPatterns.amountPattern); if (amountMatch) { amountRaised = parseFloat(amountMatch[1].replace(/\s/g, '').replace(',', '.')); } // Extraire l'objectif const goalMatch = fullText.match(numberPatterns.goalPattern); if (goalMatch) { goal = parseFloat((goalMatch[1] || goalMatch[2]).replace(/\s/g, '').replace(',', '.')); } // Extraire le pourcentage const percentMatch = fullText.match(numberPatterns.percentPattern); if (percentMatch) { percent = parseInt(percentMatch[1]); // Si nous avons le pourcentage et l'un des deux montants, calculer l'autre if (percent > 0) { if (amountRaised > 0 && goal === 0) { goal = (amountRaised / percent) * 100; } else if (goal > 0 && amountRaised === 0) { amountRaised = (goal * percent) / 100; } } } // Extraire le nombre de contributeurs const supportersMatch = fullText.match(numberPatterns.supportersPattern); if (supportersMatch) { supportersCount = parseInt(supportersMatch[1]); } // Extraire les jours restants const daysMatch = fullText.match(numberPatterns.daysPattern); if (daysMatch) { daysLeft = parseInt(daysMatch[1]); } // Si nous n'avons trouvé aucune donnée, utiliser des données de démonstration if (amountRaised === 0 && goal === 0 && percent === 0) { console.log("Aucune donnée trouvée, utilisation des données de démonstration"); return res.json({ name: title, amount_raised: 5000, goal: 10000, currency: "EUR", supporters_count: 42, days_left: 15, percent: 50, success: false, source: 'demo', message: "Données de démonstration (impossible d'extraire les données réelles)" }); } // Formater les données extraites const formattedData = { name: title, amount_raised: amountRaised, goal: goal, currency: "EUR", supporters_count: supportersCount, days_left: daysLeft, percent: percent, success: true, source: 'html' }; return res.json(formattedData); } catch (error) { console.error('Erreur complète:', error); // En cas d'erreur, renvoyer des données de démonstration res.json({ name: req.params.slug, amount_raised: 5000, goal: 10000, currency: "EUR", supporters_count: 42, days_left: 15, percent: 50, success: false, source: 'error', error: error.message, message: "Données de démonstration en raison d'une erreur" }); } }); // Page d'accueil app.get('/', (req, res) => { res.sendFile(path.join(__dirname, 'public', 'index.html')); }); // Démarrer le serveur app.listen(port, '0.0.0.0', () => { console.log(`Serveur démarré sur http://0.0.0.0:${port}`); });