futakuro-auto-thread

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

当前为 2022-12-30 提交的版本,查看 最新版本

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         futakuro-auto-thread
// @namespace    https://2chan.net/
// @version      1.0.1
// @description  ふたクロの「Live」と「新着レスに自動スクロール」を自動クリックし、スレが落ちるか1000に行ったら次スレを探して移動する
// @author       ame-chan
// @match        https://*.2chan.net/b/res/*
// @icon         https://www.2chan.net/favicon.ico
// @grant        none
// @license      MIT
// ==/UserScript==
(() => {
  'use strict';
  // ユーザー設定
  const REGEXP_TARGET_TEXT = /twitch\.tv\/rtainjapan/;
  const inlineStyle = `<style id="userscript-style">
  .userscript-dialog {
    position: fixed;
    right: 16px;
    bottom: 16px;
    padding: 8px 24px;
    max-width: 200px;
    line-height: 1.5;
    color: #fff;
    font-size: 1rem;
    background-color: #3e8ed0;
    border-radius: 6px;
    opacity: 1;
    transition: all 0.3s ease;
    transform: translateY(0px);
    z-index: 9999;
  }
  .userscript-dialog.is-hidden {
    opacity: 0;
    transform: translateY(100px);
  }
  .userscript-dialog.is-info {
    background-color: #3e8ed0;
    color: #fff;
  }
  .userscript-dialog.is-danger {
    background-color: #f14668;
    color: #fff;
  }
  </style>`;
  document.head.insertAdjacentHTML('beforeend', inlineStyle);
  const delay = (time = 500) => new Promise((resolve) => setTimeout(() => resolve(true), time));
  const setDialog = async (dialogText, status) => {
    const html = `<div class="userscript-dialog is-hidden is-${status}">${dialogText}</div>`;
    const dialogElm = document.querySelector('.userscript-dialog');
    if (dialogElm) {
      dialogElm.remove();
    }
    document.body.insertAdjacentHTML('afterbegin', html);
    await delay(100);
    document.querySelector('.userscript-dialog')?.classList.remove('is-hidden');
  };
  const getFutabaJson = async (path) => {
    const options = {
      method: 'GET',
      cache: 'no-cache',
      credentials: 'include',
    };
    const result = await fetch(path, options)
      .then((res) => {
        if (!res.ok) {
          throw new Error(res.statusText);
        }
        return res.arrayBuffer();
      })
      .catch((err) => {
        throw new Error(err);
      });
    try {
      const textDecoder = new TextDecoder('utf-8');
      const futabaJson = JSON.parse(textDecoder.decode(result));
      return futabaJson;
    } catch (e) {
      const textDecoder1 = new TextDecoder('Shift_JIS');
      const html = textDecoder1.decode(result);
      const parser = new DOMParser();
      const dom = parser.parseFromString(html, 'text/html');
      if (dom.body.textContent) {
        console.log('json-error:', dom.body.textContent);
        setDialog(dom.body.textContent, 'danger');
      }
      throw new Error(e);
    }
  };
  const autoMoveThreads = async (matchText, threadNo) => {
    const catalog = await getFutabaJson('/b/futaba.php?mode=json&sort=6');
    const threadKeys = Object.keys(catalog?.res || {});
    const targetKeyArr = [];
    for (const threadKey of threadKeys) {
      // 見ていたスレッドは飛ばす
      if (threadNo === threadKey) continue;
      try {
        const threadText = catalog.res[threadKey].com;
        if (threadText && threadText.includes(matchText)) {
          targetKeyArr.push(Number(threadKey));
        }
      } catch (e) {
        throw new Error(e);
      }
    }
    if (targetKeyArr.length) {
      try {
        const recentThreadKey = targetKeyArr.reduce((a, b) => Math.max(a, b));
        // 見ていたスレッドより古いスレッドしかないならfalse
        if (Number(threadNo) > recentThreadKey) {
          return Promise.resolve(false);
        }
        const threadStatus = await getFutabaJson(`/b/futaba.php?mode=json&res=${String(recentThreadKey)}`);
        const resCount = Object.keys(threadStatus?.res || {}).length;
        const isMin950 = resCount > 0 && resCount < 950;
        const isNotMaxRes = threadStatus.maxres === '';
        const isNotOld = threadStatus.old === 0;
        // レス数が950未満、maxresが空、oldが0なら新規スレッドとみなす
        if (isMin950 && isNotMaxRes && isNotOld) {
          return Promise.resolve(`/b/res/${recentThreadKey}.htm`);
        }
      } catch (e1) {
        return Promise.resolve(false);
      }
    }
    return Promise.resolve(false);
  };
  const observeThreadEnd = (matchText, threadNo) => {
    const sec = 1000;
    let count = 0;
    let fetchTimer = 0;
    let threadEndTimer = 0;
    let hasScrollEvent = false;
    let isRequestOK = false;
    let scrollEventHandler = () => {};
    let nextThreadCheckInterval = 10000;
    const checkThreadEnd = async () => {
      const resElms = document.querySelectorAll('.thre > div[style]');
      const lastAddElm = resElms[resElms.length - 1];
      const lastElm = lastAddElm.querySelector('table:last-child');
      const resNo = lastElm?.querySelector('[data-sno]')?.getAttribute('data-sno');
      if (!resNo) return false;
      const path = `/b/futaba.php?mode=json&res=${threadNo}&start=${resNo}&end=${resNo}`;
      const threadStatus = await getFutabaJson(path);
      if (threadStatus.old === 1 || threadStatus.maxres !== '') {
        return Promise.resolve(true);
      }
      return Promise.resolve(false);
    };
    const getTime = () => {
      const zeroPadding = (num) => String(num).padStart(2, '0');
      const time = new Date();
      const hour = zeroPadding(time.getHours());
      const minutes = zeroPadding(time.getMinutes());
      const seconds = zeroPadding(time.getSeconds());
      return `${hour}:${minutes}:${seconds}`;
    };
    const updateCheckInterval = (interval) => {
      if (count > interval / sec) {
        interval = interval * 2;
      }
      return interval;
    };
    const tryMoveThreads = async () => {
      if (isRequestOK) return;
      isRequestOK = true;
      if (hasScrollEvent) {
        hasScrollEvent = false;
        window.removeEventListener('scroll', scrollEventHandler);
      }
      if (count >= 30) {
        setDialog('次スレッドは見つかりませんでした', 'danger');
        return;
      }
      count += 1;
      nextThreadCheckInterval = updateCheckInterval(nextThreadCheckInterval);
      const dialogText = `[${getTime()}] 次のスレッドを探しています...<br>${count}巡目(${
        nextThreadCheckInterval / sec
      }秒間隔)`;
      setDialog(dialogText, 'info');
      const result = await autoMoveThreads(matchText, threadNo);
      if (typeof result === 'string') {
        return (location.href = result);
      }
      await delay(nextThreadCheckInterval);
      isRequestOK = false;
      void tryMoveThreads();
    };
    scrollEventHandler = () => {
      hasScrollEvent = true;
      if (fetchTimer) clearTimeout(fetchTimer);
      if (threadEndTimer) clearTimeout(threadEndTimer);
      threadEndTimer = setTimeout(async () => {
        // レスの数
        const resCount = document.querySelectorAll('.res_no');
        // スレが落ちたらfutakuroによって出現するID
        const threadDown = document.querySelector('#thread_down');
        if (resCount.length >= 1000 || threadDown !== null) {
          if (fetchTimer) clearTimeout(fetchTimer);
          void tryMoveThreads();
        }
      }, 3000);
      fetchTimer = setTimeout(async () => {
        const isThreadEnd = await checkThreadEnd();
        if (isThreadEnd) {
          void tryMoveThreads();
        }
      }, 6000);
    };
    window.addEventListener('scroll', scrollEventHandler, {
      passive: false,
    });
  };
  const checkAutoLiveScroll = async () => {
    /** スレ本文の要素 */ const threadTopText = document.querySelector('#master')?.innerText;
    if (typeof threadTopText === 'undefined') return;
    /** 新着レスに自動スクロールチェックボックス(futakuro) */ let liveScrollCheckbox =
      document.querySelector('#autolive_scroll');
    while (liveScrollCheckbox === null) {
      await delay(1000);
      liveScrollCheckbox = document.querySelector('#autolive_scroll');
    }
    const hasBody = typeof threadTopText === 'string';
    const matchTargetText = threadTopText.match(REGEXP_TARGET_TEXT);
    const threadNo = document.querySelector('[data-res]')?.getAttribute('data-res');
    if (liveScrollCheckbox !== null && !liveScrollCheckbox.checked && hasBody && matchTargetText) {
      const matchText = matchTargetText[0];
      liveScrollCheckbox.click();
      if (matchText && threadNo) {
        observeThreadEnd(matchText, threadNo);
      }
    }
  };
  const callback = async (_, observer) => {
    const liveWindowElm = document.querySelector('#livewindow');
    if (liveWindowElm !== null) {
      await delay(1000);
      void checkAutoLiveScroll();
      observer.disconnect();
    }
  };
  const observer = new MutationObserver(callback);
  const liveScrollCheckbox = document.querySelector('#autolive_scroll');
  if (liveScrollCheckbox === null) {
    observer.observe(document.body, {
      childList: true,
    });
  } else {
    void checkAutoLiveScroll();
  }
})();