Pixiv Bookmark Milestone Analytics

Display a dashboard table of times when artworks reach 1000 bookmark milestones. The script must remain running to collect statistics accurately.

// ==UserScript==
// @name         Pixiv Bookmark Milestone Analytics
// @namespace    http://tampermonkey.net/
// @version      0.3
// @description  Display a dashboard table of times when artworks reach 1000 bookmark milestones. The script must remain running to collect statistics accurately.
// @author       cro
// @match        https://www.pixiv.net/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=pixiv.net
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_addStyle
// ==/UserScript==
/* jshint esversion: 6 */

(function() {
    'use strict';
    GM_addStyle(".scrolltable { display:block; border-spacing:0px; display:block; overflow:auto; height: 400px; }");
    GM_addStyle(".scrolltable td { min-width: 100px; width: 100px; }");

    let time_interval = 1000 * 60;
    let milestone_interval = 1000;
    let fetch_headers = {
        "headers": {
            "accept": "application/json",
            "accept-language": "en-US,en;q=0.9,ja;q=0.8",
            "sec-ch-ua": "\"Not_A Brand\";v=\"99\", \"Google Chrome\";v=\"109\", \"Chromium\";v=\"109\"",
            "sec-ch-ua-mobile": "?0",
            "sec-ch-ua-platform": "\"Windows\"",
            "sec-fetch-dest": "empty",
            "sec-fetch-mode": "cors",
            "sec-fetch-site": "same-origin",
        },
        "referrer": "https://www.pixiv.net/dashboard",
        "referrerPolicy": "strict-origin-when-cross-origin",
        "body": null,
        "method": "GET",
        "mode": "cors",
        "credentials": "include"
    };

    let settings_key = "cro_pixiv_bma_settings";
    let settings = {
        order_asc: false,
    };
    Object.assign(settings, JSON.parse(GM_getValue(settings_key, "{}")));

    let data_key = "cro_pixiv_bma";
    let data = JSON.parse(GM_getValue(data_key, "{}"));
    let table = document.createElement("table");
    table.id = data_key;
    table.classList.add('scrolltable');

    let order_button = document.createElement("button");

    let order_button_set_text = () => void(order_button.innerText = `ORDER: ${settings.order_asc ? "ASC" : "DESC"}`);
    order_button_set_text();

    let get_latest_data = function() {
        return fetch("https://www.pixiv.net/ajax/dashboard/works/illust/request_strategy?lang=en", fetch_headers);
    };

    let initial_record = function(work) {
        return {
            title: work.illust.title,
            thumbnail: work.illust.url,
            last_milestone: 0,
            milestones: [],
        };
    };

    let update_record = function(record, bookmarks) {
        let last_milestone = Math.floor(bookmarks / milestone_interval) * milestone_interval;
        if (last_milestone <= record.last_milestone) {
            return;
        }
        record.last_milestone = last_milestone;
        record.milestones.push([last_milestone, Date.now()]);
    };

    let update_data = function(pixiv_data) {
        pixiv_data.body.data.works.forEach(function(work, index)
        {
            work.illust = pixiv_data.body.thumbnails.illust[index];

            if (!(work.workId in data))
            {
                data[work.workId] = initial_record(work);
            }

            update_record(data[work.workId], work.bookmarkCount);
        });

        GM_setValue(data_key, JSON.stringify(data));
    };

    let update_table = function() {
        let scrollTop = table.scrollTop;
        let scrollLeft = table.scrollLeft;
        table.innerHTML = "";
        let ids = Object.keys(data);
        ids = ids.sort((a, b) => b - a);
        for (let id of ids)
        {
            let record = data[id];
            let row = table.insertRow();
            let cell = row.insertCell();
            let link = document.createElement('a');
            link.href = `/artworks/${id}`;
            let thumbnail = document.createElement('img');
            thumbnail.src = record.thumbnail;
            thumbnail.height = 50;
            link.append(thumbnail);
            cell.append(link);
            cell.style.minWidth = 50;

            cell = row.insertCell();
            link = document.createElement('a');
            link.href = `/artworks/${id}`;
            link.append(record.title);
            cell.append(link);

            let milestones = [...record.milestones];
            if (settings.order_asc == false) {
                milestones.reverse();
            }

            for (let [count, timestamp] of milestones)
            {
                row.insertCell().innerHTML = `${count} - ${new Date(timestamp).toLocaleString()}`;
            }
        }
        table.scrollTop = scrollTop;
        table.scrollLeft = scrollLeft;
    };

    let inject_table = function() {
        if (window.location.pathname != "/dashboard") {
            return;
        }
        let maybe_table = document.querySelector(`#${data_key}`);
        if (!maybe_table) {
            let dock = document.querySelector("body");
            dock.prepend(table);
            dock.prepend(order_button);
        }
    };

    order_button.onclick = function() {
        settings.order_asc = !settings.order_asc;
        GM_setValue(settings_key, JSON.stringify(settings));
        order_button_set_text();
        update_table();
    };

    let update = function(pixiv_data) {
        update_data(pixiv_data);
        update_table();
    };

    let process = function() {
        if (window.location.pathname != "/dashboard") {
            return;
        }
        get_latest_data()
            .then(x => x.json())
            .then(x => update(x));
    };

    process();
    let interval = setInterval(process, time_interval);
    setInterval(inject_table, 1000);
})();