GC - Virtupets Data Collector

Collects shop wizard and trading post data for Virtupets.net

目前為 2024-06-24 提交的版本,檢視 最新版本

// ==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        GM.info
// @grant        GM.getValue
// @grant        GM.setValue
// @license      MIT
// @version      0.14
// @author       CrystalFlame
// @description Collects shop wizard and trading post data for Virtupets.net
// ==/UserScript==

const DEBUG = false;

function extractTradeInformation() {
    const tradeObjects = [];
    try {
        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 wishlist = lotElement.querySelector('span[id^="wishlist-text"]');
            tradeObject.wishlist = wishlist.innerText;

            const quicksale = lotElement.querySelector('span[id^="quicksale-text"]');
            if(quicksale) {
                tradeObject.quicksale = parseInt(quicksale.innerText.trim().replace(/\D/g, ''), 10);
            }

            tradeObjects.push(tradeObject);
        });
    }
    catch (error) {
        console.error(error);
        throw new Error("Unable to read the TP, there might be a conflicting script or an update to the layout.");
    }
    return tradeObjects;
}

function constructMessage(message, error) {
    const firstMessage = "<b>Failed to upload data to <a href=\"https://virtupets.net\">Virtupets</a>. </b>";
    const lastMessage = "Always make sure your <a href=\"https://gf.qytechs.cn/en/scripts/490596-gc-virtupets-data-collector\">script</a> is the latest version. "
    return `${error ? firstMessage : ""}${message} ${lastMessage}`;
}

async function displayMessage(message, error = true) {
    if (await shouldDisplayMessage(message)) {
        try {
            const element = document.querySelector('h1') || document.getElementById('page_banner');
            if (element) {
                const messageElement = document.createElement('div');
                messageElement.innerHTML = constructMessage(message, error);
                messageElement.style.cssText = `margin-block-end: 7px; margin-block-start: 7px; background-color: ${error ? "#ffe5e5" : "#f9ff8f"}; border: 2px solid #000; border-radius: 5px; padding: 10px; color: #000;`;
                element.insertAdjacentElement('afterend', messageElement);

                const dismissElement = document.createElement('a');
                dismissElement.href = "#";
                dismissElement.textContent = "[Dismiss]";
                dismissElement.addEventListener('click', () => dismissMessage(messageElement, message));
                messageElement.appendChild(dismissElement);
            }
        }
        catch {
            console.log("Failed to display message: {message}.");
        }
    }
}

async function dismissMessage(element, message) {
    await GM.setValue(message, new Date().getTime());
    element.remove();
}

async function shouldDisplayMessage(message) {
    try {
        const lastDismissed = await GM.getValue(message);
        if (!lastDismissed || new Date().getTime() - lastDismissed > 7 * 24 * 60 * 60 * 1000) {
            return true;
        }
        return false;
    }
    catch {
        log("Failed to check if message should display");
        return true;
    }
}

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) {
            throw new Error(`Unknown header named "${title}" in position ${i+1}, expected "${check[i]}".`);
        }
    }
    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() {
    try {
        const tokens = document?.querySelector('.mt-1 strong')?.textContent?.split(" ... ");
        let body;
        const itemName = tokens?.length >= 2 ? tokens[1]?.trim() : undefined;
        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;
            let totalShops = 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;
                totalShops += 1;
            }
            if(lowestPrice < i32Max && totalStock > 0 && dataElements.length > 0) {
                body = {
                    item_name: itemName,
                    price: lowestPrice,
                    total_stock: totalStock,
                    total_shops: totalShops
                }
                return body;
            }
        }
        else if (validateUnbuyable()) {
            log("Valid unbuyable");
            body = {
                item_name: itemName,
                total_stock: 0,
                total_shops: 0
            }
        }
        return body;
    }
    catch (error) {
        console.error(error);
        throw new Error("Unable to read the SW, there might be a conflicting script or an update to the layout.");
    }
}

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 = 1000, tries = 4) {
    const url = `https://virtupets.net/${route}`;
    function onError(error){
        if(tries > 0 && error.cause != 500) {
            return new Promise(resolve => {
                setTimeout(() => {
                    resolve(sendData(route, fetchOptions, delay, tries - 1));
                }, delay * 2**(5-tries));
            });
        }
        else {
            throw error;
        }
    }
    return fetch(url, fetchOptions).then(response => {
        if (response.status == 500) {
            return response.text().then(body => {
                console.error(`Data upload error: ${body}`);
                throw new Error(body, { cause: response.status });
            });
        }
        else if (!response.ok) {
            return response.text().then(body => {
                console.error(`Data upload error (retry ${5-tries}/5): ${body}`);
                throw new Error(body, { cause: response.status });
            });
        }
        return response.text().then(body => {
            return {ok: true, body}
        });
    }).catch(onError);
}

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

async function sendRequest(route, apiVersion, clientVersion, body) {
    return new Promise((resolve, reject) => {
        let promises = [];
        if(Array.isArray(body)) {
            if (body.length == 0) return;
            const size = 100;
            const numBatches = Math.ceil(body.length / size);

            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(apiVersion, clientVersion, batchObjects)));
            }
        }
        else {
            promises.push(sendData(route, createPostRequest(apiVersion, clientVersion, body)));
        }
        Promise.all(promises.map(p => p.catch(error => ({ ok: false, body: error })))).then(responses => {
            const warningResponses = responses.filter(response => response.ok && response.body.trim() != '');
            const successfulResponses = responses.filter(response => response.ok);
            const errors = responses.filter(response => !response.ok);

            if (errors.length > 0) {
                reject(new Error(errors[0].body));
            } else if (warningResponses.length > 0) {
                displayMessage(warningResponses[0].body, false);
            }
            if (successfulResponses.length > 0) {
                console.log("Data uploaded to https://virtupets.net");
            }
            resolve();
        });
    });
}

window.onload = async function () {
    'use strict';
    let route;
    let body;
    let apiVersion;
    let sw = /market\/wizard/.test(window.location.href);
    try {
        if (sw) {
            route = "shop-prices";
            body = extractShopPrices();
            apiVersion = "0.11";
        }
        else {
            route = "trade-lots";
            body = extractTradeInformation();
            apiVersion = "0.11";
        }
        if (route && body && apiVersion && GM.info.script.version) {
            await sendRequest(route, apiVersion, GM.info.script.version, body);
        }
    }
    catch (error) {
        displayMessage(error.message.replace(/^Error:\s*/, ''));
    }
};

QingJ © 2025

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