CatalogTagging

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

  1. // ==UserScript==
  2. // @name CatalogTagging
  3. // @description カタログをてきとうにタグ分けします
  4. // @namespace http://pussy.CatalogTagging/
  5. // @include *://*.2chan.net/*/futaba.php?mode=cat*
  6. // @version 5.2
  7. // @grant none
  8. // ==/UserScript==
  9.  
  10. (function() {
  11.  
  12. 'use strict';
  13. let doc = document;
  14.  
  15. // ---------------------------------------------------------------------------
  16. // 設定
  17. // ---------------------------------------------------------------------------
  18. let TAGS, CATALOGTAG_CSS, CATALOGTAG_TEXT_CSS, USE_CACHE;
  19. let setup = () => {
  20. // タグの設定
  21. TAGS = [
  22. { name: '未分類', default: true },
  23. { name: 'お外', expr: /http/ },
  24. { name: 'お題', imgChecker: odaiChecker },
  25. { name: 'Abema', expr: /https:\/\/ab/ },
  26. { name: '実況', expr: /そろそろ|午後ロー|鉄腕|DASH/ },
  27. { name: 'マケドニア', imgChecker: macedoniaChecker },
  28. { name: '引用', expr: /^>/ }
  29. ];
  30. // タグのスタイル
  31. CATALOGTAG_CSS = `
  32. .catalogtag {
  33. background: #ea8;
  34. font-size: 12px;
  35. max-width: 4em;
  36. overflow:hidden;
  37. padding: 0;
  38. text-align: center;
  39. }
  40. `;
  41. // 本文のスタイル(カタログに本文を出したくない人は「display: none;」とか入れればいいよ)
  42. CATALOGTAG_TEXT_CSS = `
  43. .catalogtag-text {
  44. }
  45. `;
  46. // タグ分け結果をキャッシュするか(画像解析を微調整するときはfalseにしておく)
  47. USE_CACHE = true;
  48. };
  49. // ---------------------------------------------------------------------------
  50.  
  51. // ---------------------------------------------------------------------------
  52. // 画像解析
  53. let canvas = doc.createElement('CANVAS');
  54. canvas.width = 50;
  55. canvas.height = 50;
  56. let ctx = canvas.getContext('2d');
  57. ctx.imageSmoothingEnabled = false;
  58.  
  59. /**
  60. * @param x 0から49
  61. * @param y 0から49
  62. * @return サムネの色を配列[R,G,B]で返します
  63. */
  64. let getRGB = (x, y) => {
  65. return ctx.getImageData(x, y, 1, 1).data;
  66. };
  67.  
  68. /** @return 色がだいたい同じならtrueを返します */
  69. let isLike = (c1, c2) => {
  70. for (let i = 0; i < 3; i ++) {
  71. if (Math.abs(c1[i] - c2[i]) > 32) return false;
  72. }
  73. return true;
  74. };
  75.  
  76. let macedoniaChecker = () => {
  77. let checkOK = 0;
  78. let r = [223, 32, 32];
  79. let y = [223, 223, 32];
  80. if (isLike(y, getRGB( 0, 1))) [r, y] = [y, r];
  81. if (isLike(r, getRGB( 0, 0))) checkOK++;
  82. if (isLike(y, getRGB(15, 0))) checkOK++;
  83. if (checkOK && isLike(getRGB(0, 0), getRGB(15, 0))) return false;
  84. if (isLike(r, getRGB(25, 0)) && ++checkOK >= 3) return true;
  85. if (isLike(y, getRGB(35, 15)) && ++checkOK >= 3) return true;
  86. if (isLike(r, getRGB(49, 0)) && ++checkOK >= 3) return true;
  87. if (isLike(y, getRGB( 0, 15)) && ++checkOK >= 3) return true;
  88. if (isLike(r, getRGB( 0, 25)) && ++checkOK >= 3) return true;
  89. if (isLike(y, getRGB( 0, 35)) && ++checkOK >= 3) return true;
  90. if (isLike(r, getRGB( 0, 49)) && ++checkOK >= 3) return true;
  91. if (isLike(y, getRGB(49, 15)) && ++checkOK >= 3) return true;
  92. if (isLike(r, getRGB(49, 25)) && ++checkOK >= 3) return true;
  93. if (isLike(y, getRGB(49, 35)) && ++checkOK >= 3) return true;
  94. if (isLike(r, getRGB(49, 49)) && ++checkOK >= 3) return true;
  95. return false;
  96. };
  97.  
  98. let odaiChecker = () => {
  99. if (!isLike([255, 255, 255], getRGB(0, 0))) return false;
  100. if (!isLike([255, 255, 255], getRGB(49,0))) return false;
  101. for (let y = 5; y <=8; y++) {
  102. if (isLike([0, 0, 0], getRGB(4, y)) && isLike([0, 0, 0], getRGB(45, y))) return true;
  103. }
  104. return false;
  105. };
  106.  
  107. // ---------------------------------------------------------------------------
  108. // ここから本体
  109. setup();
  110. // タグ設定を整頓する
  111. let NO_TAGGED;
  112. let TAGS_BY_NAME = {};
  113. TAGS.forEach(tag => {
  114. TAGS_BY_NAME[tag.name] = tag;
  115. if (tag.default) NO_TAGGED = tag;
  116. });
  117. if (!NO_TAGGED) {
  118. NO_TAGGED = { name: '未分類', default: true };
  119. TAGS.unshift(NO_TAGGED);
  120. TAGS_BY_NAME[NO_TAGGED.name] = NO_TAGGED;
  121. }
  122. // キャッシュを読み込む
  123. let cacheOnStrage = sessionStorage.getItem('catalogtagging_cache');
  124. let cache = cacheOnStrage && JSON.parse(cacheOnStrage) || {};
  125.  
  126. /** @return 本文と画像をつかって適当にタグを返します */
  127. let findTag = (text, img) => {
  128. let needDraw = true;
  129. for (let tag of TAGS) {
  130. if (text && tag.expr && tag.expr.test(text)) return tag;
  131. if (!img) continue;
  132. if (needDraw) {
  133. ctx.drawImage(img, 0, 0, 50, 50);
  134. needDraw = false;
  135. }
  136. if (tag.imgChecker && tag.imgChecker()) return tag;
  137. }
  138. return NO_TAGGED;
  139. };
  140.  
  141. /* カタログの<TABLE> */
  142. let catalog;
  143.  
  144. /* タグ分け本体 */
  145. let tagging = (retryCount = 0) => {
  146. doc.body.setAttribute('__catalogtagging_status', 'start');
  147. // カタログ情報を取得
  148. catalog = doc.querySelector('TABLE[border="1"][align="center"]');
  149. let maxCol = catalog.getElementsByTagName('TR')[0].getElementsByTagName('TD').length;
  150. let tdElements = catalog.getElementsByTagName('TD');
  151. let tdCount = tdElements.length;
  152. if (!tdCount || !(tdElements[0].getElementsByTagName('SMALL').length)) return false; // 本文表示無し
  153. let tds = [];
  154. for (let i = 0; i < tdCount; i ++) {
  155. tds[i] = tdElements[i];
  156. }
  157. // 初期化
  158. TAGS.forEach(tag => {
  159. tag.tds = [];
  160. tag.count = 0;
  161. });
  162. let cacheKeys = Object.keys(cache);
  163. for (let j = cacheKeys.length - Math.floor(tdCount * 1.5); 0 <= j; j --) {
  164. delete cache[cacheKeys[j]];
  165. }
  166. let retry = false; // 画像が読み込み中だったら後でもう1回実行する
  167. // 並び替え
  168. tds.forEach(td => {
  169. if (td.classList.contains('catalogtag')) return;
  170. let small = td.getElementsByTagName('SMALL')[0];
  171. small.classList.add('catalogtag-text');
  172. let a = td.getElementsByTagName('A')[0];
  173. if (!a || !a.href) return;
  174. let tag = USE_CACHE && TAGS_BY_NAME[cache[a.href]];
  175. if (!tag) {
  176. let img = td.getElementsByTagName('IMG')[0];
  177. if (!img || img.complete) {
  178. tag = findTag(small.textContent, img);
  179. cache[a.herf] = tag.name;
  180. } else {
  181. tag = findTag(small.textContent, null);
  182. retry = true;
  183. }
  184. }
  185. if (!tag.count && tag.name) {
  186. let tagLabelTd = doc.createElement('TD');
  187. tagLabelTd.textContent = tag.name;
  188. tagLabelTd.className = 'catalogtag';
  189. tag.tds = [];
  190. tag.tds.push(tagLabelTd);
  191. tag.count ++;
  192. }
  193. tag.tds.push(td);
  194. tag.count ++;
  195. });
  196. // カタログの要素を置き換えて完了
  197. let tbody = doc.createElement('TBODY');
  198. let count = 0;
  199. let tr = null;
  200. TAGS.forEach(tag => {
  201. if (count === 0 && tag == NO_TAGGED) {
  202. tag.tds.shift();
  203. }
  204. for (let td of tag.tds) {
  205. if (count % maxCol === 0) {
  206. tr = tbody.appendChild(doc.createElement('TR'));
  207. }
  208. tr.appendChild(td);
  209. count ++;
  210. }
  211. });
  212. catalog.replaceChild(tbody, catalog.firstChild);
  213. sessionStorage.setItem('catalogtagging_cache', JSON.stringify(cache));
  214. doc.body.setAttribute('__catalogtagging_status', 'done');
  215. if (retry && retryCount < 10) { // やり直しは10回まで
  216. setTimeout(() => { tagging(retryCount + 1); }, 100);
  217. }
  218. };
  219. // 念のためイベント呼び出し回数をカウントして無限ループを抑制しておく
  220. let eventCount = 0;
  221. let resetEventCount = () => { eventCount = 0; };
  222. // START!
  223. let onLoad = e => {
  224. doc.styleSheets.item(0).insertRule(CATALOGTAG_CSS, 0);
  225. doc.styleSheets.item(0).insertRule(CATALOGTAG_TEXT_CSS, 0);
  226. tagging();
  227. // MutationRecordをeventCheckerでチェックしてタグ分けしたりしなかったりする関数
  228. let onEvent = (m, eventChecker) => {
  229. if (eventCount > 10) {
  230. console.log('他の拡張と競合してるっぽい');
  231. return;
  232. }
  233. for (let i = m.length - 1; 0 <= i; i --) {
  234. if (!eventChecker(m[i])) continue;
  235. eventCount ++;
  236. setTimeout(resetEventCount, 500);
  237. tagging();
  238. return;
  239. }
  240. };
  241. // TABLEタグが再追加されたらタグ分けするオブザーバー
  242. let defaultObserver = new MutationObserver(m => {
  243. onEvent(m, n => {
  244. for (let i = n.addedNodes.length - 1; 0 <= i; i --) {
  245. let node = n.addedNodes[i];
  246. if (node.tagName === 'TABLE') return true; // 赤福
  247. if (node.id === 'catalog_loading') return true; // ふたクロ
  248. }
  249. return false;
  250. });
  251. });
  252. defaultObserver.observe(catalog.parentNode, { childList: true });
  253. // ねないこのソートが終わったらタグ分けするオブザーバー
  254. let nenaikoObserver = new MutationObserver(m => {
  255. onEvent(m, n => {
  256. if (n.attributeName !== '__nenaiko_catsort_status') return false;
  257. if (doc.body.getAttribute('__nenaiko_catsort_status') === 'start') return false;
  258. // ねないこのソートが有効になってるならデフォルトのオブザーバーは要らないので切断する
  259. if (doc.body.getAttribute('__nenaiko_catsort_status') === 'done') defaultObserver.disconnect();
  260. return true;
  261. });
  262. });
  263. nenaikoObserver.observe(doc.body, { attributes: true });
  264. };
  265. if (doc.readyState === 'complete') {
  266. onLoad();
  267. } else {
  268. addEventListener('load', onLoad);
  269. }
  270. })();
  271.