ふたクロの「Live」と「新着レスに自動スクロール」を自動クリックし、スレが落ちるか1000に行ったら次スレを探して移動する
当前为
// ==UserScript==
// @name futakuro-auto-thread
// @namespace https://2chan.net/
// @version 1.0.0
// @description ふたクロの「Live」と「新着レスに自動スクロール」を自動クリックし、スレが落ちるか1000に行ったら次スレを探して移動する
// @author You
// @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">
#fvw_mes {
display: none !important;
}
.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) => {
let count = 0;
let fetchTimer = 0;
let threadEndTimer = 0;
let hasScrollEvent = false;
let isRequestOK = false;
let scrollEventHandler = () => {};
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 tryMoveThreads = async () => {
if (isRequestOK) return;
isRequestOK = true;
if (hasScrollEvent) {
hasScrollEvent = false;
window.removeEventListener('scroll', scrollEventHandler);
}
count += 1;
setDialog(`次のスレッドを探しています... ${count}巡目`, 'info');
const result = await autoMoveThreads(matchText, threadNo);
if (typeof result === 'string') {
return (location.href = result);
}
await delay(10000);
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或关注我们的公众号极客氢云获取最新地址