您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Collects shop wizard and trading post data for Virtupets.net
当前为
// ==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.12 // @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 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); }); } 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[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; 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.1"; } 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或关注我们的公众号极客氢云获取最新地址