341 lines
12 KiB
JavaScript
341 lines
12 KiB
JavaScript
// 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 || 4000;
|
|
|
|
// 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}`);
|
|
}); |