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