From 8214b3f5b85b1806637ddddba58b09ea82198240 Mon Sep 17 00:00:00 2001 From: Victor Bodinaud Date: Thu, 27 Feb 2025 13:17:57 +0100 Subject: [PATCH] first commit --- .DS_Store | Bin 0 -> 6148 bytes Dockerfile | 23 +++ docker-compose.yml | 11 ++ package.json | 25 +++ public/index.html | 385 +++++++++++++++++++++++++++++++++++++++++++++ server.js | 341 +++++++++++++++++++++++++++++++++++++++ 6 files changed, 785 insertions(+) create mode 100644 .DS_Store create mode 100644 Dockerfile create mode 100644 docker-compose.yml create mode 100644 package.json create mode 100644 public/index.html create mode 100644 server.js diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..677174ff32f864862056048372e16b82415d186d GIT binary patch literal 6148 zcmeHKK~BRk5FD3+THsJ|;)3K45Q!gzDjYcT0h+X+LLgO9k+|i~V|X7Ym|a^{>%bME zYFAp1z1i`~<0y^+xbbRw0gM0)=z_f=n;%S`i+8LSBl|?BbIh>BIVv=G>SSBsKPn)5 zcY-N*timHU_iw@23nLlcxSREL-Be}Wl+2fx<%6Dmsj&wfqS%<@4s+C)C7jEIbBqNh zT6cnbt@OZ(4W74o?kwRQkhem~3UgFR?q`xa)HN*es%y)@3JK#qVQ^?>3YY?>z>X`x zGh1wM6wq2zz!WeA)(Xh?!P5ovh;2aqbg$R#kJtv}6PgQDVxYQs z#c+YnxQ}^x#5Q1{!^O*oi + + + + + + Suivi de Campagne Ulule + + + +

Suivi de Campagne Ulule

+ +
+

Suivi de la campagne

+
+

Chargement des données de la campagne...

+
+

+
+ +
+
+

Montant Collecté

+
--
+
--%
+
+
+
+
Objectif: -- €
+
+
+

Contributeurs

+
--
+
Jours restants: --
+
Source: --
+
+
+ +
+

Derniers contributeurs

+
Chargement des contributeurs...
+
+ +

Dernière mise à jour: --

+ + + + \ No newline at end of file diff --git a/server.js b/server.js new file mode 100644 index 0000000..6598e4d --- /dev/null +++ b/server.js @@ -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}`); +}); \ No newline at end of file