Skip to content

Instantly share code, notes, and snippets.

@SomeAspy
Last active October 30, 2025 14:32
Show Gist options
  • Select an option

  • Save SomeAspy/2b27a6d66b97db6bd1b62afac5343285 to your computer and use it in GitHub Desktop.

Select an option

Save SomeAspy/2b27a6d66b97db6bd1b62afac5343285 to your computer and use it in GitHub Desktop.
/**
* Parses numeric strings that might contain 'K' (thousands) or 'M' (millions).
* Example: "232.7K" => 232700, "1.5M" => 1500000, "500" => 500
* @param {string} valueStr The string value to parse.
* @returns {number} The parsed numeric value. Returns 0 if parsing fails or input is '-'.
*/
function parseNumericValue(valueStr) {
if (!valueStr || typeof valueStr !== 'string') {
return 0;
}
const cleanedStr = valueStr.trim().toUpperCase();
// Handle cases like '-' which mean zero/not applicable
if (cleanedStr === '-') {
return 0;
}
let multiplier = 1;
let numPart = cleanedStr;
if (cleanedStr.endsWith('K')) {
multiplier = 1000;
numPart = cleanedStr.slice(0, -1);
} else if (cleanedStr.endsWith('M')) {
multiplier = 1000000;
numPart = cleanedStr.slice(0, -1);
}
const num = parseFloat(numPart);
if (isNaN(num)) {
return 0; // Return 0 if it's not a valid number after processing
}
return Math.round(num * multiplier); // Use Math.round for cases like 232.7K
}
/**
* Parses the HTML structure containing user level data and transforms it
* into the specified JSON format.
* Assumes the HTML is present in the current document.
*/
function extractLevelData() {
// Find all the user data boxes
const userBoxes = document.querySelectorAll('.box.box-hover');
const levels = [];
userBoxes.forEach(box => {
try {
// --- Extract Avatar and User ID ---
const imgElement = box.querySelector('figure.image img');
let avatar = null;
let userId = null;
if (imgElement && imgElement.src) {
const src = imgElement.src;
const avatarMatch = src.match(/avatars\/(\d+)\/([a-f0-9_]+)\.png/);
if (avatarMatch) {
userId = avatarMatch[1];
avatar = avatarMatch[2];
} else {
// Try extracting userId from onerror attribute if it's a default avatar
const onerrorAttr = imgElement.getAttribute('onerror');
if (onerrorAttr) {
// Attempt to find a user ID within the onerror string
// This is less reliable and depends on the exact string format
const userIdMatch = onerrorAttr.match(/avatars\/(\d+)/); // Simpler regex attempt
if (userIdMatch && userIdMatch[1]) {
// Check if it's just a placeholder like '1' vs a real ID
if (userIdMatch[1].length > 5) { // Assume real IDs are longer
userId = userIdMatch[1];
}
}
}
avatar = null; // Default avatar
}
}
// --- Extract Tag (Username) ---
const tagElement = box.querySelector('p.has-text-white.is-size-5.has-text-weight-semibold:not([style*="min-width"])');
let tag = null;
if (tagElement && tagElement.textContent.startsWith('@')) {
tag = tagElement.textContent.substring(1);
}
// --- Extract Stats (Level, XP, MessageCount) by Order ---
// Select all potential stat <p> elements within the flex container THAT HAVE a min-width style
const statsElements = box.querySelectorAll('div[style*="display: flex"] > p.has-text-white.is-size-5.has-text-weight-semibold[style*="min-width"]');
let level = 0;
let xp = 0;
let messageCount = 0;
// Assign based on index position:
// Index 0: Level
if (statsElements.length > 0) {
level = parseInt(statsElements[0].textContent.trim(), 10) || 0; // Level is usually an integer
}
// Index 1: XP
if (statsElements.length > 1) {
xp = parseNumericValue(statsElements[1].textContent.trim());
}
// Index 2: Message Count
if (statsElements.length > 2) {
messageCount = parseNumericValue(statsElements[2].textContent.trim());
}
// --- Construct Level Object ---
// Only add if we successfully found a userId and tag
if (userId && tag) {
levels.push({
avatar: avatar,
level: level,
messageCount: messageCount, // Correctly assigned separate value
tag: tag,
userId: userId,
xp: xp // Correctly assigned separate value
});
} else {
// Attempt to find userId from tag if missing from image (less common)
// Requires a mapping or external lookup usually, cannot be done from HTML alone reliably.
// Log a warning if critical info is missing.
if (!userId) console.warn("Missing userId for tag:", tag, box);
if (!tag) console.warn("Missing tag for box:", box);
// console.warn("Could not extract required data (userId or tag) for box:", box);
}
} catch (error) {
console.error("Error processing user box:", error, box);
}
});
// --- Construct Final Export Object ---
const exportData = {
levels: levels
};
// --- Output JSON ---
console.log(JSON.stringify(exportData, null, 2)); // Pretty print JSON
return exportData; // Return the data structure
}
// --- Run the function ---
// Make sure the HTML content is loaded in the document before calling this.
// You would typically run this in the browser's developer console
// on the page displaying the leaderboard HTML.
extractLevelData();
@HippoProgrammer
Copy link
Copy Markdown

This was really helpful for me, it extracted the data perfectly! Problem is Lurkr now exports data slightly differently so imports don't work (at least for Noctaly, the bot I was importing into).
I wrote a very bad Python script to do similar functionality - it basically takes the output of this script and reformats it into the new format of Lurkr exports. Its basically just a few keys moved around - but I didn't feel like writing JS!
https://gist.github.com/HippoProgrammer/5dfc621e157a2c949d9ca5acedfdf7e3

A quick summary (this can be done manually if wanted).
For each 'user' object inside the 'levels' object:
avatar is now user[avatar]
tag is now user[username]
Everything else is exactly the same.

Thanks for the work on this script, it really helped!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment