first commit

This commit is contained in:
Victor Bodinaud
2025-02-27 13:17:57 +01:00
commit 8214b3f5b8
6 changed files with 785 additions and 0 deletions

341
server.js Normal file
View 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}`);
});