您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Automatically lurk on Twitch channels you follow
当前为
// ==UserScript== // @name Twitching Lurkist // @description Automatically lurk on Twitch channels you follow // @author Xspeed // @namespace xspeed.net // @license MIT // @version 8 // @match *://www.twitch.tv/* // @grant GM.getValue // @grant GM.setValue // @run-at document-start // @noframes // ==/UserScript== const activationPath = '/directory/following/autolurk'; let intervalJob = -1; let fetchOpts = { init: false }; let streamFrames = []; let autoClose = false; let autoCinema = true; let lowLatDisable = true; let whitelist = []; function log(txt) { console.log('[' + GM.info.script.name + '] ' + txt); } function clearChildren(parent) { while (parent.firstChild) { parent.removeChild(parent.lastChild); } } function startCinemaJob(streamFrame) { if (!autoCinema || streamFrame.theatre) return; const jobId = setInterval(() => { if (!autoCinema || streamFrame.theatre) { clearInterval(jobId); } const btn = streamFrame.frame.contentDocument?.querySelector('button[data-a-target="player-theatre-mode-button"]'); if (btn) { streamFrame.theatre = true; btn.click(); clearInterval(jobId); } }, 1000); } function onFrameLoaded(e) { const obj = streamFrames.find(x => x.frame == e.target); ['pushState', 'replaceState'].forEach((changeState) => { e.target.contentWindow.history[changeState] = new Proxy(e.target.contentWindow.history[changeState], { apply(target, thisArg, argList) { const [state, title, url] = argList; log(changeState + ' to ' + url); startCinemaJob(obj); return target.apply(thisArg, argList); }, }); }); } function setupFrame(url) { log('Setting up new frame for ' + url); const elem = document.createElement('iframe'); elem.src = url; elem.style.width = '1000px'; elem.style.height = '480px'; elem.style.gridColumnStart = '1'; elem.style.gridRowStart = '1'; elem.style.transform = 'scale(0.8)'; elem.style.transformOrigin = '0 0'; elem.addEventListener('load', onFrameLoaded); const container = document.createElement('div'); container.style.width = '800px'; container.style.height = '384px'; container.style.position = 'static'; container.style.display = 'grid'; container.style.placeItems = 'start'; const closeBtn = document.createElement('button'); closeBtn.innerText = 'Close stream'; closeBtn.style.gridColumnStart = '1'; closeBtn.style.gridRowStart = '1'; closeBtn.addEventListener('click', function() { container.parentElement.removeChild(container); streamFrames = streamFrames.filter(x => x.frame != elem); }); container.append(elem); container.append(closeBtn); // TODO: Restore user set quality after the video player loads localStorage.setItem('video-muted', '{"default":false,"carousel":false}'); localStorage.setItem('video-quality', '{"default":"160p30"}'); localStorage.setItem('mature', 'true'); if (lowLatDisable) localStorage.setItem('lowLatencyModeEnabled', 'false'); document.body.append(container); const result = { container: container, frame: elem, timeout: 0, wait: 0, theatre: false }; streamFrames.push(result); return result; } async function detect() { let items = await fetch(fetchOpts.url, fetchOpts.opts).then(res => res.json()) .then(obj => obj[0].data.currentUser.followedLiveUsers.edges.map(x => '/' + x.node.login.toLowerCase())); let currentUrls = []; streamFrames = streamFrames.filter(x => { if (!x.frame.contentDocument) { log('Frame ' + x.frame.src + ' invalid'); x.container.parentElement.removeChild(x.container); return false; } let url = x.frame.contentDocument.location.pathname.toLowerCase(); if (!currentUrls.includes(url)) { x.wait = 0; currentUrls.push(url); } else if (++x.wait > 2) { log('Frame ' + x.frame.contentDocument.location.pathname + ' duplicated'); x.container.parentElement.removeChild(x.container); return false; } let chatLink = x.frame.contentDocument.querySelector('a[tabname="chat"]'); if (chatLink) { x.theatre = false; chatLink.click(); return true; } let hostLink = x.frame.contentDocument.querySelector('a[data-a-target="hosting-indicator"]'); if (!hostLink) { hostLink = Array.from(x.frame.contentDocument.querySelectorAll('a.tw-link')) .find(x => x.innerText.match(/Watch\s+\w+\s+with\s+\d+\s+viewers/)); } if (hostLink) { log('Frame ' + url + ' redirecting to ' + hostLink.href); x.theatre = false; hostLink.click(); return true; } let vidElem = x.frame.contentDocument.querySelector('video'); if (vidElem && !vidElem.paused && !vidElem.ended && vidElem.readyState > 2) { x.timeout = 0; } else if (++x.timeout > 6) { log('Frame ' + url + ' timed out'); x.container.parentElement.removeChild(x.container); return false; } return true; }); if (autoClose) { streamFrames = streamFrames.filter(x => { if (!items.includes(x.frame.contentDocument.location.pathname)) { log('Frame ' + x.frame.contentDocument.location.pathname + ' auto-closing'); x.container.parentElement.removeChild(x.container); return false; } return true; }); } if (whitelist.length) items = items.filter(x => whitelist.includes(x.substr(1))); items.filter(x => !currentUrls.includes(x)).forEach(x => setupFrame(x)); } function setupTab() { if (location.pathname.startsWith(activationPath)) { if (fetchOpts.url && !fetchOpts.init) { log('Preparing layout and update loop'); fetchOpts.init = true; setupControls(); setInterval(detect, 10000); setTimeout(() => location.reload(), (4 * 3600000) + (Math.floor(Math.random() * 10000))); clearInterval(intervalJob); } return; } if (!location.pathname.startsWith('/directory/following')) return; if (document.querySelector('a[href="' + activationPath + '"]')) return; const tabs = document.body.querySelectorAll('li[role=presentation]'); if (!tabs.length) return; log('Setting up Auto-lurk navigation tab'); const lastTab = tabs[tabs.length - 1]; const newTab = document.createElement('li'); newTab.className = lastTab.className; newTab.innerHTML = lastTab.innerHTML; newTab.querySelector('a').href = activationPath; newTab.querySelector('p').innerText = 'Auto-lurk'; lastTab.parentElement.appendChild(newTab); } function createWhitelist() { const list = document.createElement('ul'); list.style.marginTop = '3px'; list.style.marginBottom = '3px'; const inp = document.createElement('input'); inp.type = 'text'; const okBtn = document.createElement('button'); okBtn.type = 'submit'; okBtn.innerText = '+'; const listLab = document.createElement('label'); listLab.append('Whitelist: '); listLab.append(inp); listLab.append(okBtn); const updateList = function() { clearChildren(list); whitelist.forEach(x => { const btn = document.createElement('a'); btn.href = '#'; btn.innerText = 'X'; btn.addEventListener('click', function(e) { e.preventDefault(); whitelist.splice(whitelist.indexOf(x), 1); updateList(); return false; }); const elem = document.createElement('li'); elem.innerText = x + ' '; elem.append(btn); list.append(elem); }); GM.setValue('whitelist', whitelist.join(',')); }; const listBox = document.createElement('form'); listBox.style.marginBottom = '3px'; listBox.append(list); listBox.append(listLab); listBox.addEventListener('submit', function(e) { e.preventDefault(); const value = inp.value.trim().toLowerCase(); if (!value || whitelist.includes(value)) return; whitelist.push(value); whitelist.sort(); updateList(); inp.value = ''; }); updateList(); return listBox; } function createToggle(name, text, init, setter) { const toggle = document.createElement('input'); toggle.type = 'checkbox'; toggle.checked = init; toggle.addEventListener('change', function() { setter(this.checked); GM.setValue(name, this.checked); }); const label = document.createElement('label'); label.style.display = 'block'; label.style.marginRight = '5px'; label.append(toggle); label.append(text); return label; } async function setupControls() { autoClose = await GM.getValue('autoClose', false); autoCinema = await GM.getValue('autoCinema', true); lowLatDisable = await GM.getValue('lowLatDisable', true); whitelist = (await GM.getValue('whitelist', '')).split(',').map(x => x.trim()).filter(x => x); // Backwards compatibility whitelist.push(...(localStorage.getItem(GM.info.script.name + '-whitelist')?.split(',')?.filter(x => x) ?? [])); const listBox = createWhitelist(); const closeBtn = document.createElement('button'); closeBtn.innerText = 'Close all streams'; closeBtn.addEventListener('click', function() { streamFrames.forEach(x => x.container.parentElement.removeChild(x.container)); streamFrames.length = 0; }); const autoTgl = createToggle('autoClose', 'Auto-close raids and hosts', autoClose, x => autoClose = x); const cinemaTgl = createToggle('autoCinema', 'Auto enable theatre mode', autoCinema, x => autoCinema = x); const bufferTgl = createToggle('lowLatDisable', 'Disable low latency mode', lowLatDisable, x => lowLatDisable = x); const panel = document.createElement('div'); panel.style.padding = '5px'; panel.style.backgroundColor = 'gray'; panel.style.position = 'fixed'; panel.style.zIndex = '1'; panel.style.right = '0'; panel.style.bottom = '0'; panel.style.opacity = '0.85'; panel.append(listBox); panel.append(autoTgl); panel.append(cinemaTgl); panel.append(bufferTgl); panel.append(closeBtn); document.documentElement.style.backgroundColor = 'black'; document.body.style.backgroundColor = 'black'; clearChildren(document.body); document.body.append(panel); } log('Script loaded on path ' + location.pathname); if (location.pathname.startsWith(activationPath)) { unsafeWindow.fetch = new Proxy(unsafeWindow.fetch, { apply(target, thisArg, argList) { const [url, opts] = argList; if (url.includes('gql.twitch.tv') && opts.body.includes('FollowingLive_CurrentUser') && opts._meta != GM.info.script.name) { log('Intercepted live list request'); const opBody = JSON.parse(opts.body).find(x => x.operationName == 'FollowingLive_CurrentUser'); fetchOpts.url = url; fetchOpts.opts = { _meta: GM.info.script.name, headers: opts.headers, method: opts.method, body: JSON.stringify([{ operationName: opBody.operationName, variables: { limit: 25, freeformTagsEnabled: false }, extensions: opBody.extensions }]) }; } return target.apply(thisArg, argList); } }); } intervalJob = setInterval(setupTab, 1250);
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址