您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Shows the amount of people in the chat
// ==UserScript== // @name Twitch [Chatters Count] // @name:pl Twitch [Ilość Czatowników] // @description Shows the amount of people in the chat // @description:pl Pokazuje liczbę użytkowników na czacie // @version 1.2.0 // @author Pabli // @namespace https://github.com/pabli24 // @homepageURL https://github.com/pabli24/twitch-chatters-count // @supportURL https://github.com/pabli24/twitch-chatters-count/issues // @license MIT // @match https://www.twitch.tv/* // @match https://m.twitch.tv/* // @icon data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI1MTIiIGhlaWdodD0iNTEyIiB2aWV3Qm94PSIwIDAgNTEyIDUxMiI+PHBhdGggZmlsbD0iI2ZmODI4MCIgZmlsbC1ydWxlPSJldmVub2RkIiBkPSJNMTU2LjYgMTk2LjNhOTkuNCA5OS40IDAgMSAxIDEyMy4xIDk2LjUgMzkuNyAzOS43IDAgMCAwIDM1LjkgMjIuN2gxOS45YTU5LjYgNTkuNiAwIDAgMSA1OS42IDU5LjZ2MzkuOGgtMzkuOHYtMzkuOGMwLTExLTguOS0xOS45LTE5LjktMTkuOWgtMTkuOWE3OS40IDc5LjQgMCAwIDEtNTkuNi0yNi45IDc5LjUgNzkuNSAwIDAgMS01OS42IDI2LjloLTE5LjljLTExIDAtMTkuOSA4LjktMTkuOSAxOS45djM5LjhoLTM5Ljh2LTM5LjhhNTkuNiA1OS42IDAgMCAxIDU5LjYtNTkuNmgxOS45YzE1LjQgMCAyOS40LTguOCAzNS45LTIyLjdhOTkuNCA5OS40IDAgMCAxLTc1LjctOTYuNlpNMjU2IDI1NmE1OS42IDU5LjYgMCAxIDEgMC0xMTkuMiA1OS42IDU5LjYgMCAwIDEgMCAxMTkuMloiLz48cGF0aCBmaWxsPSIjZmY4MjgwIiBkPSJNMCA1MTJWMGgxMTYuOHYzNi4xSDQzLjN2NDQxLjNoNzMuNlY1MTJIMFpNNTEyIDB2NTEySDM5NS4ydi0zNi4xaDczLjZWMzQuNmgtNzMuNlYwSDUxMloiLz48L3N2Zz4= // @run-at document-end // @grant GM_info // @grant GM_notification // @grant GM_openInTab // @grant GM_setValue // @grant GM_getValue // @grant GM_registerMenuCommand // @grant GM_unregisterMenuCommand // ==/UserScript== (async () => { 'use strict'; let settings = { previewCards: { value: await GM_getValue('previewCards', true), label: 'Show for preview cards in the main/directory page', }, underPlayer: { value: await GM_getValue('underPlayer', true), label: 'Show under the player next to the viewer count', }, theatreMode: { value: await GM_getValue('theatreMode', true), label: 'Show in the theatre mode when mousing over the player', }, topChat: { value: await GM_getValue('topChat', false), label: 'Show at the top of the chat', }, numberFormat: { value: await GM_getValue('numberFormat', true), label: 'Number format based on your language (en-US if disabled)', }, }; let menuCommands = {}; async function updateMenu() { Object.keys(menuCommands).forEach(key => GM_unregisterMenuCommand(menuCommands[key])); Object.entries(settings).forEach(([key, config]) => { menuCommands[key] = GM_registerMenuCommand( `${config.value ? '☑' : '☐'} ${config.label}`, async () => { settings[key].value = !settings[key].value; await GM_setValue(key, settings[key].value); GM_notification(`${config.label} is now ${config.value ? 'enabled' : 'disabled'}`, GM_info.script.name, GM_info.script.icon); updateMenu(); } ); }); } updateMenu(); let updater = null; let currentChannel = ''; const isMobile = window.location.host === 'm.twitch.tv' ? true : false; setInterval(() => { const path = window.location.pathname; if (settings.previewCards.value && (path === '/' || path.startsWith('/directory'))) { checkForNewCards(path); return; } let channelName = path.split('/')[1]; if (!channelName || ['videos', 'settings', 'subscriptions', 'inventory', 'wallet', 'privacy', 'turbo', 'downloads', 'p', 'annual-recap'].includes(channelName)) return; if (channelName === 'popout') { channelName = path.split('/')[2]; } if (currentChannel !== channelName) { currentChannel = channelName; if (updater) { clearInterval(updater); updater = null; } } if (settings.underPlayer.value) { chattersCount(); } else if (ct) { ct.remove(); ct = null; } if (settings.theatreMode.value) { tmChattersCount(); } else if (tmCt) { tmCt.remove(); tmCt = null; } if (settings.topChat.value) { chatChattersCount(); } else if (chatCt) { chatCt.remove(); chatCt = null; } if (!updater) { updateCount(channelName); updater = setInterval(() => updateCount(channelName), 60000); } }, 1000); let ct = null; let tmCt = null; let chatCt = null; let chatters = '⏳'; async function updateCount(channelName) { chatters = await getChatters(channelName); [ct, tmCt, chatCt].forEach(el => { if (el) el.textContent = `[${chatters}]`; }); } function createChattersCounter(id) { const counter = document.createElement('span'); const lang = document.documentElement.getAttribute('lang') || 'en-US'; counter.id = id; counter.title = lang === 'pl-PL' ? 'Ilość Czatowników' : 'Chatters Count'; counter.textContent = `[${chatters}]`; counter.style.cssText = ` color: var(--color-text-live, #ff8280); font-size: var(--font-size-base, 1.3rem); font-weight: var(--font-weight-semibold, 600); font-feature-settings: "tnum"; line-height: var(--line-height-body, 1.5); margin-left: 0.5rem; align-content: center; `; return counter; } function chattersCount() { ct = document.getElementById('chatters-count'); if (ct != null) return; const viewersCount = !isMobile ? document.querySelector('main [data-a-target="animated-channel-viewers-count"]')?.parentElement : document.querySelector('#channel-player [data-a-target="tw-stat-value"]')?.parentElement; if (!viewersCount) return; ct = createChattersCounter('chatters-count'); viewersCount.appendChild(ct); } function tmChattersCount() { tmCt = document.getElementById('tm-chatters-count'); if (tmCt != null) return; const tmInfoCard = document.querySelector('p[data-test-selector="stream-info-card-component__description"]'); if (!tmInfoCard) return; tmCt = createChattersCounter('tm-chatters-count'); tmInfoCard.appendChild(tmCt); } function chatChattersCount() { chatCt = document.getElementById('chat-chatters-count'); if (chatCt != null) return; const chatHeader = document.querySelector('button[data-test-selector="chat-viewer-list"]')?.parentElement; if (!chatHeader) return; chatCt = createChattersCounter('chat-chatters-count'); chatHeader.prepend(chatCt); } function checkForNewCards(path) { let previewCards = null; if (!isMobile) { previewCards = document.querySelectorAll('a[data-a-target="preview-card-image-link"]'); } else { if (path.startsWith('/directory/following')) { previewCards = document.querySelectorAll('main > div > div > h2 + div:first-of-type > div'); } else { previewCards = document.querySelectorAll('main article'); } } previewCards.forEach(addChattersToCard); } async function addChattersToCard(card) { const cardStat = card.querySelector('div.tw-media-card-stat'); if (!cardStat || card.dataset.chattersCount === 'true' || card.dataset.chattersLoading === 'true') return; card.dataset.chattersCount = 'true'; const channelName = card.getAttribute('href')?.slice(1) || card.querySelector('a.tw-link')?.getAttribute('href')?.slice(1); if (!channelName || channelName.startsWith("videos/")) return; const counter = document.createElement('span'); counter.className = 'directory-chatters-count'; counter.style.paddingLeft = '0.4rem'; const count = await getChatters(channelName); counter.textContent = `[${count}]`; cardStat.appendChild(counter); } async function getChatters(channel) { const lang = settings.numberFormat.value ? document.documentElement.getAttribute('lang') : 'en-US'; const data = await graphqlQuery(query, { name: channel }); const count = data?.data?.channel?.chatters?.count; return count != null && !isNaN(count) ? new Intl.NumberFormat(lang || 'en-US').format(count) : 'N/A'; } const query = { kind: 'Document', definitions: [ { kind: 'OperationDefinition', operation: 'query', name: { kind: 'Name', value: 'GetChannelChattersCount', }, variableDefinitions: [ { kind: 'VariableDefinition', variable: { kind: 'Variable', name: { kind: 'Name', value: 'name', }, }, type: { kind: 'NonNullType', type: { kind: 'NamedType', name: { kind: 'Name', value: 'String', }, }, }, directives: [], }, ], directives: [], selectionSet: { kind: 'SelectionSet', selections: [ { kind: 'Field', name: { kind: 'Name', value: 'channel', }, arguments: [ { kind: 'Argument', name: { kind: 'Name', value: 'name', }, value: { kind: 'Variable', name: { kind: 'Name', value: 'name', }, }, }, ], directives: [], selectionSet: { kind: 'SelectionSet', selections: [ { kind: 'Field', name: { kind: 'Name', value: 'chatters', }, arguments: [], directives: [], selectionSet: { kind: 'SelectionSet', selections: [ { kind: 'Field', name: { kind: 'Name', value: 'count', }, arguments: [], directives: [], }, ], }, }, ], }, }, ], }, }, ], loc: { start: 0, end: 191, }, }; // https://github.com/night/betterttv/blob/master/src/utils/twitch.js function searchReactChildren(node, predicate, maxDepth = 15, depth = 0) { try { if (predicate(node)) { return node; } } catch (_) {} if (!node || depth > maxDepth) { return null; } const {child, sibling} = node; if (child || sibling) { return ( searchReactChildren(child, predicate, maxDepth, depth + 1) || searchReactChildren(sibling, predicate, maxDepth, depth + 1) ); } return null; } function getReactRoot(element) { for (const key in element) { if (key.startsWith('_reactRootContainer') || key.startsWith('__reactContainer$')) { return element[key]; } } return null; } function getApolloClient() { let client; try { const reactRoot = getReactRoot(document.getElementById('root')); const node = searchReactChildren( reactRoot?._internalRoot?.current ?? reactRoot, (n) => n.pendingProps?.value?.client ); client = node.pendingProps.value.client; } catch (_) {} return client; } async function graphqlQuery(query, variables, options = {}) { const client = getApolloClient(); if (!client) return; return client.query({ query, variables, ...options }); } })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址