您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Adds tags count indicator to list of entries
当前为
// ==UserScript== // @name MFC tag counter // @namespace https://takkkane.tumblr.com/scripts/mfcTagCounter // @version 0.1.3 // @description Adds tags count indicator to list of entries // @author Nefere // @supportURL https://twitter.com/TaxDelusion // @match https://myfigurecollection.net/entry/* // @match https://myfigurecollection.net/browse.v4.php* // @match https://myfigurecollection.net/browse/calendar/* // @match https://myfigurecollection.net/* // @match https://myfigurecollection.net/item/browse/figure/ // @match https://myfigurecollection.net/item/browse/goods/ // @match https://myfigurecollection.net/item/browse/media/ // @match https://myfigurecollection.net/item/browse/calendar/* // @icon https://www.google.com/s2/favicons?sz=64&domain=myfigurecollection.net // @license MIT // @icon https://www.google.com/s2/favicons?sz=64&domain=myfigurecollection.net // @license MIT // @grant GM.getValue // @grant GM.setValue // ==/UserScript== (async function () { 'use strict'; /** * Name of the class used for a tag indicator container. * It should be not used on the page it's inserted to. **/ var TAG_CLASSNAME = "us-tag"; /** * Name of the class that does not appear on the page. * Used to return empty collections of nodes from functions. **/ var FAKE_CLASS_PLACEHOLDER = "what-i-was-looking-for"; /** * A time in miliseconds to wait between requests for /entry pages. * Too short time may results in "429 - Too many requests" error responses. * Can be increased with REQUEST_DELAY_MULTIPLIER. **/ var REQUEST_DELAY = 1000; /** * A multipler that is used on REQUEST_DELAY when 429 response error is obtained. * Should be over 1 to properly work. **/ var REQUEST_DELAY_MULTIPLIER = 1.1; /** * A time in seconds for how long the entry data saved in a cache is considered "fresh" and up to date. * After the entry data is "rotten", it is removed from cache and may be replaced with new data. **/ var CACHE_FRESH_SECONDS = 10 * 60; /** * Map entries for tagCounterCache that are yet to be persisted in the extension storage. **/ var CACHE_SAVE_ENTRIES = []; /** * How many entries have to be added to the cache so the cache can be persisted in the extension storage. * It requires using GM.getValue() and GM.setValue() **/ var CACHE_SAVE_AFTER_SETTING_VALUES_ORDER = 5; /** * A cache for tag count indicated in the entry page. * It's a Map() consisted of: * * keys: pathname of an entry page ("/entry/2") * * values: object with fields: * ** number: integer with number of tags on the entry page (24) * ** updatedTime: timestamp of when the map was updated. * Map entries may be deleted after time indicated in CACHE_FRESH_SECONDS. **/ var tagCounterCache; function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); }; async function getTagCounterCache() { return new Map(Object.entries( JSON.parse(await GM.getValue('tagCounterCache', '{}')))); }; async function saveTagCounterCache() { var newTagCounterCache = await getTagCounterCache(); for (var entry of CACHE_SAVE_ENTRIES) { newTagCounterCache.set(entry.key, entry.value); } GM.setValue('tagCounterCache', JSON.stringify(Object.fromEntries(newTagCounterCache))); tagCounterCache = newTagCounterCache; newTagCounterCache.length = 0; /* clear new data as they are persisted */ }; async function pushToTagCounterCache(url, tagCounter) { if (tagCounter) { var time = Date.now(); var entry = { key: url, value: { 'number': tagCounter, 'updatedTime': time } }; tagCounterCache.set(entry.key, entry.value); CACHE_SAVE_ENTRIES.push(entry); if (CACHE_SAVE_ENTRIES.length % CACHE_SAVE_AFTER_SETTING_VALUES_ORDER == 0) { saveTagCounterCache(); } } }; function getTagCounterFromTagCounterCache(url) { var tagCounterPair = tagCounterCache.get(url); if (tagCounterPair == null) { return 0; } var rottenPairDate = new Date(tagCounterPair.updatedTime); rottenPairDate.setSeconds(rottenPairDate.getSeconds() + CACHE_FRESH_SECONDS); if (rottenPairDate < Date.now()) { tagCounterCache.delete(url); return 0; } return tagCounterPair.number; }; function addStyles() { $("<style>") .prop("type", "text/css") .html("\ .item-icon ." + TAG_CLASSNAME + " {\ position: absolute;\ display: block;\ right: 1px;\ bottom: 1px;\ height: 16px;\ padding: 0 4px;\ font-weight: 700;\ color: gold;\ background-color: darkgreen\ }") .appendTo("head"); }; function getEntryContainers() { var pathname = window.location.pathname; var search = window.location.search; var searchParams = new URLSearchParams(search); var tbParam = searchParams.get("_tb"); if (pathname.includes("/entry/") /* encyclopedia entry */ || pathname.includes("/browse.v4.php") /* search results with filters */ || pathname.includes("/browse/calendar/") /* calendar page */ || pathname.includes("/item/browse/calendar/") /* new calendar page */ || pathname.includes("/item/browse/figure/") /* new figures page */ || pathname.includes("/item/browse/goods/") /* new goods page */ || pathname.includes("/item/browse/media/") /* new media page */ || tbParam !== null) { var result = $("#wide .result:not(.hidden)"); return result; } console.log("unsupported getEntryContainers"); return $(FAKE_CLASS_PLACEHOLDER); }; function isDetailedList() { var search = window.location.search; var searchParams = new URLSearchParams(search); var outputParam = searchParams.get("output"); /* 0 - detailedList, 1,2 - grid, 3 - diaporama */ return outputParam == 0; }; function getItemsFromContainer(entryContainer) { var icons = $(entryContainer).find(".item-icons .item-icon"); if (icons.length > 0) { return icons; } var pathname = window.location.pathname; if (pathname.includes("/browse.v4.php") /* search page, detailed list view */ && isDetailedList()) { return $(FAKE_CLASS_PLACEHOLDER); } console.log("unsupported getItemsFromContainer"); return $(FAKE_CLASS_PLACEHOLDER); }; function getTagCounterFromHtml(html) { var parser = new DOMParser(); var doc = parser.parseFromString(html, 'text/html'); var tagCounterNode = doc.querySelector('.tbx-target-TAGS .count'); return tagCounterNode.textContent; }; function addTagCounterToSearchResult(itemLinkElement, countOfTags) { var tagElement = document.createElement("span"); tagElement.setAttribute("class", TAG_CLASSNAME); tagElement.textContent = countOfTags; itemLinkElement.appendChild(tagElement); }; async function fetchAndHandle(queue) { var resultQueue = []; for (var itemElement of queue) { var itemLinkElement = itemElement.firstChild; var entryLink = itemLinkElement.getAttribute("href"); fetch(entryLink, { headers: { "User-Agent": GM.info.script.name + " " + GM.info.script.version } }).then(function (response) { if (response.ok) { return response.text(); } return Promise.reject(response); }).then(function (html) { var countOfTags = getTagCounterFromHtml(html); addTagCounterToSearchResult(itemLinkElement, countOfTags); pushToTagCounterCache(entryLink, countOfTags); }).catch(function (err) { if (err.status == 429) { console.warn('Too many requests. Added the request to fetch later', err.url); resultQueue.push(itemElement); REQUEST_DELAY = REQUEST_DELAY * REQUEST_DELAY_MULTIPLIER; console.info('Increased delay to ' + REQUEST_DELAY); } }); await sleep(REQUEST_DELAY); } return resultQueue; }; async function main() { var cacheQueue = []; var entryContainers = getEntryContainers(); entryContainers.each(function (i, entryContainer) { var itemsElements = getItemsFromContainer(entryContainer); itemsElements.each(function (i, itemElement) { cacheQueue.push(itemElement); }); }); var queue = []; tagCounterCache = await getTagCounterCache(); for (var itemElement of cacheQueue) { var itemLinkElement = itemElement.firstChild; var entryLink = itemLinkElement.getAttribute("href"); var cache = getTagCounterFromTagCounterCache(entryLink); if (cache > 0) { addTagCounterToSearchResult(itemLinkElement, cache); } else { queue.push(itemElement); } } while (queue.length) { queue = await fetchAndHandle(queue); } saveTagCounterCache(); }; addStyles(); main(); })();