VNDB Character Count

Fetch and display character count from jiten.moe API on VNDB pages

// ==UserScript==
// @name         VNDB Character Count
// @namespace    https://vndb.org/
// @version      1.0
// @description  Fetch and display character count from jiten.moe API on VNDB pages
// @author       Sirus
// @match        https://vndb.org/v*
// @grant        GM_xmlhttpRequest
// @connect      api.jiten.moe
// @license      MIT 
// ==/UserScript==

(function() {
    'use strict';

    // Extract VN ID
    const pathMatch = window.location.pathname.match(/\/(v\d+)/);
    if (!pathMatch) return;
    const vnId = pathMatch[1];

    // Helper to fetch JSON with GM_xmlhttpRequest (CORS-safe)
    function fetchJson(url) {
        return new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                method: "GET",
                url: url,
                onload: res => {
                    try {
                        resolve(JSON.parse(res.responseText));
                    } catch (e) {
                        reject(e);
                    }
                },
                onerror: reject
            });
        });
    }

    // Format numbers with commas
    function formatNumber(num) {
        return num.toLocaleString();
    }

    async function main() {
        try {
            // Get IDs linked to this VN
            const idsResp = await fetchJson(`https://api.jiten.moe/api/media-deck/by-link-id/2/${vnId}`);
            if (!Array.isArray(idsResp) || idsResp.length === 0) return;

            let characterCounts = [];
            let difficulties = [];

            // Fetch each detail
            for (const id of idsResp) {
                const detailResp = await fetchJson(`https://api.jiten.moe/api/media-deck/${id}/detail`);
                if (detailResp?.data?.mainDeck) {
                    const cc = detailResp.data.mainDeck.characterCount;
                    const diff = detailResp.data.mainDeck.difficulty;
                    characterCounts.push(cc);
                    difficulties.push(diff);
                }
            }

            if (characterCounts.length === 0) return;

            // Prepare strings
            const charCountStr = characterCounts.map(formatNumber).join(" + ");
            const difficultyStr = difficulties.map(d => `${d}/6`).join(", ");

            // Locate the Play time row
            const playTimeRow = [...document.querySelectorAll("tr")]
            .find(tr => tr.querySelector("td")?.innerText.trim() === "Play time");
            if (!playTimeRow) return;

            const table = playTimeRow.parentElement;

            // Insert Character count row
            const ccRow = document.createElement("tr");
            ccRow.innerHTML = `<td><a href="https://jiten.moe/decks/media/${idsResp[0]}/detail" target="_blank">Char. count</a></td><td>${charCountStr}</td>`;
            table.insertBefore(ccRow, playTimeRow.nextSibling);

        } catch (err) {
            console.error("VNDB userscript error:", err);
        }
    }

    main();
})();