first commit
This commit is contained in:
341
server.js
Normal file
341
server.js
Normal file
@@ -0,0 +1,341 @@
|
||||
// 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}`);
|
||||
});
|
||||
Reference in New Issue
Block a user