Fetch NFT Miner Levels from ronencoin.tech (with Cache & Refresh)

Fetch miner level data from ronencoin.tech and inject levels on OpenSea with cache and refresh button

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         Fetch NFT Miner Levels from ronencoin.tech (with Cache & Refresh)
// @namespace    https://opensea.io/collection/ronen-coin-mining-network/
// @version      1.2
// @author       spyderbibek
// @description  Fetch miner level data from ronencoin.tech and inject levels on OpenSea with cache and refresh button
// @match        https://opensea.io/collection/ronen-coin-mining-network*
// @grant        GM_xmlhttpRequest
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_deleteValue
// @connect      ronencoin.tech
// @license MIT 
// ==/UserScript==

(async function() {
    'use strict';

    // === CONFIGURATION ===
    const DEBUG = false;  // Set to false to disable debug logs
    const CACHE_KEY = 'ronen_token_levels_cache';
    const CACHE_DATE_KEY = 'ronen_cache_last_refresh';
    const CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours

    // === UTILS ===
    function debugLog(...args) {
        if (DEBUG) console.log('[NFTLevelInjector]', ...args);
    }
    function formatDate(ts) {
        const d = new Date(ts);
        return d.toLocaleString();
    }

    // Delay helper
    const delay = ms => new Promise(res => setTimeout(res, ms));

    // Cache helpers using GM storage
    async function loadCache() {
        const raw = await GM_getValue(CACHE_KEY, '{}');
        try {
            return JSON.parse(raw);
        } catch {
            return {};
        }
    }

    async function saveCache(cache) {
        await GM_setValue(CACHE_KEY, JSON.stringify(cache));
    }

    async function clearCache() {
        await GM_deleteValue(CACHE_KEY);
        await GM_setValue(CACHE_DATE_KEY, Date.now().toString());
        debugLog('Cache cleared');
        updateRefreshLabel();
    }

    async function getLastRefreshTime() {
        const last = await GM_getValue(CACHE_DATE_KEY, '0');
        return parseInt(last) || 0;
    }

    async function setLastRefreshTime(ts) {
        await GM_setValue(CACHE_DATE_KEY, ts.toString());
    }

    async function isCacheExpired() {
        const last = await getLastRefreshTime();
        return (Date.now() - last) > CACHE_TTL_MS;
    }

    // === DOM Helpers ===
    function findTokenElements() {
        return Array.from(document.querySelectorAll('a[href*="/item/ronin/"]'));
    }

    function getTokenIdFromElement(el) {
        try {
            const urlParts = el.href.split('/');
            return urlParts[urlParts.length - 1];
        } catch {
            return null;
        }
    }

    // === Fetch using GM_xmlhttpRequest with Promise wrapper ===
    function gmFetch(url) {
        return new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                method: 'GET',
                url,
                headers: {
                    'Accept': 'text/html',
                },
                onload: res => {
                    if (res.status >= 200 && res.status < 300) {
                        resolve(res.responseText);
                    } else {
                        reject(new Error(`HTTP status ${res.status}`));
                    }
                },
                onerror: err => reject(err),
            });
        });
    }

    // === Fetch level info from ronencoin.tech with exact token matching ===
    async function fetchLevel(tokenId) {
        debugLog(`Fetching level for token: ${tokenId}`);
        try {
            const url = `https://ronencoin.tech/nft_miners.php?search_token=${tokenId}&type=&level=`;
            const text = await gmFetch(url);

            const parser = new DOMParser();
            const doc = parser.parseFromString(text, 'text/html');
            const rows = doc.querySelectorAll('table tbody tr');
            if (!rows.length) {
                debugLog(`No data rows found for token ${tokenId}`);
                return null;
            }

            // Find exact token match row
            for (const row of rows) {
                const cells = row.querySelectorAll('td');
                if (cells.length < 4) continue;
                const tokenFromTable = cells[0].textContent.trim();
                if (tokenFromTable === tokenId) {
                    const level = cells[3].textContent.trim();
                    debugLog(`Found exact match level ${level} for token ${tokenId}`);
                    return level;
                }
            }

            debugLog(`No exact match found for token ${tokenId}`);
            return null;
        } catch (e) {
            console.error(`[NFTLevelInjector] Error fetching level for token ${tokenId}:`, e);
            return null;
        }
    }

    // === Inject level into page ===
    function injectLevel(tokenId, level) {
        const els = findTokenElements().filter(el => getTokenIdFromElement(el) === tokenId);
        if (els.length === 0) {
            debugLog(`No element found to inject level for token ${tokenId}`);
            return;
        }

        els.forEach(el => {
            if (el.querySelector('.miner-level')) {
                debugLog(`Level already injected for token ${tokenId}`);
                return;
            }
            const span = document.createElement('span');
            span.className = 'miner-level';
            span.style.color = '#facc15';
            span.style.marginLeft = '6px';
            span.style.fontWeight = 'bold';
            span.textContent = `(Lvl: ${level})`;
            el.appendChild(span);
            debugLog(`Injected level for token ${tokenId}`);
        });
    }

    // === Main processing of tokens, with caching ===
    async function processTokens(tokenEls, cache) {
        for (const el of tokenEls) {
            const tokenId = getTokenIdFromElement(el);
            if (!tokenId) continue;

            if (processedTokens.has(tokenId)) {
                // Already processed this token this session
                continue;
            }
            processedTokens.add(tokenId);

            if (cache[tokenId] !== undefined) {
                // Use cached data
                injectLevel(tokenId, cache[tokenId]);
                debugLog(`Used cached level for token ${tokenId}: ${cache[tokenId]}`);
                continue;
            }

            // Fetch fresh data and update cache
            const level = await fetchLevel(tokenId);
            if (level !== null) {
                cache[tokenId] = level;
                injectLevel(tokenId, level);
                debugLog(`Fetched and cached level for token ${tokenId}: ${level}`);
            }

            // Delay between requests
            await delay(800);
        }

        await saveCache(cache);
    }

    // === UI overlay with refresh button and label ===
    function createOverlay() {
        const container = document.createElement('div');
        container.style.position = 'fixed';
        container.style.bottom = '10px';
        container.style.left = '10px';
        container.style.background = 'rgba(0,0,0,0.7)';
        container.style.color = '#facc15';
        container.style.padding = '8px 12px';
        container.style.borderRadius = '6px';
        container.style.fontSize = '14px';
        container.style.zIndex = 999999;
        container.style.fontFamily = 'Arial, sans-serif';
        container.style.userSelect = 'none';
        container.style.display = 'flex';
        container.style.alignItems = 'center';
        container.style.gap = '10px';

        const btn = document.createElement('button');
        btn.textContent = 'Refresh Data';
        btn.style.cursor = 'pointer';
        btn.style.background = '#facc15';
        btn.style.border = 'none';
        btn.style.borderRadius = '4px';
        btn.style.color = '#000';
        btn.style.fontWeight = 'bold';
        btn.style.padding = '4px 8px';
        btn.style.userSelect = 'none';

        const label = document.createElement('div');
        label.style.minWidth = '150px';
        label.textContent = 'Last refreshed: N/A';

        btn.onclick = async () => {
            btn.disabled = true;
            btn.textContent = 'Refreshing...';
            await clearCache();
            await main(true); // force reload data
            btn.textContent = 'Refresh Data';
            btn.disabled = false;
        };

        container.appendChild(btn);
        container.appendChild(label);

        document.body.appendChild(container);

        return label;
    }

    // Update the "Last Refreshed" label
    async function updateRefreshLabel() {
        const lastRefresh = await getLastRefreshTime();
        if (!refreshLabel) return;
        refreshLabel.textContent = `Last refreshed: ${lastRefresh ? formatDate(lastRefresh) : 'Never'}`;
    }

    // === Globals ===
    let processedTokens = new Set();
    let refreshLabel = null;

    // === Main ===
    async function main(forceRefresh = false) {
        processedTokens = new Set();

        let cache = await loadCache();

        // If cache expired or forceRefresh, clear it to force refetch
        if (forceRefresh || await isCacheExpired()) {
            debugLog('Cache expired or forced refresh, clearing cache...');
            await clearCache();
            cache = {};
            await setLastRefreshTime(Date.now());
        }

        updateRefreshLabel();

        const tokens = findTokenElements();
        debugLog(`Initial tokens found: ${tokens.length}`);

        await processTokens(tokens, cache);

        // Set last refresh time if we didn't just clear it
        if (!forceRefresh) {
            await setLastRefreshTime(Date.now());
            updateRefreshLabel();
        }
    }

    // Start the overlay UI
    refreshLabel = createOverlay();

    // Initial run
    await main();

    // Watch for new tokens dynamically added to page
    const observer = new MutationObserver(async mutations => {
        const newTokens = [];
        mutations.forEach(mutation => {
            mutation.addedNodes.forEach(node => {
                if (node.nodeType !== Node.ELEMENT_NODE) return;

                if (node.matches && node.matches('a[href*="/item/ronin/"]')) {
                    newTokens.push(node);
                }
                newTokens.push(...(node.querySelectorAll ? Array.from(node.querySelectorAll('a[href*="/item/ronin/"]')) : []));
            });
        });
        if (newTokens.length > 0) {
            debugLog(`Detected ${newTokens.length} new token elements`);
            const cache = await loadCache();
            await processTokens(newTokens, cache);
        }
    });
    observer.observe(document.body, { childList: true, subtree: true });

})();