Pixiv Extra Stats

Add extra stats to the Pixiv dashboard (old and new).

// ==UserScript==
// @name         Pixiv Extra Stats
// @namespace    https://github.com/noccu
// @match        https://www.pixiv.net/*
// @grant        none
// @version      1.2.1
// @description  Add extra stats to the Pixiv dashboard (old and new).
// @author       noccu
// ==/UserScript==

(function () {
    'use strict';
    
    if (location.pathname.startsWith("/manage")) oldPixiv();
    else newPixiv();
    
    function newPixiv() {
        injectCSS();
        const re_newPixiv = new RegExp("(ajax/dashboard/home)|(ajax/dashboard/works/)");
        if (location.pathname.endsWith("works")) { worksPage(); }
        else if (location.pathname.endsWith("board")) { homePage(); }
        peekFetch();
        
        //Functions 
        function homePage() {
            function addStats(work, stats) {
                if (!stats) { return }
                var cont = document.createElement("div");
                cont.innerHTML = `Like rate: <span class="exStats">${stats.likeRate}</span>% | Bookmark rate: <span class="exStats">${stats.bookmarkRate}</span>% | Daily views: <span class="exStats">${stats.avgViewsPerDay}</span>`;
                cont.style.fontSize = "0.8rem";
                cont.style.textAlign = "center";
                work.appendChild(cont);
            }

            function getStats(work) {
                let views = work.getElementsByClassName("gtm-dashboard-home-latest-works-number-link-view"),
                likes = work.getElementsByClassName("gtm-dashboard-home-latest-works-number-link-like"),
                bookmarks = work.getElementsByClassName("gtm-dashboard-home-latest-works-number-link-bookmark"),
                date = work.getElementsByClassName("h8luo8-9");
                
                //assume it's all fine
                if (views.length) {
                    views = getNumber(views[0].getElementsByClassName("zpz4nj-2")[0].textContent);
                    likes = getNumber(likes[0].getElementsByClassName("zpz4nj-2")[0].textContent);
                    bookmarks = getNumber(bookmarks[0].getElementsByClassName("zpz4nj-2")[0].textContent);
                    date = new Date(date[0].textContent);
                }
                else {
                    console.error("Invalid data");
                    return;
                }
                
                return {
                    likeRate: (likes / views * 100).toFixed(2),
                    bookmarkRate: (bookmarks / views * 100).toFixed(2),
                    avgViewsPerDay: (views / Math.ceil((Date.now() - date) / 86400000)).toFixed(0)
                }
            }

            waitOn("ul > div.h8luo8-0", e => {
                e.children.forEach(w => addStats(w, getStats(w)));
            });
        }     
        //Works page
        function worksPage() {
            var indexCache;
            //This assumes querySelectorAll returns in order every time... big thonk.
            function indexColumns () {
                if (indexCache) return indexCache;
                let idx = {}, headers = document.querySelectorAll(".sc-1b2i4p6-22");
                headers.forEach((e, i) => {
                    switch (e.firstElementChild.firstElementChild.textContent) {
                        case "Likes":
                            idx.likes = i; break;
                        case "Bookmarks":
                            idx.bookmarks = i; break;
                        case "Views":
                            idx.views = i; break;
                        case "Date":
                            idx.date = i; break;
                    }
                });
                //Prevent needless processing when observer fires every few lines of scrolling...
                //We invalidate because user can change columns and I cba with that.
                indexCache = {idx, numCols: headers.length, firstIdx: idx[Object.keys(idx)[0]]};
                setTimeout(() => indexCache = undefined, 5000);
                return indexCache;
            }
            function addStats(dom, type) {
                if (type == "normal") {
                    let stats, {idx, numCols, firstIdx} = indexColumns();
                    dom = chunkArrayLike(dom, numCols);
                    for (let row of dom) {
                        if (!row[firstIdx].hasExStats) {
                            stats = getFormattedStats(row, idx);
                            for (let stat in idx) {
                                if (stats[stat]) {
                                    let cell = row[idx[stat]];
                                    cell.firstElementChild.firstElementChild.innerHTML += stats[stat];
                                    cell.hasExStats = true;
                                }
                            }
                        }
                    }
                }
                else if (type == "small") {
                    dom.forEach(statDetails => {
                        if (statDetails.hasExStats) return;
                        let date = new Date(statDetails.previousElementSibling.textContent);
                        let findStat = function(str) {
                            let ele = Array.prototype.find.call(statDetails.children, stat => stat.firstElementChild.textContent == str);
                            return {ele: ele.lastElementChild, val: getNumber(ele.lastElementChild.textContent)};
                        }
                        let views = findStat("Views"); if (!views) return;
                        let likes = findStat("Likes");
                        let bookmarks = findStat("Bookmarks");
                        if (likes) likes.ele.innerHTML += formatStat("%", likes.val, views.val);
                        if (bookmarks) bookmarks.ele.innerHTML += formatStat("%", bookmarks.val, views.val);
                        if (date) views.ele.innerHTML += formatStat("date", date, views.val);
                        statDetails.hasExStats = true;
                    });
                }
            }
            function formatStat(type, ...values) {
                switch (type) {
                    case "%":
                        return ` (<span class="exStats">${(values[0] / values[1] * 100).toFixed(2)}</span>%)`;
                    case "date":
                        return ` (<span class="exStats">${(values[1] / Math.max(((Date.now() - values[0]) / 86400000), 1)).toFixed(0)}</span>)`;
                }
            }
            function getFormattedStats(dom, {likes, bookmarks, views, date}) {
                let stats = {};
                if (views) {
                    views = getNumber(dom[views].textContent);
                    if (likes) {
                        likes = getNumber(dom[likes].textContent);
                        stats.likes = formatStat("%", likes, views);
                    }
                    if (bookmarks) {
                        bookmarks = getNumber(dom[bookmarks].textContent);
                        stats.bookmarks = formatStat("%", bookmarks, views);
                    }
                    if (date) {
                        date = new Date(dom[date].textContent);
                        stats.views = formatStat("date", date, views);
                    }
                }
                return stats;
            }

            waitOn("div.sc-1b2i4p6-25, div.sc-18qovzs-10", e => {
                // iirc getElementsByClassName returns consistently in-order so that's why we use it.
                const values = document.getElementsByClassName("sc-1b2i4p6-25"); // Live!
                const values_small = document.getElementsByClassName("sc-18qovzs-10"); // Live!
                const OBSERVER = new MutationObserver(m => {
                    m.some(r => {
                        if (r.addedNodes.length) {
                            //normal
                            if (r.target.className.startsWith("sc-1b2i4p6-2")) {
                                console.log("Rebuilding stats");
                                addStats(values, "normal");
                                return true;
                            }
                            //small
                            else if (r.target.parentElement.className.startsWith("sc-18qovzs-0")) {
                                console.log("Rebuilding stats (small)");
                                addStats(values_small, "small");
                                return true;
                            }
                        }
                    });
                });
                OBSERVER.observe(document.getElementsByClassName("sc-17pv5r7-10")[0], {childList: true, subtree: true});
                values.length ? addStats(values, "normal") : addStats(values_small, "small");
            });
        }
        
        // Helpers
        function getNumber(n) {
            if (n) {
                return parseInt(n.replaceAll(",", ""));
            }
        }
        function injectCSS() {
            let css = document.createElement("style");
            css.type = "text/css";

            css.innerHTML = ".exStats {color: rgb(0, 150, 250);}";
            document.head.appendChild(css);
        }
        function peekFetch() {
            window.oFetch = fetch;
            window.fetch = function (url, opt) {
                if (typeof url == "string") {
                    let m = url.match(re_newPixiv);
                    if (m) {
                        if (m[1]) {
                            homePage();
                        }
                        else if (m[2]) {
                            worksPage();
                        }
                    }
                }
                return window.oFetch(url, opt);
            };
        }
        function chunkArrayLike(a, size) {
            let r = [];
            for (let i = 0; i < a.length; i += size) {
                r.push( Array.prototype.slice.call(a, i, i + size) );
            }
            return r;
        }
    }

    //Code by cromachina
    function oldPixiv() {
        document.body.querySelectorAll('li[class="image-item"]').forEach(function (node)
        {
            let bookmark_node = node.querySelector('a[class*="bookmark-count"]');
            let view_node = node.querySelector('a[class*="views"] span');
            if (bookmark_node && view_node)
            {
                let bookmark_count = parseInt(bookmark_node.text);
                let views = parseInt(view_node.textContent);
                let bookmark_percent = (bookmark_count / views * 100).toFixed(2);
                bookmark_node.append(` (${bookmark_percent}%)`);
            }
        });
    }

    //Helpers
    function waitOn(selector, action, interval) {
        let elm = document.querySelector(selector);
        if (elm) action(elm);
        else setTimeout(() => waitOn(selector, action, interval), interval || 500);
    }
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址