// ==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();
})();