GC - Virtupets Data Collector

Logs shop wizard and trading post data to a database.

当前为 2024-03-22 提交的版本,查看 最新版本

// ==UserScript==
// @name         GC - Virtupets Data Collector
// @namespace    https://gf.qytechs.cn/en/users/1278031-crystalflame
// @match        *://*.grundos.cafe/island/tradingpost/browse/
// @match        *://*.grundos.cafe/island/tradingpost/lot/user/*
// @match        *://*.grundos.cafe/island/tradingpost/lot/*
// @match        *://*.grundos.cafe/market/wizard/*
// @run-at       document-end
// @icon         https://www.google.com/s2/favicons?sz=64&domain=grundos.cafe
// @grant        none
// @license      MIT
// @version      0.1
// @author       CrystalFlame
// @description Logs shop wizard and trading post data to a database.
// ==/UserScript==

const DEBUG = false;

function extractTradeInformation() {
    const tradeObjects = [];

    const lotElements = document.querySelectorAll('.trade-lot');

    lotElements.forEach((lotElement) => {
        const tradeObject = {};

        const lotNumberMatch = lotElement.querySelector('span strong').innerText?.match(/Lot #(\d+)/);
        tradeObject.id = parseInt(lotNumberMatch[1]);

        const username = lotElement.querySelector('span strong + a').innerText;
        tradeObject.username = username;

        const listedOnElement = lotElement.querySelector('span:nth-child(2)');
        tradeObject.time = createTimestamp(listedOnElement.innerText?.replace('Listed On: ', ''));

        const itemElements = lotElement.querySelectorAll('.trade-item');
        tradeObject.items = [];
        const regex = /r\d{1,3}/g;

        itemElements.forEach((itemElement) => {
            tradeObject.items.push(
                itemElement.querySelector('.item-info span:nth-child(1)')?.innerText
            );
        });

        const spans = lotElement.querySelectorAll("span");
        if (spans && spans.length > 0) {
            const wishlist = spans[spans.length - 1];
            tradeObject.wishlist = wishlist.textContent.substring(wishlist.textContent.indexOf(':') + 1).trim();
        }

        tradeObjects.push(tradeObject);
    });

    return tradeObjects;
}

function validateTable() {
    const header = document.querySelectorAll('.market_grid .header');
    const check = [ 'owner', 'item', 'stock', 'price'];
    if(check.length != header.length) return false;
    for (let i = 0; i < header.length; i += 1) {
        const title = header[i].querySelector('strong').textContent.toLowerCase();
        if(check[i] != title) return false;
    }

    return true;
}

function validateSearchRange() {
    if (document.querySelector('main .center .mt-1 span')?.textContent?.toLowerCase() == '(searching between 1 and 99,999 np)') {
        return true;
    }
    return false;
}

function validateUnbuyable() {
    const notFoundMsg = "i did not find anything. :( please try again, and i will search elsewhere!";
    const wrongHeaders = document.querySelectorAll('.market_grid .header').length > 0;
    const wrongMessage = document.querySelector('main p.center').textContent.toLowerCase() != notFoundMsg;
    if (wrongHeaders || wrongMessage) {
        return false;
    }
    return true;
}

function log(message) {
    if ((DEBUG) == true) {
        console.log(message);
    }
}

function extractShopPrices() {
    const tokens = document.querySelector('.mt-1 strong').textContent.split(" ... ");
    let body;
    const itemName = tokens[1]?.trim();
    if(!validateSearchRange() || !itemName) {
        log("Not a valid search!");
       return body;
    }
    else if(validateTable())
    {
        log("Valid search");
        const dataElements = document.querySelectorAll('.market_grid .data');
        const i32Max = 2147483647;
        let lowestPrice = i32Max;
        let totalStock = 0;

        for (let i = 0; i < dataElements.length; i += 4) {
            //const owner = dataElements[i].querySelector('a').textContent;
            //const item = dataElements[i + 1].querySelector('span').textContent;
            const stock = parseInt(dataElements[i + 2].querySelector('span').textContent);
            const price = parseInt(dataElements[i + 3].querySelector('strong').textContent.replace(/[^0-9]/g, ''));

            lowestPrice = Math.min(price, lowestPrice);
            totalStock += stock;
        }
        if(lowestPrice < i32Max && totalStock > 0 && dataElements.length > 0) {
            body = {
                item_name: itemName,
                price: lowestPrice,
                total_stock: totalStock
            }
            return body;
        }
    }
    else if (validateUnbuyable()) {
        log("Valid unbuyable");
        body = {
                item_name: itemName,
                total_stock: 0
        }
    }
    return body;
}

const monthMap = {jan: "1", feb: "2", mar: "3", apr: "4", may: "5", jun: "6", jul: "7", aug: "8", sep: "9", oct: "10", nov: "11", dec: "12"};
function createTimestamp(str) {
    const parts = str.split(" ");
    const month = monthMap[parts[0].slice(0, 3).toLowerCase()].padStart(2, '0');
    const day = parts[1].padStart(2, '0');
    const time = parts[3].split(':');
    const ampm = parts[4].toLowerCase();
    let hour = parseInt(time[0]);
    if (ampm === "pm" && hour < 12) {
        hour += 12;
    } else if (ampm === "am" && hour === 12) {
        hour = 0;
    }
    const convertedHour = String(hour).padStart(2, '0');
    const minutes = time[1].padStart(2, '0');
    const currentYear = new Date().getFullYear();
    const currentMonth = new Date().getMonth();
    const year = month == "12" && currentMonth == 0 ? currentYear - 1 : currentYear;
    return `${year}-${month}-${day}T${convertedHour}:${minutes}:00.000`;
}


function wait(delay){
    return new Promise((resolve) => setTimeout(resolve, delay));
}

function sendData(route, fetchOptions, delay = 500, tries = 5) {
    const url = `https://virtupets.net/${route}`;
    function onError(err){
        if(tries > 0){
            return wait(delay).then(() => sendData(url, fetchOptions, delay, tries - 1));
        }
        log(`Request to ${route} failed.`);
    }
    return fetch(url, fetchOptions).catch(onError);
}

function createPostRequest(version, body) {
    return {
        method: "POST",
        headers: {
            "Content-Type": "application/json",
            "Version": version,
        },
        body: JSON.stringify(body),
    }
}

async function sendRequest(route, version, body) {
    if(Array.isArray(body)) {
        const size = 100;
        const numBatches = Math.ceil(body.length / size);

        let promises = [];
        for (let i = 0; i < numBatches; i++) {
            const startIndex = i * size;
            const endIndex = Math.min((i + 1) * size, body.length);
            const batchObjects = body.slice(startIndex, endIndex);
            promises.push(sendData(route, createPostRequest(version, batchObjects)));
        }

        const statuses = await Promise.all(promises);
    }
    else {
        await sendData(route, createPostRequest(version, body));
    }
    console.log("Data uploaded.");
}

window.onload = async function () {
    'use strict';
    let route;
    let body;
    let version;
    if(/market\/wizard/.test(window.location.href)) {
        route = "shop-prices";
        body = extractShopPrices();
        version = "0.1"
    }
    else {
        route = "trade-lots";
        body = extractTradeInformation();
        version = "0.1"
    }
    if (route && body && version) {
        await sendRequest(route, version, body);
    }
};

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址