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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Scraping des matchs
- Normalisation & sécurité
- Automatisation
- Authentification

---

Expand Down
64 changes: 57 additions & 7 deletions app/backend/src/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,26 +46,76 @@ app.use('/api/matches', matchesRoutes);

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

// 🔹 Endpoint pour récupérer les matchs
/**
* GET /api/matches
* - Optionnel : ?club=NomClub
* - Optionnel :
* ?club=NomClub
* ?championshipId=<ID>
*/
app.get('/api/matches', async (req, res) => {
try {
const { club } = req.query;
const { club, championshipId } = req.query;
let filter = {};

if (club) {
filter = { $or: [{ opponentName: decodeURIComponent(club) }] };
// Filtrer sur le nom de l'adversaire
filter.opponentName = decodeURIComponent(club);
}

if (championshipId) {
filter.championshipId = championshipId;
}

// Récupérer tous les matchs correspondants
const matches = await Match.find(filter)
.populate('championshipId') // si tu veux récupérer les infos du championnat
.sort({ date: 1 });
.populate('championshipId') // pour récupérer les infos du championnat
.sort({ date: 1 }) // tri par date croissante
.lean(); // JSON simple, sans méthodes Mongoose

// Formater chaque match pour le front
const formatted = matches.map((m) => ({
id: m._id,
championshipId: m.championshipId?._id || null,
championshipName: m.championshipId?.name || '',
date: m.date,
homeAway: m.homeAway,
opponentName: m.opponentName,
status: m.status,
scoreFor: m.scoreFor,
scoreAgainst: m.scoreAgainst,
setsDetail: m.setsDetail || [],
}));

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

// 🔹 Endpoint pour récupérer tous les standings
/**
* GET /api/standings
* - Optionnel : ?championshipId=<ID>
*/
app.get('/api/standings', async (req, res) => {
try {
const { championshipId } = req.query;
let filter = {};

// Si un championnat est précisé, on filtre dessus
if (championshipId) {
filter.championshipId = championshipId;
}

// Récupère tous les standings correspondants
const standings = await Standing.find(filter)
.sort({ rank: 1 }) // tri par classement
.lean(); // pour renvoyer un simple JSON sans les méthodes Mongoose

res.json(matches);
res.json(standings);
} catch (err) {
res.status(500).json({ error: err.message });
}
Expand Down
9 changes: 9 additions & 0 deletions app/backend/src/scripts/checkDB.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import mongoose from 'mongoose';
import dotenv from 'dotenv';
dotenv.config();

mongoose
.connect(process.env.MONGO_URI)
.then(() => console.log('✅ Connecté à MongoDB :', process.env.MONGO_URI))
.catch((err) => console.error('❌ Erreur connexion MongoDB :', err))
.finally(() => mongoose.disconnect());
26 changes: 26 additions & 0 deletions app/backend/src/scripts/clearDatabase.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import mongoose from 'mongoose';
import dotenv from 'dotenv';
import Match from '../models/Match.js';
import Standing from '../models/Standing.js';

dotenv.config();

async function clearDB() {
try {
await mongoose.connect(process.env.MONGO_URI || 'mongodb://127.0.0.1:27017/saintbarthvolley');
console.log('✅ Connecté à MongoDB');

const deletedMatches = await Match.deleteMany({});
console.log(`🗑️ ${deletedMatches.deletedCount} matchs supprimés`);

const deletedStandings = await Standing.deleteMany({});
console.log(`🗑️ ${deletedStandings.deletedCount} standings supprimés`);

await mongoose.disconnect();
console.log('✅ Déconnecté de MongoDB');
} catch (err) {
console.error('❌ Erreur lors de la suppression :', err);
}
}

clearDB();
93 changes: 66 additions & 27 deletions app/backend/src/scripts/scraping.js
Original file line number Diff line number Diff line change
@@ -1,26 +1,27 @@
// src/scripts/scraping.js
import dotenv from 'dotenv';
dotenv.config();

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 CHAMPIONSHIP_ID = '6985bd859903c98a455ca98d'; // ton championnat
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
// 🔹 Utilitaire pour parser date FR -> ISO
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();
}

// 🔹 Transforme string sets en array d'objets pour Mongo
function parseSetsDetail(setsStr) {
if (!setsStr) return [];
return setsStr.split(',').map((s, idx) => {
Expand All @@ -43,11 +44,12 @@ async function scrapeFFVB() {

const $ = cheerio.load(html);

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

$('tr').each((i, tr) => {
const tds = $(tr).find('td');
Expand All @@ -64,14 +66,19 @@ async function scrapeFFVB() {
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}`);
// 🔹 Déterminer le status correctement
const matchDate = new Date(dateISO);
let status = 'scheduled';
if (matchDate <= now && (scoreFor > 0 || scoreAgainst > 0)) {
status = 'played';
}

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',
status,
scoreFor,
scoreAgainst,
setsDetail,
Expand All @@ -81,31 +88,55 @@ async function scrapeFFVB() {

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,
const existing = await Match.findOne({
championshipId: CHAMPIONSHIP_ID,
opponentName: m.opponentName,
date: m.date,
});

if (existing) {
// 🔹 Vérifier si on a de nouvelles infos avant mise à jour
const needsUpdate =
existing.status !== m.status ||
existing.scoreFor !== m.scoreFor ||
existing.scoreAgainst !== m.scoreAgainst ||
JSON.stringify(existing.setsDetail) !== JSON.stringify(m.setsDetail);

if (needsUpdate) {
await Match.updateOne({ _id: existing._id }, m);
console.log(`🔄 Match mis à jour : ${m.opponentName} (${m.date})`);
}
} else {
await Match.create(m);
console.log(`➕ Match créé : ${m.opponentName} (${m.date})`);
}
}
console.log(`🎉 ${matches.length} matchs mis à jour.`);
console.log(`🎉 ${matches.length} matchs analysés.`);

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

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

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;
if (!teamName) return;

const points = parseInt($(tds[2]).text()) || 0;
const played = parseInt($(tds[3]).text()) || 0;
const wins = parseInt($(tds[4]).text()) || 0;
const losses = parseInt($(tds[5]).text()) || 0;
const setsFor = parseInt($(tds[13]).text()) || 0;
const setsAgainst = parseInt($(tds[14]).text()) || 0;
const coefficientSets = parseFloat($(tds[15]).text()) || 0;
const pointsFor = parseInt($(tds[16]).text()) || 0;
const pointsAgainst = parseInt($(tds[17]).text()) || 0;
const coefficientPoints = parseFloat($(tds[18]).text()) || 0;

standings.push({
championshipId: CHAMPIONSHIP_ID,
Expand All @@ -116,22 +147,30 @@ async function scrapeFFVB() {
losses,
setsFor,
setsAgainst,
coefficientSets,
pointsFor,
pointsAgainst,
coefficientPoints,
points,
});
});

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

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

// 🔹 Connexion MongoDB
// -----------------------------
// 🔹 Connexion MongoDB via .env
// -----------------------------
mongoose
.connect(process.env.MONGO_URI || 'mongodb://127.0.0.1:27017/saintbarthvolley')
.then(() => scrapeFFVB())
Expand Down
Binary file removed docs/projet/images/delUser.png
Binary file not shown.
Binary file removed docs/projet/images/getUser.png
Binary file not shown.
Binary file removed docs/projet/images/putUser.png
Binary file not shown.