first commit
This commit is contained in:
23
Dockerfile
Normal file
23
Dockerfile
Normal file
@@ -0,0 +1,23 @@
|
||||
# Utiliser une image Node.js officielle comme base
|
||||
FROM node:18-alpine
|
||||
|
||||
# Créer le répertoire de travail dans le conteneur
|
||||
WORKDIR /app
|
||||
|
||||
# Copier les fichiers de dépendances
|
||||
COPY package*.json ./
|
||||
|
||||
# Installer les dépendances
|
||||
RUN npm install
|
||||
|
||||
# Copier le reste des fichiers de l'application
|
||||
COPY . .
|
||||
|
||||
# Créer le dossier public s'il n'existe pas
|
||||
RUN mkdir -p public
|
||||
|
||||
# Exposer le port sur lequel l'application s'exécute
|
||||
EXPOSE 4000
|
||||
|
||||
# Commande pour démarrer l'application
|
||||
CMD ["node", "server.js"]
|
||||
11
docker-compose.yml
Normal file
11
docker-compose.yml
Normal file
@@ -0,0 +1,11 @@
|
||||
version: '3'
|
||||
|
||||
services:
|
||||
ulule-tracker:
|
||||
build: .
|
||||
ports:
|
||||
- "4000:4000"
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ./server.js:/app/server.js
|
||||
- ./public:/app/public
|
||||
25
package.json
Normal file
25
package.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "ulule-tracker",
|
||||
"version": "1.0.0",
|
||||
"description": "Application de suivi de campagne Ulule en temps réel",
|
||||
"main": "server.js",
|
||||
"scripts": {
|
||||
"start": "node server.js",
|
||||
"dev": "nodemon server.js"
|
||||
},
|
||||
"keywords": [
|
||||
"ulule",
|
||||
"crowdfunding",
|
||||
"tracker"
|
||||
],
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"axios": "^1.6.2",
|
||||
"cheerio": "^1.0.0-rc.12",
|
||||
"express": "^4.18.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"nodemon": "^2.0.22"
|
||||
}
|
||||
}
|
||||
385
public/index.html
Normal file
385
public/index.html
Normal file
@@ -0,0 +1,385 @@
|
||||
<!-- public/index.html -->
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Suivi de Campagne Ulule</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
background-color: #2c5282; /* Fond bleu */
|
||||
color: white;
|
||||
}
|
||||
h1 {
|
||||
color: white;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
.dashboard {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
margin-top: 30px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.card {
|
||||
background-color: #1a365d; /* Bleu plus foncé pour les cartes */
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
|
||||
padding: 20px;
|
||||
margin: 10px;
|
||||
width: 280px;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
.card:hover {
|
||||
transform: translateY(-5px);
|
||||
}
|
||||
.contributors-list {
|
||||
width: calc(100% - 60px);
|
||||
max-height: 500px;
|
||||
overflow-y: auto;
|
||||
text-align: left;
|
||||
}
|
||||
.contributor-item {
|
||||
padding: A10px;
|
||||
border-bottom: 1px solid #2d4a7d;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.contributor-name {
|
||||
font-weight: bold;
|
||||
color: #e3b505;
|
||||
}
|
||||
.contributor-reward {
|
||||
color: #a0aec0;
|
||||
font-size: 14px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
.contributor-amount {
|
||||
color: #ffd500;
|
||||
font-weight: bold;
|
||||
}
|
||||
.card h2 {
|
||||
margin-top: 0;
|
||||
color: white;
|
||||
font-size: 18px;
|
||||
}
|
||||
.value {
|
||||
font-size: 48px;
|
||||
font-weight: bold;
|
||||
margin: 20px 0;
|
||||
color: #e3b505; /* Valeurs en jaune */
|
||||
}
|
||||
.percentage {
|
||||
font-size: 24px;
|
||||
color: #ffd500; /* Pourcentage en jaune vif */
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.goal {
|
||||
color: #a0aec0; /* Texte gris clair pour la lisibilité */
|
||||
font-size: 14px;
|
||||
}
|
||||
.config-panel {
|
||||
background-color: #1a365d; /* Bleu plus foncé pour le panneau de configuration */
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
|
||||
padding: 20px;
|
||||
margin: 20px 0;
|
||||
text-align: left;
|
||||
}
|
||||
input {
|
||||
padding: 8px;
|
||||
width: 100%;
|
||||
margin: 8px 0;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
}
|
||||
button {
|
||||
background-color: #e3b505; /* Bouton jaune */
|
||||
color: #333;
|
||||
font-weight: bold;
|
||||
border: none;
|
||||
padding: 10px 15px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
margin-top: 10px;
|
||||
}
|
||||
button:hover {
|
||||
background-color: #ffc107; /* Jaune plus clair au survol */
|
||||
}
|
||||
.loading {
|
||||
display: inline-block;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: 3px solid rgba(255, 255, 255, 0.3);
|
||||
border-radius: 50%;
|
||||
border-top-color: white;
|
||||
animation: spin 1s ease-in-out infinite;
|
||||
}
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
.error {
|
||||
color: #ff6b6b;
|
||||
margin-top: 10px;
|
||||
}
|
||||
.last-update {
|
||||
color: #a0aec0; /* Texte gris clair pour la lisibilité */
|
||||
font-size: 12px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
.progress-bar {
|
||||
background-color: #233876;
|
||||
border-radius: 8px;
|
||||
height: 20px;
|
||||
width: 100%;
|
||||
margin: 15px 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
.progress-fill {
|
||||
background-color: #e3b505;
|
||||
height: 100%;
|
||||
border-radius: 8px;
|
||||
transition: width 0.5s ease-in-out;
|
||||
}
|
||||
.source-tag {
|
||||
display: inline-block;
|
||||
padding: 3px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 10px;
|
||||
margin-top: 10px;
|
||||
background-color: #4299e1;
|
||||
}
|
||||
.api-tag { background-color: #48bb78; }
|
||||
.script-tag { background-color: #4299e1; }
|
||||
.html-tag { background-color: #ed8936; }
|
||||
.demo-tag { background-color: #a0aec0; }
|
||||
.error-tag { background-color: #f56565; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Suivi de Campagne Ulule</h1>
|
||||
|
||||
<div class="config-panel">
|
||||
<h2>Suivi de la campagne</h2>
|
||||
<div>
|
||||
<p id="campaign-info">Chargement des données de la campagne...</p>
|
||||
</div>
|
||||
<p id="error-message" class="error"></p>
|
||||
</div>
|
||||
|
||||
<div class="dashboard">
|
||||
<div class="card">
|
||||
<h2>Montant Collecté</h2>
|
||||
<div id="amount" class="value">--</div>
|
||||
<div id="percentage" class="percentage">--%</div>
|
||||
<div class="progress-bar">
|
||||
<div id="progress-fill" class="progress-fill" style="width: 0%"></div>
|
||||
</div>
|
||||
<div id="goal" class="goal">Objectif: -- €</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h2>Contributeurs</h2>
|
||||
<div id="supporters" class="value">--</div>
|
||||
<div id="days-left" class="goal">Jours restants: --</div>
|
||||
<div id="source-tag" class="source-tag">Source: --</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card contributors-list">
|
||||
<h2>Derniers contributeurs</h2>
|
||||
<div id="contributors-container">Chargement des contributeurs...</div>
|
||||
</div>
|
||||
|
||||
<p class="last-update">Dernière mise à jour: <span id="last-update">--</span></p>
|
||||
|
||||
<script>
|
||||
let intervalId = null;
|
||||
// Configuration en dur
|
||||
const PROJECT_URL = 'https://fr.ulule.com/suppdonn-knights-of-water/'; // Remplacez par l'URL de votre campagne
|
||||
const REFRESH_INTERVAL = 20; // Intervalle de rafraîchissement en secondes
|
||||
|
||||
// Éléments DOM
|
||||
const errorMessage = document.getElementById('error-message');
|
||||
const campaignInfo = document.getElementById('campaign-info');
|
||||
const amountElement = document.getElementById('amount');
|
||||
const percentageElement = document.getElementById('percentage');
|
||||
const progressFill = document.getElementById('progress-fill');
|
||||
const goalElement = document.getElementById('goal');
|
||||
const supportersElement = document.getElementById('supporters');
|
||||
const daysLeftElement = document.getElementById('days-left');
|
||||
const lastUpdateElement = document.getElementById('last-update');
|
||||
const sourceTag = document.getElementById('source-tag');
|
||||
const contributorsContainer = document.getElementById('contributors-container');
|
||||
|
||||
function extractProjectSlug(url) {
|
||||
try {
|
||||
const urlObj = new URL(url);
|
||||
const pathParts = urlObj.pathname.split('/').filter(part => part);
|
||||
if (pathParts.length > 0) {
|
||||
return pathParts[pathParts.length - 1];
|
||||
}
|
||||
throw new Error('URL de projet invalide');
|
||||
} catch (error) {
|
||||
throw new Error('URL de projet invalide');
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchProjectData(projectSlug) {
|
||||
try {
|
||||
// Appeler notre API locale
|
||||
const response = await fetch(`/api/ulule/${projectSlug}`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Erreur API: ${response.status}`);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
throw new Error(`Impossible de récupérer les données du projet: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchContributors(projectSlug) {
|
||||
try {
|
||||
// Essayez de récupérer la page des contributeurs
|
||||
const response = await fetch(`/api/ulule/${projectSlug}/contributors`);
|
||||
|
||||
if (!response.ok) {
|
||||
return { contributors: [] };
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error("Erreur lors de la récupération des contributeurs:", error);
|
||||
return { contributors: [] };
|
||||
}
|
||||
}
|
||||
|
||||
function updateDashboard(data) {
|
||||
// Mettre à jour le titre de la campagne
|
||||
document.title = `Suivi de ${data.name}`;
|
||||
campaignInfo.textContent = `Campagne: ${data.name}`;
|
||||
|
||||
// Formater le montant avec l'espace comme séparateur de milliers
|
||||
const formatter = new Intl.NumberFormat('fr-FR', {
|
||||
style: 'currency',
|
||||
currency: data.currency,
|
||||
maximumFractionDigits: 0
|
||||
});
|
||||
|
||||
// Mettre à jour les valeurs
|
||||
const amountRaised = formatter.format(data.amount_raised);
|
||||
const goalAmount = formatter.format(data.goal);
|
||||
const percentFunded = data.percent || (data.goal > 0 ? Math.round((data.amount_raised / data.goal) * 100) : 0);
|
||||
|
||||
// Mettre à jour l'interface
|
||||
amountElement.textContent = amountRaised;
|
||||
percentageElement.textContent = `${percentFunded}%`;
|
||||
progressFill.style.width = `${Math.min(percentFunded, 100)}%`;
|
||||
goalElement.textContent = `Objectif: ${goalAmount}`;
|
||||
supportersElement.textContent = data.supporters_count;
|
||||
|
||||
if (data.days_left > 0) {
|
||||
daysLeftElement.textContent = `Jours restants: ${data.days_left}`;
|
||||
} else if (data.days_left === 0) {
|
||||
daysLeftElement.textContent = `Dernier jour!`;
|
||||
} else {
|
||||
daysLeftElement.textContent = `Campagne terminée`;
|
||||
}
|
||||
|
||||
// Afficher la source des données
|
||||
sourceTag.textContent = `Source: ${data.source || 'inconnue'}`;
|
||||
sourceTag.className = 'source-tag'; // Réinitialiser
|
||||
if (data.source) {
|
||||
sourceTag.classList.add(`${data.source}-tag`);
|
||||
}
|
||||
|
||||
lastUpdateElement.textContent = new Date().toLocaleString('fr-FR');
|
||||
}
|
||||
|
||||
function updateContributors(data) {
|
||||
if (!data.contributors || data.contributors.length === 0) {
|
||||
contributorsContainer.innerHTML = '<p>Aucun contributeur trouvé ou impossible de récupérer la liste.</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
const formatter = new Intl.NumberFormat('fr-FR', {
|
||||
style: 'currency',
|
||||
currency: 'EUR',
|
||||
maximumFractionDigits: 0
|
||||
});
|
||||
|
||||
let html = '';
|
||||
|
||||
data.contributors.forEach(contributor => {
|
||||
html += `
|
||||
<div class="contributor-item">
|
||||
<div>
|
||||
<div class="contributor-name">${contributor.name || 'Contributeur anonyme'}</div>
|
||||
<div class="contributor-reward">${contributor.reward || 'Aucune contrepartie'}</div>
|
||||
</div>
|
||||
<div class="contributor-amount">${contributor.amount ? formatter.format(contributor.amount) : ''}</div>
|
||||
</div>`;
|
||||
});
|
||||
|
||||
contributorsContainer.innerHTML = html;
|
||||
}
|
||||
|
||||
// Fonction pour démarrer le suivi automatiquement
|
||||
async function startTracking() {
|
||||
try {
|
||||
// Extraire le slug du projet depuis l'URL
|
||||
const projectSlug = extractProjectSlug(PROJECT_URL);
|
||||
campaignInfo.textContent = `Chargement des données pour ${projectSlug}...`;
|
||||
|
||||
// Récupérer les données initiales
|
||||
const projectData = await fetchProjectData(projectSlug);
|
||||
updateDashboard(projectData);
|
||||
|
||||
// Essayer de récupérer les contributeurs
|
||||
try {
|
||||
const contributorsData = await fetchContributors(projectSlug);
|
||||
updateContributors(contributorsData);
|
||||
} catch (contribError) {
|
||||
console.error("Erreur contributeurs:", contribError);
|
||||
contributorsContainer.innerHTML = '<p>Impossible de récupérer la liste des contributeurs.</p>';
|
||||
}
|
||||
|
||||
// Arrêter l'intervalle précédent s'il existe
|
||||
if (intervalId) {
|
||||
clearInterval(intervalId);
|
||||
}
|
||||
|
||||
// Configurer le rafraîchissement automatique
|
||||
intervalId = setInterval(async () => {
|
||||
try {
|
||||
// Mettre à jour les données du projet
|
||||
const updatedData = await fetchProjectData(projectSlug);
|
||||
updateDashboard(updatedData);
|
||||
|
||||
// Mettre à jour les contributeurs
|
||||
const contributorsData = await fetchContributors(projectSlug);
|
||||
updateContributors(contributorsData);
|
||||
} catch (error) {
|
||||
console.error('Erreur lors de la mise à jour:', error);
|
||||
}
|
||||
}, REFRESH_INTERVAL * 1000);
|
||||
|
||||
} catch (error) {
|
||||
errorMessage.textContent = error.message;
|
||||
campaignInfo.textContent = "Erreur lors du chargement des données";
|
||||
}
|
||||
}
|
||||
|
||||
// Démarrer le suivi dès le chargement de la page
|
||||
document.addEventListener('DOMContentLoaded', startTracking);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
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