futakuro-auto-thread

ふたクロの「Live」と「新着レスに自動スクロール」を自動クリックし、スレが落ちるか1000に行ったら次スレを探して移動する

当前为 2023-01-01 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name futakuro-auto-thread
  3. // @namespace https://2chan.net/
  4. // @version 1.0.4
  5. // @description ふたクロの「Live」と「新着レスに自動スクロール」を自動クリックし、スレが落ちるか1000に行ったら次スレを探して移動する
  6. // @author ame-chan
  7. // @match https://*.2chan.net/b/res/*
  8. // @icon https://www.2chan.net/favicon.ico
  9. // @grant none
  10. // @license MIT
  11. // ==/UserScript==
  12. (() => {
  13. 'use strict';
  14. // ユーザー設定
  15. const REGEXP_TARGET_TEXT = /twitch\.tv\/rtainjapan/;
  16. const inlineStyle = `<style id="userscript-style">
  17. .userscript-dialog {
  18. position: fixed;
  19. right: 16px;
  20. bottom: 16px;
  21. padding: 8px 24px;
  22. max-width: 200px;
  23. line-height: 1.5;
  24. color: #fff;
  25. font-size: 1rem;
  26. background-color: #3e8ed0;
  27. border-radius: 6px;
  28. opacity: 1;
  29. transition: all 0.3s ease;
  30. transform: translateY(0px);
  31. z-index: 9999;
  32. }
  33. .userscript-dialog.is-hidden {
  34. opacity: 0;
  35. transform: translateY(100px);
  36. }
  37. .userscript-dialog.is-info {
  38. background-color: #3e8ed0;
  39. color: #fff;
  40. }
  41. .userscript-dialog.is-danger {
  42. background-color: #f14668;
  43. color: #fff;
  44. }
  45. </style>`;
  46. document.head.insertAdjacentHTML('beforeend', inlineStyle);
  47. const delay = (time = 500) => new Promise((resolve) => setTimeout(() => resolve(true), time));
  48. const setDialog = async (dialogText, status) => {
  49. const html = `<div class="userscript-dialog is-hidden is-${status}">${dialogText}</div>`;
  50. const dialogElm = document.querySelector('.userscript-dialog');
  51. if (dialogElm) {
  52. dialogElm.remove();
  53. }
  54. document.body.insertAdjacentHTML('afterbegin', html);
  55. await delay(100);
  56. document.querySelector('.userscript-dialog')?.classList.remove('is-hidden');
  57. };
  58. const getFutabaJson = async (path) => {
  59. const options = {
  60. method: 'GET',
  61. cache: 'no-cache',
  62. credentials: 'include',
  63. };
  64. const result = await fetch(path, options)
  65. .then((res) => {
  66. if (!res.ok) {
  67. throw new Error(res.statusText);
  68. }
  69. return res.arrayBuffer();
  70. })
  71. .catch((err) => {
  72. throw new Error(err);
  73. });
  74. try {
  75. const textDecoder = new TextDecoder('utf-8');
  76. const futabaJson = JSON.parse(textDecoder.decode(result));
  77. return futabaJson;
  78. } catch (e) {
  79. const textDecoder1 = new TextDecoder('Shift_JIS');
  80. const html = textDecoder1.decode(result);
  81. const parser = new DOMParser();
  82. const dom = parser.parseFromString(html, 'text/html');
  83. const bodyText = dom?.body?.textContent;
  84. if (bodyText) {
  85. console.log('json-error:', bodyText);
  86. setDialog(bodyText, 'danger');
  87. if (bodyText.includes('満員')) {
  88. await delay(20000);
  89. return {
  90. res: {},
  91. maxres: '',
  92. old: 0,
  93. };
  94. }
  95. }
  96. throw new Error(e);
  97. }
  98. };
  99. const autoMoveThreads = async (matchText, threadNo) => {
  100. const catalog = await getFutabaJson('/b/futaba.php?mode=json&sort=6');
  101. const threadKeys = Object.keys(catalog?.res || {});
  102. const targetKeyArr = [];
  103. for (const threadKey of threadKeys) {
  104. // 見ていたスレッドは飛ばす
  105. if (threadNo === threadKey) continue;
  106. try {
  107. const threadText = catalog.res[threadKey].com;
  108. if (threadText && threadText.includes(matchText)) {
  109. targetKeyArr.push(Number(threadKey));
  110. }
  111. } catch (e) {
  112. throw new Error(e);
  113. }
  114. }
  115. if (targetKeyArr.length) {
  116. try {
  117. const recentThreadKey = targetKeyArr.reduce((a, b) => Math.max(a, b));
  118. // 見ていたスレッドより古いスレッドしかないならfalse
  119. if (Number(threadNo) > recentThreadKey) {
  120. return Promise.resolve(false);
  121. }
  122. const threadStatus = await getFutabaJson(`/b/futaba.php?mode=json&res=${String(recentThreadKey)}`);
  123. const resCount = Object.keys(threadStatus?.res || {}).length;
  124. const isMin950 = resCount > 0 && resCount < 950;
  125. const isNotMaxRes = threadStatus.maxres === '';
  126. const isNotOld = threadStatus.old === 0;
  127. // レス数が950未満、maxresが空、oldが0なら新規スレッドとみなす
  128. if (isMin950 && isNotMaxRes && isNotOld) {
  129. return Promise.resolve(`/b/res/${recentThreadKey}.htm`);
  130. }
  131. } catch (e1) {
  132. return Promise.resolve(false);
  133. }
  134. }
  135. return Promise.resolve(false);
  136. };
  137. const observeThreadEnd = (matchText, threadNo) => {
  138. const sec = 1000;
  139. let count = 0;
  140. let fetchTimer = 0;
  141. let isRequestOK = false;
  142. let scrollEventHandler = () => {};
  143. let nextThreadCheckInterval = 10000;
  144. const checkThreadEnd = async () => {
  145. const resElms = document.querySelectorAll('.thre > div[style]');
  146. const lastAddElm = resElms[resElms.length - 1];
  147. const lastElm = lastAddElm.querySelector('table:last-child');
  148. const resNo = lastElm?.querySelector('[data-sno]')?.getAttribute('data-sno');
  149. if (!resNo) return false;
  150. const path = `/b/futaba.php?mode=json&res=${threadNo}&start=${resNo}&end=${resNo}`;
  151. const threadStatus = await getFutabaJson(path);
  152. const resCount = Object.keys(threadStatus?.res || {}).length;
  153. if (threadStatus.maxres !== '' || (threadStatus.old === 1 && resCount >= 950)) {
  154. return Promise.resolve(true);
  155. }
  156. return Promise.resolve(false);
  157. };
  158. const getTime = () => {
  159. const zeroPadding = (num) => String(num).padStart(2, '0');
  160. const time = new Date();
  161. const hour = zeroPadding(time.getHours());
  162. const minutes = zeroPadding(time.getMinutes());
  163. const seconds = zeroPadding(time.getSeconds());
  164. return `${hour}:${minutes}:${seconds}`;
  165. };
  166. const updateCheckInterval = (interval) => {
  167. if (count > interval / sec) {
  168. interval = interval * 2;
  169. }
  170. return interval;
  171. };
  172. const tryMoveThreads = async () => {
  173. if (isRequestOK) return;
  174. isRequestOK = true;
  175. window.removeEventListener('scroll', scrollEventHandler);
  176. if (count >= 30) {
  177. setDialog('次スレッドは見つかりませんでした', 'danger');
  178. return;
  179. }
  180. count += 1;
  181. nextThreadCheckInterval = updateCheckInterval(nextThreadCheckInterval);
  182. const dialogText = `[${getTime()}] 次のスレッドを探しています...<br>${count}巡目(${
  183. nextThreadCheckInterval / sec
  184. }秒間隔)`;
  185. setDialog(dialogText, 'info');
  186. const result = await autoMoveThreads(matchText, threadNo);
  187. if (typeof result === 'string') {
  188. return (location.href = result);
  189. }
  190. await delay(nextThreadCheckInterval);
  191. isRequestOK = false;
  192. void tryMoveThreads();
  193. };
  194. scrollEventHandler = () => {
  195. if (fetchTimer) clearTimeout(fetchTimer);
  196. fetchTimer = setTimeout(async () => {
  197. const isThreadEnd = await checkThreadEnd();
  198. if (isThreadEnd) {
  199. void tryMoveThreads();
  200. }
  201. }, 6000);
  202. };
  203. const threadDown = document.querySelector('#thread_down');
  204. const observeCallback = (_, observer) => {
  205. // スレが落ちたらfutakuroによって出現するID
  206. const threadDown = document.querySelector('#thread_down');
  207. if (threadDown !== null) {
  208. if (fetchTimer) clearTimeout(fetchTimer);
  209. observer.disconnect();
  210. void tryMoveThreads();
  211. }
  212. };
  213. const borderAreaElm = document.querySelector('#border_area');
  214. if (threadDown !== null) {
  215. if (fetchTimer) clearTimeout(fetchTimer);
  216. void tryMoveThreads();
  217. } else if (borderAreaElm !== null) {
  218. const observer = new MutationObserver(observeCallback);
  219. observer.observe(borderAreaElm, {
  220. childList: true,
  221. });
  222. }
  223. window.addEventListener('scroll', scrollEventHandler, {
  224. passive: false,
  225. });
  226. };
  227. const checkAutoLiveScroll = async () => {
  228. /** スレ本文の要素 */ const threadTopText = document.querySelector('#master')?.innerText;
  229. if (typeof threadTopText === 'undefined') return;
  230. /** 新着レスに自動スクロールチェックボックス(futakuro) */ let liveScrollCheckbox =
  231. document.querySelector('#autolive_scroll');
  232. while (liveScrollCheckbox === null) {
  233. await delay(1000);
  234. liveScrollCheckbox = document.querySelector('#autolive_scroll');
  235. }
  236. const hasBody = typeof threadTopText === 'string';
  237. const matchTargetText = threadTopText.match(REGEXP_TARGET_TEXT);
  238. const threadNo = document.querySelector('[data-res]')?.getAttribute('data-res');
  239. if (liveScrollCheckbox !== null && !liveScrollCheckbox.checked && hasBody && matchTargetText) {
  240. const matchText = matchTargetText[0];
  241. liveScrollCheckbox.click();
  242. if (matchText && threadNo) {
  243. observeThreadEnd(matchText, threadNo);
  244. }
  245. }
  246. };
  247. const callback = async (_, observer) => {
  248. const liveWindowElm = document.querySelector('#livewindow');
  249. if (liveWindowElm !== null) {
  250. await delay(1000);
  251. void checkAutoLiveScroll();
  252. observer.disconnect();
  253. }
  254. };
  255. const observer = new MutationObserver(callback);
  256. const liveScrollCheckbox = document.querySelector('#autolive_scroll');
  257. if (liveScrollCheckbox === null) {
  258. observer.observe(document.body, {
  259. childList: true,
  260. });
  261. } else {
  262. void checkAutoLiveScroll();
  263. }
  264. })();