futakuro-auto-thread

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

目前为 2023-01-01 提交的版本。查看 最新版本

// ==UserScript==
// @name         futakuro-auto-thread
// @namespace    https://2chan.net/
// @version      1.0.4
// @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');
      const bodyText = dom?.body?.textContent;
      if (bodyText) {
        console.log('json-error:', bodyText);
        setDialog(bodyText, 'danger');
        if (bodyText.includes('満員')) {
          await delay(20000);
          return {
            res: {},
            maxres: '',
            old: 0,
          };
        }
      }
      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 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);
      const resCount = Object.keys(threadStatus?.res || {}).length;
      if (threadStatus.maxres !== '' || (threadStatus.old === 1 && resCount >= 950)) {
        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;
      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 = () => {
      if (fetchTimer) clearTimeout(fetchTimer);
      fetchTimer = setTimeout(async () => {
        const isThreadEnd = await checkThreadEnd();
        if (isThreadEnd) {
          void tryMoveThreads();
        }
      }, 6000);
    };
    const threadDown = document.querySelector('#thread_down');
    const observeCallback = (_, observer) => {
      // スレが落ちたらfutakuroによって出現するID
      const threadDown = document.querySelector('#thread_down');
      if (threadDown !== null) {
        if (fetchTimer) clearTimeout(fetchTimer);
        observer.disconnect();
        void tryMoveThreads();
      }
    };
    const borderAreaElm = document.querySelector('#border_area');
    if (threadDown !== null) {
      if (fetchTimer) clearTimeout(fetchTimer);
      void tryMoveThreads();
    } else if (borderAreaElm !== null) {
      const observer = new MutationObserver(observeCallback);
      observer.observe(borderAreaElm, {
        childList: true,
      });
    }
    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();
  }
})();

QingJ © 2025

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