MFC tag counter

Adds tags count indicator to list of entries

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

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==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();
})();