Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- // ==UserScript==
- // @name solo stats csv
- // @namespace http://tampermonkey.net/
- // @version 6.0
- // @description Fetches Geoguessr API data to get advanced stats for solo games and displays them on the profile page.
- // @author You
- // @match https://www.geoguessr.com/*/me/profile
- // @match https://www.geoguessr.com/me/profile
- // @icon https://www.geoguessr.com/_next/static/media/favicon.bffdd9d3.png
- // @grant GM_xmlhttpRequest
- // @grant GM_addStyle
- // @require https://code.jquery.com/jquery-3.6.0.min.js
- // @require https://unpkg.com/[email protected]/dist/axios.min.js
- // @require https://cdn.jsdelivr.net/npm/[email protected]/dist/progressbar.min.js
- // @require https://cdn.jsdelivr.net/npm/[email protected]/dist/chart.min.js
- // @connect https://game-server.geoguessr.com/
- // ==/UserScript==
- (function() {
- 'use strict';
- //*** Constants and Variables ***//
- // Constants
- const API_BASE_URL = 'https://www.geoguessr.com/api/v3/games';
- // Variables
- let playerId = '';
- //*** Helper Functions ***//
- // Function to convert JSON to CSV
- function jsonToCsv(json) {
- console.log(json);
- const reversedJson = json.slice().reverse(); // Reverse the JSON array
- // Define new header names
- const newHeaders = {
- time: "game_datetime",
- mapName: "map",
- points: "game_score",
- gameMode: "game_mode",
- isDailyChallenge: "is_daily_challenge",
- gameToken: "game_token",
- challengeToken: "challenge_token"
- };
- // Replace keys with new header names
- const keys = Object.keys(reversedJson[0]);
- const renamedHeaders = keys.map(key => newHeaders[key] || key); // Use new headers if defined, otherwise keep original
- const csv = reversedJson.map(row =>
- keys.map(key => JSON.stringify(row[key], (_, value) => value === null ? '' : value)).join(',')
- );
- return [renamedHeaders.join(','), ...csv].join('\r\n'); // Use renamed headers for the header row
- }
- function jsonToCsvForSecondCsv(json, newHeaders) {
- console.log(json);
- // Extract original keys from the data
- const keys = Object.keys(json[0]); // Correctly reference the 'json' parameter
- // Replace original keys with new headers (or keep original if no mapping exists)
- const renamedHeaders = keys.map(key => newHeaders[key] || key);
- // Generate CSV rows using renamed headers
- const csv = json.map(row => // Use 'json' directly here
- keys.map(key => JSON.stringify(row[key], (_, value) => value === null ? '' : value)).join(',')
- );
- return [renamedHeaders.join(','), ...csv].join('\r\n'); // Include renamed headers in the CSV
- }
- //delay setup
- function delay(ms) {
- return new Promise(resolve => setTimeout(resolve, ms));
- }
- // Function to download a CSV file
- function downloadCsv(data, filename) {
- const blob = new Blob([data], { type: 'text/csv;charset=utf-8;' });
- const url = URL.createObjectURL(blob);
- const link = document.createElement("a");
- link.setAttribute("href", url);
- // Correct replacement string
- const baseFilename = filename.replace(/ - (\d{4}-\d{2}-\d{2})T(\d{2})(\d{2})(\d{2})\.\d{3}/, ' - $1-$2$3');
- link.setAttribute("download", baseFilename);
- link.style.visibility = 'hidden';
- document.body.appendChild(link);
- link.click();
- document.body.removeChild(link);
- }
- // Function to count new games
- function countNewGames(gameIds, lastDownloadDate) {
- if (!lastDownloadDate) {
- // If no previous timestamp is found, all games are considered "new"
- return gameIds.length;
- }
- // Count the games with timestamps later than the last download timestamp
- const newGames = gameIds.filter(game => new Date(game.time) > lastDownloadDate);
- console.log(`${newGames.length} new games found since last download.`);
- return newGames.length;
- }
- //*** API Interactions ***//
- // Function to get game tokens- Phase 1
- async function getStandardGameTokens(session) {
- const startTime = new Date();
- console.log("getStandardGameTokens started at:", startTime.toLocaleString());
- const gameTokens = [];
- let paginationToken = null;
- try {
- while (true) {
- const response = await session.get('https://www.geoguessr.com/api/v4/feed/private', { params: { paginationToken } });
- const data = response.data;
- paginationToken = data.paginationToken;
- if (!playerId && data.entries.length > 0) {
- playerId = data.entries[0].user.id;
- console.log("Player ID:", playerId);
- }
- data.entries.forEach(entry => {
- try {
- extractStandardGameTokens(entry, gameTokens);
- } catch (error) {
- console.error(error);
- }
- });
- if (!paginationToken) break;
- }
- } catch (error) {
- console.error("An error occurred while fetching game tokens:", error);
- }
- console.log("Number of Game Tokens found:", gameTokens.length);
- const endTimePhase1 = new Date();
- const durationPhase1 = (endTimePhase1 - startTime) / 1000;
- console.log("getStandardGameTokens finished at:", endTimePhase1.toLocaleString());
- console.log("Time taken for getStandardGameTokens:", durationPhase1, "seconds");
- const gameTokensCsv = jsonToCsv(gameTokens.map(gameToken => ({
- time: gameToken.time,
- mapName: gameToken.mapName,
- points: gameToken.points,
- gameMode: gameToken.gameMode,
- isDailyChallenge: gameToken.isDailyChallenge,
- gameToken: gameToken.gameToken,
- challengeToken: gameToken.challengeToken,
- })));
- downloadCsv(gameTokensCsv, 'newGames.csv');
- return gameTokens;
- }
- // extract game tokens
- function extractStandardGameTokens(entry, gameTokens) {
- try {
- const payloadJson = JSON.parse(entry.payload);
- const entryTime = entry.time; // Store entry time for fallback
- if (Array.isArray(payloadJson)) {
- payloadJson.forEach(payloadItem => {
- let gameData = null;
- let time = entryTime; // Default to entry time
- let gameToken = null;
- let challengeToken = null;
- if (payloadItem.payload && payloadItem.payload.gameMode === 'Standard') {
- gameData = payloadItem.payload;
- time = payloadItem.time || entryTime; // Use payload time if available, otherwise fallback
- gameToken = gameData.gameToken;
- challengeToken = gameData.challengeToken;
- } else if (payloadItem.gameMode === 'Standard') {
- gameData = payloadItem;
- time = payloadItem.time || entryTime; // Use payload time if available, otherwise fallback
- gameToken = gameData.gameToken;
- challengeToken = gameData.challengeToken;
- }
- if (gameData) {
- const mapName = gameData.mapName;
- const points = gameData.points;
- const gameMode = gameData.gameMode;
- const isDailyChallenge = gameData.isDailyChallenge;
- gameTokens.push({
- time: time,
- mapName: mapName,
- points: points,
- gameMode: gameMode,
- isDailyChallenge: isDailyChallenge,
- gameToken: gameToken,
- challengeToken: challengeToken,
- });
- }
- });
- } else if (payloadJson && payloadJson.gameMode === 'Standard') {
- const time = entry.time;
- const mapName = payloadJson.mapName;
- const points = payloadJson.points;
- const gameMode = payloadJson.gameMode;
- const isDailyChallenge = payloadJson.isDailyChallenge;
- const gameToken = payloadJson.gameToken;
- const challengeToken = payloadJson.challengeToken;
- gameTokens.push({
- time: time,
- mapName: mapName,
- points: points,
- gameMode: gameMode,
- isDailyChallenge: isDailyChallenge,
- gameToken: gameToken,
- challengeToken: challengeToken,
- });
- }
- } catch (error) {
- console.error("Error parsing payload:", error, entry.payload);
- }
- }
- // function to fetch duels - Phase 2 - iterates through the gameIds and feches data
- async function fetchStandardGames(session, gameTokens) {
- const games = [];
- for (const game of gameTokens) { // Use gameTokens
- try {
- const response = await session.get(`${API_BASE_URL}/${game.token}`); // Use game.token
- games.push(response.data);
- await delay(1000); // Keep the delay
- } catch (error) {
- console.error(`Error fetching game data for token ${game.token}:`, error); // Use game.token
- await delay(1000);
- }
- }
- return games;
- }
- //***Data Processing ***//
- // Function to extract player round data from duel JSON - Phase 2 - Processes data from fetchduels
- function extractStandardGameData(games) {
- const allRoundsData = [];
- games.forEach(game => {
- game.rounds.forEach((round, roundIndex) => { // Add roundIndex
- const guess = game.player.guesses[roundIndex] || {}; // Get guess for this round, handle missing guesses
- allRoundsData.push({
- token: game.token,
- roundCount: game.roundCount,
- timeLimit: game.timeLimit,
- roundStartTime: round.startTime,
- mapName: game.mapName,
- forbidMoving: game.forbidMoving,
- forbidZooming: game.forbidZooming,
- forbidRotating: game.forbidRotating,
- roundNumber: roundIndex+1,
- roundLat: round.lat,
- roundLng: round.lng,
- countryStreakCode: round.streakLocationCode || null,
- guessLat: guess.lat || null, // Add guess latitude
- guessLng: guess.lng || null, // Add guess longitude
- roundScoreInPoints: guess.roundScoreInPoints || null, // Add round score
- distanceInMeters: guess.distanceInMeters || null, // Add distance
- guessTime: guess.time || null, // Add guess time
- type: game.type ?? null,
- mode: game.mode ?? null,
- state: game.state ?? null,
- timedOut: guess.timedOut || null, // Add timedOut
- timedOutWithGuess: guess.timedOutWithGuess || null, // Add timedOutWithGuess
- skippedRound: guess.skippedRound || null // Add skippedRound
- });
- });
- });
- // After the allRoundsData array is populated in extractStandardGameData
- allRoundsData.sort((a, b) => new Date(a.roundStartTime) - new Date(b.roundStartTime));
- return allRoundsData;
- }
- //*** Event Handlers ***//
- //function to handle CSV button click - Phase 2 - called when user confirms number duels to download
- async function handleDownloadCsv(session, gameTokens) {
- const numGames = gameTokens.length;
- const progressPopup = document.createElement('div');
- const topOffset = 80;
- const rightOffset = 80;
- progressPopup.style.cssText = `
- position: fixed;
- top: ${topOffset}px;
- right: ${rightOffset}px;
- transform: translate(0, 0);
- background-color: #fff;
- padding: 20px;
- border: 1px solid #ccc;
- box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
- z-index: 10003;
- width: auto;
- min-width: 300px;
- `;
- progressPopup.innerHTML = `
- <p><span id="gamesFetchedCount">0</span> of ${numGames} fetched</p>
- <div id="progressBar"></div>
- <button id="cancelFetchButton" style="background-color: red; color: white; padding: 10px 20px; margin-top: 10px;">Cancel</button>
- `;
- document.body.appendChild(progressPopup);
- const progressBar = new ProgressBar.Line('#progressBar', {
- strokeWidth: 4,
- easing: 'easeInOut',
- duration: 1400,
- color: '#007bff',
- trailColor: '#eee',
- svgStyle: { width: '100%', height: '10px' },
- text: {
- style: {
- color: '#333',
- position: 'absolute',
- left: '50%',
- top: '50%',
- padding: 0,
- margin: 0,
- transform: {
- prefix: true,
- x: '-50%',
- y: '-50%'
- }
- },
- autoStyleContainer: false
- },
- step: (state, bar) => {
- bar.setText(Math.round(bar.value() * 100) + ' %');
- }
- });
- let fetchedCount = 0;
- const games = [];
- let cancelled = false;
- const cancelFetchButton = document.getElementById('cancelFetchButton');
- cancelFetchButton.onclick = () => {
- cancelled = true;
- document.body.removeChild(progressPopup);
- console.log("Game fetching cancelled by user.");
- };
- for (let i = 0; i < numGames; i++) {
- if (cancelled) break;
- try {
- const response = await session.get(`${API_BASE_URL}/${gameTokens[i].gameToken}`); // Use gameTokens[i].gameToken
- games.push(response.data);
- fetchedCount++;
- document.getElementById('gamesFetchedCount').textContent = fetchedCount;
- progressBar.set(fetchedCount / numGames);
- await delay(1000);
- } catch (error) {
- console.error(`Error fetching game data for token ${gameTokens[i].gameToken}:`, error); // Use gameTokens[i].gameToken
- await delay(1000);
- }
- }
- if (!cancelled) {
- document.body.removeChild(progressPopup);
- const standardGameData = extractStandardGameData(games);
- // Add the newHeaders object here
- const newHeaders = {
- token: "game_token",
- roundCount: "round_count",
- timeLimit: "time_limit",
- roundStartTime: "round_start_datetime",
- mapName: "map",
- forbidMoving: "forbid_moving",
- forbidZooming: "forbid_zooming",
- forbidRotating: "forbid_rotating",
- roundNumber: "game_round_number",
- roundLat: "loc_lat",
- roundLng: "loc_lng",
- countryStreakCode: "loc_cc",
- guessLat: "guess_lat",
- guessLng: "guess_lng",
- roundScoreInPoints: "score",
- distanceInMeters: "distance",
- guessTime: "guess_time",
- type: "game_type",
- mode: "game_mode",
- state: "game_state",
- timedOut: "timed_out",
- timedOutWithGuess: "timed_out_with_guess",
- skippedRound: "skipped_round"
- };
- const csvData = jsonToCsvForSecondCsv(standardGameData, newHeaders);
- downloadCsv(csvData, 'newGamesRounds.csv');
- console.log("Standard games stats download complete.");
- }
- }
- //*** UI Popups ***//
- //Show confirmation popup at page load
- function showConfirmationPopup() {
- return new Promise((resolve) => {
- const popup = document.createElement('div');
- const topOffset = 80; // 80px from the top
- const rightOffset = 80; // 80px from the right
- popup.style.cssText = `
- position: fixed;
- top: ${topOffset}px;
- right: ${rightOffset}px;
- transform: translate(0, 0); /* Remove default centering transform */
- background-color: #fff;
- padding: 20px;
- border: 1px solid #ccc;
- box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
- z-index: 10000;
- `;
- popup.innerHTML = `
- <p>Do You Want to Fetch Solo Game Data?</p>
- <button id="confirmButton" style="background-color: green; color: white; padding: 10px 20px; margin-right: 10px;">Yes ✓</button>
- <button id="cancelButton" style="background-color: red; color: white; padding: 10px 20px;">No X</button>
- `;
- document.body.appendChild(popup);
- const confirmButton = document.getElementById('confirmButton');
- const cancelButton = document.getElementById('cancelButton');
- confirmButton.onclick = () => {
- document.body.removeChild(popup);
- resolve(true); // Resolve with true when confirmed
- };
- cancelButton.onclick = () => {
- document.body.removeChild(popup);
- resolve(false); // Resolve with false when canceled
- };
- });
- }
- //Sow searching popup
- function showSearchingPopup() {
- const popup = document.createElement('div');
- const topOffset = 80;
- const rightOffset = 80;
- popup.style.cssText = `
- position: fixed;
- top: ${topOffset}px;
- right: ${rightOffset}px;
- transform: translate(0, 0);
- background-color: #fff; /* White background */
- padding: 20px;
- border: 1px solid #ccc;
- box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
- z-index: 10001;
- width: auto; /* Adjust width as needed */
- min-width: 200px; /* Minimum width to prevent it from being too small */
- `;
- popup.innerHTML = `<p>Searching For Games...</p>`;
- document.body.appendChild(popup);
- return popup;
- }
- //Hide popup
- function hidePopup(popup) {
- if (popup && popup.parentNode) { // Check if the popup exists and is in the DOM
- popup.parentNode.removeChild(popup);
- }
- }
- //Show phase 1 complete popup
- function showPhase1CompletePopup(gameIds, lastDownloadDate) {
- return new Promise((resolve) => {
- const popup = document.createElement('div');
- const topOffset = 80;
- const rightOffset = 80;
- popup.style.cssText = `
- position: fixed;
- top: ${topOffset}px;
- right: ${rightOffset}px;
- transform: translate(0, 0);
- background-color: #fff !important; /* White background - override other styles */
- padding: 20px;
- border: 1px solid #ccc;
- box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
- z-index: 10002;
- width: auto;
- min-width: 250px;
- `;
- const newGamesCount = countNewGames(gameIds, lastDownloadDate); // Use helper function to count new games
- const lastDownloadText = lastDownloadDate
- ? `Last download occurred on ${lastDownloadDate.toLocaleString()}`
- : "No previous downloads detected"; // Dynamic message
- popup.innerHTML = `
- <p>${gameIds.length} Solo Games Found and Downloaded</p>
- <p>${lastDownloadText}</p>
- <p>${newGamesCount} new games have occurred since then</p>
- <p>How many do you want to fetch?</p>
- <input type="number" id="numGames" value="${newGamesCount}" min="1" max="${gameIds.length}" style="width: 100px; margin-right: 10px;"> <!-- Adjusted width -->
- <button id="confirmButton" style="background-color: green; color: white; padding: 10px 20px; margin-right: 10px;">Confirm</button>
- <button id="cancelButton" style="background-color: red; color: white; padding: 10px 20px;">Cancel</button>
- `;
- document.body.appendChild(popup);
- const confirmButton = document.getElementById('confirmButton');
- const cancelButton = document.getElementById('cancelButton');
- const numGamesInput = document.getElementById('numGames'); // Updated input ID
- confirmButton.onclick = () => {
- let numGames = parseInt(numGamesInput.value, 10);
- if (isNaN(numGames) || numGames < 1) {
- alert("Please enter a valid number greater than 0.");
- return;
- }
- numGames = Math.min(numGames, gameIds.length);
- // Save the current timestamp in local storage
- localStorage.setItem("lastSoloDownloadTimestamp", JSON.stringify(new Date()));
- document.body.removeChild(popup);
- resolve(numGames);
- };
- cancelButton.onclick = () => {
- document.body.removeChild(popup);
- resolve(false);
- };
- });
- }
- //*** Initialization ***//
- //Initialization
- async function init() {
- // Retrieve the last download timestamp from local storage
- const lastDownloadTimestamp = localStorage.getItem("lastSoloDownloadTimestamp");
- let lastDownloadDate = null;
- if (lastDownloadTimestamp) {
- lastDownloadDate = new Date(JSON.parse(lastDownloadTimestamp)); // Parse the timestamp into a Date object
- console.log("Last download occurred on:", lastDownloadDate.toLocaleString());
- } else {
- console.log("No previous downloads detected.");
- }
- const userConfirmed = await showConfirmationPopup();
- if (userConfirmed) {
- const scriptStartTime = performance.now();
- console.time('total_script_time');
- console.log("Script execution started at:", new Date().toLocaleString());
- const session = axios.create({
- withCredentials: true
- });
- const searchingPopup = showSearchingPopup();
- getStandardGameTokens(session).then(standardGameTokens => {
- const phase1EndTime = performance.now();
- const phase1Duration = (phase1EndTime - scriptStartTime) / 1000;
- console.log("Phase 1 (Game Tokens and CSV Download) finished at:", new Date().toLocaleString());
- hidePopup(searchingPopup);
- showPhase1CompletePopup(standardGameTokens, lastDownloadDate).then(numGames => {
- if (numGames) {
- const gameTokensOnly = standardGameTokens.filter(tokenObj => tokenObj.gameToken !== undefined);
- const recentGameTokens = gameTokensOnly.slice(0, numGames);
- const phase2StartTime = performance.now();
- console.time('phase2_loading');
- console.log("Phase 2 started at:", new Date().toLocaleString());
- handleDownloadCsv(session, recentGameTokens).then(() => {
- const phase2EndTime = performance.now();
- const phase2Duration = (phase2EndTime - phase2StartTime) / 1000;
- console.timeEnd('phase2_loading');
- console.timeEnd('total_script_time');
- const scriptEndTime = performance.now();
- const scriptDuration = (scriptEndTime - scriptStartTime) / 1000;
- console.log("Phase 2 finished at:", new Date().toLocaleString());
- console.log("Total time for Phase 2:", phase2Duration, "seconds");
- console.log("Total script execution time:", scriptDuration, "seconds");
- }).catch(error => {
- console.timeEnd('phase2_loading');
- console.timeEnd('total_script_time');
- const scriptEndTime = performance.now();
- const scriptDuration = (scriptEndTime - scriptStartTime) / 1000;
- console.log("Script finished at:", new Date().toLocaleString());
- console.log("Total script execution time:", scriptDuration, "seconds");
- });
- console.log(`Fetching ${numGames} games...`);
- } else {
- console.log("User chose not to proceed to Phase 2.");
- console.timeEnd('total_script_time');
- const scriptEndTime = performance.now();
- const scriptDuration = (scriptEndTime - scriptStartTime) / 1000;
- console.log("Script finished at:", new Date().toLocaleString());
- console.log("Total script execution time:", scriptDuration, "seconds");
- }
- }).catch(error => {
- console.timeEnd('total_script_time');
- const scriptEndTime = performance.now();
- const scriptDuration = (scriptEndTime - scriptStartTime) / 1000;
- console.log("Script finished at:", new Date().toLocaleString());
- console.log("Total script execution time:", scriptDuration, "seconds");
- });
- }).catch(error => {
- console.timeEnd('total_script_time');
- const scriptEndTime = performance.now();
- const scriptDuration = (scriptEndTime - scriptStartTime) / 1000;
- console.log("Script finished at:", new Date().toLocaleString());
- console.log("Total script execution time:", scriptDuration, "seconds");
- });
- } else {
- console.log("Script execution cancelled by user.");
- }
- }
- $(document).ready(init);
- })();
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement