您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
ニコニ広告のチケット選択画面に有効期限を表示します。
// ==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}`); });