futakuro-auto-thread

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

Per 01-01-2023. Zie de nieuwste versie.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey, Greasemonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Userscripts.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een gebruikersscriptbeheerder nodig.

(Ik heb al een user script manager, laat me het downloaden!)

Advertisement:

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

(Ik heb al een beheerder - laat me doorgaan met de installatie!)

Advertisement:

// ==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();
  }
})();