refactor: remove patch_detector and use gameVersion field in match_collector
This commit is contained in:
@@ -43,18 +43,6 @@ jobs:
|
|||||||
working-directory: ./match_collector
|
working-directory: ./match_collector
|
||||||
run: npm run format:check
|
run: npm run format:check
|
||||||
|
|
||||||
- name: Install dependencies for patch_detector
|
|
||||||
working-directory: ./patch_detector
|
|
||||||
run: npm ci
|
|
||||||
|
|
||||||
- name: Lint patch_detector
|
|
||||||
working-directory: ./patch_detector
|
|
||||||
run: npm run lint
|
|
||||||
|
|
||||||
- name: Check formatting for patch_detector
|
|
||||||
working-directory: ./patch_detector
|
|
||||||
run: npm run format:check
|
|
||||||
|
|
||||||
build-and-push-images:
|
build-and-push-images:
|
||||||
needs: lint-and-format
|
needs: lint-and-format
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -79,15 +67,6 @@ jobs:
|
|||||||
git.vhaudiquet.fr/vhaudiquet/lolstats-frontend:latest
|
git.vhaudiquet.fr/vhaudiquet/lolstats-frontend:latest
|
||||||
git.vhaudiquet.fr/vhaudiquet/lolstats-frontend:${{ github.sha }}
|
git.vhaudiquet.fr/vhaudiquet/lolstats-frontend:${{ github.sha }}
|
||||||
|
|
||||||
- name: Build and push patch_detector docker image
|
|
||||||
uses: docker/build-push-action@v6
|
|
||||||
with:
|
|
||||||
context: ./patch_detector
|
|
||||||
push: true
|
|
||||||
tags: |
|
|
||||||
git.vhaudiquet.fr/vhaudiquet/lolstats-patch_detector:latest
|
|
||||||
git.vhaudiquet.fr/vhaudiquet/lolstats-patch_detector:${{ github.sha }}
|
|
||||||
|
|
||||||
- name: Build and push match_collector docker image
|
- name: Build and push match_collector docker image
|
||||||
uses: docker/build-push-action@v6
|
uses: docker/build-push-action@v6
|
||||||
with:
|
with:
|
||||||
|
|||||||
12
README.md
12
README.md
@@ -5,10 +5,9 @@ https://buildpath.win
|
|||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
BuildPath is made of four components:
|
BuildPath is made of three components:
|
||||||
- a MongoDB document database
|
- a MongoDB document database
|
||||||
- `patch_detector`, a daemon which runs periodically to detect new League of Legends patch from official APIs
|
- `match_collector`, a daemon which runs periodically to collect matches from League of Legends challenger players, automatically organizing them by patch version (extracted from match data), and generating statistics
|
||||||
- `match_collector`, a daemon which runs periodically to collect matches from League of Legends challenger players on current patch, and generate statistics from those matches, saving them in the database
|
|
||||||
- `frontend`, which is a Nuxt.JS project hosting an API serving the statistics but also the full web frontend (Vue.JS)
|
- `frontend`, which is a Nuxt.JS project hosting an API serving the statistics but also the full web frontend (Vue.JS)
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
@@ -19,9 +18,9 @@ Developing BuildPath requires Docker for a local MongoDB instance, and a NodeJS/
|
|||||||
Then, for the first-time setup:
|
Then, for the first-time setup:
|
||||||
```bash
|
```bash
|
||||||
# Install npm and node
|
# Install npm and node
|
||||||
sudo apt install npm nodejs # Ubuntu
|
sudo apt install npm nodejs node-typescript # Ubuntu/Debian
|
||||||
sudo pacman -S npm nodejs # Arch
|
sudo pacman -S npm nodejs typescript # Arch
|
||||||
sudo dnf install nodejs # Fedora
|
sudo dnf install nodejs typescript # Fedora
|
||||||
|
|
||||||
# Install docker. Follow instructions on
|
# Install docker. Follow instructions on
|
||||||
# https://docs.docker.com/engine/install/
|
# https://docs.docker.com/engine/install/
|
||||||
@@ -30,7 +29,6 @@ sudo dnf install nodejs # Fedora
|
|||||||
cd dev && npm i && cd .. # Install dependencies for the dev environment
|
cd dev && npm i && cd .. # Install dependencies for the dev environment
|
||||||
cd frontend && npm i && cd .. # Install dependencies for frontend
|
cd frontend && npm i && cd .. # Install dependencies for frontend
|
||||||
cd match_collector && npm i && cd .. # Install dependencies for match_collector
|
cd match_collector && npm i && cd .. # Install dependencies for match_collector
|
||||||
cd patch_detector && npm i && cd .. # Install dependencies for patch_detector
|
|
||||||
```
|
```
|
||||||
|
|
||||||
BuildPath needs data to work, either for generating statistics in the `match_collector` or for the frontend.
|
BuildPath needs data to work, either for generating statistics in the `match_collector` or for the frontend.
|
||||||
|
|||||||
@@ -1,34 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
|
|
||||||
import { spawn } from 'child_process';
|
|
||||||
import path from 'path';
|
|
||||||
import { fileURLToPath } from 'url';
|
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
|
||||||
const __dirname = path.dirname(__filename);
|
|
||||||
|
|
||||||
// Cache directory - use dev/cdragon by default
|
|
||||||
const cacheDir = process.env.CDRAGON_CACHE_DIR || path.join(__dirname, '..', 'data', 'cdragon');
|
|
||||||
|
|
||||||
// Dev MongoDB credentials (matching docker-compose.yml defaults)
|
|
||||||
const mongoUser = process.env.MONGO_USER || 'root';
|
|
||||||
const mongoPass = process.env.MONGO_PASS || 'password';
|
|
||||||
const mongoHost = process.env.MONGO_HOST || 'localhost:27017';
|
|
||||||
|
|
||||||
// Run patch_detector with the cache directory and dev MongoDB credentials
|
|
||||||
const patchDetector = spawn('npx', ['tsx', '../patch_detector/index.ts'], {
|
|
||||||
cwd: path.join(__dirname, '..'),
|
|
||||||
env: {
|
|
||||||
...process.env,
|
|
||||||
NODE_ENV: 'development',
|
|
||||||
CDRAGON_CACHE_DIR: cacheDir,
|
|
||||||
MONGO_USER: mongoUser,
|
|
||||||
MONGO_PASS: mongoPass,
|
|
||||||
MONGO_HOST: mongoHost
|
|
||||||
},
|
|
||||||
stdio: 'inherit'
|
|
||||||
});
|
|
||||||
|
|
||||||
patchDetector.on('close', (code) => {
|
|
||||||
process.exit(code || 0);
|
|
||||||
});
|
|
||||||
@@ -20,39 +20,69 @@ async function setupDatabase() {
|
|||||||
|
|
||||||
// Check if data directory exists and has files
|
// Check if data directory exists and has files
|
||||||
const dataDir = path.join(__dirname, '../data');
|
const dataDir = path.join(__dirname, '../data');
|
||||||
const patchFile = path.join(dataDir, "patches.json");
|
|
||||||
if(!fs.existsSync(dataDir) || !fs.existsSync(patchFile)) {
|
// Try to get latest patch version from existing data files or database
|
||||||
fs.mkdirSync(dataDir, { recursive: true });
|
let latestPatch = await getLatestPatchVersion();
|
||||||
console.log('🚫 No data files found. Downloading latest snapshot...');
|
|
||||||
|
// If no patch found, download snapshot
|
||||||
|
if (!latestPatch) {
|
||||||
|
if (!fs.existsSync(dataDir)) {
|
||||||
|
fs.mkdirSync(dataDir, { recursive: true });
|
||||||
|
}
|
||||||
|
console.log('🚫 No match data found. Downloading latest snapshot...');
|
||||||
await downloadAndExtractSnapshot();
|
await downloadAndExtractSnapshot();
|
||||||
|
|
||||||
|
// Try again after download
|
||||||
|
latestPatch = await getLatestPatchVersion();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get latest patch version
|
if (!latestPatch) {
|
||||||
const latestPatch = await getLatestPatchVersion();
|
console.error('❌ Could not determine latest patch version');
|
||||||
|
console.log('💡 Make sure you have match data files in the data directory');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
console.log(`🎯 Latest patch version: ${latestPatch}`);
|
console.log(`🎯 Latest patch version: ${latestPatch}`);
|
||||||
|
|
||||||
// Check if data directory exists and has files
|
// Check if data directory exists and has files
|
||||||
// Support both old format (patch_matches.json) and new platform-specific format (patch_PLATFORM_matches.json)
|
// Support both old format (patch_matches.json) and new platform-specific format (patch_PLATFORM_matches.json)
|
||||||
|
// Also support both "XX.Y" and "XX.Y.Z" patch formats in filenames
|
||||||
console.log('🔍 Checking for data files...');
|
console.log('🔍 Checking for data files...');
|
||||||
const platforms = ['EUW1', 'EUN1', 'NA1', 'KR'];
|
const platforms = ['EUW1', 'EUN1', 'NA1', 'KR'];
|
||||||
const dataFiles = [
|
const dataFiles = [];
|
||||||
{ path: 'patches.json', required: true, description: 'Patches data' }
|
|
||||||
];
|
|
||||||
|
|
||||||
// Check for platform-specific match files
|
// Check for platform-specific match files
|
||||||
|
// Files may be named with either "16.8" or "16.8.1" format
|
||||||
let foundPlatformFiles = [];
|
let foundPlatformFiles = [];
|
||||||
for (const platform of platforms) {
|
for (const platform of platforms) {
|
||||||
const platformFile = `${latestPatch}_${platform}.json`;
|
// Try both formats: "16.8_PLATFORM.json" and "16.8.1_PLATFORM.json"
|
||||||
const fullPath = path.join(dataDir, platformFile);
|
const files = fs.readdirSync(dataDir);
|
||||||
if (fs.existsSync(fullPath)) {
|
const matchFile = files.find(f => {
|
||||||
|
const match = f.match(/^(\d+\.\d+(?:\.\d+)?)_([A-Z0-9]+)\.json$/);
|
||||||
|
const patchFromName = match ? match[1].split('.').slice(0, 2).join('.') : null;
|
||||||
|
return match && patchFromName === latestPatch && match[2] === platform;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (matchFile) {
|
||||||
foundPlatformFiles.push(platform);
|
foundPlatformFiles.push(platform);
|
||||||
dataFiles.push({ path: platformFile, required: false, description: `Match data for ${platform}` });
|
dataFiles.push({ path: matchFile, required: false, description: `Match data for ${platform}` });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If no platform-specific files found, look for old format
|
// If no platform-specific files found, look for old format
|
||||||
if (foundPlatformFiles.length === 0) {
|
if (foundPlatformFiles.length === 0) {
|
||||||
dataFiles.push({ path: `${latestPatch}_matches.json`, required: true, description: 'Match data' });
|
// Try to find any match file for this patch
|
||||||
|
const files = fs.readdirSync(dataDir);
|
||||||
|
const matchFile = files.find(f => {
|
||||||
|
const match = f.match(/^(\d+\.\d+(?:\.\d+)?)(?:_matches)?\.json$/);
|
||||||
|
const patchFromName = match ? match[1].split('.').slice(0, 2).join('.') : null;
|
||||||
|
return match && patchFromName === latestPatch;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (matchFile) {
|
||||||
|
dataFiles.push({ path: matchFile, required: true, description: 'Match data' });
|
||||||
|
} else {
|
||||||
|
dataFiles.push({ path: `${latestPatch}_matches.json`, required: true, description: 'Match data' });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let filesExist = true;
|
let filesExist = true;
|
||||||
@@ -91,11 +121,7 @@ async function setupDatabase() {
|
|||||||
// 4. Wait for MongoDB to be ready
|
// 4. Wait for MongoDB to be ready
|
||||||
await waitForMongoDB();
|
await waitForMongoDB();
|
||||||
|
|
||||||
// 5. Import patches data
|
// 5. Check existing matches count and import if needed
|
||||||
console.log('📦 Importing patches data...');
|
|
||||||
await importPatchesData();
|
|
||||||
|
|
||||||
// 6. Check existing matches count and import if needed
|
|
||||||
console.log('Checking existing matches count...');
|
console.log('Checking existing matches count...');
|
||||||
|
|
||||||
// Check for platform-specific collections or fall back to old format
|
// Check for platform-specific collections or fall back to old format
|
||||||
@@ -129,11 +155,7 @@ async function setupDatabase() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 7. Fetch CDragon data for the current patch
|
// 7. Run match collector to generate stats (this also handles CDragon caching)
|
||||||
console.log('🎮 Fetching CDragon data...');
|
|
||||||
await fetchCDragonData();
|
|
||||||
|
|
||||||
// 8. Run match collector to generate stats
|
|
||||||
console.log('📊 Generating champion stats...');
|
console.log('📊 Generating champion stats...');
|
||||||
await generateChampionStats();
|
await generateChampionStats();
|
||||||
|
|
||||||
@@ -148,48 +170,70 @@ async function setupDatabase() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function getLatestPatchVersion() {
|
async function getLatestPatchVersion() {
|
||||||
try {
|
const dataDir = path.join(__dirname, '../data');
|
||||||
const filePath = path.join(__dirname, '../data/patches.json');
|
|
||||||
if(!fs.existsSync(filePath)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const fileContent = fs.readFileSync(filePath, 'utf8');
|
// First, try to get patch from match data files (format: PATCH_PLATFORM.json or PATCH_matches.json)
|
||||||
|
if (fs.existsSync(dataDir)) {
|
||||||
|
const files = fs.readdirSync(dataDir);
|
||||||
|
const patches = new Set();
|
||||||
|
|
||||||
// Check if it's line-delimited JSON or array format
|
for (const file of files) {
|
||||||
let patchesData;
|
// Match patterns like "16.8.1_EUW1.json" or "15.1_EUW1.json" or "15.1_matches.json" or "15.1.json"
|
||||||
if (fileContent.trim().startsWith('[')) {
|
// Patch version can be either "XX.Y" or "XX.Y.Z" format
|
||||||
// Array format
|
const match = file.match(/^(\d+\.\d+(?:\.\d+)?)(?:_[A-Z0-9]+)?(?:_matches)?\.json$/);
|
||||||
patchesData = JSON.parse(fileContent);
|
if (match) {
|
||||||
if (!Array.isArray(patchesData)) {
|
// Normalize to "XX.Y" format (strip the third part if present)
|
||||||
throw new Error('Patches data should be an array');
|
const patch = match[1].split('.').slice(0, 2).join('.');
|
||||||
|
patches.add(patch);
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
// Line-delimited JSON format
|
|
||||||
patchesData = fileContent.split('\n')
|
|
||||||
.filter(line => line.trim() !== '')
|
|
||||||
.map(line => JSON.parse(line));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert dates to Date objects for proper sorting
|
if (patches.size > 0) {
|
||||||
patchesData = patchesData.map(patch => ({
|
// Sort patches and return the latest (highest version number)
|
||||||
...patch,
|
const sortedPatches = Array.from(patches).sort((a, b) => {
|
||||||
date: new Date(patch.date.$date || patch.date)
|
const [aMajor, aMinor] = a.split('.').map(Number);
|
||||||
}));
|
const [bMajor, bMinor] = b.split('.').map(Number);
|
||||||
|
if (aMajor !== bMajor) return bMajor - aMajor;
|
||||||
// Sort patches by date (newest first) and get the latest
|
return bMinor - aMinor;
|
||||||
const sortedPatches = patchesData.sort((a, b) => b.date - a.date);
|
});
|
||||||
const latestPatch = sortedPatches[0];
|
return sortedPatches[0];
|
||||||
|
|
||||||
if (!latestPatch || !latestPatch.patch) {
|
|
||||||
throw new Error('Could not find patch version in patches data');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return latestPatch.patch;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('❌ Failed to get latest patch version:', error);
|
|
||||||
throw error;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fallback: try to get from database collections
|
||||||
|
try {
|
||||||
|
const client = new MongoClient(getMongoUri());
|
||||||
|
await client.connect();
|
||||||
|
|
||||||
|
const db = client.db('matches');
|
||||||
|
const collections = await db.listCollections().toArray();
|
||||||
|
const collectionNames = collections.map(c => c.name);
|
||||||
|
|
||||||
|
const patches = new Set();
|
||||||
|
for (const name of collectionNames) {
|
||||||
|
// Collection names are either "patch_platform" or just "patch"
|
||||||
|
const patch = name.split('_')[0];
|
||||||
|
if (patch && /^\d+\.\d+$/.test(patch)) {
|
||||||
|
patches.add(patch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await client.close();
|
||||||
|
|
||||||
|
if (patches.size > 0) {
|
||||||
|
const sortedPatches = Array.from(patches).sort((a, b) => {
|
||||||
|
const [aMajor, aMinor] = a.split('.').map(Number);
|
||||||
|
const [bMajor, bMinor] = b.split('.').map(Number);
|
||||||
|
if (aMajor !== bMajor) return bMajor - aMajor;
|
||||||
|
return bMinor - aMinor;
|
||||||
|
});
|
||||||
|
return sortedPatches[0];
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// Database not available, continue with other methods
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function downloadAndExtractSnapshot() {
|
async function downloadAndExtractSnapshot() {
|
||||||
@@ -256,83 +300,25 @@ async function waitForMongoDB() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function importPatchesData() {
|
|
||||||
const client = new MongoClient(getMongoUri());
|
|
||||||
await client.connect();
|
|
||||||
|
|
||||||
try {
|
|
||||||
const filePath = path.join(__dirname, '../data/patches.json');
|
|
||||||
const fileContent = fs.readFileSync(filePath, 'utf8');
|
|
||||||
|
|
||||||
// Check if it's line-delimited JSON or array format
|
|
||||||
let patchesData;
|
|
||||||
if (fileContent.trim().startsWith('[')) {
|
|
||||||
// Array format
|
|
||||||
patchesData = JSON.parse(fileContent);
|
|
||||||
if (!Array.isArray(patchesData)) {
|
|
||||||
throw new Error('Patches data should be an array');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Line-delimited JSON format
|
|
||||||
patchesData = fileContent.split('\n')
|
|
||||||
.filter(line => line.trim() !== '')
|
|
||||||
.map(line => {
|
|
||||||
const doc = JSON.parse(line);
|
|
||||||
return convertMongoExtendedJson(doc);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert any extended JSON in array format too
|
|
||||||
if (Array.isArray(patchesData)) {
|
|
||||||
patchesData = patchesData.map(doc => convertMongoExtendedJson(doc));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sort patches by date (newest first)
|
|
||||||
patchesData.sort((a, b) => {
|
|
||||||
const dateA = new Date(a.date || a.date.$date || 0);
|
|
||||||
const dateB = new Date(b.date || b.date.$date || 0);
|
|
||||||
return dateB - dateA; // Descending order (newest first)
|
|
||||||
});
|
|
||||||
|
|
||||||
const db = client.db('patches');
|
|
||||||
const collection = db.collection('patches');
|
|
||||||
|
|
||||||
// Clear existing data
|
|
||||||
await collection.deleteMany({});
|
|
||||||
|
|
||||||
// Insert sorted data
|
|
||||||
const result = await collection.insertMany(patchesData);
|
|
||||||
console.log(`✅ Imported ${result.insertedCount} patches (sorted by date)`);
|
|
||||||
|
|
||||||
// Create index
|
|
||||||
await collection.createIndex({ date: -1 });
|
|
||||||
console.log('✅ Created patches index');
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('❌ Failed to import patches:', error);
|
|
||||||
throw error;
|
|
||||||
} finally {
|
|
||||||
await client.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function importMatchesData(patchVersion, foundPlatformFiles = []) {
|
async function importMatchesData(patchVersion, foundPlatformFiles = []) {
|
||||||
const dataDir = path.join(__dirname, '../data');
|
const dataDir = path.join(__dirname, '../data');
|
||||||
|
const files = fs.readdirSync(dataDir);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// If platform-specific files were found, import each one
|
// If platform-specific files were found, import each one
|
||||||
if (foundPlatformFiles.length > 0) {
|
if (foundPlatformFiles.length > 0) {
|
||||||
for (const platform of foundPlatformFiles) {
|
for (const platform of foundPlatformFiles) {
|
||||||
// Try both formats: patch_PLATFORM.json and patch_PLATFORM_matches.json
|
// Find the actual file for this platform (could be "16.8_PLATFORM.json" or "16.8.1_PLATFORM.json")
|
||||||
let matchesFile = path.join(dataDir, `${patchVersion}_${platform}.json`);
|
const matchFile = files.find(f => {
|
||||||
const collectionName = `${patchVersion}_${platform}`;
|
const match = f.match(/^(\d+\.\d+(?:\.\d+)?)_([A-Z0-9]+)\.json$/);
|
||||||
|
const patchFromName = match ? match[1].split('.').slice(0, 2).join('.') : null;
|
||||||
|
return match && patchFromName === patchVersion && match[2] === platform;
|
||||||
|
});
|
||||||
|
|
||||||
// Fallback to _matches.json suffix if the direct file doesn't exist
|
if (matchFile) {
|
||||||
if (!fs.existsSync(matchesFile)) {
|
const matchesFile = path.join(dataDir, matchFile);
|
||||||
matchesFile = path.join(dataDir, `${patchVersion}_${platform}_matches.json`);
|
const collectionName = `${patchVersion}_${platform}`;
|
||||||
}
|
|
||||||
|
|
||||||
if (fs.existsSync(matchesFile)) {
|
|
||||||
console.log(`📥 Importing matches for ${platform}...`);
|
console.log(`📥 Importing matches for ${platform}...`);
|
||||||
execSync(
|
execSync(
|
||||||
`node ${path.join(__dirname, 'process-matches.js')} ${matchesFile} ${collectionName} 1000`,
|
`node ${path.join(__dirname, 'process-matches.js')} ${matchesFile} ${collectionName} 1000`,
|
||||||
@@ -348,15 +334,16 @@ async function importMatchesData(patchVersion, foundPlatformFiles = []) {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Fall back to old format (single file without platform suffix)
|
// Fall back to old format (single file without platform suffix)
|
||||||
// Try both formats: patch_matches.json and patch.json
|
// Find any match file for this patch
|
||||||
let matchesFile = path.join(dataDir, `${patchVersion}_matches.json`);
|
const matchFile = files.find(f => {
|
||||||
const collectionName = patchVersion;
|
const match = f.match(/^(\d+\.\d+(?:\.\d+)?)(?:_matches)?\.json$/);
|
||||||
|
const patchFromName = match ? match[1].split('.').slice(0, 2).join('.') : null;
|
||||||
|
return match && patchFromName === patchVersion;
|
||||||
|
});
|
||||||
|
|
||||||
if (!fs.existsSync(matchesFile)) {
|
if (matchFile) {
|
||||||
matchesFile = path.join(dataDir, `${patchVersion}.json`);
|
const matchesFile = path.join(dataDir, matchFile);
|
||||||
}
|
const collectionName = patchVersion;
|
||||||
|
|
||||||
if (fs.existsSync(matchesFile)) {
|
|
||||||
execSync(
|
execSync(
|
||||||
`node ${path.join(__dirname, 'process-matches.js')} ${matchesFile} ${collectionName} 1000`,
|
`node ${path.join(__dirname, 'process-matches.js')} ${matchesFile} ${collectionName} 1000`,
|
||||||
{
|
{
|
||||||
@@ -404,24 +391,6 @@ async function generateChampionStats() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchCDragonData() {
|
|
||||||
try {
|
|
||||||
console.log('🔄 Running CDragon fetcher...');
|
|
||||||
|
|
||||||
// Run the fetch-cdragon script
|
|
||||||
const fetchCDragonPath = path.join(__dirname, 'fetch-cdragon.js');
|
|
||||||
execSync(`node ${fetchCDragonPath}`, {
|
|
||||||
stdio: 'inherit',
|
|
||||||
cwd: path.join(__dirname, '..')
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('✅ CDragon data fetched');
|
|
||||||
} catch (error) {
|
|
||||||
console.error('❌ Failed to fetch CDragon data:', error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getMatchCount(patchVersion, platform = null) {
|
async function getMatchCount(patchVersion, platform = null) {
|
||||||
const client = new MongoClient(getMongoUri());
|
const client = new MongoClient(getMongoUri());
|
||||||
await client.connect();
|
await client.connect();
|
||||||
@@ -582,9 +551,6 @@ if (import.meta.url === `file://${process.argv[1]}`) {
|
|||||||
case 'generate-stats':
|
case 'generate-stats':
|
||||||
generateChampionStats().catch(console.error);
|
generateChampionStats().catch(console.error);
|
||||||
break;
|
break;
|
||||||
case 'import-patches':
|
|
||||||
importPatchesData().catch(console.error);
|
|
||||||
break;
|
|
||||||
case 'match-count':
|
case 'match-count':
|
||||||
if (args[1]) {
|
if (args[1]) {
|
||||||
getMatchCount(args[1]).then(count => console.log(`Match count: ${count}`)).catch(console.error);
|
getMatchCount(args[1]).then(count => console.log(`Match count: ${count}`)).catch(console.error);
|
||||||
@@ -605,7 +571,6 @@ if (import.meta.url === `file://${process.argv[1]}`) {
|
|||||||
|
|
||||||
export {
|
export {
|
||||||
setupDatabase,
|
setupDatabase,
|
||||||
importPatchesData,
|
|
||||||
importMatchesData,
|
importMatchesData,
|
||||||
generateChampionStats,
|
generateChampionStats,
|
||||||
checkDatabaseStatus,
|
checkDatabaseStatus,
|
||||||
|
|||||||
@@ -19,11 +19,38 @@ async function connectToDatabase() {
|
|||||||
return client
|
return client
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchLatestPatch(client: MongoClient) {
|
/**
|
||||||
const database = client.db('patches')
|
* Get the latest patch from existing match collections in the database.
|
||||||
const patches = database.collection('patches')
|
* Collections are named like "15.1_EUW1", "15.2_NA1", etc.
|
||||||
const latestPatch = await patches.find().limit(1).sort({ date: -1 }).next()
|
*/
|
||||||
return latestPatch!.patch as string
|
async function fetchLatestPatch(client: MongoClient): Promise<string> {
|
||||||
|
const matchesDb = client.db('matches')
|
||||||
|
const collections = await matchesDb.listCollections().toArray()
|
||||||
|
const collectionNames = collections.map(c => c.name)
|
||||||
|
|
||||||
|
// Extract unique patch versions from collection names
|
||||||
|
const patches = new Set<string>()
|
||||||
|
for (const name of collectionNames) {
|
||||||
|
// Collection names are either "patch_platform" or just "patch"
|
||||||
|
const patch = name.split('_')[0]
|
||||||
|
if (patch && /^\d+\.\d+$/.test(patch)) {
|
||||||
|
patches.add(patch)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (patches.size === 0) {
|
||||||
|
throw new Error('No patch collections found in database')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort patches and return the latest (highest version number)
|
||||||
|
const sortedPatches = Array.from(patches).sort((a, b) => {
|
||||||
|
const [aMajor, aMinor] = a.split('.').map(Number)
|
||||||
|
const [bMajor, bMinor] = b.split('.').map(Number)
|
||||||
|
if (aMajor !== bMajor) return bMajor - aMajor
|
||||||
|
return bMinor - aMinor
|
||||||
|
})
|
||||||
|
|
||||||
|
return sortedPatches[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -6,10 +6,21 @@ type Match = {
|
|||||||
}
|
}
|
||||||
info: {
|
info: {
|
||||||
endOfGameResult: string
|
endOfGameResult: string
|
||||||
frameInterval: number
|
gameCreation: number
|
||||||
|
gameDuration: number
|
||||||
|
gameEndTimestamp: number
|
||||||
gameId: number
|
gameId: number
|
||||||
|
gameMode: string
|
||||||
|
gameName: string
|
||||||
|
gameStartTimestamp: number
|
||||||
|
gameType: string
|
||||||
|
gameVersion: string
|
||||||
|
mapId: number
|
||||||
participants: Participant[]
|
participants: Participant[]
|
||||||
|
platformId: string
|
||||||
|
queueId: number
|
||||||
teams: Team[]
|
teams: Team[]
|
||||||
|
tournamentCode: string
|
||||||
}
|
}
|
||||||
timeline: Timeline
|
timeline: Timeline
|
||||||
}
|
}
|
||||||
|
|||||||
99
match_collector/src/cdragon_cache.ts
Normal file
99
match_collector/src/cdragon_cache.ts
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
import { writeFile, mkdir } from 'fs/promises'
|
||||||
|
import { existsSync } from 'fs'
|
||||||
|
import { join, resolve } from 'path'
|
||||||
|
import { fileURLToPath } from 'url'
|
||||||
|
import { dirname } from 'path'
|
||||||
|
|
||||||
|
// Get current directory for relative path resolution
|
||||||
|
const __filename = fileURLToPath(import.meta.url)
|
||||||
|
const __dirname = dirname(__filename)
|
||||||
|
|
||||||
|
// CDragon base URL for specific patch
|
||||||
|
const CDRAGON_BASE = 'https://raw.communitydragon.org/'
|
||||||
|
|
||||||
|
// Assets to cache for each patch
|
||||||
|
const CDRAGON_ASSETS = [
|
||||||
|
{
|
||||||
|
name: 'items.json',
|
||||||
|
path: 'plugins/rcp-be-lol-game-data/global/default/v1/items.json'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'perks.json',
|
||||||
|
path: 'plugins/rcp-be-lol-game-data/global/default/v1/perks.json'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'perkstyles.json',
|
||||||
|
path: 'plugins/rcp-be-lol-game-data/global/default/v1/perkstyles.json'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'summoner-spells.json',
|
||||||
|
path: 'plugins/rcp-be-lol-game-data/global/default/v1/summoner-spells.json'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'champion-summary.json',
|
||||||
|
path: 'plugins/rcp-be-lol-game-data/global/default/v1/champion-summary.json'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Download CDragon assets for a specific patch.
|
||||||
|
* This caches game data locally for faster access.
|
||||||
|
*/
|
||||||
|
async function downloadCDragonAssets(patch: string) {
|
||||||
|
// Convert patch format for CDragon: "16.4" -> "16.4" (already in correct format)
|
||||||
|
// CDragon uses patch format without the last minor version
|
||||||
|
const cdragonPatch = patch
|
||||||
|
console.log(`\n=== Downloading CDragon assets for patch ${cdragonPatch} ===`)
|
||||||
|
|
||||||
|
// Get cache directory from environment or use default
|
||||||
|
// In development, use a local directory relative to project root; in production (Docker), use /cdragon
|
||||||
|
const defaultCacheDir =
|
||||||
|
process.env.NODE_ENV === 'development'
|
||||||
|
? resolve(__dirname, '../../dev/data/cdragon')
|
||||||
|
: '/cdragon'
|
||||||
|
const cacheDir = process.env.CDRAGON_CACHE_DIR || defaultCacheDir
|
||||||
|
const patchDir = join(cacheDir, cdragonPatch)
|
||||||
|
|
||||||
|
// Create patch directory if it doesn't exist
|
||||||
|
if (!existsSync(patchDir)) {
|
||||||
|
await mkdir(patchDir, { recursive: true })
|
||||||
|
console.log(`Created directory: ${patchDir}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Download each asset
|
||||||
|
for (const asset of CDRAGON_ASSETS) {
|
||||||
|
const url = `${CDRAGON_BASE}${cdragonPatch}/${asset.path}`
|
||||||
|
const filePath = join(patchDir, asset.name)
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log(`Downloading ${asset.name}...`)
|
||||||
|
const response = await fetch(url)
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
console.error(`Failed to download ${asset.name}: ${response.status} ${response.statusText}`)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json()
|
||||||
|
await writeFile(filePath, JSON.stringify(data, null, 2))
|
||||||
|
console.log(`Saved ${asset.name} to ${filePath}`)
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error downloading ${asset.name}:`, error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a symlink or copy to 'latest' directory for easy access
|
||||||
|
const latestDir = join(cacheDir, 'latest')
|
||||||
|
const latestFile = join(latestDir, 'patch.txt')
|
||||||
|
|
||||||
|
if (!existsSync(latestDir)) {
|
||||||
|
await mkdir(latestDir, { recursive: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
await writeFile(latestFile, patch)
|
||||||
|
console.log(`Updated latest patch reference to ${patch}`)
|
||||||
|
|
||||||
|
console.log('CDragon assets download complete!')
|
||||||
|
}
|
||||||
|
|
||||||
|
export { downloadCDragonAssets }
|
||||||
@@ -6,9 +6,56 @@ import { MongoClient } from 'mongodb'
|
|||||||
import champion_stat from './champion_stat'
|
import champion_stat from './champion_stat'
|
||||||
import { Match } from './api'
|
import { Match } from './api'
|
||||||
import { PLATFORMS, getPlatformBaseUrl, getRegionalBaseUrl, getRegionForPlatform } from './platform'
|
import { PLATFORMS, getPlatformBaseUrl, getRegionalBaseUrl, getRegionForPlatform } from './platform'
|
||||||
|
import { downloadCDragonAssets } from './cdragon_cache'
|
||||||
|
|
||||||
main()
|
main()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract patch version from gameVersion string.
|
||||||
|
* gameVersion format is like "15.1.123.4567" -> we want "15.1"
|
||||||
|
*/
|
||||||
|
function extractPatchFromGameVersion(gameVersion: string): string {
|
||||||
|
const parts = gameVersion.split('.')
|
||||||
|
if (parts.length >= 2) {
|
||||||
|
return `${parts[0]}.${parts[1]}`
|
||||||
|
}
|
||||||
|
return gameVersion
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the latest patch from existing match collections in the database.
|
||||||
|
* Collections are named like "15.1_EUW1", "15.2_NA1", etc.
|
||||||
|
*/
|
||||||
|
async function getLatestPatchFromCollections(client: MongoClient): Promise<string | null> {
|
||||||
|
const matchesDb = client.db('matches')
|
||||||
|
const collections = await matchesDb.listCollections().toArray()
|
||||||
|
const collectionNames = collections.map(c => c.name)
|
||||||
|
|
||||||
|
// Extract unique patch versions from collection names
|
||||||
|
const patches = new Set<string>()
|
||||||
|
for (const name of collectionNames) {
|
||||||
|
// Collection names are either "patch_platform" or just "patch"
|
||||||
|
const patch = name.split('_')[0]
|
||||||
|
if (patch && /^\d+\.\d+$/.test(patch)) {
|
||||||
|
patches.add(patch)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (patches.size === 0) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort patches and return the latest (highest version number)
|
||||||
|
const sortedPatches = Array.from(patches).sort((a, b) => {
|
||||||
|
const [aMajor, aMinor] = a.split('.').map(Number)
|
||||||
|
const [bMajor, bMinor] = b.split('.').map(Number)
|
||||||
|
if (aMajor !== bMajor) return bMajor - aMajor
|
||||||
|
return bMinor - aMinor
|
||||||
|
})
|
||||||
|
|
||||||
|
return sortedPatches[0]
|
||||||
|
}
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
// Check if we're in development mode with pre-loaded data
|
// Check if we're in development mode with pre-loaded data
|
||||||
if (process.env.NODE_ENV === 'development' && process.env.USE_IMPORTED_DATA === 'true') {
|
if (process.env.NODE_ENV === 'development' && process.env.USE_IMPORTED_DATA === 'true') {
|
||||||
@@ -17,13 +64,10 @@ async function main() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Original production mode
|
// Production mode: collect matches and organize by their gameVersion
|
||||||
console.log('MatchCollector - Hello !')
|
console.log('MatchCollector - Hello !')
|
||||||
const client = await connectToDatabase()
|
const client = await connectToDatabase()
|
||||||
const [latestPatch, latestPatchTime] = await fetchLatestPatchDate(client)
|
console.log('Connected to database')
|
||||||
console.log(
|
|
||||||
'Connected to database, latest patch ' + latestPatch + ' was epoch: ' + latestPatchTime
|
|
||||||
)
|
|
||||||
|
|
||||||
console.log('Using RIOT_API_KEY: ' + api_key)
|
console.log('Using RIOT_API_KEY: ' + api_key)
|
||||||
if (api_key != null && api_key != undefined && api_key != '') {
|
if (api_key != null && api_key != undefined && api_key != '') {
|
||||||
@@ -31,20 +75,22 @@ async function main() {
|
|||||||
for (const [platform, region] of Object.entries(PLATFORMS)) {
|
for (const [platform, region] of Object.entries(PLATFORMS)) {
|
||||||
console.log(`\n=== Processing platform: ${platform} (region: ${region}) ===`)
|
console.log(`\n=== Processing platform: ${platform} (region: ${region}) ===`)
|
||||||
|
|
||||||
const alreadySeenGameList = await alreadySeenGames(client, latestPatch, platform)
|
// Get already seen games for all patches (we'll check by gameVersion when saving)
|
||||||
console.log(
|
const alreadySeenGameList = await alreadySeenGamesAllPatches(client, platform)
|
||||||
'We already have ' + alreadySeenGameList.length + ' matches for this patch/platform !'
|
console.log('We already have ' + alreadySeenGameList.length + ' matches for this platform !')
|
||||||
)
|
|
||||||
|
|
||||||
const challengerLeague = await fetchChallengerLeague(platform)
|
const challengerLeague = await fetchChallengerLeague(platform)
|
||||||
console.log('ChallengerLeague: got ' + challengerLeague.entries.length + ' entries')
|
console.log('ChallengerLeague: got ' + challengerLeague.entries.length + ' entries')
|
||||||
|
|
||||||
|
// Use 30 days ago as start time for collecting matches
|
||||||
|
const startTime = Math.floor(Date.now() / 1000) - 30 * 24 * 60 * 60
|
||||||
|
|
||||||
const gameList: string[] = []
|
const gameList: string[] = []
|
||||||
let i = 0
|
let i = 0
|
||||||
for (const challenger of challengerLeague.entries) {
|
for (const challenger of challengerLeague.entries) {
|
||||||
console.log('Entry ' + i + '/' + challengerLeague.entries.length + ' ...')
|
console.log('Entry ' + i + '/' + challengerLeague.entries.length + ' ...')
|
||||||
const puuid = challenger.puuid
|
const puuid = challenger.puuid
|
||||||
const challengerGameList = await summonerGameList(puuid, latestPatchTime, region)
|
const challengerGameList = await summonerGameList(puuid, startTime, region)
|
||||||
for (const game of challengerGameList) {
|
for (const game of challengerGameList) {
|
||||||
if (!gameList.includes(game) && !alreadySeenGameList.includes(game)) {
|
if (!gameList.includes(game) && !alreadySeenGameList.includes(game)) {
|
||||||
gameList.push(game)
|
gameList.push(game)
|
||||||
@@ -64,14 +110,25 @@ async function main() {
|
|||||||
const gameMatch = await match(game, matchRegion)
|
const gameMatch = await match(game, matchRegion)
|
||||||
const gameTimeline = await matchTimeline(game, matchRegion)
|
const gameTimeline = await matchTimeline(game, matchRegion)
|
||||||
gameMatch.timeline = gameTimeline
|
gameMatch.timeline = gameTimeline
|
||||||
await saveMatch(client, gameMatch, latestPatch, platform)
|
// Extract patch from gameVersion and save to appropriate collection
|
||||||
|
const patch = extractPatchFromGameVersion(gameMatch.info.gameVersion)
|
||||||
|
await saveMatch(client, gameMatch, patch, platform)
|
||||||
i++
|
i++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Generating stats...')
|
// Get the latest patch from collections and generate stats for it
|
||||||
await champion_stat.makeChampionsStats(client, latestPatch, Object.keys(PLATFORMS))
|
const latestPatch = await getLatestPatchFromCollections(client)
|
||||||
|
if (latestPatch) {
|
||||||
|
console.log(`Generating stats for latest patch: ${latestPatch}...`)
|
||||||
|
await champion_stat.makeChampionsStats(client, latestPatch, Object.keys(PLATFORMS))
|
||||||
|
|
||||||
|
// Download CDragon assets for the latest patch
|
||||||
|
await downloadCDragonAssets(latestPatch)
|
||||||
|
} else {
|
||||||
|
console.log('No matches found in database, skipping stat generation')
|
||||||
|
}
|
||||||
|
|
||||||
console.log('All done. Closing client.')
|
console.log('All done. Closing client.')
|
||||||
await client.close()
|
await client.close()
|
||||||
@@ -117,13 +174,6 @@ async function connectToDatabase() {
|
|||||||
return client
|
return client
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchLatestPatchDate(client: MongoClient) {
|
|
||||||
const database = client.db('patches')
|
|
||||||
const patches = database.collection('patches')
|
|
||||||
const latestPatch = await patches.find().limit(1).sort({ date: -1 }).next()
|
|
||||||
return [latestPatch!.patch, Math.floor(latestPatch!.date.valueOf() / 1000)]
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fetchChallengerLeague(platform: string) {
|
async function fetchChallengerLeague(platform: string) {
|
||||||
const queue = 'RANKED_SOLO_5x5'
|
const queue = 'RANKED_SOLO_5x5'
|
||||||
const endpoint = `/lol/league/v4/challengerleagues/by-queue/${queue}`
|
const endpoint = `/lol/league/v4/challengerleagues/by-queue/${queue}`
|
||||||
@@ -138,7 +188,7 @@ async function fetchChallengerLeague(platform: string) {
|
|||||||
return challengerLeague
|
return challengerLeague
|
||||||
}
|
}
|
||||||
|
|
||||||
async function summonerGameList(puuid: string, startTime: string, region: string) {
|
async function summonerGameList(puuid: string, startTime: number, region: string) {
|
||||||
const baseUrl = getRegionalBaseUrl(region)
|
const baseUrl = getRegionalBaseUrl(region)
|
||||||
const endpoint = `/lol/match/v5/matches/by-puuid/${puuid}/ids`
|
const endpoint = `/lol/match/v5/matches/by-puuid/${puuid}/ids`
|
||||||
const url = `${baseUrl}${endpoint}?queue=420&type=ranked&startTime=${startTime}&api_key=${api_key}`
|
const url = `${baseUrl}${endpoint}?queue=420&type=ranked&startTime=${startTime}&api_key=${api_key}`
|
||||||
@@ -174,18 +224,31 @@ async function matchTimeline(matchId: string, region: string) {
|
|||||||
return timeline
|
return timeline
|
||||||
}
|
}
|
||||||
|
|
||||||
async function alreadySeenGames(client: MongoClient, latestPatch: string, platform: string) {
|
/**
|
||||||
const database = client.db('matches')
|
* Get already seen games across all patches for a specific platform.
|
||||||
const collectionName = `${latestPatch}_${platform}`
|
* This is used when we don't know the patch beforehand (we get it from gameVersion).
|
||||||
const matches = database.collection(collectionName)
|
*/
|
||||||
|
async function alreadySeenGamesAllPatches(client: MongoClient, platform: string) {
|
||||||
|
const matchesDb = client.db('matches')
|
||||||
|
const collections = await matchesDb.listCollections().toArray()
|
||||||
|
const collectionNames = collections.map(c => c.name)
|
||||||
|
|
||||||
const alreadySeen = await matches.distinct('metadata.matchId')
|
// Find all collections for this platform (format: "patch_platform")
|
||||||
return alreadySeen
|
const platformCollections = collectionNames.filter(name => name.endsWith(`_${platform}`))
|
||||||
|
|
||||||
|
const allSeen: string[] = []
|
||||||
|
for (const collectionName of platformCollections) {
|
||||||
|
const matches = matchesDb.collection(collectionName)
|
||||||
|
const seen = await matches.distinct('metadata.matchId')
|
||||||
|
allSeen.push(...seen)
|
||||||
|
}
|
||||||
|
|
||||||
|
return allSeen
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveMatch(client: MongoClient, match: Match, latestPatch: string, platform: string) {
|
async function saveMatch(client: MongoClient, match: Match, patch: string, platform: string) {
|
||||||
const database = client.db('matches')
|
const database = client.db('matches')
|
||||||
const collectionName = `${latestPatch}_${platform}`
|
const collectionName = `${patch}_${platform}`
|
||||||
const matches = database.collection(collectionName)
|
const matches = database.collection(collectionName)
|
||||||
await matches.insertOne(match)
|
await matches.insertOne(match)
|
||||||
}
|
}
|
||||||
@@ -198,7 +261,16 @@ async function runWithPreloadedData() {
|
|||||||
|
|
||||||
const client = await connectToDatabase()
|
const client = await connectToDatabase()
|
||||||
try {
|
try {
|
||||||
const [latestPatch] = await fetchLatestPatchDate(client)
|
// Get the latest patch from collections instead of patches database
|
||||||
|
const latestPatch = await getLatestPatchFromCollections(client)
|
||||||
|
|
||||||
|
if (!latestPatch) {
|
||||||
|
console.error('❌ No match data found in database')
|
||||||
|
console.log('💡 Please run the data import script first:')
|
||||||
|
console.log(' node dev/scripts/setup-db.js')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
console.log(`Latest patch: ${latestPatch}`)
|
console.log(`Latest patch: ${latestPatch}`)
|
||||||
|
|
||||||
// Check if we have matches for this patch (including platform-specific collections)
|
// Check if we have matches for this patch (including platform-specific collections)
|
||||||
@@ -235,6 +307,9 @@ async function runWithPreloadedData() {
|
|||||||
await champion_stat.makeChampionsStats(client, latestPatch)
|
await champion_stat.makeChampionsStats(client, latestPatch)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Download CDragon assets for the latest patch
|
||||||
|
await downloadCDragonAssets(latestPatch)
|
||||||
|
|
||||||
console.log('🎉 All stats generated successfully!')
|
console.log('🎉 All stats generated successfully!')
|
||||||
console.log('🚀 Your development database is ready for frontend testing!')
|
console.log('🚀 Your development database is ready for frontend testing!')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
1
patch_detector/.gitignore
vendored
1
patch_detector/.gitignore
vendored
@@ -1 +0,0 @@
|
|||||||
node_modules
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"semi": false,
|
|
||||||
"singleQuote": true,
|
|
||||||
"tabWidth": 2,
|
|
||||||
"trailingComma": "none",
|
|
||||||
"printWidth": 100,
|
|
||||||
"bracketSpacing": true,
|
|
||||||
"arrowParens": "avoid",
|
|
||||||
"endOfLine": "lf"
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
FROM node:lts-alpine
|
|
||||||
RUN mkdir -p /home/node/app/node_modules && chown -R node:node /home/node/app
|
|
||||||
WORKDIR /home/node/app
|
|
||||||
USER node
|
|
||||||
COPY --chown=node:node package*.json ./
|
|
||||||
RUN npm install
|
|
||||||
COPY --chown=node:node . .
|
|
||||||
CMD /bin/sh -c "node --import=tsx index.ts; sleep 1h"
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
import { defineConfig } from 'eslint/config'
|
|
||||||
import js from '@eslint/js'
|
|
||||||
import tseslint from 'typescript-eslint'
|
|
||||||
import prettier from 'eslint-config-prettier'
|
|
||||||
|
|
||||||
export default defineConfig([
|
|
||||||
js.configs.recommended,
|
|
||||||
tseslint.configs.recommended,
|
|
||||||
prettier,
|
|
||||||
{
|
|
||||||
rules: {
|
|
||||||
semi: 'off',
|
|
||||||
'prefer-const': 'error'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
])
|
|
||||||
@@ -1,169 +0,0 @@
|
|||||||
import { MongoClient } from 'mongodb'
|
|
||||||
import { writeFile, mkdir } from 'fs/promises'
|
|
||||||
import { existsSync } from 'fs'
|
|
||||||
import { join } from 'path'
|
|
||||||
|
|
||||||
// CDragon base URL for specific patch
|
|
||||||
const CDRAGON_BASE = 'https://raw.communitydragon.org/'
|
|
||||||
|
|
||||||
// Assets to cache for each patch
|
|
||||||
const CDRAGON_ASSETS = [
|
|
||||||
{
|
|
||||||
name: 'items.json',
|
|
||||||
path: 'plugins/rcp-be-lol-game-data/global/default/v1/items.json'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'perks.json',
|
|
||||||
path: 'plugins/rcp-be-lol-game-data/global/default/v1/perks.json'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'perkstyles.json',
|
|
||||||
path: 'plugins/rcp-be-lol-game-data/global/default/v1/perkstyles.json'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'summoner-spells.json',
|
|
||||||
path: 'plugins/rcp-be-lol-game-data/global/default/v1/summoner-spells.json'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'champion-summary.json',
|
|
||||||
path: 'plugins/rcp-be-lol-game-data/global/default/v1/champion-summary.json'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
main()
|
|
||||||
|
|
||||||
async function main() {
|
|
||||||
const client = await connectToDatabase()
|
|
||||||
const dbPatch = await getLatestPatchFromDatabase(client)
|
|
||||||
|
|
||||||
// In dev mode, get patch from database (the one we have match data for)
|
|
||||||
if (process.env.NODE_ENV === 'development') {
|
|
||||||
console.log('Development mode: downloading cache for database patch')
|
|
||||||
await client.close()
|
|
||||||
|
|
||||||
if (dbPatch) {
|
|
||||||
console.log('Latest patch in database: ' + dbPatch)
|
|
||||||
await downloadAssets(dbPatch)
|
|
||||||
} else {
|
|
||||||
console.log('No patch found in database!')
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Production mode: check database and update if new patch
|
|
||||||
const newPatch = await fetchLatestPatch()
|
|
||||||
console.log('Latest patch is: ' + newPatch)
|
|
||||||
const newDate = new Date()
|
|
||||||
|
|
||||||
if (!(await compareLatestSavedPatch(client, newPatch, newDate))) {
|
|
||||||
await downloadAssets(newPatch)
|
|
||||||
}
|
|
||||||
|
|
||||||
await client.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fetchLatestPatch() {
|
|
||||||
const url = 'https://ddragon.leagueoflegends.com/api/versions.json'
|
|
||||||
const patchDataResponse = await fetch(url)
|
|
||||||
const patchData = await patchDataResponse.json()
|
|
||||||
const patch: string = patchData[0]
|
|
||||||
return patch
|
|
||||||
}
|
|
||||||
|
|
||||||
async function connectToDatabase() {
|
|
||||||
// Create a MongoClient with a MongoClientOptions object to set the Stable API version
|
|
||||||
let uri = `mongodb://${process.env.MONGO_USER}:${process.env.MONGO_PASS}@${process.env.MONGO_HOST}`
|
|
||||||
if (
|
|
||||||
process.env.MONGO_URI != undefined &&
|
|
||||||
process.env.MONGO_URI != null &&
|
|
||||||
process.env.MONGO_URI != ''
|
|
||||||
) {
|
|
||||||
uri = process.env.MONGO_URI
|
|
||||||
}
|
|
||||||
const client = new MongoClient(uri)
|
|
||||||
await client.connect()
|
|
||||||
return client
|
|
||||||
}
|
|
||||||
|
|
||||||
async function compareLatestSavedPatch(client: MongoClient, newPatch: string, newDate: Date) {
|
|
||||||
const database = client.db('patches')
|
|
||||||
const patches = database.collection('patches')
|
|
||||||
const latestPatch = await patches.find().limit(1).sort({ date: -1 }).next()
|
|
||||||
|
|
||||||
if (latestPatch == null) {
|
|
||||||
console.log('No previous patch recorded in database.')
|
|
||||||
} else {
|
|
||||||
console.log('Latest patch in database is: ' + latestPatch.patch)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (latestPatch == null || latestPatch.patch != newPatch) {
|
|
||||||
await patches.insertOne({ patch: newPatch, date: newDate })
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getLatestPatchFromDatabase(client: MongoClient): Promise<string | null> {
|
|
||||||
const database = client.db('patches')
|
|
||||||
const patches = database.collection('patches')
|
|
||||||
const latestPatch = await patches.find().limit(1).sort({ date: -1 }).next()
|
|
||||||
|
|
||||||
if (latestPatch == null) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
return latestPatch.patch
|
|
||||||
}
|
|
||||||
|
|
||||||
async function downloadAssets(patch: string) {
|
|
||||||
// Convert patch format for CDragon: "16.4.1" -> "16.4"
|
|
||||||
// CDragon uses patch format without the last minor version
|
|
||||||
const cdragonPatch = patch.split('.').slice(0, 2).join('.')
|
|
||||||
console.log(`Downloading CDragon assets for patch ${cdragonPatch} (from ${patch})...`)
|
|
||||||
|
|
||||||
// Get cache directory from environment or use default
|
|
||||||
const cacheDir = process.env.CDRAGON_CACHE_DIR || '/cdragon'
|
|
||||||
const patchDir = join(cacheDir, cdragonPatch)
|
|
||||||
|
|
||||||
// Create patch directory if it doesn't exist
|
|
||||||
if (!existsSync(patchDir)) {
|
|
||||||
await mkdir(patchDir, { recursive: true })
|
|
||||||
console.log(`Created directory: ${patchDir}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Download each asset
|
|
||||||
for (const asset of CDRAGON_ASSETS) {
|
|
||||||
const url = `${CDRAGON_BASE}${cdragonPatch}/${asset.path}`
|
|
||||||
const filePath = join(patchDir, asset.name)
|
|
||||||
|
|
||||||
try {
|
|
||||||
console.log(`Downloading ${asset.name}...`)
|
|
||||||
const response = await fetch(url)
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
console.error(`Failed to download ${asset.name}: ${response.status} ${response.statusText}`)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json()
|
|
||||||
await writeFile(filePath, JSON.stringify(data, null, 2))
|
|
||||||
console.log(`Saved ${asset.name} to ${filePath}`)
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Error downloading ${asset.name}:`, error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a symlink or copy to 'latest' directory for easy access
|
|
||||||
const latestDir = join(cacheDir, 'latest')
|
|
||||||
const latestFile = join(latestDir, 'patch.txt')
|
|
||||||
|
|
||||||
if (!existsSync(latestDir)) {
|
|
||||||
await mkdir(latestDir, { recursive: true })
|
|
||||||
}
|
|
||||||
|
|
||||||
await writeFile(latestFile, patch)
|
|
||||||
console.log(`Updated latest patch reference to ${patch}`)
|
|
||||||
|
|
||||||
console.log('CDragon assets download complete!')
|
|
||||||
}
|
|
||||||
2118
patch_detector/package-lock.json
generated
2118
patch_detector/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,31 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "patch_detector",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"main": "index.ts",
|
|
||||||
"type": "module",
|
|
||||||
"scripts": {
|
|
||||||
"test": "echo \"Error: no test specified\" && exit 1",
|
|
||||||
"lint": "eslint .",
|
|
||||||
"lint:fix": "eslint --fix .",
|
|
||||||
"format": "prettier --write .",
|
|
||||||
"format:check": "prettier --check ."
|
|
||||||
},
|
|
||||||
"author": "",
|
|
||||||
"license": "ISC",
|
|
||||||
"description": "",
|
|
||||||
"dependencies": {
|
|
||||||
"mongodb": "^6.10.0"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@eslint/js": "^9.39.2",
|
|
||||||
"@types/node": "^22.10.1",
|
|
||||||
"@typescript-eslint/eslint-plugin": "^8.53.1",
|
|
||||||
"@typescript-eslint/parser": "^8.53.1",
|
|
||||||
"eslint": "^9.39.2",
|
|
||||||
"eslint-config-prettier": "^10.1.8",
|
|
||||||
"prettier": "^3.8.0",
|
|
||||||
"tsx": "^4.19.2",
|
|
||||||
"typescript": "^5.9.3",
|
|
||||||
"typescript-eslint": "^8.53.1"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"types": ["node"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user