您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
ふたクロの「Live」と「新着レスに自動スクロール」を自動クリックし、スレが落ちるか1000に行ったら次スレを探して移動する
当前为
// ==UserScript== // @name futakuro-auto-thread // @namespace https://2chan.net/ // @version 1.0.3 // @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 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); 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; 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(); } })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址