您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
カタログをてきとうにタグ分けします
// ==UserScript== // @name CatalogTagging // @description カタログをてきとうにタグ分けします // @namespace http://pussy.CatalogTagging/ // @include *://*.2chan.net/*/futaba.php?mode=cat* // @version 5.2 // @grant none // ==/UserScript== (function() { 'use strict'; let doc = document; // --------------------------------------------------------------------------- // 設定 // --------------------------------------------------------------------------- let TAGS, CATALOGTAG_CSS, CATALOGTAG_TEXT_CSS, USE_CACHE; let setup = () => { // タグの設定 TAGS = [ { name: '未分類', default: true }, { name: 'お外', expr: /http/ }, { name: 'お題', imgChecker: odaiChecker }, { name: 'Abema', expr: /https:\/\/ab/ }, { name: '実況', expr: /そろそろ|午後ロー|鉄腕|DASH/ }, { name: 'マケドニア', imgChecker: macedoniaChecker }, { name: '引用', expr: /^>/ } ]; // タグのスタイル CATALOGTAG_CSS = ` .catalogtag { background: #ea8; font-size: 12px; max-width: 4em; overflow:hidden; padding: 0; text-align: center; } `; // 本文のスタイル(カタログに本文を出したくない人は「display: none;」とか入れればいいよ) CATALOGTAG_TEXT_CSS = ` .catalogtag-text { } `; // タグ分け結果をキャッシュするか(画像解析を微調整するときはfalseにしておく) USE_CACHE = true; }; // --------------------------------------------------------------------------- // --------------------------------------------------------------------------- // 画像解析 let canvas = doc.createElement('CANVAS'); canvas.width = 50; canvas.height = 50; let ctx = canvas.getContext('2d'); ctx.imageSmoothingEnabled = false; /** * @param x 0から49 * @param y 0から49 * @return サムネの色を配列[R,G,B]で返します */ let getRGB = (x, y) => { return ctx.getImageData(x, y, 1, 1).data; }; /** @return 色がだいたい同じならtrueを返します */ let isLike = (c1, c2) => { for (let i = 0; i < 3; i ++) { if (Math.abs(c1[i] - c2[i]) > 32) return false; } return true; }; let macedoniaChecker = () => { let checkOK = 0; let r = [223, 32, 32]; let y = [223, 223, 32]; if (isLike(y, getRGB( 0, 1))) [r, y] = [y, r]; if (isLike(r, getRGB( 0, 0))) checkOK++; if (isLike(y, getRGB(15, 0))) checkOK++; if (checkOK && isLike(getRGB(0, 0), getRGB(15, 0))) return false; if (isLike(r, getRGB(25, 0)) && ++checkOK >= 3) return true; if (isLike(y, getRGB(35, 15)) && ++checkOK >= 3) return true; if (isLike(r, getRGB(49, 0)) && ++checkOK >= 3) return true; if (isLike(y, getRGB( 0, 15)) && ++checkOK >= 3) return true; if (isLike(r, getRGB( 0, 25)) && ++checkOK >= 3) return true; if (isLike(y, getRGB( 0, 35)) && ++checkOK >= 3) return true; if (isLike(r, getRGB( 0, 49)) && ++checkOK >= 3) return true; if (isLike(y, getRGB(49, 15)) && ++checkOK >= 3) return true; if (isLike(r, getRGB(49, 25)) && ++checkOK >= 3) return true; if (isLike(y, getRGB(49, 35)) && ++checkOK >= 3) return true; if (isLike(r, getRGB(49, 49)) && ++checkOK >= 3) return true; return false; }; let odaiChecker = () => { if (!isLike([255, 255, 255], getRGB(0, 0))) return false; if (!isLike([255, 255, 255], getRGB(49,0))) return false; for (let y = 5; y <=8; y++) { if (isLike([0, 0, 0], getRGB(4, y)) && isLike([0, 0, 0], getRGB(45, y))) return true; } return false; }; // --------------------------------------------------------------------------- // ここから本体 setup(); // タグ設定を整頓する let NO_TAGGED; let TAGS_BY_NAME = {}; TAGS.forEach(tag => { TAGS_BY_NAME[tag.name] = tag; if (tag.default) NO_TAGGED = tag; }); if (!NO_TAGGED) { NO_TAGGED = { name: '未分類', default: true }; TAGS.unshift(NO_TAGGED); TAGS_BY_NAME[NO_TAGGED.name] = NO_TAGGED; } // キャッシュを読み込む let cacheOnStrage = sessionStorage.getItem('catalogtagging_cache'); let cache = cacheOnStrage && JSON.parse(cacheOnStrage) || {}; /** @return 本文と画像をつかって適当にタグを返します */ let findTag = (text, img) => { let needDraw = true; for (let tag of TAGS) { if (text && tag.expr && tag.expr.test(text)) return tag; if (!img) continue; if (needDraw) { ctx.drawImage(img, 0, 0, 50, 50); needDraw = false; } if (tag.imgChecker && tag.imgChecker()) return tag; } return NO_TAGGED; }; /* カタログの<TABLE> */ let catalog; /* タグ分け本体 */ let tagging = (retryCount = 0) => { doc.body.setAttribute('__catalogtagging_status', 'start'); // カタログ情報を取得 catalog = doc.querySelector('TABLE[border="1"][align="center"]'); let maxCol = catalog.getElementsByTagName('TR')[0].getElementsByTagName('TD').length; let tdElements = catalog.getElementsByTagName('TD'); let tdCount = tdElements.length; if (!tdCount || !(tdElements[0].getElementsByTagName('SMALL').length)) return false; // 本文表示無し let tds = []; for (let i = 0; i < tdCount; i ++) { tds[i] = tdElements[i]; } // 初期化 TAGS.forEach(tag => { tag.tds = []; tag.count = 0; }); let cacheKeys = Object.keys(cache); for (let j = cacheKeys.length - Math.floor(tdCount * 1.5); 0 <= j; j --) { delete cache[cacheKeys[j]]; } let retry = false; // 画像が読み込み中だったら後でもう1回実行する // 並び替え tds.forEach(td => { if (td.classList.contains('catalogtag')) return; let small = td.getElementsByTagName('SMALL')[0]; small.classList.add('catalogtag-text'); let a = td.getElementsByTagName('A')[0]; if (!a || !a.href) return; let tag = USE_CACHE && TAGS_BY_NAME[cache[a.href]]; if (!tag) { let img = td.getElementsByTagName('IMG')[0]; if (!img || img.complete) { tag = findTag(small.textContent, img); cache[a.herf] = tag.name; } else { tag = findTag(small.textContent, null); retry = true; } } if (!tag.count && tag.name) { let tagLabelTd = doc.createElement('TD'); tagLabelTd.textContent = tag.name; tagLabelTd.className = 'catalogtag'; tag.tds = []; tag.tds.push(tagLabelTd); tag.count ++; } tag.tds.push(td); tag.count ++; }); // カタログの要素を置き換えて完了 let tbody = doc.createElement('TBODY'); let count = 0; let tr = null; TAGS.forEach(tag => { if (count === 0 && tag == NO_TAGGED) { tag.tds.shift(); } for (let td of tag.tds) { if (count % maxCol === 0) { tr = tbody.appendChild(doc.createElement('TR')); } tr.appendChild(td); count ++; } }); catalog.replaceChild(tbody, catalog.firstChild); sessionStorage.setItem('catalogtagging_cache', JSON.stringify(cache)); doc.body.setAttribute('__catalogtagging_status', 'done'); if (retry && retryCount < 10) { // やり直しは10回まで setTimeout(() => { tagging(retryCount + 1); }, 100); } }; // 念のためイベント呼び出し回数をカウントして無限ループを抑制しておく let eventCount = 0; let resetEventCount = () => { eventCount = 0; }; // START! let onLoad = e => { doc.styleSheets.item(0).insertRule(CATALOGTAG_CSS, 0); doc.styleSheets.item(0).insertRule(CATALOGTAG_TEXT_CSS, 0); tagging(); // MutationRecordをeventCheckerでチェックしてタグ分けしたりしなかったりする関数 let onEvent = (m, eventChecker) => { if (eventCount > 10) { console.log('他の拡張と競合してるっぽい'); return; } for (let i = m.length - 1; 0 <= i; i --) { if (!eventChecker(m[i])) continue; eventCount ++; setTimeout(resetEventCount, 500); tagging(); return; } }; // TABLEタグが再追加されたらタグ分けするオブザーバー let defaultObserver = new MutationObserver(m => { onEvent(m, n => { for (let i = n.addedNodes.length - 1; 0 <= i; i --) { let node = n.addedNodes[i]; if (node.tagName === 'TABLE') return true; // 赤福 if (node.id === 'catalog_loading') return true; // ふたクロ } return false; }); }); defaultObserver.observe(catalog.parentNode, { childList: true }); // ねないこのソートが終わったらタグ分けするオブザーバー let nenaikoObserver = new MutationObserver(m => { onEvent(m, n => { if (n.attributeName !== '__nenaiko_catsort_status') return false; if (doc.body.getAttribute('__nenaiko_catsort_status') === 'start') return false; // ねないこのソートが有効になってるならデフォルトのオブザーバーは要らないので切断する if (doc.body.getAttribute('__nenaiko_catsort_status') === 'done') defaultObserver.disconnect(); return true; }); }); nenaikoObserver.observe(doc.body, { attributes: true }); }; if (doc.readyState === 'complete') { onLoad(); } else { addEventListener('load', onLoad); } })();