MFC tag counter

Adds tags count indicator to list of entries

目前為 2024-10-09 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 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();
})();