您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
ニコニ広告支援者リストを取得・整形して表示
// ==UserScript== // @name ニコニ広告者集計ツール // @namespace https://greasyfork.org/users/prozent55 // @version 0.0.1 // @description ニコニ広告支援者リストを取得・整形して表示 // @match https://www.nicovideo.jp/watch* // @grant none // @run-at document-end // @license MIT // ==/UserScript== (function() { 'use strict'; // ------------------------------ // UI // ------------------------------ const panelHTML = ` <details id="ad-supporter-panel"> <summary>📊 広告支援者</summary> <div class="asp-body"> <div class="asp-left"> <div class="asp-row"> <label class="asp-label"><input type="checkbox" id="asp-honorific"> 敬称</label> <input type="text" id="asp-honorific-text" value="さん"> </div> <div class="asp-row"> <label class="asp-label">リストの作り方:</label> <select id="asp-list-type"> <option value="raw">01_未加工</option> <option value="merge">02_同名を統合</option> <option value="merge_rev">03_同名を統合(逆順)</option> <option value="reverse">04_逆順</option> </select> </div> <div class="asp-row"> <label class="asp-label">リストの表示:</label> <select id="asp-view-type"> <option value="default">01_デフォルト</option> <option value="count">02_名前×回数</option> </select> </div> <div class="asp-row"> <label class="asp-label">テキストの表示:</label> <select id="asp-text-type"> <option value="name">01_名前</option> <option value="name_count">02_名前×回数</option> <option value="wrap30">03_30文字折返し</option> <option value="wrap45">04_45文字・3列</option> <option value="wrap60">05_60文字・4列</option> </select> </div> <div class="asp-row asp-buttons"> <button id="asp-fetch">取得</button> <button id="asp-format">表示変更</button> </div> </div> <div class="asp-right"> <textarea id="asp-output" rows="8"></textarea> <div class="asp-row" style="margin-top:6px; justify-content: flex-end;"> <button id="asp-copy">コピー</button> </div> </div> </div> <div class="asp-footer"> <div id="asp-api-log">APIログ: 待機中</div> </div> </details> `; const style = document.createElement('style'); style.textContent = ` #ad-supporter-panel { position: fixed; right: 10px; bottom: 10px; width: 640px; max-height: 70vh; overflow: auto; z-index: 999999; font-family: sans-serif; border: 1px solid #ccc; background: #fafafa; box-shadow: 0 2px 8px rgba(0,0,0,0.2); } #ad-supporter-panel summary { cursor: pointer; background: #444; color: #fff; padding: 6px; font-weight: bold; list-style: none; } .asp-body { display: flex; padding: 10px; } .asp-left { flex: 1; margin-right: 10px; border-right: 1px solid #ccc; padding-right: 10px; font-size: 0.9em; } .asp-row { display: flex; align-items: center; margin: 4px 0; gap: 6px; } .asp-label { display: inline-block; width: 100px; } .asp-buttons { margin-top: 10px; } #ad-supporter-panel select, #ad-supporter-panel input[type="text"] { width: 140px; background: #fff; border: 1px solid #bbb; padding: 3px; } #asp-output { width: 100%; min-height: 140px; resize: vertical; font-family: monospace; background: #fff; border: 1px solid #bbb; } #ad-supporter-panel button { padding: 4px 8px; border: 1px solid #888; background: #eee; cursor: pointer; } #ad-supporter-panel button:hover { background: #ddd; } #ad-supporter-panel button:disabled { background: #ccc !important; color: #666 !important; cursor: not-allowed !important; } .asp-right { flex: 2; padding-left: 10px; } .asp-footer { margin: 5px 10px; border-top: 1px solid #ccc; padding-top: 4px; font-size: 0.9em; } `; document.head.appendChild(style); document.body.insertAdjacentHTML('beforeend', panelHTML); const logEl = document.getElementById("asp-api-log"); const outputEl = document.getElementById("asp-output"); let fetchedHistories = []; let fetching = false; const log = (msg) => { logEl.textContent = `APIログ: ${msg}`; }; function setButtonsDisabled(disabled) { document.getElementById("asp-fetch").disabled = disabled; document.getElementById("asp-format").disabled = disabled; } // ------------------------------ // API取得 // ------------------------------ async function fetchAllHistories(videoId) { let offset = 0; const limit = 100; let allHistories = []; while (true) { const url = `https://api.nicoad.nicovideo.jp/v1/contents/video/${videoId}/histories?offset=${offset}&limit=${limit}`; log(`取得中 offset=${offset}`); const res = await fetch(url); const json = await res.json(); if (json.meta.status === 200) { const histories = json.data.histories; if (!histories || histories.length === 0) break; allHistories = allHistories.concat(histories); log(`${histories.length}件取得 (合計 ${allHistories.length})`); offset += limit; } else if (json.meta.status === 400) { log("status=400 → 取得終了"); break; } else { log(`status=${json.meta.status} → 終了`); break; } } return allHistories; } // ------------------------------ // 幅計算(全角=2, 半角=1) // ------------------------------ function strWidth(str) { let width = 0; for (let i = 0; i < str.length; i++) { const code = str.charCodeAt(i); if ((code >= 0x20 && code <= 0x7E) || (code >= 0xFF61 && code <= 0xFF9F)) { width += 1; // 半角 } else { width += 2; // 全角 } } return width; } function padWidth(str, targetWidth) { const w = strWidth(str); if (w >= targetWidth) return str; return str + " ".repeat(targetWidth - w); } // ------------------------------ // 列整形 // ------------------------------ function formatColumnsAdaptive(names, baseCols = 3, colWidth = 30) { const result = []; let row = []; let usedCols = 0; const flush = () => { if (row.length) { result.push(row.join("")); row = []; usedCols = 0; } }; for (const name of names) { const w = strWidth(name); let cellCols, cellWidth; if (w > colWidth * 2) { flush(); result.push(name); // 全角31文字以上 → 単独行 continue; } else if (w > colWidth) { cellCols = 2; // 全角16〜30文字 → 2セル cellWidth = colWidth * 2; } else { cellCols = 1; // 全角15文字以内 → 1セル cellWidth = colWidth; } if (usedCols + cellCols > baseCols) flush(); row.push(padWidth(name, cellWidth)); usedCols += cellCols; if (usedCols >= baseCols) flush(); } flush(); return result.join("\n"); } // ------------------------------ // 整形ロジック // ------------------------------ function formatHistories(histories) { const honorific = document.getElementById("asp-honorific").checked ? document.getElementById("asp-honorific-text").value : ""; const listType = document.getElementById("asp-list-type").value; const viewType = document.getElementById("asp-view-type").value; const textType = document.getElementById("asp-text-type").value; let list = [...histories]; if (listType === "reverse") { list = list.slice().reverse(); } if (listType === "merge" || listType === "merge_rev") { const map = {}; list.forEach(h => { const key = h.advertiserName || "不明"; if (!map[key]) map[key] = { count: 0, adPoint: 0 }; map[key].count++; map[key].adPoint += h.adPoint || 0; }); list = Object.entries(map).map(([name, v]) => ({ advertiserName: name, count: v.count, adPoint: v.adPoint })); if (listType === "merge_rev") list = list.reverse(); } let lines = list.map(h => { const name = (h.advertiserName || "不明") + honorific; if (viewType === "count" || textType === "name_count") { const cnt = h.count || 1; return `${name}×${cnt}`; } else { return name; } }); if (textType === "wrap30") { lines = lines.join(" ").match(/.{1,30}/g) || []; return lines.join("\n"); } else if (textType === "wrap45") { return formatColumnsAdaptive(lines, 3, 30); } else if (textType === "wrap60") { return formatColumnsAdaptive(lines, 4, 30); } return lines.join("\n"); } // ------------------------------ // イベント // ------------------------------ document.getElementById("asp-fetch").addEventListener("click", async () => { if (fetching) { log("既に取得中です…"); return; } fetching = true; setButtonsDisabled(true); try { const videoIdMatch = location.pathname.match(/\/watch\/((?:sm|nm)\d+)/); if (!videoIdMatch) { log("動画IDが取得できません"); return; } const videoId = videoIdMatch[1]; log(`動画ID: ${videoId}`); fetchedHistories = await fetchAllHistories(videoId); outputEl.value = formatHistories(fetchedHistories); log(`完了: ${fetchedHistories.length}件`); } finally { fetching = false; setButtonsDisabled(false); } }); document.getElementById("asp-format").addEventListener("click", () => { if (fetchedHistories.length === 0) { log("データ未取得"); return; } outputEl.value = formatHistories(fetchedHistories); log("表示形式を適用"); }); document.getElementById("asp-copy").addEventListener("click", () => { navigator.clipboard.writeText(outputEl.value); log("コピー完了"); }); })();