Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion app/backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"scripts": {
"start": "node server.js",
"dev": "nodemon server.js",
"ffvb": "node src/scripts/scraping.js",
"lint": "eslint src/**/*.js",
"lint:fix": "eslint src/**/*.js --fix",
"format": "prettier --write \"src/**/*.js\"",
Expand All @@ -16,10 +17,13 @@
},
"dependencies": {
"bcrypt": "^6.0.0",
"chalk": "^5.6.2",
"cheerio": "^1.2.0",
"cors": "^2.8.5",
"dotenv": "^17.2.3",
"express": "^5.2.1",
"mongoose": "^9.1.5"
"mongoose": "^9.1.5",
"puppeteer": "^24.37.1"
},
"devDependencies": {
"@eslint/js": "^9.39.2",
Expand Down
40 changes: 35 additions & 5 deletions app/backend/src/app.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,22 @@
// src/app.js
import express from 'express';
import cors from 'cors';
import dotenv from 'dotenv';
dotenv.config();

// Création de l'application Express
const app = express();

// Middlewares
app.use(cors());
// Pour parser le corps des requêtes en JSON
app.use(express.json());
app.use(express.json()); // Pour parser le corps des requêtes en JSON

// Route par défaut pour tester l'API
app.get('/', (req, res) => {
res.send('API Volley fonctionne !');
});

// Importer les routes
// Importer les routes existantes
import usersRoutes from './routes/users.js';
import clubsRoutes from './routes/clubs.js';
import seasonsRoutes from './routes/seasons.js';
Expand All @@ -26,7 +30,7 @@ import championshipsRoutes from './routes/championships.js';
import standingsRoutes from './routes/standings.js';
import matchesRoutes from './routes/matches.js';

// Utiliser les routes
// Utiliser les routes existantes
app.use('/api/users', usersRoutes);
app.use('/api/clubs', clubsRoutes);
app.use('/api/seasons', seasonsRoutes);
Expand All @@ -40,5 +44,31 @@ app.use('/api/championships', championshipsRoutes);
app.use('/api/standings', standingsRoutes);
app.use('/api/matches', matchesRoutes);

// Exporter l'application
// 🔹 Nouveaux endpoints pour récupérer les matchs
import Match from './models/Match.js';
// 🔹 Endpoints pour les matchs

/**
* GET /api/matches
* - Optionnel : ?club=NomClub
*/
app.get('/api/matches', async (req, res) => {
try {
const { club } = req.query;
let filter = {};

if (club) {
filter = { $or: [{ opponentName: decodeURIComponent(club) }] };
}

const matches = await Match.find(filter)
.populate('championshipId') // si tu veux récupérer les infos du championnat
.sort({ date: 1 });

res.json(matches);
} catch (err) {
res.status(500).json({ error: err.message });
}
});

export default app;
2 changes: 1 addition & 1 deletion app/backend/src/models/Match.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ const matchSchema = new mongoose.Schema(
{
championshipId: {
type: mongoose.Schema.Types.ObjectId,
ref: 'ChampionshipFFVB',
ref: 'Championship', // <-- utilisation du modèle Championship existant
required: true,
},
opponentName: {
Expand Down
138 changes: 138 additions & 0 deletions app/backend/src/scripts/scraping.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
// src/scripts/scraping.js
import puppeteer from 'puppeteer';
import mongoose from 'mongoose';
import * as cheerio from 'cheerio';
import Match from '../models/Match.js';
import Standing from '../models/Standing.js';
import dotenv from 'dotenv';
dotenv.config();

// 🔹 Constantes
const CHAMPIONSHIP_ID = '6985bd859903c98a455ca98d';
const CLUB_TEAM_NAME = "AS SAINT-BARTHELEMY D'ANJOU V.B.";
const FFVB_URL =
'https://www.ffvbbeach.org/ffvbapp/resu/vbspo_calendrier.php?saison=2025%2F2026&codent=ABCCS&poule=3MB&division=&tour=&calend=COMPLET&x=15&y=15';

// 🔹 Utilitaires
function parseDateFR(dateStr, timeStr) {
const [day, month, year] = dateStr.split('/').map(Number);
const fullYear = year < 100 ? 2000 + year : year;
const [hours, minutes] = timeStr.split(':').map(Number);
return new Date(fullYear, month - 1, day, hours, minutes).toISOString();
}

function parseSetsDetail(setsStr) {
if (!setsStr) return [];
return setsStr.split(',').map((s, idx) => {
const [scoreFor, scoreAgainst] = s.trim().split(':').map(Number);
return { setNumber: idx + 1, scoreFor: scoreFor || 0, scoreAgainst: scoreAgainst || 0 };
});
}

// 🔹 Fonction principale
async function scrapeFFVB() {
try {
console.log('📡 Lancement de Puppeteer...');
const browser = await puppeteer.launch({ headless: true });
const page = await browser.newPage();

console.log('📊 Récupération du HTML...');
await page.goto(FFVB_URL, { waitUntil: 'networkidle2' });
const html = await page.content();
await browser.close();

const $ = cheerio.load(html);

// ---------------------
// 🔹 Scraping des matchs
// ---------------------
console.log('📊 Parsing des matchs...');
const matches = [];

$('tr').each((i, tr) => {
const tds = $(tr).find('td');
if (tds.length < 6) return;

const homeTeam = $(tds[3]).text().trim();
const awayTeam = $(tds[5]).text().trim();
if (!homeTeam || !awayTeam) return;

if (homeTeam === CLUB_TEAM_NAME || awayTeam === CLUB_TEAM_NAME) {
const homeAway = homeTeam === CLUB_TEAM_NAME ? 'home' : 'away';
const scoreFor = homeAway === 'home' ? parseInt($(tds[6]).text()) || 0 : parseInt($(tds[7]).text()) || 0;
const scoreAgainst = homeAway === 'home' ? parseInt($(tds[7]).text()) || 0 : parseInt($(tds[6]).text()) || 0;
const dateISO = parseDateFR($(tds[1]).text().trim(), $(tds[2]).text().trim());
const setsDetail = parseSetsDetail($(tds[8]).text().trim());

console.log(`🏐 Match: ${homeTeam} vs ${awayTeam} - ${dateISO}`);
matches.push({
championshipId: CHAMPIONSHIP_ID,
clubName: CLUB_TEAM_NAME, // pour filtrer plus tard
opponentName: homeAway === 'home' ? awayTeam : homeTeam,
date: dateISO,
homeAway,
status: $(tds[6]).text().trim() || $(tds[7]).text().trim() ? 'played' : 'scheduled',
scoreFor,
scoreAgainst,
setsDetail,
});
}
});

console.log('💾 Mise à jour des matchs dans MongoDB...');
for (const m of matches) {
await Match.findOneAndUpdate({ championshipId: CHAMPIONSHIP_ID, opponentName: m.opponentName, date: m.date }, m, {
upsert: true,
});
}
console.log(`🎉 ${matches.length} matchs mis à jour.`);

// ---------------------
// 🔹 Scraping du classement (standings)
// ---------------------
console.log('📊 Parsing des standings...');
const standings = [];

// Exemple : chaque ligne du classement
$('table.standings tr').each((i, tr) => {
const tds = $(tr).find('td');
if (tds.length < 8) return; // on a besoin de 8 colonnes pour ton modèle

const rank = parseInt($(tds[0]).text()) || 0;
const teamName = $(tds[1]).text().trim();
const played = parseInt($(tds[2]).text()) || 0;
const wins = parseInt($(tds[3]).text()) || 0;
const losses = parseInt($(tds[4]).text()) || 0;
const setsFor = parseInt($(tds[5]).text()) || 0;
const setsAgainst = parseInt($(tds[6]).text()) || 0;
const points = parseInt($(tds[7]).text()) || 0;

standings.push({
championshipId: CHAMPIONSHIP_ID,
teamName,
rank,
played,
wins,
losses,
setsFor,
setsAgainst,
points,
});
});

console.log('💾 Mise à jour des standings dans MongoDB...');
for (const s of standings) {
await Standing.findOneAndUpdate({ championshipId: CHAMPIONSHIP_ID, teamName: s.teamName }, s, { upsert: true });
}

console.log(`🎉 ${standings.length} équipes mises à jour.`);
} catch (err) {
console.error('❌ Erreur pendant le scraping :', err);
}
}

// 🔹 Connexion MongoDB
mongoose
.connect(process.env.MONGO_URI || 'mongodb://127.0.0.1:27017/saintbarthvolley')
.then(() => scrapeFFVB())
.finally(() => mongoose.disconnect());
43 changes: 43 additions & 0 deletions app/backend/src/scripts/scrapingTest.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import puppeteer from 'puppeteer';
import * as cheerio from 'cheerio';

const CLUB_TEAM_NAME = "AS SAINT-BARTHELEMY D'ANJOU V.B.";
const URL =
'https://www.ffvbbeach.org/ffvbapp/resu/vbspo_calendrier.php?saison=2025%2F2026&codent=ABCCS&poule=3MB&calend=COMPLET';

async function testScraping() {
console.log('📡 Lancement de Puppeteer...');

const browser = await puppeteer.launch({ headless: true });
const page = await browser.newPage();

console.log('📊 Récupération du HTML...');
await page.goto(URL, { waitUntil: 'networkidle2' });

const html = await page.content();
const $ = cheerio.load(html);

console.log('📊 Parsing des matchs...');

$('tr').each((i, tr) => {
const tds = $(tr).find('td');
if (tds.length < 6) return; // ignorer les lignes qui ne sont pas des matchs

const date = $(tds[1]).text().trim();
const time = $(tds[2]).text().trim();
const homeTeam = $(tds[3]).text().trim();
const awayTeam = $(tds[5]).text().trim();

if (!homeTeam || !awayTeam) return;

// uniquement les matchs du club
if (homeTeam === CLUB_TEAM_NAME || awayTeam === CLUB_TEAM_NAME) {
console.log(`🏐 Match: ${homeTeam} vs ${awayTeam} - ${date} ${time}`);
}
});

await browser.close();
console.log('🎉 Test terminé !');
}

testScraping();
4 changes: 2 additions & 2 deletions docs/projet/scraping.md
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,7 @@ Juste : *HTML → JS object*
### Télécharger la page

```jsx
axios.get(url)
puppeteer.get(url)
```

### Parser le HTML
Expand Down Expand Up @@ -366,7 +366,7 @@ Quand tout est stable :
Ex :

```bash
npm run scrape:ffvb
npm run ffvb
```


Expand Down