33a962e6b8
Extract 3410-line server.js into 12 focused modules:
- src/db.js: database init and migrations
- src/models/{player,course}.js: DB helper functions
- src/scrapers/{browser,player-http,player-puppeteer,course-puppeteer}.js
- src/services/{player-service,rating-calculator}.js
- src/routes/{players,courses,pages}.js
Remove dead code: duplicate saveRatingHistoryToDB, legacy
getPlayerCompetitionRatings/getPredictedRating/getAllRatingsWithScraping,
unused getCourseFromDB/getLatestOfficialRoundDate/testPDGARateLimit,
legacy cache Map, and POST /api/predicted-rating route.
Consolidate 5 duplicated Puppeteer launch blocks into launchBrowser().
server.js is now 28 lines: imports, middleware, mount routers, bootstrap.
276 lines
7.9 KiB
JavaScript
276 lines
7.9 KiB
JavaScript
const { db } = require('../db');
|
|
const { getPlayerFromDB, getRoundHistoryFromDB, savePredictedRatingToDB, savePlayerToDB } = require('../models/player');
|
|
const { fetchPlayerDataHTTP, parsePlayerData } = require('../scrapers/player-http');
|
|
const { calculatePredictedRating } = require('./rating-calculator');
|
|
|
|
async function getPlayerDataFromDB(pdgaNumber) {
|
|
try {
|
|
const cachedPlayer = await getPlayerFromDB(pdgaNumber);
|
|
if (cachedPlayer) {
|
|
console.log(`Loading PDGA ${pdgaNumber} from DB (source of truth)`);
|
|
|
|
let predictedRating = cachedPlayer.predicted_rating;
|
|
let stdDev = cachedPlayer.std_dev;
|
|
if (!predictedRating || predictedRating === 0) {
|
|
predictedRating = await getPredictedRatingFromDB(pdgaNumber);
|
|
const updatedPlayer = await getPlayerFromDB(pdgaNumber);
|
|
stdDev = updatedPlayer?.std_dev;
|
|
}
|
|
|
|
return {
|
|
pdgaNumber: cachedPlayer.pdga_number,
|
|
name: cachedPlayer.name,
|
|
rating: cachedPlayer.current_rating,
|
|
ratingChange: cachedPlayer.rating_change,
|
|
predictedRating: predictedRating > 0 ? predictedRating : null,
|
|
stdDev: stdDev > 0 ? stdDev : null
|
|
};
|
|
}
|
|
return null;
|
|
} catch (err) {
|
|
console.error(`Database error for PDGA ${pdgaNumber}:`, err.message);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
async function scrapePDGARating(pdgaNumber, retries = 3) {
|
|
console.log(`=== Refreshing PDGA ${pdgaNumber} from PDGA website ===`);
|
|
|
|
for (let attempt = 1; attempt <= retries; attempt++) {
|
|
try {
|
|
console.log(`Attempt ${attempt}/${retries} for PDGA ${pdgaNumber} (using HTTP)`);
|
|
|
|
const html = await fetchPlayerDataHTTP(pdgaNumber);
|
|
const result = parsePlayerData(html, pdgaNumber);
|
|
|
|
try {
|
|
await savePlayerToDB(result);
|
|
console.log(`Saved PDGA ${pdgaNumber} to database`);
|
|
} catch (dbErr) {
|
|
console.error(`Failed to save PDGA ${pdgaNumber} to database:`, dbErr.message);
|
|
}
|
|
|
|
console.log(`Successfully scraped PDGA ${pdgaNumber} on attempt ${attempt}`);
|
|
return result;
|
|
|
|
} catch (error) {
|
|
console.error(`Attempt ${attempt}/${retries} failed for PDGA ${pdgaNumber}:`, error.message);
|
|
|
|
if (attempt === retries) {
|
|
return {
|
|
pdgaNumber,
|
|
name: 'Error',
|
|
rating: 0,
|
|
ratingChange: null,
|
|
predictedRating: null
|
|
};
|
|
}
|
|
|
|
let retryDelay = 2000 * attempt;
|
|
|
|
if (error.rateLimitInfo) {
|
|
const retryAfter = error.rateLimitInfo.headers['retry-after'];
|
|
if (retryAfter) {
|
|
retryDelay = Math.max(retryDelay, (parseInt(retryAfter) + 1) * 1000);
|
|
console.log(`Using Retry-After header: waiting ${retryDelay/1000}s`);
|
|
}
|
|
}
|
|
|
|
if (error.code === 'ECONNRESET') {
|
|
retryDelay = Math.max(retryDelay, 10000);
|
|
console.log(`Connection reset detected: waiting ${retryDelay/1000}s`);
|
|
}
|
|
|
|
await new Promise(resolve => setTimeout(resolve, retryDelay));
|
|
}
|
|
}
|
|
}
|
|
|
|
async function getPredictedRatingFromDB(pdgaNumber) {
|
|
try {
|
|
const roundHistory = await getRoundHistoryFromDB(pdgaNumber);
|
|
if (roundHistory.length > 0) {
|
|
console.log(`Using ${roundHistory.length} cached rounds for PDGA ${pdgaNumber} prediction`);
|
|
|
|
const roundRatings = roundHistory.map(round => ({
|
|
rating: round.rating,
|
|
date: new Date(round.date),
|
|
competition: round.competition_name || 'Unknown'
|
|
}));
|
|
|
|
const result = calculatePredictedRating(roundRatings);
|
|
|
|
await savePredictedRatingToDB(pdgaNumber, result.rating, result.stdDev);
|
|
|
|
return result.rating;
|
|
}
|
|
return 0;
|
|
} catch (err) {
|
|
console.error(`Error getting predicted rating from DB for ${pdgaNumber}:`, err.message);
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
async function getAllRatingsFromDB(progressCallback = null) {
|
|
try {
|
|
const allPlayers = await new Promise((resolve, reject) => {
|
|
db.all(
|
|
'SELECT pdga_number, name, current_rating, rating_change FROM players ORDER BY pdga_number',
|
|
[],
|
|
(err, rows) => {
|
|
if (err) reject(err);
|
|
else resolve(rows || []);
|
|
}
|
|
);
|
|
});
|
|
|
|
console.log(`Loading ${allPlayers.length} players from database...`);
|
|
|
|
const ratings = [];
|
|
const total = allPlayers.length;
|
|
|
|
for (let i = 0; i < allPlayers.length; i++) {
|
|
const player = allPlayers[i];
|
|
const pdgaNumber = player.pdga_number;
|
|
|
|
if (progressCallback) {
|
|
progressCallback({
|
|
current: i + 1,
|
|
total,
|
|
pdgaNumber,
|
|
status: 'loading'
|
|
});
|
|
}
|
|
|
|
try {
|
|
const playerData = await getPlayerDataFromDB(pdgaNumber);
|
|
|
|
if (playerData) {
|
|
ratings.push(playerData);
|
|
}
|
|
|
|
if (progressCallback) {
|
|
progressCallback({
|
|
current: i + 1,
|
|
total,
|
|
pdgaNumber,
|
|
status: 'completed',
|
|
name: playerData ? playerData.name : player.name
|
|
});
|
|
}
|
|
} catch (error) {
|
|
console.error(`Failed to load PDGA ${pdgaNumber} from database:`, error.message);
|
|
const errorData = {
|
|
pdgaNumber: parseInt(pdgaNumber),
|
|
name: player.name || 'Database Error',
|
|
rating: player.current_rating,
|
|
ratingChange: player.rating_change,
|
|
predictedRating: null
|
|
};
|
|
ratings.push(errorData);
|
|
|
|
if (progressCallback) {
|
|
progressCallback({
|
|
current: i + 1,
|
|
total,
|
|
pdgaNumber,
|
|
status: 'error',
|
|
name: player.name || 'Database Error'
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
return ratings.sort((a, b) => (b.rating || 0) - (a.rating || 0));
|
|
} catch (error) {
|
|
console.error('Error loading players from database:', error);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
async function refreshAllPlayersInDB(progressCallback = null) {
|
|
try {
|
|
const allPlayers = await new Promise((resolve, reject) => {
|
|
db.all(
|
|
'SELECT pdga_number, name FROM players ORDER BY pdga_number',
|
|
[],
|
|
(err, rows) => {
|
|
if (err) reject(err);
|
|
else resolve(rows || []);
|
|
}
|
|
);
|
|
});
|
|
|
|
console.log(`Refreshing ${allPlayers.length} players from database...`);
|
|
|
|
const ratings = [];
|
|
const total = allPlayers.length;
|
|
|
|
for (let i = 0; i < allPlayers.length; i++) {
|
|
const player = allPlayers[i];
|
|
const pdgaNumber = player.pdga_number;
|
|
|
|
console.log(`Refreshing PDGA ${pdgaNumber}... (${i + 1}/${total})`);
|
|
|
|
if (progressCallback) {
|
|
progressCallback({
|
|
current: i + 1,
|
|
total,
|
|
pdgaNumber,
|
|
status: 'loading'
|
|
});
|
|
}
|
|
|
|
try {
|
|
const playerData = await scrapePDGARating(pdgaNumber);
|
|
ratings.push(playerData);
|
|
|
|
if (progressCallback) {
|
|
progressCallback({
|
|
current: i + 1,
|
|
total,
|
|
pdgaNumber,
|
|
status: 'completed',
|
|
name: playerData.name
|
|
});
|
|
}
|
|
|
|
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
} catch (error) {
|
|
console.error(`Failed to refresh PDGA ${pdgaNumber}:`, error.message);
|
|
const errorData = {
|
|
pdgaNumber: parseInt(pdgaNumber),
|
|
name: player.name || 'Error',
|
|
rating: 0,
|
|
ratingChange: null,
|
|
predictedRating: null
|
|
};
|
|
ratings.push(errorData);
|
|
|
|
if (progressCallback) {
|
|
progressCallback({
|
|
current: i + 1,
|
|
total,
|
|
pdgaNumber,
|
|
status: 'error',
|
|
name: player.name || 'Error'
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
return ratings.sort((a, b) => (b.rating || 0) - (a.rating || 0));
|
|
} catch (error) {
|
|
console.error('Error refreshing all players:', error);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
module.exports = {
|
|
getPlayerDataFromDB,
|
|
scrapePDGARating,
|
|
getPredictedRatingFromDB,
|
|
getAllRatingsFromDB,
|
|
refreshAllPlayersInDB
|
|
};
|