auction house to sheet for Heart

Scrapes auction data and sends it the google sheet.

// ==UserScript==
// @name         auction house to sheet for Heart
// @author       aquagloop
// @match        https://www.torn.com/amarket.php*
// @namespace    https://github.com/RyuFive/TornScripts/raw/main/Auction%20Names.user.js
// @version      1.2
// @description  Scrapes auction data and sends it the google sheet.
// @grant        GM_xmlhttpRequest
// @connect      api.torn.com
// @connect      script.google.com
// @icon         https://www.google.com/s2/favicons?sz=64&domain=torn.com
// @license      MIT
// ==/UserScript==

function waitForKeyElements (
    selectorTxt,/* Required: The jQuery selector string that
                        specifies the desired element(s).
                    */
    actionFunction, /* Required: The code to run when elements are
                        found. It is passed a jNode to the matched
                        element.
                    */
    bWaitOnce, /* Optional: If false, will continue to scan for
                        new elements even after the first match is
                        found.
                    */
    iframeSelector /* Optional: If set, identifies the iframe to
                        search.
                    */
) {
    var targetNodes, btargetsFound;

    if (typeof iframeSelector == "undefined")
        targetNodes = $(selectorTxt);
    else
        targetNodes = $(iframeSelector).contents ()
                                           .find (selectorTxt);

    if (targetNodes && targetNodes.length > 0) {
        btargetsFound   = true;
        /*--- Found target node(s).  Go through each and act if they
            are new.
        */
        targetNodes.each ( function () {
            var jThis= $(this);
            var alreadyFound = jThis.data ('alreadyFound')  ||  false;

            if (!alreadyFound) {
                //--- Call the payload function.
                var cancelFound     = actionFunction (jThis);
                if (cancelFound)
                    btargetsFound   = false;
                else
                    jThis.data ('alreadyFound', true);
            }
        } );
    }
    else {
        btargetsFound   = false;
    }

    //--- Get the timer-control variable for this selector.
    var controlObj      = waitForKeyElements.controlObj  ||  {};
    var controlKey      = selectorTxt.replace (/[^\w]/g, "_");
    var timeControl     = controlObj [controlKey];

    //--- Now set or clear the timer as appropriate.
    if (btargetsFound  &&  bWaitOnce  &&  timeControl) {
        //--- The only condition where we need to clear the timer.
        clearInterval (timeControl);
        delete controlObj [controlKey]
    }
    else {
        //--- Set a timer, if needed.
        if ( ! timeControl) {
            timeControl = setInterval ( function () {
                    waitForKeyElements (    selectorTxt,
                                            actionFunction,
                                            bWaitOnce,
                                            iframeSelector
                                        );
                },
                300
            );
            controlObj [controlKey] = timeControl;
        }
    }
    waitForKeyElements.controlObj   = controlObj;
}


(function () {
    'use strict';

    const SCRIPT_PREFIX = '[Auction Scraper]';
    const STORAGE_KEY = 'torn_api_key';
    const GOOGLE_SHEET_WEB_APP_URL = 'https://script.google.com/macros/s/AKfycbyqLLBeXpX-UdmsdhprLTC6AOghWA5azx7uivGuPX5qO_Zz1JXDYd7W40HHJRd_a7vl/exec';
    let processedPageStarts = [];

    function log(message, ...args) { console.log(`${SCRIPT_PREFIX} ${message}`, ...args); }
    function error(message, ...args) { console.error(`${SCRIPT_PREFIX} ${message}`, ...args); }

    function getApiKey() {
        let key = localStorage.getItem(STORAGE_KEY);
        if (!key) {
            key = prompt("Enter your Torn API Key:");
            if (key) {
                localStorage.setItem(STORAGE_KEY, key);
            } else {
                alert("No API key entered. The script will not run.");
            }
        }
        return key;
    }

    function getUrlParameterFromHash(parameter) {
        const hash = window.location.hash;
        const urlParams = new URLSearchParams(hash.substring(hash.indexOf('?')));
        return urlParams.get(parameter);
    }

    function sendDataToGoogleSheet(data) {
        if (GOOGLE_SHEET_WEB_APP_URL === 'YOUR_WEB_APP_URL_HERE') {
            error("Please update the Google Sheet URL in the script.");
            return;
        }
        log('Data being sent to Google Sheet:', data);
        GM_xmlhttpRequest({
            method: 'POST',
            url: GOOGLE_SHEET_WEB_APP_URL,
            headers: { 'Content-Type': 'application/json' },
            data: JSON.stringify(data),
            onload: function (response) {
                log('Google Sheets request finished. Status:', response.status, response.responseText);
            },
            onerror: function (response) {
                error('FATAL ERROR: Could not connect to Google Sheets URL.', response);
            }
        });
    }

    function fetchItemDetails(itemUID, apiKey) {
        return new Promise((resolve) => {
            const apiUrl = `https://api.torn.com/torn/${itemUID}?selections=itemdetails&key=${apiKey}`;
            GM_xmlhttpRequest({
                method: 'GET',
                url: apiUrl,
                onload: function (response) {
                    try {
                        const json = JSON.parse(response.responseText);
                        if (json.error) {
                            error(`Torn API Error for UID ${itemUID}:`, json.error.error);
                            resolve(null);
                        } else {
                            resolve(json.itemdetails);
                        }
                    } catch (e) {
                        error(`Failed to parse API response for UID ${itemUID}:`, e, response.responseText);
                        resolve(null);
                    }
                },
                onerror: function (response) {
                    error(`Network error fetching details for UID ${itemUID}:`, response);
                    resolve(null);
                }
            });
        });
    }

    async function parseAuctionItems() {
        const startValue = getUrlParameterFromHash('start');
        if (startValue === null || processedPageStarts.includes(startValue)) return;
        processedPageStarts.push(startValue);
        log("Parsing auction items from start value:", startValue);

        const apiKey = getApiKey();
        if (!apiKey) return;

        const liElements = document.querySelectorAll('div.items-list-wrap > ul.items-list > li[id]');
        const promises = [];

        liElements.forEach((liElement) => {
            if (liElement.dataset.processed) return;
            liElement.dataset.processed = 'true';

            const itemHoverElement = liElement.querySelector('span.item-hover');
            const itemUID = itemHoverElement ? itemHoverElement.getAttribute('armoury') : null;
            const priceElement = liElement.querySelector('div.c-bid-wrap');
            const itemPrice = priceElement ? priceElement.textContent.trim().replace(/[^0-9]/g, '') : null;

            const sellerWrapDiv = liElement.querySelector('div.seller-wrap');
            let sellerID = null;
            let highBidderName = 'N/A';
            let highBidderID = 'N/A';
            if (sellerWrapDiv) {
                const sellerLink = sellerWrapDiv.querySelector('div.name a[href*="profiles.php?XID="]');
                sellerID = sellerLink ? sellerLink.getAttribute('href').split('profiles.php?XID=')[1] : null;

                const highBidderLink = sellerWrapDiv.querySelector('div.namehight a[href*="profiles.php?XID="]');
                if (highBidderLink) {
                    highBidderName = highBidderLink.textContent.trim();
                    highBidderID = highBidderLink.getAttribute('href').split('profiles.php?XID=')[1];
                }
            }

            const timeWrapDiv = liElement.querySelector('div.time-wrap > span[title]');
            const auctionEnds = timeWrapDiv ? timeWrapDiv.getAttribute('title') : 'N/A';

            if (itemUID && itemPrice) {
                liElement.style.border = "1px solid orange";
                const promise = fetchItemDetails(itemUID, apiKey).then(details => {
                    if (!details) {
                        liElement.style.border = "1px solid red";
                        return null;
                    }
                    liElement.style.border = "1px solid limegreen";
                    const bonuses = Object.values(details.bonuses || {});
                    const defenseValue = details.armor ?? details.defense;

                    return {
                        Timestamp: new Date().toISOString(),
                        ItemUUID: itemUID,
                        SellerID: sellerID,
                        ItemPrice: itemPrice,
                        ItemName: details.name || 'N/A',
                        Color: details.rarity || 'N/A',
                        Quality: typeof details.quality === 'number' ? details.quality.toFixed(2) : 'N/A',
                        Damage: typeof details.damage === 'number' ? details.damage.toFixed(2) : 'N/A',
                        Accuracy: typeof details.accuracy === 'number' ? details.accuracy.toFixed(2) : 'N/A',
                        Defense: typeof defenseValue === 'number' ? defenseValue.toFixed(2) : 'N/A',
                        Bonus1Name: bonuses.length > 0 ? bonuses[0].bonus : '',
                        Bonus1Value: bonuses.length > 0 ? bonuses[0].value : '',
                        Bonus2Name: bonuses.length > 1 ? bonuses[1].bonus : '',
                        Bonus2Value: bonuses.length > 1 ? bonuses[1].value : '',
                        AuctionEnds: auctionEnds,
                        HighBidderName: highBidderName,
                        HighBidderID: highBidderID
                    };
                });
                promises.push(promise);
            }
        });

        if (promises.length > 0) {
            const results = await Promise.all(promises);
            const finalData = results.filter(item => item !== null);
            if (finalData.length > 0) {
                log(`Assembled ${finalData.length} valid items. Preparing to send.`);
                sendDataToGoogleSheet(finalData);
            }
        }
    }

    waitForKeyElements(".item-cont-wrap", parseAuctionItems, false);
})();