Greasyfork Update Checks

Display today's script installations and update checks.

  1. // ==UserScript==
  2. // @name Greasyfork Update Checks
  3. // @description Display today's script installations and update checks.
  4. // @icon https://greasyfork.org/vite/assets/blacklogo96-CxYTSM_T.png
  5. // @version 1.8
  6. // @author afkarxyz
  7. // @namespace https://github.com/afkarxyz/userscripts/
  8. // @supportURL https://github.com/afkarxyz/userscripts/issues
  9. // @license MIT
  10. // @match https://greasyfork.org/*
  11. // @match https://sleazyfork.org/*
  12. // @grant GM_xmlhttpRequest
  13. // @connect api.greasyfork.org
  14. // @connect api.sleazyfork.org
  15. // ==/UserScript==
  16.  
  17. (function() {
  18. 'use strict';
  19. function formatNumber(num) {
  20. return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
  21. }
  22. function displayScriptStats() {
  23. document.head.appendChild(Object.assign(document.createElement('style'), {
  24. textContent: '.script-list-installs, .script-list-update-checks { opacity: 1; }'
  25. }));
  26. function addStat(element, label, className) {
  27. const list = element.querySelector('.inline-script-stats');
  28. if (!list) return;
  29. const dt = document.createElement('dt');
  30. const dd = document.createElement('dd');
  31. dt.className = className;
  32. dd.className = className;
  33. dt.textContent = label;
  34. dd.textContent = '...';
  35. list.lastElementChild.parentNode.insertBefore(dt, list.lastElementChild.nextSibling);
  36. dt.after(dd);
  37. return dd;
  38. }
  39. document.querySelectorAll('li[data-script-id]').forEach(script => {
  40. const installsElement = addStat(script, 'Installs', 'script-list-installs');
  41. const checksElement = addStat(script, 'Checks', 'script-list-update-checks');
  42. script.dataset.installsElement = installsElement.id = `installs-${script.dataset.scriptId}`;
  43. script.dataset.checksElement = checksElement.id = `checks-${script.dataset.scriptId}`;
  44. });
  45. }
  46. const collectScriptIds = () =>
  47. Array.from(document.querySelectorAll('li[data-script-id]'))
  48. .map(el => ({
  49. scriptId: el.getAttribute('data-script-id'),
  50. element: el
  51. }));
  52. function getCurrentLanguage() {
  53. const pathMatch = window.location.pathname.match(/^\/([a-z]{2}(?:-[A-Z]{2})?)\//);
  54. if (pathMatch) {
  55. return pathMatch[1];
  56. }
  57. return document.documentElement.lang || 'en';
  58. }
  59. function fetchStats(scriptInfo) {
  60. return new Promise((resolve) => {
  61. const domain = window.location.hostname.includes('sleazyfork') ? 'sleazyfork.org' : 'greasyfork.org';
  62. const language = getCurrentLanguage();
  63. const apiUrl = `https://api.${domain}/${language}/scripts/${scriptInfo.scriptId}/stats.json`;
  64. GM_xmlhttpRequest({
  65. method: 'GET',
  66. url: apiUrl,
  67. responseType: 'json',
  68. onload: function(response) {
  69. try {
  70. const data = response.response;
  71. if (!data || typeof data !== 'object' || Object.keys(data).length === 0) {
  72. resolve({ installs: 0, checks: 0 });
  73. return;
  74. }
  75. const dates = Object.keys(data).sort();
  76. const latestDate = dates[dates.length - 1];
  77. if (!data[latestDate] || typeof data[latestDate] !== 'object') {
  78. resolve({ installs: 0, checks: 0 });
  79. return;
  80. }
  81. const stats = {
  82. installs: data[latestDate].installs || 0,
  83. checks: data[latestDate].update_checks || 0
  84. };
  85. resolve(stats);
  86. } catch (error) {
  87. console.error(error);
  88. resolve({ installs: 0, checks: 0 });
  89. }
  90. },
  91. onerror: function(error) {
  92. console.error(error);
  93. resolve({ installs: 0, checks: 0 });
  94. }
  95. });
  96. });
  97. }
  98. function updateStats(scriptInfo, stats) {
  99. if (!stats) return;
  100. const element = scriptInfo.element;
  101. const installsElement = document.getElementById(element.dataset.installsElement);
  102. const checksElement = document.getElementById(element.dataset.checksElement);
  103. if (installsElement) installsElement.textContent = formatNumber(stats.installs);
  104. if (checksElement) checksElement.textContent = formatNumber(stats.checks);
  105. }
  106. async function init() {
  107. displayScriptStats();
  108. const scriptInfos = collectScriptIds();
  109. const fetchPromises = scriptInfos.map(async (scriptInfo) => {
  110. try {
  111. const stats = await fetchStats(scriptInfo);
  112. updateStats(scriptInfo, stats);
  113. } catch (error) {
  114. console.error(error);
  115. updateStats(scriptInfo, { installs: 0, checks: 0 });
  116. }
  117. });
  118. await Promise.all(fetchPromises);
  119. }
  120. if (document.readyState === 'loading') {
  121. document.addEventListener('DOMContentLoaded', init);
  122. } else {
  123. init();
  124. }
  125. })();