CatalogTagging

カタログをてきとうにタグ分けします

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==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);
}
})();