Greasyfork/Sleazyfork Update Checks Display

Display today's script installations and update checks.

当前为 2024-12-08 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Greasyfork/Sleazyfork Update Checks Display
  3. // @description Display today's script installations and update checks.
  4. // @icon https://greasyfork.org/vite/assets/blacklogo96-CxYTSM_T.png
  5. // @version 1.0
  6. // @author afkarxyz
  7. // @namespace https://github.com/afkarxyz/misc-scripts/
  8. // @supportURL https://github.com/afkarxyz/misc-scripts/issues
  9. // @license MIT
  10. // @match https://greasyfork.org/*/users/*
  11. // @match https://sleazyfork.org/*/users/*
  12. // @grant GM_setValue
  13. // @grant GM_getValue
  14. // ==/UserScript==
  15.  
  16. (function() {
  17. 'use strict';
  18.  
  19. const CACHE_DURATION = 10 * 60 * 1000;
  20.  
  21. const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
  22.  
  23. const collectScriptLinks = () =>
  24. Array.from(document.querySelectorAll('li[data-script-id]')).map(element => ({
  25. url: `https://greasyfork.org/en/scripts/${element.getAttribute('data-script-id')}-${
  26. element.getAttribute('data-script-name')
  27. .toLowerCase()
  28. .replace(/\s+/g, '-')
  29. .replace(/[^a-z0-9-]/g, '')
  30. }/stats`,
  31. name: element.getAttribute('data-script-name'),
  32. id: element.getAttribute('data-script-id'),
  33. element: element
  34. }));
  35.  
  36. const fetchWithProxy = async (url) => {
  37. try {
  38. const response = await fetch(`https://api.allorigins.win/raw?url=${encodeURIComponent(url)}`);
  39. return response.ok ? await response.text() : null;
  40. } catch (error) {
  41. console.error(`Error fetching ${url}:`, error);
  42. return null;
  43. }
  44. };
  45.  
  46. const parseStatsFromHTML = (html) => {
  47. if (!html) return null;
  48. const doc = new DOMParser().parseFromString(html, 'text/html');
  49. const rows = doc.querySelectorAll('.stats-table tbody tr');
  50. if (!rows.length) return null;
  51. const cells = rows[rows.length - 1].querySelectorAll('td.numeric');
  52. return {
  53. installs: parseInt(cells[0]?.textContent?.trim()) || 0,
  54. updateChecks: parseInt(cells[1]?.textContent?.trim()) || 0
  55. };
  56. };
  57.  
  58. const insertOrUpdateStats = (element, label, value, className) => {
  59. const metadataList = element.querySelector('.inline-script-stats');
  60. if (!metadataList) return;
  61.  
  62. const lastRow = metadataList.lastElementChild;
  63. if (!lastRow) return;
  64.  
  65. let termElement = metadataList.querySelector(`dt.${className}`);
  66. let descElement = metadataList.querySelector(`dd.${className}`);
  67.  
  68. if (!termElement) {
  69. termElement = document.createElement('dt');
  70. termElement.className = className;
  71. lastRow.parentNode.insertBefore(termElement, lastRow.nextSibling);
  72. }
  73.  
  74. if (!descElement) {
  75. descElement = document.createElement('dd');
  76. descElement.className = className;
  77. termElement.after(descElement);
  78. }
  79.  
  80. termElement.textContent = label;
  81. descElement.textContent = value;
  82. };
  83.  
  84. const initializeStatsLabels = (scripts) => {
  85. scripts.forEach(scriptInfo => {
  86. insertOrUpdateStats(
  87. scriptInfo.element,
  88. 'Installs',
  89. '⌛ Checking...',
  90. 'script-list-installs'
  91. );
  92. insertOrUpdateStats(
  93. scriptInfo.element,
  94. 'Checks',
  95. '⌛ Checking...',
  96. 'script-list-update-checks'
  97. );
  98. });
  99. };
  100.  
  101. const getCachedStats = (scriptId) => {
  102. const cacheKey = `script_stats_${scriptId}`;
  103. const cachedData = GM_getValue(cacheKey);
  104. if (cachedData) {
  105. const { timestamp, stats } = JSON.parse(cachedData);
  106. const now = Date.now();
  107. if (now - timestamp < CACHE_DURATION) {
  108. return stats;
  109. }
  110. }
  111. return null;
  112. };
  113.  
  114. const setCachedStats = (scriptId, stats) => {
  115. const cacheKey = `script_stats_${scriptId}`;
  116. const cacheEntry = JSON.stringify({
  117. timestamp: Date.now(),
  118. stats: stats
  119. });
  120. GM_setValue(cacheKey, cacheEntry);
  121. };
  122.  
  123. const processSingleScript = async (scriptInfo) => {
  124. console.log(`Processing: ${scriptInfo.name}`);
  125. const cachedStats = getCachedStats(scriptInfo.id);
  126. if (cachedStats) {
  127. insertOrUpdateStats(
  128. scriptInfo.element,
  129. 'Installs',
  130. cachedStats.installs.toLocaleString(),
  131. 'script-list-installs'
  132. );
  133. insertOrUpdateStats(
  134. scriptInfo.element,
  135. 'Checks',
  136. cachedStats.updateChecks.toLocaleString(),
  137. 'script-list-update-checks'
  138. );
  139. return {
  140. ...scriptInfo,
  141. ...cachedStats,
  142. cached: true
  143. };
  144. }
  145. const html = await fetchWithProxy(scriptInfo.url);
  146. const stats = parseStatsFromHTML(html);
  147. const result = {
  148. ...scriptInfo,
  149. ...(stats || { installs: 0, updateChecks: 0 }),
  150. error: !stats ? 'Failed to fetch stats' : null
  151. };
  152.  
  153. if (!result.error) {
  154. setCachedStats(scriptInfo.id, {
  155. installs: result.installs,
  156. updateChecks: result.updateChecks
  157. });
  158. insertOrUpdateStats(
  159. scriptInfo.element,
  160. 'Installs',
  161. result.installs.toLocaleString(),
  162. 'script-list-installs'
  163. );
  164. insertOrUpdateStats(
  165. scriptInfo.element,
  166. 'Checks',
  167. result.updateChecks.toLocaleString(),
  168. 'script-list-update-checks'
  169. );
  170. } else {
  171. insertOrUpdateStats(
  172. scriptInfo.element,
  173. 'Installs',
  174. 'Failed to load',
  175. 'script-list-installs'
  176. );
  177. insertOrUpdateStats(
  178. scriptInfo.element,
  179. 'Checks',
  180. 'Failed to load',
  181. 'script-list-update-checks'
  182. );
  183. }
  184.  
  185. return result;
  186. };
  187.  
  188. const initScriptStats = async () => {
  189. try {
  190. const scripts = collectScriptLinks();
  191. console.log(`Found ${scripts.length} scripts to process`);
  192. initializeStatsLabels(scripts);
  193. const results = [];
  194. for (const scriptInfo of scripts) {
  195. const result = await processSingleScript(scriptInfo);
  196. results.push(result);
  197. console.log('Result:', result);
  198. await sleep(result.cached ? 100 : 1000);
  199. }
  200. const totals = results.reduce((acc, curr) => ({
  201. totalInstalls: acc.totalInstalls + (curr.error ? 0 : curr.installs),
  202. totalUpdateChecks: acc.totalUpdateChecks + (curr.error ? 0 : curr.updateChecks),
  203. successCount: acc.successCount + (curr.error ? 0 : 1),
  204. errorCount: acc.errorCount + (curr.error ? 1 : 0),
  205. cachedCount: acc.cachedCount + (curr.cached ? 1 : 0)
  206. }), {
  207. totalInstalls: 0,
  208. totalUpdateChecks: 0,
  209. successCount: 0,
  210. errorCount: 0,
  211. cachedCount: 0
  212. });
  213. console.log('\nAll results:', results);
  214. console.log('\nTotals:', totals);
  215. return { results, totals };
  216. } catch (error) {
  217. console.error('Error in initScriptStats:', error);
  218. }
  219. };
  220.  
  221. const style = document.createElement('style');
  222. style.textContent = `
  223. .script-list-installs,
  224. .script-list-update-checks {
  225. opacity: 0.7;
  226. font-style: italic;
  227. }
  228. .script-list-installs:not(:empty),
  229. .script-list-update-checks:not(:empty) {
  230. opacity: 1;
  231. font-style: normal;
  232. }
  233. `;
  234. document.head.appendChild(style);
  235.  
  236. document.readyState === 'loading'
  237. ? document.addEventListener('DOMContentLoaded', initScriptStats)
  238. : initScriptStats();
  239. })();