Greasyfork/Sleazyfork Update Checks Display

Display today's script installations and update checks.

目前為 2024-12-08 提交的版本,檢視 最新版本

// ==UserScript==
// @name         Greasyfork/Sleazyfork Update Checks Display
// @description  Display today's script installations and update checks.
// @icon         https://gf.qytechs.cn/vite/assets/blacklogo96-CxYTSM_T.png
// @version      1.0
// @author       afkarxyz
// @namespace    https://github.com/afkarxyz/misc-scripts/
// @supportURL   https://github.com/afkarxyz/misc-scripts/issues
// @license      MIT
// @match        https://gf.qytechs.cn/*/users/*
// @match        https://sleazyfork.org/*/users/*
// @grant        GM_setValue
// @grant        GM_getValue
// ==/UserScript==

(function() {
    'use strict';

    const CACHE_DURATION = 10 * 60 * 1000;

    const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));

    const collectScriptLinks = () => 
      Array.from(document.querySelectorAll('li[data-script-id]')).map(element => ({
        url: `https://gf.qytechs.cn/en/scripts/${element.getAttribute('data-script-id')}-${
          element.getAttribute('data-script-name')
            .toLowerCase()
            .replace(/\s+/g, '-')
            .replace(/[^a-z0-9-]/g, '')
        }/stats`,
        name: element.getAttribute('data-script-name'),
        id: element.getAttribute('data-script-id'),
        element: element
      }));

    const fetchWithProxy = async (url) => {
      try {
        const response = await fetch(`https://api.allorigins.win/raw?url=${encodeURIComponent(url)}`);
        return response.ok ? await response.text() : null;
      } catch (error) {
        console.error(`Error fetching ${url}:`, error);
        return null;
      }
    };

    const parseStatsFromHTML = (html) => {
      if (!html) return null;
      
      const doc = new DOMParser().parseFromString(html, 'text/html');
      const rows = doc.querySelectorAll('.stats-table tbody tr');
      
      if (!rows.length) return null;
      
      const cells = rows[rows.length - 1].querySelectorAll('td.numeric');
      return {
        installs: parseInt(cells[0]?.textContent?.trim()) || 0,
        updateChecks: parseInt(cells[1]?.textContent?.trim()) || 0
      };
    };

    const insertOrUpdateStats = (element, label, value, className) => {
      const metadataList = element.querySelector('.inline-script-stats');
      if (!metadataList) return;

      const lastRow = metadataList.lastElementChild;
      if (!lastRow) return;

      let termElement = metadataList.querySelector(`dt.${className}`);
      let descElement = metadataList.querySelector(`dd.${className}`);

      if (!termElement) {
        termElement = document.createElement('dt');
        termElement.className = className;
        lastRow.parentNode.insertBefore(termElement, lastRow.nextSibling);
      }

      if (!descElement) {
        descElement = document.createElement('dd');
        descElement.className = className;
        termElement.after(descElement);
      }

      termElement.textContent = label;
      descElement.textContent = value;
    };

    const initializeStatsLabels = (scripts) => {
      scripts.forEach(scriptInfo => {
        insertOrUpdateStats(
          scriptInfo.element,
          'Installs',
          '⌛ Checking...',
          'script-list-installs'
        );
        insertOrUpdateStats(
          scriptInfo.element,
          'Checks',
          '⌛ Checking...',
          'script-list-update-checks'
        );
      });
    };

    const getCachedStats = (scriptId) => {
      const cacheKey = `script_stats_${scriptId}`;
      const cachedData = GM_getValue(cacheKey);
      
      if (cachedData) {
        const { timestamp, stats } = JSON.parse(cachedData);
        const now = Date.now();
        
        if (now - timestamp < CACHE_DURATION) {
          return stats;
        }
      }
      
      return null;
    };

    const setCachedStats = (scriptId, stats) => {
      const cacheKey = `script_stats_${scriptId}`;
      const cacheEntry = JSON.stringify({
        timestamp: Date.now(),
        stats: stats
      });
      
      GM_setValue(cacheKey, cacheEntry);
    };

    const processSingleScript = async (scriptInfo) => {
      console.log(`Processing: ${scriptInfo.name}`);
      
      const cachedStats = getCachedStats(scriptInfo.id);
      
      if (cachedStats) {
        insertOrUpdateStats(
          scriptInfo.element,
          'Installs',
          cachedStats.installs.toLocaleString(),
          'script-list-installs'
        );
        insertOrUpdateStats(
          scriptInfo.element,
          'Checks',
          cachedStats.updateChecks.toLocaleString(),
          'script-list-update-checks'
        );
        
        return {
          ...scriptInfo,
          ...cachedStats,
          cached: true
        };
      }
      
      const html = await fetchWithProxy(scriptInfo.url);
      const stats = parseStatsFromHTML(html);
      
      const result = {
        ...scriptInfo,
        ...(stats || { installs: 0, updateChecks: 0 }),
        error: !stats ? 'Failed to fetch stats' : null
      };

      if (!result.error) {
        setCachedStats(scriptInfo.id, { 
          installs: result.installs, 
          updateChecks: result.updateChecks 
        });
        
        insertOrUpdateStats(
          scriptInfo.element,
          'Installs',
          result.installs.toLocaleString(),
          'script-list-installs'
        );
        insertOrUpdateStats(
          scriptInfo.element,
          'Checks',
          result.updateChecks.toLocaleString(),
          'script-list-update-checks'
        );
      } else {
        insertOrUpdateStats(
          scriptInfo.element,
          'Installs',
          'Failed to load',
          'script-list-installs'
        );
        insertOrUpdateStats(
          scriptInfo.element,
          'Checks',
          'Failed to load',
          'script-list-update-checks'
        );
      }

      return result;
    };

    const initScriptStats = async () => {
      try {
        const scripts = collectScriptLinks();
        console.log(`Found ${scripts.length} scripts to process`);
        
        initializeStatsLabels(scripts);
        
        const results = [];
        
        for (const scriptInfo of scripts) {
          const result = await processSingleScript(scriptInfo);
          results.push(result);
          console.log('Result:', result);
          
          await sleep(result.cached ? 100 : 1000);
        }
        
        const totals = results.reduce((acc, curr) => ({
          totalInstalls: acc.totalInstalls + (curr.error ? 0 : curr.installs),
          totalUpdateChecks: acc.totalUpdateChecks + (curr.error ? 0 : curr.updateChecks),
          successCount: acc.successCount + (curr.error ? 0 : 1),
          errorCount: acc.errorCount + (curr.error ? 1 : 0),
          cachedCount: acc.cachedCount + (curr.cached ? 1 : 0)
        }), { 
          totalInstalls: 0, 
          totalUpdateChecks: 0, 
          successCount: 0, 
          errorCount: 0,
          cachedCount: 0 
        });
        
        console.log('\nAll results:', results);
        console.log('\nTotals:', totals);
        
        return { results, totals };
      } catch (error) {
        console.error('Error in initScriptStats:', error);
      }
    };

    const style = document.createElement('style');
    style.textContent = `
      .script-list-installs,
      .script-list-update-checks {
        opacity: 0.7;
        font-style: italic;
      }
      
      .script-list-installs:not(:empty),
      .script-list-update-checks:not(:empty) {
        opacity: 1;
        font-style: normal;
      }
    `;
    document.head.appendChild(style);

    document.readyState === 'loading' 
      ? document.addEventListener('DOMContentLoaded', initScriptStats)
      : initScriptStats();
})();

QingJ © 2025

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