MFC tag counter

Adds tags count indicator to list of entries

目前为 2024-10-09 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name MFC tag counter
  3. // @namespace https://takkkane.tumblr.com/scripts/mfcTagCounter
  4. // @version 0.1.3
  5. // @description Adds tags count indicator to list of entries
  6. // @author Nefere
  7. // @supportURL https://twitter.com/TaxDelusion
  8. // @match https://myfigurecollection.net/entry/*
  9. // @match https://myfigurecollection.net/browse.v4.php*
  10. // @match https://myfigurecollection.net/browse/calendar/*
  11.  
  12. // @match https://myfigurecollection.net/*
  13. // @match https://myfigurecollection.net/item/browse/figure/
  14. // @match https://myfigurecollection.net/item/browse/goods/
  15. // @match https://myfigurecollection.net/item/browse/media/
  16. // @match https://myfigurecollection.net/item/browse/calendar/*
  17. // @icon https://www.google.com/s2/favicons?sz=64&domain=myfigurecollection.net
  18. // @license MIT
  19. // @icon https://www.google.com/s2/favicons?sz=64&domain=myfigurecollection.net
  20. // @license MIT
  21. // @grant GM.getValue
  22. // @grant GM.setValue
  23. // ==/UserScript==
  24.  
  25. (async function () {
  26. 'use strict';
  27.  
  28. /**
  29. * Name of the class used for a tag indicator container.
  30. * It should be not used on the page it's inserted to.
  31. **/
  32. var TAG_CLASSNAME = "us-tag";
  33.  
  34. /**
  35. * Name of the class that does not appear on the page.
  36. * Used to return empty collections of nodes from functions.
  37. **/
  38. var FAKE_CLASS_PLACEHOLDER = "what-i-was-looking-for";
  39.  
  40. /**
  41. * A time in miliseconds to wait between requests for /entry pages.
  42. * Too short time may results in "429 - Too many requests" error responses.
  43. * Can be increased with REQUEST_DELAY_MULTIPLIER.
  44. **/
  45. var REQUEST_DELAY = 1000;
  46.  
  47. /**
  48. * A multipler that is used on REQUEST_DELAY when 429 response error is obtained.
  49. * Should be over 1 to properly work.
  50. **/
  51. var REQUEST_DELAY_MULTIPLIER = 1.1;
  52.  
  53. /**
  54. * A time in seconds for how long the entry data saved in a cache is considered "fresh" and up to date.
  55. * After the entry data is "rotten", it is removed from cache and may be replaced with new data.
  56. **/
  57. var CACHE_FRESH_SECONDS = 10 * 60;
  58.  
  59. /**
  60. * Map entries for tagCounterCache that are yet to be persisted in the extension storage.
  61. **/
  62. var CACHE_SAVE_ENTRIES = [];
  63.  
  64. /**
  65. * How many entries have to be added to the cache so the cache can be persisted in the extension storage.
  66. * It requires using GM.getValue() and GM.setValue()
  67. **/
  68. var CACHE_SAVE_AFTER_SETTING_VALUES_ORDER = 5;
  69.  
  70. /**
  71. * A cache for tag count indicated in the entry page.
  72. * It's a Map() consisted of:
  73. * * keys: pathname of an entry page ("/entry/2")
  74. * * values: object with fields:
  75. * ** number: integer with number of tags on the entry page (24)
  76. * ** updatedTime: timestamp of when the map was updated.
  77. * Map entries may be deleted after time indicated in CACHE_FRESH_SECONDS.
  78. **/
  79. var tagCounterCache;
  80.  
  81. function sleep(ms) {
  82. return new Promise(resolve => setTimeout(resolve, ms));
  83. };
  84. async function getTagCounterCache() {
  85. return new Map(Object.entries(
  86. JSON.parse(await GM.getValue('tagCounterCache', '{}'))));
  87. };
  88. async function saveTagCounterCache() {
  89. var newTagCounterCache = await getTagCounterCache();
  90. for (var entry of CACHE_SAVE_ENTRIES) {
  91. newTagCounterCache.set(entry.key, entry.value);
  92. }
  93. GM.setValue('tagCounterCache', JSON.stringify(Object.fromEntries(newTagCounterCache)));
  94. tagCounterCache = newTagCounterCache;
  95. newTagCounterCache.length = 0; /* clear new data as they are persisted */
  96. };
  97. async function pushToTagCounterCache(url, tagCounter) {
  98. if (tagCounter) {
  99. var time = Date.now();
  100. var entry = {
  101. key: url,
  102. value: {
  103. 'number': tagCounter,
  104. 'updatedTime': time
  105. }
  106. };
  107. tagCounterCache.set(entry.key, entry.value);
  108. CACHE_SAVE_ENTRIES.push(entry);
  109. if (CACHE_SAVE_ENTRIES.length % CACHE_SAVE_AFTER_SETTING_VALUES_ORDER == 0) {
  110. saveTagCounterCache();
  111. }
  112. }
  113. };
  114. function getTagCounterFromTagCounterCache(url) {
  115. var tagCounterPair = tagCounterCache.get(url);
  116. if (tagCounterPair == null) {
  117. return 0;
  118. }
  119. var rottenPairDate = new Date(tagCounterPair.updatedTime);
  120. rottenPairDate.setSeconds(rottenPairDate.getSeconds() + CACHE_FRESH_SECONDS);
  121. if (rottenPairDate < Date.now()) {
  122. tagCounterCache.delete(url);
  123. return 0;
  124. }
  125. return tagCounterPair.number;
  126. };
  127. function addStyles() {
  128. $("<style>")
  129. .prop("type", "text/css")
  130. .html("\
  131. .item-icon ." + TAG_CLASSNAME + " {\
  132. position: absolute;\
  133. display: block;\
  134. right: 1px;\
  135. bottom: 1px;\
  136. height: 16px;\
  137. padding: 0 4px;\
  138. font-weight: 700;\
  139. color: gold;\
  140. background-color: darkgreen\
  141. }")
  142. .appendTo("head");
  143. };
  144. function getEntryContainers() {
  145. var pathname = window.location.pathname;
  146. var search = window.location.search;
  147. var searchParams = new URLSearchParams(search);
  148. var tbParam = searchParams.get("_tb");
  149. if (pathname.includes("/entry/") /* encyclopedia entry */
  150. || pathname.includes("/browse.v4.php") /* search results with filters */
  151. || pathname.includes("/browse/calendar/") /* calendar page */
  152. || pathname.includes("/item/browse/calendar/") /* new calendar page */
  153. || pathname.includes("/item/browse/figure/") /* new figures page */
  154. || pathname.includes("/item/browse/goods/") /* new goods page */
  155. || pathname.includes("/item/browse/media/") /* new media page */
  156. || tbParam !== null) {
  157. var result = $("#wide .result:not(.hidden)");
  158. return result;
  159. }
  160. console.log("unsupported getEntryContainers");
  161. return $(FAKE_CLASS_PLACEHOLDER);
  162. };
  163. function isDetailedList() {
  164. var search = window.location.search;
  165. var searchParams = new URLSearchParams(search);
  166. var outputParam = searchParams.get("output"); /* 0 - detailedList, 1,2 - grid, 3 - diaporama */
  167. return outputParam == 0;
  168. };
  169. function getItemsFromContainer(entryContainer) {
  170. var icons = $(entryContainer).find(".item-icons .item-icon");
  171. if (icons.length > 0) {
  172. return icons;
  173. }
  174. var pathname = window.location.pathname;
  175. if (pathname.includes("/browse.v4.php") /* search page, detailed list view */
  176. && isDetailedList()) {
  177. return $(FAKE_CLASS_PLACEHOLDER);
  178. }
  179. console.log("unsupported getItemsFromContainer");
  180. return $(FAKE_CLASS_PLACEHOLDER);
  181. };
  182. function getTagCounterFromHtml(html) {
  183. var parser = new DOMParser();
  184. var doc = parser.parseFromString(html, 'text/html');
  185. var tagCounterNode = doc.querySelector('.tbx-target-TAGS .count');
  186. return tagCounterNode.textContent;
  187. };
  188. function addTagCounterToSearchResult(itemLinkElement, countOfTags) {
  189. var tagElement = document.createElement("span");
  190. tagElement.setAttribute("class", TAG_CLASSNAME);
  191. tagElement.textContent = countOfTags;
  192. itemLinkElement.appendChild(tagElement);
  193. };
  194.  
  195. async function fetchAndHandle(queue) {
  196. var resultQueue = [];
  197. for (var itemElement of queue) {
  198. var itemLinkElement = itemElement.firstChild;
  199. var entryLink = itemLinkElement.getAttribute("href");
  200.  
  201. fetch(entryLink, {
  202. headers: {
  203. "User-Agent": GM.info.script.name + " " + GM.info.script.version
  204. }
  205. }).then(function (response) {
  206. if (response.ok) {
  207. return response.text();
  208. }
  209. return Promise.reject(response);
  210. }).then(function (html) {
  211. var countOfTags = getTagCounterFromHtml(html);
  212. addTagCounterToSearchResult(itemLinkElement, countOfTags);
  213. pushToTagCounterCache(entryLink, countOfTags);
  214. }).catch(function (err) {
  215. if (err.status == 429) {
  216. console.warn('Too many requests. Added the request to fetch later', err.url);
  217. resultQueue.push(itemElement);
  218. REQUEST_DELAY = REQUEST_DELAY * REQUEST_DELAY_MULTIPLIER;
  219. console.info('Increased delay to ' + REQUEST_DELAY);
  220. }
  221. });
  222. await sleep(REQUEST_DELAY);
  223.  
  224. }
  225. return resultQueue;
  226. };
  227. async function main() {
  228. var cacheQueue = [];
  229. var entryContainers = getEntryContainers();
  230. entryContainers.each(function (i, entryContainer) {
  231. var itemsElements = getItemsFromContainer(entryContainer);
  232. itemsElements.each(function (i, itemElement) {
  233. cacheQueue.push(itemElement);
  234. });
  235. });
  236.  
  237. var queue = [];
  238. tagCounterCache = await getTagCounterCache();
  239. for (var itemElement of cacheQueue) {
  240. var itemLinkElement = itemElement.firstChild;
  241. var entryLink = itemLinkElement.getAttribute("href");
  242. var cache = getTagCounterFromTagCounterCache(entryLink);
  243. if (cache > 0) {
  244. addTagCounterToSearchResult(itemLinkElement, cache);
  245. } else {
  246. queue.push(itemElement);
  247. }
  248. }
  249. while (queue.length) {
  250. queue = await fetchAndHandle(queue);
  251. }
  252. saveTagCounterCache();
  253.  
  254. };
  255.  
  256. addStyles();
  257. main();
  258. })();