NicoAd Ticket Info

ニコニ広告のチケット選択画面に有効期限を表示します。

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください。
// ==UserScript==
// @name         NicoAd Ticket Info
// @version      1.0.2
// @description  ニコニ広告のチケット選択画面に有効期限を表示します。
// @author       蝙蝠の目
// @match        https://nicoad.nicovideo.jp/*/publish/*
// @grant        GM.xmlHttpRequest
// @grant        GM_xmlhttpRequest
// @connect      api.koken.nicovideo.jp
// @namespace https://greasyfork.org/ja/users/808813
// ==/UserScript==

(async () => {
    "use strict";

    function fetchText(url, { method } = {}) {
        method = method || "GET";

        const requestApi = GM_xmlhttpRequest || (GM && GM.xmlHttpRequest);
        if (!requestApi) {
            throw new Error("ユーザースクリプトAPI(GM_xmlhttpRequest または GM.xmlHttpRequest)が見つかりません。");
        }

        return new Promise((resolve, reject) => {
            try {
                requestApi({
                    method,
                    url,
                    onload: (res) => resolve(res.responseText),
                    onerror: reject
                });
            } catch (e) {
                reject(e);
            }
        });
    }

    function processTicketList(element) {
        for (const child of element.children) {
            processTicketListItem(child);
        }
    }

    function processTicketListItem(element) {
        const infoElement = element.querySelector(".info");
        if (!infoElement) return;

        const nameElement = infoElement.querySelector(".ticket-name");
        if (!nameElement) return;
        const ticketName = nameElement.textContent;

        const ticketData = ticketNameMap[ticketName];
        if (!ticketData) return;

        let isFirstElement = true;
        for (const group of ticketData) {
            const elm = document.createElement("span");
            if (isFirstElement) {
                elm.style.marginTop = "0.5rem";
                isFirstElement = false;
            }
            elm.textContent = `${group.text} まで ×`;
            elm.style.display = "flex";
            elm.style.color = group.expiringSoon ? "red" : "black";
            elm.style.fontSize = "1.2rem";

            const elm2 = document.createElement("strong");
            elm2.textContent = group.size;
            elm2.style.paddingLeft = "0.1rem";
            elm.append(elm2);

            infoElement.append(elm);
        }
    }

    function groupBy(list, fn) {
        const res = {};
        for (const item of list) {
            const key = fn(item);
            if (!res.hasOwnProperty(key)) res[key] = [];
            res[key].push(item);
        }
        return res;
    }

    function getDateId(date) {
        return new Date(date.getFullYear(), date.getMonth(), date.getDate()).getTime();
    }

    function DateIdToString(dateId) {
        const date = new Date(dateId);
        let year = date.getFullYear().toString();
        let month = (date.getMonth() + 1).toString();
        let day = date.getDate().toString();
        while (year.length < 4) year = "0" + year;
        while (month.length < 2) month = "0" + month;
        while (day.length < 2) day = "0" + day;
        return year + "." + month + "." + day;
    }

    function getTicketNameMap(tickets, serverTime) {
        const serverDateId = getDateId(new Date(serverTime * 1000));
        const res = {};

        const groupedByName = groupBy(tickets, ticket => ticket.ticketName);
        for (const ticketName in groupedByName) {
            const groupedByDateId = groupBy(
                groupedByName[ticketName],
                (ticket) => getDateId(new Date(ticket.expiredAt * 1000))
            );

            res[ticketName] = [];
            for (const dateIdStr in groupedByDateId) {
                const dateId = Number(dateIdStr);
                res[ticketName].push({
                    dateId,
                    text: DateIdToString(dateId),
                    size: groupedByDateId[dateId].length,
                    expiringSoon: dateId - serverDateId < 1000 * 60 * 60 * 24 * 7,
                });
            }
            res[ticketName].sort((a, b) => a.dateId - b.dateId);
        }

        return res;
    }

    const data = JSON.parse(await fetchText("https://api.koken.nicovideo.jp/v1/tickets")).data;
    const ticketNameMap = getTicketNameMap(data.tickets, data.serverTime);

    const processedTicketLists = new WeakSet();
    window.setInterval(() => {
        for (const element of document.querySelectorAll("ul.wrapper")) {
            if (processedTicketLists.has(element)) continue;
            processedTicketLists.add(element);
            processTicketList(element);
        }
    }, 500);

})().catch(e => {
    console.error(`[NicoAd Ticket Info] ${e instanceof Error ? e.message : e}`);
});