ニコニ広告のチケット選択画面に有効期限を表示します。
// ==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}`);
});