Files
pdga-rating/src/services/player-service.js
T
Samuel Enocsson 33a962e6b8 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.
2026-02-18 22:20:58 +01:00

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
};