futaba_CatalogTagging

ふたばのカタログをてきとうにタグ分けします

目前為 2018-09-23 提交的版本,檢視 最新版本

// ==UserScript==
// @name        futaba_CatalogTagging
// @description ふたばのカタログをてきとうにタグ分けします
// @namespace   http://pussy.CatalogTagging/
// @include     http://*.2chan.net/*/futaba.php?mode=cat*
// @include     https://*.2chan.net/*/futaba.php?mode=cat*
// @version     4.2
// @grant       none
// ==/UserScript==

(function() {

'use strict';
var doc = document;

// ---------------------------------------------------------------------------
// 設定
// ---------------------------------------------------------------------------
var TAGS, CATALOGTAG_CSS, CATALOGTAG_TEXT_CSS, USE_CACHE;
var setup = () => {
	// タグの設定
	TAGS = [
		{ name: 'お題', imgChecker: odaiChecker },
		{ name: 'お外', expr: /http/ },
		{ name: '実況', expr: /そろそろ|午後ロー|鉄腕|DASH/ },
		{ name: 'マケドニア', imgChecker: macedoniaChecker },
		{ name: '引用', expr: /^>/ }
	];
	// タグのスタイル
	CATALOGTAG_CSS = `
		.catalogtag {
			background: #ea8;
			color: #ffe;
			font-size: 12px;
		}
	`;
	// 本文のスタイル(カタログに本文を出したくない人は「display: none;」とか入れればいいよ)
	CATALOGTAG_TEXT_CSS = `
		.catalogtag-text {
		}
	`;
	// タグ分け結果をキャッシュするか(画像解析を微調整するときはfalseにしておく)
	USE_CACHE = true;
};
// ---------------------------------------------------------------------------

// ---------------------------------------------------------------------------
// 画像解析
var canvas = doc.createElement('CANVAS');
canvas.width = 50;
canvas.height = 50;
var ctx = canvas.getContext('2d');
ctx.imageSmoothingEnabled = false;

/**
 * @param x 0から49
 * @param y 0から49
 * @return サムネの色を配列[R,G,B]で返します
 */
var getRGB = (x, y) => {
	return ctx.getImageData(x, y, 1, 1).data;
};

/** @return 色がだいたい同じならtrueを返します */
var isLike = (c1, c2) => {
	for (var i = 0; i < 3; i ++) {
		if (Math.abs(c1[i] - c2[i]) > 32) return false;
	}
	return true;
};

var macedoniaChecker = () => {
	var c1 = getRGB(0, 0);
	if (c1[0] < 128 || c1[2] > 128) return false;
	var c2 = getRGB(15, 0);
	if (c2[0] < 128 || c2[2] > 128 || isLike(c1, c2)) return false;
	return isLike(c2, getRGB(35, 0)) && isLike(c1, getRGB(49, 0));
};

var odaiChecker = () => {
	if (!isLike([255, 255, 255], getRGB(0, 0))) return false;
	if (!isLike([255, 255, 255], getRGB(49,0))) return false;
	for (var 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();
// タグ設定を整頓する
var NO_TAGGED = { name: '', expr: null };
TAGS.unshift(NO_TAGGED);
var TAGS_BY_NAME = {};
TAGS.forEach(tag => { TAGS_BY_NAME[tag.name] = tag; });
// キャッシュを読み込む
var cacheOnStrage = sessionStorage.getItem('catalogtagging_cache');
var cache = cacheOnStrage && JSON.parse(cacheOnStrage) || {};

/** @return 本文と画像をつかって適当にタグを返します */
var findTag = (text, img) => {
	if (text) {
		for (var i = 1; i < TAGS.length; i ++) {
			if (TAGS[i].expr && TAGS[i].expr.test(text)) return TAGS[i];
		}
	}
	if (!img) return NO_TAGGED;
	ctx.drawImage(img, 0, 0, 50, 50);
	for (var j = 1; j < TAGS.length; j ++) {
		if (TAGS[j].imgChecker && TAGS[j].imgChecker()) return TAGS[j];
	}
	return NO_TAGGED;
};

/* カタログの<TABLE> */
var catalog;

/* タグ分け本体 */
var tagging = (retryCount = 0) => {
	doc.body.setAttribute('__catalogtagging_status', 'start');
	// カタログ情報を取得
	catalog = doc.querySelector('TABLE[border="1"][align="center"]');
	var maxCol = catalog.getElementsByTagName('TR')[0].getElementsByTagName('TD').length;
	var tdElements = catalog.getElementsByTagName('TD');
	var tdCount = tdElements.length;
	if (!tdCount || !(tdElements[0].getElementsByTagName('SMALL').length)) return false; // 本文表示無し
	var tds = [];
	for (var i = 0; i < tdCount; i ++) {
		tds[i] = tdElements[i];
	}
	// 初期化
	TAGS.forEach(tag => {
		tag.f = doc.createDocumentFragment();
		tag.currentTr = null;
		tag.colCount = 0;
	});
	var cacheKeys = Object.keys(cache);
	for (var j = cacheKeys.length - Math.floor(tdCount * 1.5); 0 <= j; j --) {
		delete cache[cacheKeys[j]];
	}
	var retry = false; // 画像が読み込み中だったら後でもう1回実行する
	// 並び替え
	tds.forEach(td => {
		if (td.classList.contains('catalogtag')) return;
		var small = td.getElementsByTagName('SMALL')[0];
		small.classList.add('catalogtag-text');
		var a = td.getElementsByTagName('A')[0];
		if (!a || !a.href) return;
		var tag = USE_CACHE && TAGS_BY_NAME[cache[a.href]];
		if (!tag) {
			var 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.colCount && tag.name) {
			var tagLabelTd = tag.f.appendChild(doc.createElement('TR')).appendChild(doc.createElement('TD'));
			tagLabelTd.textContent = tag.name;
			tagLabelTd.className = 'catalogtag';
			tagLabelTd.setAttribute('colspan', maxCol);
		}
		if (tag.colCount % maxCol === 0) {
			tag.currentTr = tag.f.appendChild(doc.createElement('TR'));
		}
		tag.currentTr.appendChild(td);
		tag.colCount ++;
	});
	// カタログの要素を置き換えて完了
	var tbody = doc.createElement('TBODY');
	TAGS.forEach(t => { tbody.appendChild(t.f); });
	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);
	}
};
// 念のためイベント呼び出し回数をカウントして無限ループを抑制しておく
var eventCount = 0;
var resetEventCount = () => { eventCount = 0; };
// START!
var onLoad = e => {
	doc.styleSheets.item(0).insertRule(CATALOGTAG_CSS, 0);
	doc.styleSheets.item(0).insertRule(CATALOGTAG_TEXT_CSS, 0);
	tagging();
	// MutationRecordをeventCheckerでチェックしてタグ分けしたりしなかったりする関数
	var onEvent = (m, eventChecker) => {
		if (eventCount > 10) {
			console.log('他の拡張と競合してるっぽい');
			return;
		}
		for (var i = m.length - 1; 0 <= i; i --) {
			if (!eventChecker(m[i])) continue;
			eventCount ++;
			setTimeout(resetEventCount, 500);
			tagging();
			return;
		}
	};
	// TABLEタグが再追加されたらタグ分けするオブザーバー
	var defaultObserver = new MutationObserver(m => {
		onEvent(m, n => {
			for (var i = n.addedNodes.length - 1; 0 <= i; i --) {
				var 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 });
	// ねないこのソートが終わったらタグ分けするオブザーバー
	var 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);
}
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址