diff --git a/scripts/bootstrap-user-history.js b/scripts/bootstrap-user-history.js new file mode 100644 index 00000000..b8db9cd9 --- /dev/null +++ b/scripts/bootstrap-user-history.js @@ -0,0 +1,77 @@ +const fs = require("fs"); +const path = require("path"); + +const DATA_DIR = process.env.DATA_DIR || path.join(__dirname, "..", "data"); +const dailyDir = path.join(DATA_DIR, "daily"); +const historyDir = path.join(DATA_DIR, "historical-user-data"); + +if (!fs.existsSync(dailyDir)) { + console.error(`Daily directory not found at: ${dailyDir}`); + process.exit(1); +} + +if (!fs.existsSync(historyDir)) { + fs.mkdirSync(historyDir, { recursive: true }); +} + +console.log("Reading daily snapshots..."); +const files = fs + .readdirSync(dailyDir) + .filter((f) => /^\d{4}-\d{2}-\d{2}-\d\.json$/.test(f)); +console.log(`Found ${files.length} daily snapshots.`); + +const userHistories = {}; + +for (const file of files) { + const filePath = path.join(dailyDir, file); + const dateStr = file.split("-").slice(0, 3).join("-"); + + try { + const data = JSON.parse(fs.readFileSync(filePath, "utf8")); + if (!Array.isArray(data)) continue; + + for (const user of data) { + if (!user.id || !user.data) continue; + + if (!userHistories[user.id]) { + userHistories[user.id] = []; + } + + // Check if entry for this date already exists for the user + const exists = userHistories[user.id].some( + (entry) => entry.date === dateStr, + ); + if (!exists) { + userHistories[user.id].push({ + date: dateStr, + easy: user.data.easySolved || 0, + medium: user.data.mediumSolved || 0, + hard: user.data.hardSolved || 0, + }); + } + } + } catch (err) { + console.error(`Error reading ${file}:`, err.message); + } +} + +console.log("Writing user history files..."); +let count = 0; +for (const username of Object.keys(userHistories)) { + // Sort by date chronologically + userHistories[username].sort((a, b) => new Date(a.date) - new Date(b.date)); + + const userFile = path.join(historyDir, `${username}.json`); + try { + fs.writeFileSync( + userFile, + JSON.stringify(userHistories[username], null, 2), + "utf8", + ); + count++; + } catch (err) { + console.error(`Failed to write history file for ${username}:`, err.message); + } +} + +console.log(`Migration complete. Generated history files for ${count} users.`); diff --git a/scripts/fetch-student-info.js b/scripts/fetch-student-info.js index ace4c9ae..3da66de6 100644 --- a/scripts/fetch-student-info.js +++ b/scripts/fetch-student-info.js @@ -1,3 +1,6 @@ +const fs = require("fs"); +const path = require("path"); + function getFileName(daysAgo) { const now = new Date(); now.setDate(now.getDate() - daysAgo); @@ -14,9 +17,6 @@ function getFileName(daysAgo) { async function fetchStudentHistory(username) { let history = []; let ranking = null; - let missingFilesCount = 0; - const maxDays = 365; - const chunkSize = 100; try { const liveApiUrl = `https://leetcode-api-dun.vercel.app/${username}`; @@ -33,65 +33,38 @@ async function fetchStudentHistory(username) { ); } - let done = false; - - for (let chunkStart = 0; chunkStart < maxDays; chunkStart += chunkSize) { - if (done) break; - - const fetchPromises = []; - const chunkEnd = Math.min(chunkStart + chunkSize, maxDays); - - for (let daysAgo = chunkStart; daysAgo < chunkEnd; daysAgo++) { - const fileName = getFileName(daysAgo); - const rawUrl = `https://raw.githubusercontent.com/codepvg/leetcode-ranking-data/main/daily/${fileName}`; - - const p = fetch(rawUrl) - .then(async (res) => { - if (!res.ok) { - return { daysAgo, fileName, ok: false }; - } - const data = await res.json(); - return { daysAgo, fileName, ok: true, data }; - }) - .catch((err) => { - return { daysAgo, fileName, ok: false, error: err }; - }); - - fetchPromises.push(p); - } - - const results = await Promise.all(fetchPromises); - - for (const result of results) { - if (!result.ok) { - missingFilesCount++; - if (missingFilesCount >= 7) { - done = true; - break; - } - continue; - } - - missingFilesCount = 0; - - const user = result.data.find((u) => u.id === username); - - if (user) { - const dateStr = result.fileName.split("-").slice(0, 3).join("-"); + try { + // If a local data folder is present, try reading from it first (development helper) + const localDir = process.env.DATA_DIR || path.join(__dirname, "..", "data"); + const localFilePath = path.join( + localDir, + "historical-user-data", + `${username}.json`, + ); - history.push({ - date: dateStr, - easy: user.data.easySolved, - medium: user.data.mediumSolved, - hard: user.data.hardSolved, - }); + if (fs.existsSync(localFilePath)) { + console.log(`[Dev] Loading historical data locally for: ${username}`); + history = JSON.parse(fs.readFileSync(localFilePath, "utf8")); + } else { + // Fallback to fetching from remote GitHub repository (production behavior) + const rawUrl = `https://raw.githubusercontent.com/codepvg/leetcode-ranking-data/main/historical-user-data/${username}.json`; + const response = await fetch(rawUrl); + if (response.ok) { + history = await response.json(); } else { - done = true; - break; + console.warn( + `No historical data found for user: ${username} (HTTP ${response.status})`, + ); } } + } catch (err) { + console.error( + `Failed to fetch historical data for ${username}:`, + err.message, + ); } + // Ensure history is sorted chronologically history.sort((a, b) => new Date(a.date) - new Date(b.date)); return { diff --git a/scripts/sync-leaderboard.js b/scripts/sync-leaderboard.js index 105698e7..62a2e4c4 100644 --- a/scripts/sync-leaderboard.js +++ b/scripts/sync-leaderboard.js @@ -31,6 +31,47 @@ function getFileName(daysAgo) { return `${year}-${month}-${date}-${day}.json`; } +function updateUserHistory(user, DATA_DIR) { + const historyDir = path.join(DATA_DIR, "historical-user-data"); + if (!fs.existsSync(historyDir)) { + fs.mkdirSync(historyDir, { recursive: true }); + } + + const userHistoryPath = path.join(historyDir, `${user.id}.json`); + let history = []; + + if (fs.existsSync(userHistoryPath)) { + try { + history = JSON.parse(fs.readFileSync(userHistoryPath, "utf8")); + } catch (err) { + console.error( + `Failed to parse history for ${user.id}, resetting:`, + err.message, + ); + } + } + + const dateStr = getFileName(0).split("-").slice(0, 3).join("-"); + const existingIndex = history.findIndex((entry) => entry.date === dateStr); + + const newEntry = { + date: dateStr, + easy: user.data.easySolved, + medium: user.data.mediumSolved, + hard: user.data.hardSolved, + }; + + if (existingIndex !== -1) { + history[existingIndex] = newEntry; + } else { + history.push(newEntry); + } + + history.sort((a, b) => new Date(a.date) - new Date(b.date)); + + fs.writeFileSync(userHistoryPath, JSON.stringify(history, null, 2), "utf8"); +} + function assignCompetitionRanks(sortedData) { let currentRank = 1; for (let i = 0; i < sortedData.length; i++) { @@ -165,6 +206,12 @@ async function computeRankChanges(currentSorted, filename) { try { fs.writeFileSync(filepath, JSON.stringify(overallData, null, 2), "utf8"); console.log("Daily data saved successfully"); + + console.log("Updating historical user files..."); + overallData.forEach((user) => { + updateUserHistory(user, DATA_DIR); + }); + console.log("Historical user files updated successfully"); } catch (err) { console.error(`Failed to write json file: `, err.message); process.exit(1);