Refactor: split server.js monolith into modular architecture
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.
This commit is contained in:
@@ -0,0 +1,275 @@
|
||||
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
|
||||
};
|
||||
Reference in New Issue
Block a user