futakuro-auto-thread

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

目前為 2022-12-30 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 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();
  }
})();